- Normalize page titles to text-2xl font-semibold (was font-bold in TopicsView, DocumentView, SharedView) - Normalize section titles to text-lg font-semibold (was text-xl in SettingsPreferencesTab, SettingsAiTab, SettingsCloudTab, BackupCodesDisplay, CloudCredentialModal) - Normalize panel headings to text-sm font-semibold (SettingsAccountTab, DocumentView) - Normalize AdminOverviewView page title from text-xl to text-2xl font-semibold - Replace decorative sidebar skeleton inline styles with static Tailwind width classes (w-12/w-16/w-20) - Add focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 to all interactive buttons across the app - Add active:bg-* states to primary and secondary buttons for coherent press feedback - Update nav-link scoped CSS in AppSidebar and AdminSidebar to include active:bg-gray-200 and focus-visible ring - Add 7 new tests: VISUAL-01 skeleton class invariant, VISUAL-04 focus-visible invariant, VISUAL-03 typography invariant (36 test files, 270 tests pass)
85 lines
3.1 KiB
Vue
85 lines
3.1 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-1">Save your backup codes</h3>
|
|
<p class="text-sm text-gray-500">
|
|
Store these codes somewhere safe. Each can only be used once if you lose access to your authenticator app.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 2-column grid of backup codes -->
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div
|
|
v-for="code in codes"
|
|
:key="code"
|
|
class="font-mono text-sm text-gray-800 bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-center"
|
|
>
|
|
{{ code }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Copy all button -->
|
|
<button
|
|
type="button"
|
|
@click="copyAll"
|
|
class="flex items-center gap-2 text-sm font-semibold text-indigo-600 hover:text-indigo-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded"
|
|
>
|
|
<!-- Clipboard icon -->
|
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
|
|
</svg>
|
|
{{ copied ? 'Copied' : 'Copy all codes' }}
|
|
</button>
|
|
|
|
<!-- Acknowledgment checkbox -->
|
|
<div class="flex items-start gap-3 pt-2">
|
|
<input
|
|
id="backup-codes-ack"
|
|
v-model="acknowledged"
|
|
type="checkbox"
|
|
class="mt-0.5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
/>
|
|
<label for="backup-codes-ack" class="text-sm text-gray-700 cursor-pointer">
|
|
I have saved these codes in a secure place. I understand they will not be shown again.
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Enable 2FA CTA — disabled until acknowledged -->
|
|
<button
|
|
type="button"
|
|
@click="acknowledged && $emit('acknowledged')"
|
|
:disabled="!acknowledged"
|
|
class="w-full px-6 py-3 bg-indigo-600 text-white rounded-lg text-sm font-semibold hover:bg-indigo-700 active:bg-indigo-800 transition-colors min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
|
:class="!acknowledged ? 'opacity-50 cursor-not-allowed' : ''"
|
|
>
|
|
Enable two-factor authentication
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
|
|
const props = defineProps({
|
|
codes: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
})
|
|
|
|
defineEmits(['acknowledged'])
|
|
|
|
const acknowledged = ref(false)
|
|
const copied = ref(false)
|
|
|
|
async function copyAll() {
|
|
try {
|
|
await navigator.clipboard.writeText(props.codes.join('\n'))
|
|
copied.value = true
|
|
setTimeout(() => { copied.value = false }, 2000)
|
|
} catch {
|
|
// Clipboard API may fail in some environments — fail silently
|
|
}
|
|
}
|
|
</script>
|