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,216 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onUnmounted } from 'vue'
3
+ import { RouterView } from 'vue-router'
4
+ import { useColorMode } from '@vueuse/core'
5
+ import {
6
+ Sun,
7
+ Moon,
8
+ Sparkles,
9
+ ShieldCheck,
10
+ Zap,
11
+ } from '@lucide/vue'
12
+ import AppLogo from '@/components/AppLogo.vue'
13
+
14
+ const colorMode = useColorMode()
15
+
16
+ const toggleColorMode = () => {
17
+ colorMode.value = colorMode.value === 'dark' ? 'light' : 'dark'
18
+ }
19
+
20
+ // Testimonial Carousel
21
+ interface Testimonial {
22
+ quote: string
23
+ author: string
24
+ role: string
25
+ company: string
26
+ avatar: string
27
+ }
28
+
29
+ const testimonials: Testimonial[] = [
30
+ {
31
+ quote: 'Nala reduced our frontend delivery cycles by over 70%. The architecture, real API adapters, and UI primitives are exceptionally well-crafted.',
32
+ author: 'Sarah Jenkins',
33
+ role: 'VP of Product',
34
+ company: 'Horizon SaaS',
35
+ avatar: 'SJ',
36
+ },
37
+ {
38
+ quote: 'The strict TypeScript hygiene, Tailwind v4 design tokens, and zero-boilerplate auto-imports make pairing with AI agents effortless.',
39
+ author: 'David Chen',
40
+ role: 'Lead Architect',
41
+ company: 'CloudScale Technologies',
42
+ avatar: 'DC',
43
+ },
44
+ {
45
+ quote: 'By far the cleanest, fastest, and most comprehensive Vue 3 enterprise admin dashboard available. Exceptional attention to detail.',
46
+ author: 'Elena Rostova',
47
+ role: 'Chief Technology Officer',
48
+ company: 'Synapse AI',
49
+ avatar: 'ER',
50
+ },
51
+ ]
52
+
53
+ const activeIndex = ref(0)
54
+ let carouselTimer: ReturnType<typeof setInterval> | null = null
55
+
56
+ onMounted(() => {
57
+ carouselTimer = setInterval(() => {
58
+ activeIndex.value = (activeIndex.value + 1) % testimonials.length
59
+ }, 6000)
60
+ })
61
+
62
+ onUnmounted(() => {
63
+ if (carouselTimer) clearInterval(carouselTimer)
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <div class="min-h-dvh lg:h-screen w-full flex flex-col lg:grid lg:grid-cols-12 bg-background text-foreground transition-colors duration-200 lg:overflow-hidden">
69
+ <!-- Left Column: Authentication Form Container (5 Cols / 7 Cols responsive) -->
70
+ <div class="flex-1 flex flex-col justify-between p-6 sm:p-8 lg:p-8 xl:p-10 lg:col-span-6 xl:col-span-5 relative overflow-y-auto lg:h-full">
71
+ <!-- Top Brand Navigation Header -->
72
+ <header class="flex items-center justify-between w-full shrink-0">
73
+ <router-link to="/" class="inline-flex items-center gap-2.5 group focus:outline-none">
74
+ <AppLogo size="md" :withText="true" />
75
+ </router-link>
76
+
77
+ <div class="flex items-center gap-2">
78
+ <!-- Theme Toggle -->
79
+ <button
80
+ type="button"
81
+ class="h-8 w-8 rounded-lg border border-border/80 flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors cursor-pointer"
82
+ title="Toggle theme"
83
+ @click="toggleColorMode"
84
+ >
85
+ <Sun v-if="colorMode === 'dark'" class="h-4 w-4" />
86
+ <Moon v-else class="h-4 w-4" />
87
+ </button>
88
+ </div>
89
+ </header>
90
+
91
+ <!-- Center Auth Content Area -->
92
+ <main class="my-auto py-6 w-full max-w-md mx-auto flex flex-col justify-center">
93
+ <RouterView v-slot="{ Component }">
94
+ <Transition name="page" mode="out-in">
95
+ <component :is="Component" />
96
+ </Transition>
97
+ </RouterView>
98
+ </main>
99
+
100
+ <!-- Bottom Footer -->
101
+ <footer class="pt-4 flex flex-col sm:flex-row items-center justify-between text-xs text-muted-foreground gap-2 border-t border-border/40 shrink-0">
102
+ <p>&copy; {{ new Date().getFullYear() }} Nala Dashboard. All rights reserved.</p>
103
+ <div class="flex items-center gap-4">
104
+ <a href="#" class="hover:text-foreground transition-colors">Privacy Policy</a>
105
+ <a href="#" class="hover:text-foreground transition-colors">Terms of Service</a>
106
+ <a href="#" class="hover:text-foreground transition-colors">Security</a>
107
+ </div>
108
+ </footer>
109
+ </div>
110
+
111
+ <!-- Right Column: Hero Side-Banner Panel (7 Cols / 8 Cols on desktop) -->
112
+ <aside class="hidden lg:flex lg:col-span-6 xl:col-span-7 relative flex-col justify-between p-8 xl:p-10 overflow-hidden bg-muted/30 border-l border-border/60 lg:h-full">
113
+ <!-- Ambient Gradient Backdrops -->
114
+ <div class="absolute inset-0 bg-linear-to-br from-primary/10 via-primary/5 to-transparent pointer-events-none" />
115
+ <div class="absolute -top-24 -right-24 h-96 w-96 rounded-full bg-primary/15 blur-3xl pointer-events-none" />
116
+ <div class="absolute -bottom-24 -left-24 h-96 w-96 rounded-full bg-primary/10 blur-3xl pointer-events-none" />
117
+
118
+ <!-- Top Badge / Header info -->
119
+ <div class="relative z-10 flex items-center justify-between shrink-0">
120
+ <div class="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-xs font-medium text-primary backdrop-blur-xs">
121
+ <Sparkles class="h-3.5 w-3.5" />
122
+ Enterprise Admin Platform
123
+ </div>
124
+
125
+ <div class="flex items-center gap-4 text-xs text-muted-foreground">
126
+ <span class="inline-flex items-center gap-1.5">
127
+ <span class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
128
+ v3.1.1 Production Ready
129
+ </span>
130
+ </div>
131
+ </div>
132
+
133
+ <!-- Center Hero Graphic & Value Proposition -->
134
+ <div class="relative z-10 my-auto py-4 xl:py-6 space-y-5 xl:space-y-6 max-w-xl">
135
+ <div class="space-y-2">
136
+ <h2 class="text-2xl xl:text-3xl font-bold tracking-tight text-foreground">
137
+ The Enterprise Architecture for High-Velocity Teams.
138
+ </h2>
139
+ <p class="text-xs xl:text-sm text-muted-foreground leading-relaxed">
140
+ Built with Vue 3 Composition API, Tailwind CSS v4 OKLCH tokens, Reka UI primitives, and real API adapters with safe offline fallbacks.
141
+ </p>
142
+ </div>
143
+
144
+ <!-- Key Capability Badges -->
145
+ <div class="grid grid-cols-2 gap-3 pt-1">
146
+ <div class="flex items-start gap-2.5 rounded-xl border border-border/70 bg-card/60 p-3 backdrop-blur-xs shadow-2xs">
147
+ <ShieldCheck class="h-4 w-4 text-primary mt-0.5 shrink-0" />
148
+ <div>
149
+ <h4 class="text-xs font-semibold text-foreground">Enterprise Security</h4>
150
+ <p class="text-[11px] text-muted-foreground mt-0.5">2FA TOTP & SAML Single Sign-On</p>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="flex items-start gap-2.5 rounded-xl border border-border/70 bg-card/60 p-3 backdrop-blur-xs shadow-2xs">
155
+ <Zap class="h-4 w-4 text-amber-500 mt-0.5 shrink-0" />
156
+ <div>
157
+ <h4 class="text-xs font-semibold text-foreground">Sub-Second HMR</h4>
158
+ <p class="text-[11px] text-muted-foreground mt-0.5">Vite 8 bundler with zero-import</p>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- Dynamic Testimonial Card with Transition -->
164
+ <div class="rounded-2xl border border-border/80 bg-card/80 p-5 backdrop-blur-md shadow-xs relative overflow-hidden transition-all duration-300">
165
+ <div class="space-y-3">
166
+ <p class="text-xs xl:text-sm text-foreground/90 italic leading-relaxed min-h-12 xl:min-h-14">
167
+ "{{ testimonials[activeIndex].quote }}"
168
+ </p>
169
+
170
+ <div class="flex items-center justify-between pt-2 border-t border-border/50">
171
+ <div class="flex items-center gap-3">
172
+ <div class="h-8 w-8 rounded-full bg-primary/15 text-primary font-semibold text-xs flex items-center justify-center border border-primary/20 shrink-0">
173
+ {{ testimonials[activeIndex].avatar }}
174
+ </div>
175
+ <div>
176
+ <div class="text-xs font-semibold text-foreground">{{ testimonials[activeIndex].author }}</div>
177
+ <div class="text-[11px] text-muted-foreground">{{ testimonials[activeIndex].role }} &middot; {{ testimonials[activeIndex].company }}</div>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Carousel Dots -->
182
+ <div class="flex items-center gap-1.5">
183
+ <button
184
+ v-for="(_, index) in testimonials"
185
+ :key="index"
186
+ type="button"
187
+ :class="[
188
+ 'h-1.5 rounded-full transition-all cursor-pointer',
189
+ activeIndex === index ? 'w-5 bg-primary' : 'w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/50'
190
+ ]"
191
+ @click="activeIndex = index"
192
+ />
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Bottom Platform Metrics Strip -->
200
+ <div class="relative z-10 grid grid-cols-3 gap-4 pt-4 border-t border-border/50 shrink-0">
201
+ <div>
202
+ <div class="text-xl xl:text-2xl font-bold tracking-tight text-foreground">46+</div>
203
+ <div class="text-xs text-muted-foreground mt-0.5">UI Primitives</div>
204
+ </div>
205
+ <div>
206
+ <div class="text-xl xl:text-2xl font-bold tracking-tight text-foreground">99.9%</div>
207
+ <div class="text-xs text-muted-foreground mt-0.5">SLA Availability</div>
208
+ </div>
209
+ <div>
210
+ <div class="text-xl xl:text-2xl font-bold tracking-tight text-foreground">&lt;100ms</div>
211
+ <div class="text-xs text-muted-foreground mt-0.5">API Latency</div>
212
+ </div>
213
+ </div>
214
+ </aside>
215
+ </div>
216
+ </template>
@@ -115,6 +115,106 @@ const router = createRouter({
115
115
  name: 'lock-screen',
116
116
  component: () => import('@/views/auth/LockScreenView.vue'),
117
117
  },
118
+ {
119
+ path: '2fa',
120
+ name: 'two-factor',
121
+ component: () => import('@/views/auth/TwoFactorChallengeView.vue'),
122
+ },
123
+ {
124
+ path: 'two-factor',
125
+ redirect: '/auth/2fa',
126
+ },
127
+ {
128
+ path: 'sso',
129
+ name: 'sso-login',
130
+ component: () => import('@/views/auth/SsoLoginView.vue'),
131
+ },
132
+ {
133
+ path: 'magic-link',
134
+ name: 'magic-link',
135
+ component: () => import('@/views/auth/MagicLinkView.vue'),
136
+ },
137
+ {
138
+ path: 'select-tenant',
139
+ name: 'select-tenant',
140
+ component: () => import('@/views/auth/SelectTenantView.vue'),
141
+ },
142
+ {
143
+ path: 'select-workspace',
144
+ redirect: '/auth/select-tenant',
145
+ },
146
+ {
147
+ path: 'suspended',
148
+ name: 'suspended',
149
+ component: () => import('@/views/auth/SuspendedView.vue'),
150
+ },
151
+ {
152
+ path: 'deactivated',
153
+ redirect: '/auth/suspended',
154
+ },
155
+ {
156
+ path: 'frozen',
157
+ redirect: '/auth/suspended',
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ path: '/auth-split',
163
+ component: () => import('@/layouts/AuthSplitLayout.vue'),
164
+ children: [
165
+ {
166
+ path: '',
167
+ redirect: '/auth-split/login',
168
+ },
169
+ {
170
+ path: 'login',
171
+ name: 'split-login',
172
+ component: () => import('@/views/auth/LoginView.vue'),
173
+ },
174
+ {
175
+ path: 'register',
176
+ name: 'split-register',
177
+ component: () => import('@/views/auth/RegisterView.vue'),
178
+ },
179
+ {
180
+ path: '2fa',
181
+ name: 'split-2fa',
182
+ component: () => import('@/views/auth/TwoFactorChallengeView.vue'),
183
+ },
184
+ {
185
+ path: 'sso',
186
+ name: 'split-sso',
187
+ component: () => import('@/views/auth/SsoLoginView.vue'),
188
+ },
189
+ {
190
+ path: 'magic-link',
191
+ name: 'split-magic-link',
192
+ component: () => import('@/views/auth/MagicLinkView.vue'),
193
+ },
194
+ {
195
+ path: 'select-tenant',
196
+ name: 'split-select-tenant',
197
+ component: () => import('@/views/auth/SelectTenantView.vue'),
198
+ },
199
+ {
200
+ path: 'select-workspace',
201
+ redirect: '/auth-split/select-tenant',
202
+ },
203
+ {
204
+ path: 'forgot-password',
205
+ name: 'split-forgot-password',
206
+ component: () => import('@/views/auth/ForgotPasswordView.vue'),
207
+ },
208
+ {
209
+ path: 'reset-password',
210
+ name: 'split-reset-password',
211
+ component: () => import('@/views/auth/ResetPasswordView.vue'),
212
+ },
213
+ {
214
+ path: 'suspended',
215
+ name: 'split-suspended',
216
+ component: () => import('@/views/auth/SuspendedView.vue'),
217
+ },
118
218
  ],
119
219
  },
120
220
  {
@@ -1,6 +1,4 @@
1
1
 
2
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
3
-
4
2
  @font-face {
5
3
  font-family: 'Inter';
6
4
  src: url('./assets/fonts/InterVariable.woff2') format('woff2-variations');
@@ -109,7 +109,7 @@ function fillDemoPassword() {
109
109
  </CardDescription>
110
110
  </CardHeader>
111
111
 
112
- <CardContent class="space-y-4">
112
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-4">
113
113
  <form @submit.prevent="handleUnlock" class="space-y-4">
114
114
  <div class="space-y-2">
115
115
  <div class="flex items-center justify-between">
@@ -0,0 +1,278 @@
1
+ <script setup lang="ts">
2
+ import { ref, onUnmounted } 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 { Alert, AlertDescription } from '@/components/ui/alert'
9
+ import { toast } from '@/components/ui/sonner'
10
+ import {
11
+ Sparkles,
12
+ Mail,
13
+ ArrowRight,
14
+ ArrowLeft,
15
+ Loader2,
16
+ CheckCircle2,
17
+ ExternalLink,
18
+ RotateCw,
19
+ AlertCircle,
20
+ Clock,
21
+ Shield,
22
+ } from '@lucide/vue'
23
+
24
+ const router = useRouter()
25
+
26
+ const email = ref('')
27
+ const isSubmitted = ref(false)
28
+ const isSending = ref(false)
29
+ const isResending = ref(false)
30
+ const errorMessage = ref('')
31
+ const countdown = ref(60)
32
+ let timer: ReturnType<typeof setInterval> | null = null
33
+
34
+ const startCountdown = () => {
35
+ countdown.value = 60
36
+ if (timer) clearInterval(timer)
37
+ timer = setInterval(() => {
38
+ if (countdown.value > 0) {
39
+ countdown.value--
40
+ } else {
41
+ if (timer) clearInterval(timer)
42
+ }
43
+ }, 1000)
44
+ }
45
+
46
+ onUnmounted(() => {
47
+ if (timer) clearInterval(timer)
48
+ })
49
+
50
+ const handleSendMagicLink = () => {
51
+ const trimmed = email.value.trim()
52
+ if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
53
+ errorMessage.value = 'Please enter a valid email address.'
54
+ return
55
+ }
56
+
57
+ isSending.value = true
58
+ errorMessage.value = ''
59
+
60
+ setTimeout(() => {
61
+ isSending.value = false
62
+ isSubmitted.value = true
63
+ startCountdown()
64
+ toast.success('Magic link has been dispatched to your inbox!')
65
+ }, 1000)
66
+ }
67
+
68
+ const handleResend = () => {
69
+ if (countdown.value > 0 || isResending.value) return
70
+ isResending.value = true
71
+ errorMessage.value = ''
72
+
73
+ setTimeout(() => {
74
+ isResending.value = false
75
+ startCountdown()
76
+ toast.info('A fresh magic link has been sent!')
77
+ }, 900)
78
+ }
79
+
80
+ const openEmailClient = (provider: 'gmail' | 'outlook') => {
81
+ if (provider === 'gmail') {
82
+ window.open('https://mail.google.com', '_blank', 'noopener,noreferrer')
83
+ } else {
84
+ window.open('https://outlook.live.com', '_blank', 'noopener,noreferrer')
85
+ }
86
+ }
87
+
88
+ const simulateInstantLogin = () => {
89
+ toast.success('Simulated magic link clicked from email client!')
90
+ router.push('/')
91
+ }
92
+ </script>
93
+
94
+ <template>
95
+ <Card flush class="w-full max-w-md shadow-xl border-border/80 bg-card">
96
+ <!-- State 2: Magic Link Dispatched (Awaiting Email Click) -->
97
+ <template v-if="isSubmitted">
98
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-2 text-center">
99
+ <div class="relative mx-auto mb-2 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
100
+ <Mail class="h-7 w-7 animate-pulse" />
101
+ <div class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500 text-white shadow-xs">
102
+ <CheckCircle2 class="h-3.5 w-3.5" />
103
+ </div>
104
+ </div>
105
+ <CardTitle class="text-2xl font-bold tracking-tight">Check your email</CardTitle>
106
+ <CardDescription class="text-sm">
107
+ We've sent a passwordless sign-in link to
108
+ <span class="font-semibold text-foreground">{{ email }}</span>.
109
+ </CardDescription>
110
+ </CardHeader>
111
+
112
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-4">
113
+ <!-- Explanatory note -->
114
+ <div class="rounded-lg border border-border/70 bg-muted/20 p-3.5 text-xs text-muted-foreground space-y-1.5">
115
+ <div class="flex items-center gap-1.5 font-medium text-foreground">
116
+ <Shield class="h-3.5 w-3.5 text-primary" />
117
+ <span>Secure Passwordless Authentication</span>
118
+ </div>
119
+ <p>
120
+ Click the button inside the email to sign in instantly. The link expires in 15 minutes and can only be used once.
121
+ </p>
122
+ </div>
123
+
124
+ <!-- Direct Email Providers Launcher -->
125
+ <div class="space-y-2 pt-1">
126
+ <span class="text-[11px] font-medium text-muted-foreground">Quick open mailbox:</span>
127
+ <div class="grid grid-cols-2 gap-2">
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ size="sm"
132
+ class="h-9 text-xs justify-center cursor-pointer"
133
+ @click="openEmailClient('gmail')"
134
+ >
135
+ Open Gmail
136
+ <ExternalLink class="h-3.5 w-3.5 ml-1.5 opacity-70" />
137
+ </Button>
138
+ <Button
139
+ type="button"
140
+ variant="outline"
141
+ size="sm"
142
+ class="h-9 text-xs justify-center cursor-pointer"
143
+ @click="openEmailClient('outlook')"
144
+ >
145
+ Open Outlook
146
+ <ExternalLink class="h-3.5 w-3.5 ml-1.5 opacity-70" />
147
+ </Button>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Simulated One-Click Callback Demo -->
152
+ <div class="rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs space-y-2">
153
+ <div class="flex items-center justify-between text-[11px] text-primary font-medium">
154
+ <span>Developer Sandbox Demo</span>
155
+ <Sparkles class="h-3.5 w-3.5" />
156
+ </div>
157
+ <p class="text-[11px] text-muted-foreground">
158
+ Testing locally without access to an actual mail inbox? Simulate clicking the magic link callback below:
159
+ </p>
160
+ <Button
161
+ type="button"
162
+ class="w-full h-8 text-xs font-medium cursor-pointer"
163
+ @click="simulateInstantLogin"
164
+ >
165
+ Simulate Email Link Click
166
+ <ArrowRight class="h-3.5 w-3.5 ml-1.5" />
167
+ </Button>
168
+ </div>
169
+
170
+ <!-- Resend Countdown -->
171
+ <div class="pt-2 text-center">
172
+ <p class="text-xs text-muted-foreground">
173
+ Didn't receive the email?
174
+ <button
175
+ v-if="countdown === 0"
176
+ type="button"
177
+ class="text-primary hover:underline font-medium ml-1 inline-flex items-center gap-1 cursor-pointer"
178
+ :disabled="isResending"
179
+ @click="handleResend"
180
+ >
181
+ <RotateCw v-if="isResending" class="h-3 w-3 animate-spin" />
182
+ Resend magic link
183
+ </button>
184
+ <span v-else class="text-foreground/70 font-mono ml-1 inline-flex items-center gap-1">
185
+ <Clock class="h-3 w-3 text-muted-foreground" />
186
+ Resend in {{ countdown }}s
187
+ </span>
188
+ </p>
189
+ </div>
190
+ </CardContent>
191
+
192
+ <CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
193
+ <button
194
+ type="button"
195
+ class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
196
+ @click="isSubmitted = false"
197
+ >
198
+ <ArrowLeft class="h-3.5 w-3.5" />
199
+ Use a different email address
200
+ </button>
201
+ </CardFooter>
202
+ </template>
203
+
204
+ <!-- State 1: Magic Link Request Form -->
205
+ <template v-else>
206
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-1.5 text-center">
207
+ <div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
208
+ <Sparkles class="h-6 w-6" />
209
+ </div>
210
+ <CardTitle class="text-2xl font-bold tracking-tight">Passwordless Sign In</CardTitle>
211
+ <CardDescription class="text-sm">
212
+ Enter your email address and we'll send you a secure magic link to sign in instantly without a password.
213
+ </CardDescription>
214
+ </CardHeader>
215
+
216
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-4">
217
+ <!-- Error Alert Banner -->
218
+ <Alert v-if="errorMessage" variant="destructive" class="py-2.5 px-3.5">
219
+ <AlertCircle class="h-4 w-4" />
220
+ <AlertDescription class="text-xs font-medium ml-2">
221
+ {{ errorMessage }}
222
+ </AlertDescription>
223
+ </Alert>
224
+
225
+ <form @submit.prevent="handleSendMagicLink" class="space-y-4">
226
+ <div class="space-y-1.5">
227
+ <Label for="magic-email" class="text-xs font-medium">Email address</Label>
228
+ <div class="relative flex items-center">
229
+ <Mail class="absolute left-3 h-4 w-4 text-muted-foreground pointer-events-none" />
230
+ <Input
231
+ id="magic-email"
232
+ v-model="email"
233
+ type="email"
234
+ placeholder="name@example.com"
235
+ class="pl-9 h-10 text-sm"
236
+ :disabled="isSending"
237
+ autofocus
238
+ />
239
+ </div>
240
+ </div>
241
+
242
+ <Button
243
+ type="submit"
244
+ class="w-full font-medium h-10 cursor-pointer"
245
+ :disabled="!email.trim() || isSending"
246
+ >
247
+ <Loader2 v-if="isSending" class="h-4 w-4 animate-spin mr-2" />
248
+ <span v-else class="flex items-center justify-center gap-1.5">
249
+ Send Magic Link
250
+ <ArrowRight class="h-3.5 w-3.5" />
251
+ </span>
252
+ </Button>
253
+ </form>
254
+
255
+ <!-- Demo auto-fill hint -->
256
+ <div class="pt-1 text-center">
257
+ <button
258
+ type="button"
259
+ class="text-[11px] text-muted-foreground hover:text-primary transition-colors cursor-pointer"
260
+ @click="email = 'alex.morgan@example.com'"
261
+ >
262
+ Click to fill demo: <span class="font-mono text-foreground">alex.morgan@example.com</span>
263
+ </button>
264
+ </div>
265
+ </CardContent>
266
+
267
+ <CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
268
+ <router-link
269
+ to="/auth/login"
270
+ class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
271
+ >
272
+ <ArrowLeft class="h-3.5 w-3.5" />
273
+ Return to password sign in
274
+ </router-link>
275
+ </CardFooter>
276
+ </template>
277
+ </Card>
278
+ </template>