astryx-dracula 0.1.3 → 0.2.0

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.
Files changed (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Login Split',
4
+ description: 'Two-column auth with cover art panel',
5
+ category: 'auth',
6
+ };
@@ -0,0 +1,318 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState, type CSSProperties} from 'react';
4
+ import {VStack, HStack, StackItem} from '@astryxdesign/core/Layout';
5
+ import {Grid} from '@astryxdesign/core/Grid';
6
+ import {Center} from '@astryxdesign/core/Center';
7
+ import {Card} from '@astryxdesign/core/Card';
8
+ import {Section} from '@astryxdesign/core/Section';
9
+ import {Text, Heading} from '@astryxdesign/core/Text';
10
+ import {Icon} from '@astryxdesign/core/Icon';
11
+ import {EmptyState} from '@astryxdesign/core/EmptyState';
12
+ import {TextInput} from '@astryxdesign/core/TextInput';
13
+ import {Button} from '@astryxdesign/core/Button';
14
+ import {Link} from '@astryxdesign/core/Link';
15
+ import {Divider} from '@astryxdesign/core/Divider';
16
+
17
+ // ============= ICONS (verified lucide-react exports) =============
18
+ // Moon is the castle brand mark (see login, login-card); CircleCheck ←
19
+ // CheckCircleIcon. The SquaresPlusIcon brand mark has no place here.
20
+ import {Moon, CircleCheck} from 'lucide-react';
21
+
22
+ // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
23
+ const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
24
+ <svg
25
+ viewBox="0 0 24 24"
26
+ fill="currentColor"
27
+ width={16}
28
+ height={16}
29
+ aria-hidden="true"
30
+ {...props}>
31
+ <path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
32
+ </svg>
33
+ );
34
+
35
+ const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
36
+ <svg
37
+ viewBox="0 0 24 24"
38
+ fill="currentColor"
39
+ width={16}
40
+ height={16}
41
+ aria-hidden="true"
42
+ {...props}>
43
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
44
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
45
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
46
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
47
+ </svg>
48
+ );
49
+
50
+ // Cover art is inline Dracula SVG — moonlit rooftops in brand tokens, no
51
+ // external image. Fills the panel via slice, like the placeholder before it.
52
+ const coverArt: CSSProperties = {
53
+ width: '100%',
54
+ height: '100%',
55
+ display: 'block',
56
+ };
57
+
58
+ const nightCoverArt = (
59
+ <svg
60
+ viewBox="0 0 400 300"
61
+ preserveAspectRatio="xMidYMid slice"
62
+ style={coverArt}
63
+ role="img"
64
+ aria-label="Harvest moon over the castle rooftops">
65
+ <rect width="400" height="300" fill="var(--dracula-bg)" />
66
+ <circle cx="68" cy="52" r="2" fill="var(--dracula-fg)" />
67
+ <circle cx="140" cy="36" r="1.5" fill="var(--dracula-comment)" />
68
+ <circle cx="330" cy="70" r="2" fill="var(--dracula-fg)" />
69
+ <circle cx="292" cy="34" r="1.5" fill="var(--dracula-comment)" />
70
+ <circle cx="360" cy="140" r="1.5" fill="var(--dracula-comment)" />
71
+ <circle cx="200" cy="118" r="52" fill="var(--dracula-yellow)" />
72
+ <circle cx="182" cy="104" r="44" fill="var(--dracula-bg)" />
73
+ <g
74
+ fill="var(--dracula-current-line)"
75
+ stroke="var(--dracula-comment)"
76
+ strokeWidth="3"
77
+ strokeLinejoin="round">
78
+ <rect x="20" y="220" width="110" height="80" />
79
+ <path d="M20 220 L75 178 L130 220 Z" />
80
+ <rect x="150" y="200" width="100" height="100" />
81
+ <path d="M150 200 L200 162 L250 200 Z" />
82
+ <rect x="270" y="228" width="110" height="72" />
83
+ <path d="M270 228 L325 190 L380 228 Z" />
84
+ </g>
85
+ <g fill="var(--dracula-purple)">
86
+ <rect x="58" y="244" width="14" height="18" />
87
+ <rect x="188" y="224" width="14" height="18" />
88
+ <rect x="312" y="250" width="14" height="18" />
89
+ </g>
90
+ </svg>
91
+ );
92
+
93
+ // Grid emits minmax(MIN, 1fr) where MIN is a hard floor, so MIN plus the
94
+ // grid inset and page padding must fit the narrowest phone or the column is
95
+ // clipped. 320 − 2×24 (page) − 2×16 (stacked inset) = 240.
96
+ const COLUMN_MIN_WIDTH = 240;
97
+ // repeat:'fit' (auto-fit) collapses the two columns to one — expanding to fill —
98
+ // below 2×MIN + 32(gap) = 512px. The container query reorders the image and
99
+ // tightens the inset at that same point, keyed to the card width (not the
100
+ // window) so it never desyncs.
101
+ // minHeight:100% fills the host so the centered card never leaves an unpainted
102
+ // band; padding keeps it off the surface edges.
103
+ const pageStyle: CSSProperties = {
104
+ minHeight: '100%',
105
+ backgroundColor: 'var(--color-background-body)',
106
+ padding: 'var(--spacing-6)',
107
+ };
108
+ const cardWrap: CSSProperties = {
109
+ width: '100%',
110
+ maxWidth: 1000,
111
+ marginInline: 'auto',
112
+ };
113
+
114
+ // The container query lives in a plain <style> tag so it needs NO CSS compiler.
115
+ // - Pad the grid, not the Card: the form's Section escapes Card's
116
+ // --container-padding-* vars, which would cancel the inset on the form side.
117
+ // container-type makes the grid the query container for the stack point.
118
+ // - repeat:'fit' (auto-fit) collapses the two columns to one below 511px; the
119
+ // query reorders the image (order:-1) and tightens the inset at that point,
120
+ // keyed to the card width (not the window) so it never desyncs.
121
+ const LOGIN_SPLIT_CSS = `
122
+ .login-split-grid {
123
+ container-type: inline-size;
124
+ container-name: login-split;
125
+ padding: var(--spacing-8);
126
+ }
127
+ .login-split-image {
128
+ width: 100%;
129
+ order: 0;
130
+ }
131
+ @container login-split (max-width: 511px) {
132
+ .login-split-grid {
133
+ padding: var(--spacing-4);
134
+ }
135
+ .login-split-image {
136
+ order: -1;
137
+ }
138
+ }
139
+ `;
140
+
141
+ export default function LoginSplit() {
142
+ const [email, setEmail] = useState('');
143
+ const [password, setPassword] = useState('');
144
+ const [loginFailed, setLoginFailed] = useState(false);
145
+ const [isLoading, setIsLoading] = useState(false);
146
+ const [isSuccess, setIsSuccess] = useState(false);
147
+
148
+ const handleLogin = () => {
149
+ if (!email || !password) {
150
+ setLoginFailed(true);
151
+ return;
152
+ }
153
+ setIsLoading(true);
154
+ setLoginFailed(false);
155
+ setTimeout(() => {
156
+ setIsLoading(false);
157
+ setIsSuccess(true);
158
+ }, 2000);
159
+ };
160
+
161
+ return (
162
+ <Center axis="both" style={pageStyle}>
163
+ <style>{LOGIN_SPLIT_CSS}</style>
164
+ <VStack gap={4} width="100%">
165
+ <div style={cardWrap}>
166
+ <Card padding={0} width="100%">
167
+ <Grid
168
+ columns={{minWidth: COLUMN_MIN_WIDTH, repeat: 'fit'}}
169
+ gap={8}
170
+ align="stretch"
171
+ className="login-split-grid">
172
+ {/* Form */}
173
+ <Section variant="transparent" padding={0} height="100%">
174
+ <VStack gap={4} height="100%">
175
+ <HStack gap={2} vAlign="center">
176
+ <Icon icon={Moon} color="accent" />
177
+ <Text type="body" weight="bold">
178
+ Castle Dracula
179
+ </Text>
180
+ </HStack>
181
+
182
+ <StackItem size="fill">
183
+ <Center axis="vertical" height="100%">
184
+ {isSuccess ? (
185
+ <EmptyState
186
+ title="You're in for the night"
187
+ description="Drifting to your crypt…"
188
+ icon={<Icon icon={CircleCheck} size="lg" />}
189
+ />
190
+ ) : (
191
+ <VStack gap={4} hAlign="stretch" width="100%">
192
+ <VStack gap={1}>
193
+ <Heading level={2}>
194
+ Welcome back to the night
195
+ </Heading>
196
+ <Text type="body" color="secondary">
197
+ Sign in to your crypt
198
+ </Text>
199
+ </VStack>
200
+
201
+ <VStack gap={2}>
202
+ <TextInput
203
+ label="Email"
204
+ isLabelHidden
205
+ type="email"
206
+ placeholder="you@castle.dracula"
207
+ value={email}
208
+ onChange={setEmail}
209
+ size="lg"
210
+ />
211
+ <VStack gap={1}>
212
+ <TextInput
213
+ label="Password"
214
+ isLabelHidden
215
+ placeholder="Whisper your password"
216
+ type="password"
217
+ value={password}
218
+ onChange={(v: string) => {
219
+ setPassword(v);
220
+ setLoginFailed(false);
221
+ }}
222
+ size="lg"
223
+ status={
224
+ loginFailed
225
+ ? {
226
+ type: 'error',
227
+ message:
228
+ 'Wrong incantation. Try again.',
229
+ }
230
+ : undefined
231
+ }
232
+ />
233
+ {loginFailed && (
234
+ <VStack hAlign="end">
235
+ <Link
236
+ href="#/templates/login-split"
237
+ size="sm"
238
+ color="secondary"
239
+ type="supporting">
240
+ Forgot your password?
241
+ </Link>
242
+ </VStack>
243
+ )}
244
+ </VStack>
245
+ </VStack>
246
+
247
+ <Button
248
+ label="Enter the night"
249
+ variant="primary"
250
+ size="lg"
251
+ isLoading={isLoading}
252
+ onClick={handleLogin}
253
+ />
254
+
255
+ <Divider label="Or continue with" />
256
+
257
+ <Grid columns={2} gap={3} justify="stretch">
258
+ <Button
259
+ label="Login with Apple"
260
+ variant="secondary"
261
+ icon={<AppleIcon />}
262
+ size="lg"
263
+ />
264
+ <Button
265
+ label="Login with Google"
266
+ variant="secondary"
267
+ icon={<GoogleIcon />}
268
+ size="lg"
269
+ />
270
+ </Grid>
271
+ </VStack>
272
+ )}
273
+ </Center>
274
+ </StackItem>
275
+
276
+ {!isSuccess && (
277
+ <Text type="supporting" color="secondary">
278
+ New to the castle?{' '}
279
+ <Link href="#/templates/login-split" type="supporting">
280
+ Sign up
281
+ </Link>
282
+ </Text>
283
+ )}
284
+ </VStack>
285
+ </Section>
286
+
287
+ {/* Cover art — the transparent Card clips it to rounded
288
+ corners (overflow:clip + radius), so the art needs no radius. */}
289
+ <div className="login-split-image">
290
+ <Card
291
+ variant="transparent"
292
+ padding={0}
293
+ width="100%"
294
+ height="100%">
295
+ {nightCoverArt}
296
+ </Card>
297
+ </div>
298
+ </Grid>
299
+ </Card>
300
+ </div>
301
+
302
+ <VStack hAlign="center">
303
+ <Text type="supporting" color="secondary">
304
+ By clicking continue, you agree to our{' '}
305
+ <Link href="#/templates/login-split" type="supporting">
306
+ Terms of Service
307
+ </Link>{' '}
308
+ and{' '}
309
+ <Link href="#/templates/login-split" type="supporting">
310
+ Privacy Policy
311
+ </Link>
312
+ .
313
+ </Text>
314
+ </VStack>
315
+ </VStack>
316
+ </Center>
317
+ );
318
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Login SSO',
4
+ description: 'Email-first SSO with password fallback',
5
+ category: 'auth',
6
+ };
@@ -0,0 +1,277 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState, type CSSProperties} from 'react';
4
+ import {VStack, HStack} from '@astryxdesign/core/Layout';
5
+ import {Center} from '@astryxdesign/core/Center';
6
+ import {Text, Heading} from '@astryxdesign/core/Text';
7
+ import {TextInput} from '@astryxdesign/core/TextInput';
8
+ import {Button} from '@astryxdesign/core/Button';
9
+ import {Card} from '@astryxdesign/core/Card';
10
+ import {Section} from '@astryxdesign/core/Section';
11
+ import {Link} from '@astryxdesign/core/Link';
12
+ import {Divider} from '@astryxdesign/core/Divider';
13
+ import {Icon} from '@astryxdesign/core/Icon';
14
+ import {Avatar} from '@astryxdesign/core/Avatar';
15
+
16
+ // ============= ICONS (verified lucide-react exports) =============
17
+ // ShieldCheck ← ShieldCheckIcon.
18
+ import {ShieldCheck} from 'lucide-react';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Styles
22
+ // ---------------------------------------------------------------------------
23
+
24
+ // Standalone auth page paints its own body background (no host shell).
25
+ const pageStyle: CSSProperties = {
26
+ minHeight: '100%',
27
+ backgroundColor: 'var(--color-background-body)',
28
+ padding: 'var(--spacing-6)',
29
+ };
30
+
31
+ type SSOProvider = {
32
+ name: string;
33
+ abbr: string;
34
+ };
35
+ const SSO_PROVIDERS: Record<string, SSOProvider> = {
36
+ 'google.com': {name: 'Google Workspace', abbr: 'G'},
37
+ 'microsoft.com': {name: 'Microsoft Entra ID', abbr: 'M'},
38
+ 'okta.com': {name: 'Okta', abbr: 'O'},
39
+ 'meta.com': {name: 'Meta SSO', abbr: 'M'},
40
+ 'apple.com': {name: 'Apple Business', abbr: 'A'},
41
+ };
42
+
43
+ function getProvider(email: string) {
44
+ const domain = email.split('@')[1]?.toLowerCase();
45
+ return domain ? (SSO_PROVIDERS[domain] ?? null) : null;
46
+ }
47
+
48
+ function isValidEmail(email: string) {
49
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Component
54
+ // ---------------------------------------------------------------------------
55
+
56
+ type Step = 'email' | 'sso-confirm' | 'password-fallback';
57
+
58
+ export default function LoginSso() {
59
+ const [step, setStep] = useState<Step>('email');
60
+ const [email, setEmail] = useState('');
61
+ const [password, setPassword] = useState('');
62
+ const [loginFailed, setLoginFailed] = useState(false);
63
+ const [isLoading, setIsLoading] = useState(false);
64
+
65
+ const provider = getProvider(email);
66
+ const emailValid = isValidEmail(email);
67
+
68
+ const handleContinue = () => {
69
+ if (!emailValid) {
70
+ return;
71
+ }
72
+ if (provider) {
73
+ setStep('sso-confirm');
74
+ } else {
75
+ setStep('password-fallback');
76
+ }
77
+ };
78
+
79
+ const handleBack = () => {
80
+ setStep('email');
81
+ setLoginFailed(false);
82
+ setIsLoading(false);
83
+ };
84
+
85
+ const handleSignIn = () => {
86
+ if (!password) {
87
+ setLoginFailed(true);
88
+ return;
89
+ }
90
+ setIsLoading(true);
91
+ setLoginFailed(false);
92
+ setTimeout(() => {
93
+ setIsLoading(false);
94
+ setLoginFailed(true);
95
+ }, 2000);
96
+ };
97
+
98
+ return (
99
+ <Center axis="both" style={pageStyle}>
100
+ <Card padding={8} width="100%" maxWidth={400}>
101
+ <VStack gap={4} hAlign="stretch">
102
+ {/* ── Step 1: Email entry ── */}
103
+ {step === 'email' && (
104
+ <>
105
+ <VStack gap={1} hAlign="center">
106
+ <Heading level={2}>Welcome back to the night</Heading>
107
+ <Text type="body" color="secondary">
108
+ Whisper your details to enter the night
109
+ </Text>
110
+ </VStack>
111
+
112
+ <VStack gap={2}>
113
+ <TextInput
114
+ label="Work email"
115
+ isLabelHidden
116
+ type="email"
117
+ placeholder="you@castle.dracula"
118
+ value={email}
119
+ onChange={setEmail}
120
+ size="lg"
121
+ onKeyDown={(e: React.KeyboardEvent) => {
122
+ if (e.key === 'Enter') {
123
+ handleContinue();
124
+ }
125
+ }}
126
+ />
127
+ <TextInput
128
+ label="Password"
129
+ isLabelHidden
130
+ type="password"
131
+ placeholder="Whisper your password"
132
+ value={password}
133
+ onChange={setPassword}
134
+ size="lg"
135
+ />
136
+ </VStack>
137
+
138
+ <Link href="#/templates/login-sso" size="sm" color="secondary" type="supporting">
139
+ Having trouble signing in?
140
+ </Link>
141
+
142
+ <Button
143
+ label="Enter the night"
144
+ variant="primary"
145
+ size="lg"
146
+ onClick={handleContinue}
147
+ isDisabled={!emailValid}
148
+ />
149
+
150
+ <Divider label="Or sign in with" />
151
+
152
+ <Button
153
+ label="Continue with SSO"
154
+ variant="secondary"
155
+ size="lg"
156
+ onClick={handleContinue}
157
+ isDisabled={!emailValid}
158
+ />
159
+
160
+ <VStack hAlign="center">
161
+ <Text type="supporting" color="secondary">
162
+ New to the castle?{' '}
163
+ <Link href="#/templates/login-sso" type="supporting">
164
+ Request access
165
+ </Link>
166
+ </Text>
167
+ </VStack>
168
+ </>
169
+ )}
170
+
171
+ {/* ── Step 2a: SSO provider detected ── */}
172
+ {step === 'sso-confirm' && provider && (
173
+ <>
174
+ <VStack gap={2} hAlign="center">
175
+ <Avatar name={provider.name} size={48} />
176
+ <Heading level={2}>Sign in with {provider.name}</Heading>
177
+ <Text type="body" color="secondary">
178
+ You will be redirected back after signing in.
179
+ </Text>
180
+ </VStack>
181
+
182
+ <Card padding={0}>
183
+ <Section variant="muted" padding={4}>
184
+ <HStack gap={2} vAlign="center">
185
+ <Icon icon={ShieldCheck} color="secondary" />
186
+ <VStack gap={0}>
187
+ <Text type="label">{provider.name}</Text>
188
+ <Text type="supporting" color="secondary">
189
+ {email}
190
+ </Text>
191
+ </VStack>
192
+ </HStack>
193
+ </Section>
194
+ </Card>
195
+
196
+ <VStack gap={3}>
197
+ <Button
198
+ label={`Continue with ${provider.name}`}
199
+ variant="primary"
200
+ size="lg"
201
+ isLoading={isLoading}
202
+ onClick={() => setIsLoading(true)}
203
+ />
204
+ <Button
205
+ label="Use a different email"
206
+ variant="ghost"
207
+ size="lg"
208
+ onClick={handleBack}
209
+ />
210
+ </VStack>
211
+ </>
212
+ )}
213
+
214
+ {/* ── Step 2b: No SSO — password fallback ── */}
215
+ {step === 'password-fallback' && (
216
+ <>
217
+ <VStack gap={1} hAlign="center">
218
+ <Heading level={2}>Welcome back to the night</Heading>
219
+ <Text type="body" color="secondary">
220
+ {email}
221
+ </Text>
222
+ </VStack>
223
+
224
+ <VStack gap={4}>
225
+ <VStack gap={1}>
226
+ <TextInput
227
+ label="Password"
228
+ type="password"
229
+ value={password}
230
+ size="lg"
231
+ onChange={(v: string) => {
232
+ setPassword(v);
233
+ setLoginFailed(false);
234
+ }}
235
+ status={
236
+ loginFailed
237
+ ? {
238
+ type: 'error',
239
+ message: 'Wrong incantation. Try again.',
240
+ }
241
+ : undefined
242
+ }
243
+ />
244
+ {loginFailed && (
245
+ <VStack hAlign="end">
246
+ <Link
247
+ href="#/templates/login-sso"
248
+ size="sm"
249
+ color="secondary"
250
+ type="supporting">
251
+ Forgot password?
252
+ </Link>
253
+ </VStack>
254
+ )}
255
+ </VStack>
256
+
257
+ <Button
258
+ label="Enter the night"
259
+ variant="primary"
260
+ size="lg"
261
+ isLoading={isLoading}
262
+ onClick={handleSignIn}
263
+ />
264
+ <Button
265
+ label="Use a different email"
266
+ variant="ghost"
267
+ size="lg"
268
+ onClick={handleBack}
269
+ />
270
+ </VStack>
271
+ </>
272
+ )}
273
+ </VStack>
274
+ </Card>
275
+ </Center>
276
+ );
277
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Basic Login',
4
+ description: 'Auth form with email and password inputs',
5
+ category: 'auth',
6
+ };