create-nala 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,307 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+ import { Checkbox } from '@/components/ui/checkbox'
9
+ import { Alert, AlertDescription } from '@/components/ui/alert'
10
+ import { toast } from '@/components/ui/sonner'
11
+ import {
12
+ ShieldCheck,
13
+ CheckCircle2,
14
+ ArrowRight,
15
+ ArrowLeft,
16
+ KeyRound,
17
+ Smartphone,
18
+ AlertCircle,
19
+ Loader2,
20
+ Shield,
21
+ Info,
22
+ } from '@lucide/vue'
23
+
24
+ const router = useRouter()
25
+
26
+ // Authentication state
27
+ const isRecoveryMode = ref(false)
28
+ const recoveryCode = ref('')
29
+ const digits = ref<string[]>(['', '', '', '', '', ''])
30
+ const inputRefs = ref<HTMLInputElement[]>([])
31
+ const trustDevice = ref(true)
32
+ const isVerifying = ref(false)
33
+ const isVerified = ref(false)
34
+ const errorMessage = ref('')
35
+ const userEmail = ref('alex.morgan@example.com')
36
+
37
+ // Check if input is complete
38
+ const isCodeComplete = computed(() => {
39
+ if (isRecoveryMode.value) {
40
+ return recoveryCode.value.trim().length >= 8
41
+ }
42
+ return digits.value.every((d: string) => d.length === 1)
43
+ })
44
+
45
+ onMounted(() => {
46
+ focusFirstInput()
47
+ })
48
+
49
+ const setInputRef = (el: unknown, index: number) => {
50
+ if (el) {
51
+ inputRefs.value[index] = el as HTMLInputElement
52
+ }
53
+ }
54
+
55
+ const focusFirstInput = () => {
56
+ setTimeout(() => {
57
+ inputRefs.value[0]?.focus()
58
+ }, 150)
59
+ }
60
+
61
+ const handleDigitInput = (index: number, event: Event) => {
62
+ errorMessage.value = ''
63
+ const target = event.target as HTMLInputElement
64
+ const value = target.value.replace(/\D/g, '')
65
+
66
+ digits.value[index] = value ? value.slice(-1) : ''
67
+
68
+ // Auto-advance to next input
69
+ if (value && index < 5) {
70
+ inputRefs.value[index + 1]?.focus()
71
+ }
72
+
73
+ // Auto-verify if all 6 digits are filled
74
+ if (digits.value.every((d: string) => d.length === 1)) {
75
+ handleVerify()
76
+ }
77
+ }
78
+
79
+ const handleKeyDown = (index: number, event: KeyboardEvent) => {
80
+ if (event.key === 'Backspace' && !digits.value[index] && index > 0) {
81
+ inputRefs.value[index - 1]?.focus()
82
+ }
83
+ }
84
+
85
+ const handlePaste = (event: ClipboardEvent) => {
86
+ event.preventDefault()
87
+ const pastedData = event.clipboardData?.getData('text') || ''
88
+ const numbers = pastedData.replace(/\D/g, '').slice(0, 6)
89
+
90
+ if (numbers.length > 0) {
91
+ for (let i = 0; i < 6; i++) {
92
+ digits.value[i] = numbers[i] || ''
93
+ }
94
+ const nextIndex = Math.min(numbers.length, 5)
95
+ inputRefs.value[nextIndex]?.focus()
96
+
97
+ if (numbers.length === 6) {
98
+ handleVerify()
99
+ }
100
+ }
101
+ }
102
+
103
+ const handleVerify = () => {
104
+ if (!isCodeComplete.value || isVerifying.value) return
105
+ isVerifying.value = true
106
+ errorMessage.value = ''
107
+
108
+ setTimeout(() => {
109
+ isVerifying.value = false
110
+ const enteredCode = isRecoveryMode.value ? recoveryCode.value.trim() : digits.value.join('')
111
+
112
+ // Simulated test failure condition
113
+ if (enteredCode === '000000') {
114
+ errorMessage.value = 'Invalid verification code. Please check your authenticator app and try again.'
115
+ toast.error('Authentication failed. Invalid code.')
116
+ digits.value = ['', '', '', '', '', '']
117
+ focusFirstInput()
118
+ } else {
119
+ isVerified.value = true
120
+ toast.success('Two-factor authentication verified successfully!')
121
+ }
122
+ }, 1000)
123
+ }
124
+
125
+ const toggleRecoveryMode = () => {
126
+ isRecoveryMode.value = !isRecoveryMode.value
127
+ errorMessage.value = ''
128
+ if (!isRecoveryMode.value) {
129
+ digits.value = ['', '', '', '', '', '']
130
+ focusFirstInput()
131
+ }
132
+ }
133
+
134
+ const handleProceed = () => {
135
+ router.push('/')
136
+ }
137
+ </script>
138
+
139
+ <template>
140
+ <Card flush class="w-full max-w-md shadow-xl border-border/80 bg-card">
141
+ <!-- Success State -->
142
+ <template v-if="isVerified">
143
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-2 text-center">
144
+ <div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
145
+ <CheckCircle2 class="h-6 w-6" />
146
+ </div>
147
+ <CardTitle class="text-2xl font-bold tracking-tight">Identity verified</CardTitle>
148
+ <CardDescription class="text-sm">
149
+ Two-factor authentication verified successfully. Welcome back to your account!
150
+ </CardDescription>
151
+ </CardHeader>
152
+
153
+ <CardContent class="px-6 sm:px-8 pb-8 space-y-4">
154
+ <div class="rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-4 text-xs text-muted-foreground">
155
+ <div class="flex items-center gap-2 font-medium text-emerald-600 dark:text-emerald-400 mb-1">
156
+ <Shield class="h-4 w-4" />
157
+ Device Trusted Session
158
+ </div>
159
+ {{ trustDevice ? 'This device has been marked as trusted for 30 days.' : 'Single session verified.' }}
160
+ </div>
161
+
162
+ <Button class="w-full font-medium cursor-pointer" @click="handleProceed">
163
+ <span class="flex items-center justify-center gap-2">
164
+ Continue to Dashboard
165
+ <ArrowRight class="h-4 w-4" />
166
+ </span>
167
+ </Button>
168
+ </CardContent>
169
+ </template>
170
+
171
+ <!-- Challenge State -->
172
+ <template v-else>
173
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-1.5 text-center">
174
+ <div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
175
+ <component :is="isRecoveryMode ? KeyRound : ShieldCheck" class="h-6 w-6" />
176
+ </div>
177
+ <CardTitle class="text-2xl font-bold tracking-tight">
178
+ {{ isRecoveryMode ? 'Emergency Recovery Code' : 'Two-Factor Challenge' }}
179
+ </CardTitle>
180
+ <CardDescription class="text-sm">
181
+ <template v-if="isRecoveryMode">
182
+ Enter one of your 8-character backup codes saved during 2FA setup.
183
+ </template>
184
+ <template v-else>
185
+ Enter the 6-digit security code generated by your authenticator app for
186
+ <span class="font-medium text-foreground">{{ userEmail }}</span>.
187
+ </template>
188
+ </CardDescription>
189
+ </CardHeader>
190
+
191
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-5">
192
+ <!-- Error Alert Banner -->
193
+ <Alert v-if="errorMessage" variant="destructive" class="py-2.5 px-3.5">
194
+ <AlertCircle class="h-4 w-4" />
195
+ <AlertDescription class="text-xs font-medium ml-2">
196
+ {{ errorMessage }}
197
+ </AlertDescription>
198
+ </Alert>
199
+
200
+ <!-- Standard TOTP Mode (6 Segments) -->
201
+ <div v-if="!isRecoveryMode" class="space-y-4">
202
+ <div class="flex items-center justify-between">
203
+ <Label class="text-xs font-medium text-muted-foreground">Authenticator Code</Label>
204
+ <span class="text-[11px] text-muted-foreground flex items-center gap-1">
205
+ <Smartphone class="h-3 w-3" />
206
+ TOTP App
207
+ </span>
208
+ </div>
209
+
210
+ <div class="flex justify-between items-center gap-2">
211
+ <input
212
+ v-for="(_, index) in digits"
213
+ :key="index"
214
+ :ref="(el) => setInputRef(el, index)"
215
+ type="text"
216
+ inputmode="numeric"
217
+ pattern="[0-9]*"
218
+ maxlength="1"
219
+ :value="digits[index]"
220
+ :disabled="isVerifying"
221
+ class="h-12 w-11 sm:w-12 text-center text-xl font-bold rounded-lg border border-input bg-background focus:border-primary focus:ring-2 focus:ring-primary/20 focus:outline-none transition-all disabled:opacity-50"
222
+ @input="handleDigitInput(index, $event)"
223
+ @keydown="handleKeyDown(index, $event)"
224
+ @paste="handlePaste"
225
+ />
226
+ </div>
227
+
228
+ <!-- Trust Device Option -->
229
+ <div class="flex items-center space-x-2 pt-1">
230
+ <Checkbox id="trust-device" v-model="trustDevice" />
231
+ <label
232
+ for="trust-device"
233
+ class="text-xs font-medium leading-none cursor-pointer text-muted-foreground select-none hover:text-foreground transition-colors"
234
+ >
235
+ Don't ask for codes on this device for 30 days
236
+ </label>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Backup Recovery Code Mode -->
241
+ <div v-else class="space-y-3">
242
+ <div class="space-y-1.5">
243
+ <Label for="recovery-code" class="text-xs font-medium">Backup Recovery Code</Label>
244
+ <Input
245
+ id="recovery-code"
246
+ v-model="recoveryCode"
247
+ placeholder="e.g. NALA-2026-AUTH"
248
+ class="font-mono text-center tracking-wider uppercase h-11"
249
+ maxlength="20"
250
+ :disabled="isVerifying"
251
+ @keyup.enter="handleVerify"
252
+ />
253
+ </div>
254
+ <p class="text-[11px] text-muted-foreground text-center">
255
+ Each backup recovery code can only be used once.
256
+ </p>
257
+ </div>
258
+
259
+ <!-- Submit Button -->
260
+ <Button
261
+ class="w-full font-medium h-10 cursor-pointer"
262
+ :disabled="!isCodeComplete || isVerifying"
263
+ @click="handleVerify"
264
+ >
265
+ <Loader2 v-if="isVerifying" class="h-4 w-4 animate-spin mr-2" />
266
+ <span v-else class="flex items-center justify-center gap-1.5">
267
+ {{ isRecoveryMode ? 'Verify Recovery Code' : 'Verify & Continue' }}
268
+ <ArrowRight class="h-3.5 w-3.5" />
269
+ </span>
270
+ </Button>
271
+
272
+ <!-- Mode Switcher Trigger -->
273
+ <div class="pt-2 text-center">
274
+ <button
275
+ type="button"
276
+ class="text-xs text-primary hover:underline font-medium inline-flex items-center gap-1.5 cursor-pointer"
277
+ @click="toggleRecoveryMode"
278
+ >
279
+ <component :is="isRecoveryMode ? Smartphone : KeyRound" class="h-3.5 w-3.5" />
280
+ {{ isRecoveryMode ? 'Use 6-digit authenticator code instead' : "Lost phone? Use a backup recovery code" }}
281
+ </button>
282
+ </div>
283
+
284
+ <!-- Demo Hint Box -->
285
+ <div class="rounded-lg border border-border/60 bg-muted/30 p-3 text-[11px] text-muted-foreground space-y-1">
286
+ <div class="flex items-center gap-1.5 font-medium text-foreground">
287
+ <Info class="h-3.5 w-3.5 text-primary" />
288
+ <span>Demo Instructions</span>
289
+ </div>
290
+ <p>
291
+ Enter any 6 digits (e.g. <span class="font-mono font-semibold text-foreground">123456</span>) to succeed, or <span class="font-mono font-semibold text-destructive">000000</span> to test invalid code state.
292
+ </p>
293
+ </div>
294
+ </CardContent>
295
+
296
+ <CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
297
+ <router-link
298
+ to="/auth/login"
299
+ class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
300
+ >
301
+ <ArrowLeft class="h-3.5 w-3.5" />
302
+ Return to sign in
303
+ </router-link>
304
+ </CardFooter>
305
+ </template>
306
+ </Card>
307
+ </template>