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: 'Card Grid',
4
+ description: 'Browsable grid with tabs and filters',
5
+ category: 'marketing',
6
+ };
@@ -0,0 +1,489 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState, useMemo, type CSSProperties} from 'react';
4
+ import {Layout, LayoutHeader, LayoutContent} from '@astryxdesign/core/Layout';
5
+ import {Text, Heading} from '@astryxdesign/core/Text';
6
+ import {Card} from '@astryxdesign/core/Card';
7
+ import {ToggleButton, ToggleButtonGroup} from '@astryxdesign/core/ToggleButton';
8
+ import {TextInput} from '@astryxdesign/core/TextInput';
9
+ import {Divider} from '@astryxdesign/core/Divider';
10
+ import {Section} from '@astryxdesign/core/Section';
11
+ import {Grid} from '@astryxdesign/core/Grid';
12
+ import {HStack, VStack, StackItem} from '@astryxdesign/core/Stack';
13
+ import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
14
+ import {OverflowList} from '@astryxdesign/core/OverflowList';
15
+ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
16
+ import {Button} from '@astryxdesign/core/Button';
17
+ import {EmptyState} from '@astryxdesign/core/EmptyState';
18
+ import {Icon} from '@astryxdesign/core/Icon';
19
+ import {Search} from 'lucide-react';
20
+
21
+ interface LibraryItem {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ category: string;
26
+ type: 'Component' | 'Pattern' | 'Utility';
27
+ }
28
+
29
+ // Dracula accent per category shelf, drawn from the fixed badge vocabulary.
30
+ const CATEGORY_HUES: Record<string, string> = {
31
+ Layout: 'var(--dracula-purple)',
32
+ Forms: 'var(--dracula-cyan)',
33
+ Navigation: 'var(--dracula-pink)',
34
+ Feedback: 'var(--dracula-yellow)',
35
+ Data: 'var(--dracula-green)',
36
+ };
37
+
38
+ const CATEGORIES = ['All', 'Layout', 'Forms', 'Navigation', 'Feedback', 'Data'];
39
+
40
+ const ITEMS: LibraryItem[] = [
41
+ {
42
+ id: '1',
43
+ name: 'Stack',
44
+ description:
45
+ 'Vertical and horizontal stack layouts with configurable gap and alignment.',
46
+ category: 'Layout',
47
+ type: 'Component',
48
+ },
49
+ {
50
+ id: '2',
51
+ name: 'Grid',
52
+ description:
53
+ 'Responsive grid container with auto-fit columns and gap control.',
54
+ category: 'Layout',
55
+ type: 'Component',
56
+ },
57
+ {
58
+ id: '3',
59
+ name: 'Card',
60
+ description:
61
+ 'Surface container with optional padding, border, and shadow variants.',
62
+ category: 'Layout',
63
+ type: 'Component',
64
+ },
65
+ {
66
+ id: '4',
67
+ name: 'Center',
68
+ description: 'Centers its child both horizontally and vertically.',
69
+ category: 'Layout',
70
+ type: 'Utility',
71
+ },
72
+ {
73
+ id: '5',
74
+ name: 'Section',
75
+ description: 'Semantic page section with optional heading and divider.',
76
+ category: 'Layout',
77
+ type: 'Pattern',
78
+ },
79
+ {
80
+ id: '6',
81
+ name: 'Collapsible',
82
+ description: 'Expandable region with animated height transition.',
83
+ category: 'Layout',
84
+ type: 'Component',
85
+ },
86
+ {
87
+ id: '7',
88
+ name: 'TextInput',
89
+ description:
90
+ 'Single-line text field with label, placeholder, and validation states.',
91
+ category: 'Forms',
92
+ type: 'Component',
93
+ },
94
+ {
95
+ id: '8',
96
+ name: 'TextArea',
97
+ description: 'Multi-line text field with auto-resize and character count.',
98
+ category: 'Forms',
99
+ type: 'Component',
100
+ },
101
+ {
102
+ id: '9',
103
+ name: 'CheckboxInput',
104
+ description: 'Checkbox with label, indeterminate state, and group support.',
105
+ category: 'Forms',
106
+ type: 'Component',
107
+ },
108
+ {
109
+ id: '10',
110
+ name: 'RadioList',
111
+ description: 'Group of radio buttons with accessible fieldset wrapper.',
112
+ category: 'Forms',
113
+ type: 'Component',
114
+ },
115
+ {
116
+ id: '11',
117
+ name: 'Switch',
118
+ description: 'Toggle switch for binary on/off settings.',
119
+ category: 'Forms',
120
+ type: 'Component',
121
+ },
122
+ {
123
+ id: '12',
124
+ name: 'Selector',
125
+ description:
126
+ 'Dropdown or inline option selector with single and multi-select modes.',
127
+ category: 'Forms',
128
+ type: 'Component',
129
+ },
130
+ {
131
+ id: '13',
132
+ name: 'TabList',
133
+ description:
134
+ 'Horizontal tab navigation with underline indicator and keyboard support.',
135
+ category: 'Navigation',
136
+ type: 'Component',
137
+ },
138
+ {
139
+ id: '14',
140
+ name: 'TopNav',
141
+ description: 'Application top bar with logo, nav links, and action slots.',
142
+ category: 'Navigation',
143
+ type: 'Pattern',
144
+ },
145
+ {
146
+ id: '15',
147
+ name: 'SideNav',
148
+ description:
149
+ 'Vertical sidebar navigation with collapsible groups and active states.',
150
+ category: 'Navigation',
151
+ type: 'Pattern',
152
+ },
153
+ {
154
+ id: '16',
155
+ name: 'Breadcrumbs',
156
+ description: 'Path trail navigation with separator and truncation support.',
157
+ category: 'Navigation',
158
+ type: 'Component',
159
+ },
160
+ {
161
+ id: '17',
162
+ name: 'Pagination',
163
+ description:
164
+ 'Page navigation with prev/next controls and page count display.',
165
+ category: 'Navigation',
166
+ type: 'Component',
167
+ },
168
+ {
169
+ id: '18',
170
+ name: 'MobileNav',
171
+ description:
172
+ 'Bottom tab bar for mobile viewports with icon and label slots.',
173
+ category: 'Navigation',
174
+ type: 'Pattern',
175
+ },
176
+ {
177
+ id: '19',
178
+ name: 'Badge',
179
+ description:
180
+ 'Compact label for status, count, or category with semantic color variants.',
181
+ category: 'Feedback',
182
+ type: 'Component',
183
+ },
184
+ {
185
+ id: '20',
186
+ name: 'Banner',
187
+ description:
188
+ 'Full-width alert bar for info, success, warning, and error messages.',
189
+ category: 'Feedback',
190
+ type: 'Component',
191
+ },
192
+ {
193
+ id: '21',
194
+ name: 'Spinner',
195
+ description: 'Animated loading indicator with size and color variants.',
196
+ category: 'Feedback',
197
+ type: 'Component',
198
+ },
199
+ {
200
+ id: '22',
201
+ name: 'ProgressBar',
202
+ description: 'Horizontal bar indicating task completion percentage.',
203
+ category: 'Feedback',
204
+ type: 'Component',
205
+ },
206
+ {
207
+ id: '23',
208
+ name: 'StatusDot',
209
+ description:
210
+ 'Small dot indicator for presence, health, or pipeline status.',
211
+ category: 'Feedback',
212
+ type: 'Component',
213
+ },
214
+ {
215
+ id: '24',
216
+ name: 'Tooltip',
217
+ description:
218
+ 'Contextual label that appears on hover with configurable placement.',
219
+ category: 'Feedback',
220
+ type: 'Component',
221
+ },
222
+ {
223
+ id: '25',
224
+ name: 'Table',
225
+ description:
226
+ 'Feature-rich data table with sorting, selection, and column resizing.',
227
+ category: 'Data',
228
+ type: 'Component',
229
+ },
230
+ {
231
+ id: '26',
232
+ name: 'Avatar',
233
+ description:
234
+ 'User profile image with fallback initials and status dot support.',
235
+ category: 'Data',
236
+ type: 'Component',
237
+ },
238
+ {
239
+ id: '27',
240
+ name: 'Skeleton',
241
+ description:
242
+ 'Placeholder shimmer for loading states matching content shapes.',
243
+ category: 'Data',
244
+ type: 'Utility',
245
+ },
246
+ {
247
+ id: '28',
248
+ name: 'HoverCard',
249
+ description: 'Rich popover that appears on hover with arbitrary content.',
250
+ category: 'Data',
251
+ type: 'Component',
252
+ },
253
+ {
254
+ id: '29',
255
+ name: 'PowerSearch',
256
+ description:
257
+ 'Command-palette style search with grouped results and keyboard nav.',
258
+ category: 'Data',
259
+ type: 'Pattern',
260
+ },
261
+ {
262
+ id: '30',
263
+ name: 'Typeahead',
264
+ description:
265
+ 'Autocomplete input with async suggestion loading and selection.',
266
+ category: 'Data',
267
+ type: 'Component',
268
+ },
269
+ ];
270
+
271
+ const thumbnailImage: CSSProperties = {
272
+ width: '100%',
273
+ height: '100%',
274
+ display: 'block',
275
+ };
276
+
277
+ // =============================================================================
278
+ // Side Nav
279
+ // =============================================================================
280
+
281
+ function LibraryCard({item}: {item: LibraryItem}) {
282
+ const hue = CATEGORY_HUES[item.category] ?? 'var(--dracula-purple)';
283
+ return (
284
+ <Card padding={0}>
285
+ <AspectRatio ratio={16 / 9}>
286
+ <svg
287
+ viewBox="0 0 400 300"
288
+ preserveAspectRatio="xMidYMid slice"
289
+ style={thumbnailImage}
290
+ role="img"
291
+ aria-label={`${item.name} thumbnail`}>
292
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
293
+ <g
294
+ transform="translate(200 150)"
295
+ fill="none"
296
+ stroke="var(--dracula-comment)"
297
+ strokeWidth="5"
298
+ strokeLinecap="round"
299
+ strokeLinejoin="round">
300
+ <rect x="-44" y="-44" width="88" height="88" rx="5" />
301
+ <circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
302
+ <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
303
+ </g>
304
+ </svg>
305
+ </AspectRatio>
306
+ <Section variant="transparent" padding={4}>
307
+ <VStack gap={1}>
308
+ <Heading level={3}>{item.name}</Heading>
309
+ <Text type="body" color="secondary">
310
+ {item.description}
311
+ </Text>
312
+ </VStack>
313
+ </Section>
314
+ </Card>
315
+ );
316
+ }
317
+
318
+ function LibrarySection({
319
+ category,
320
+ items,
321
+ }: {
322
+ category: string;
323
+ items: LibraryItem[];
324
+ }) {
325
+ return (
326
+ <VStack gap={6}>
327
+ <HStack justify="between" vAlign="center">
328
+ <Heading level={2}>{category}</Heading>
329
+ <Text type="supporting" color="secondary" hasTabularNumbers>
330
+ {items.length} {items.length === 1 ? 'item' : 'items'}
331
+ </Text>
332
+ </HStack>
333
+ <Grid columns={{minWidth: 320}} gap={4}>
334
+ {items.map(item => (
335
+ <LibraryCard key={item.id} item={item} />
336
+ ))}
337
+ </Grid>
338
+ </VStack>
339
+ );
340
+ }
341
+
342
+ export default function LibraryGrid() {
343
+ const [activeTab, setActiveTab] = useState('All');
344
+ const [search, setSearch] = useState('');
345
+ const [sortOrder, setSortOrder] = useState('A-Z');
346
+
347
+ const filtered = useMemo(() => {
348
+ let items =
349
+ activeTab === 'All' ? ITEMS : ITEMS.filter(i => i.category === activeTab);
350
+ if (search.trim()) {
351
+ const q = search.toLowerCase();
352
+ items = items.filter(
353
+ i =>
354
+ i.name.toLowerCase().includes(q) ||
355
+ i.description.toLowerCase().includes(q),
356
+ );
357
+ }
358
+ const sorted = [...items];
359
+ if (sortOrder === 'A-Z') {
360
+ sorted.sort((a, b) => a.name.localeCompare(b.name));
361
+ } else if (sortOrder === 'Z-A') {
362
+ sorted.sort((a, b) => b.name.localeCompare(a.name));
363
+ } else if (sortOrder === 'Newest') {
364
+ sorted.sort((a, b) => Number(b.id) - Number(a.id));
365
+ }
366
+ return sorted;
367
+ }, [activeTab, search, sortOrder]);
368
+
369
+ const groupedSections = useMemo(() => {
370
+ if (activeTab !== 'All') {
371
+ return null;
372
+ }
373
+ const order = CATEGORIES.filter(c => c !== 'All');
374
+ const map = new Map<string, LibraryItem[]>();
375
+ for (const item of filtered) {
376
+ let group = map.get(item.category);
377
+ if (!group) {
378
+ group = [];
379
+ map.set(item.category, group);
380
+ }
381
+ group.push(item);
382
+ }
383
+ return order
384
+ .filter(cat => map.has(cat))
385
+ .map(cat => ({category: cat, items: map.get(cat) ?? []}));
386
+ }, [activeTab, filtered]);
387
+
388
+ return (
389
+ <Layout
390
+ header={
391
+ <LayoutHeader hasDivider padding={6}>
392
+ <Heading level={1}>Library</Heading>
393
+ </LayoutHeader>
394
+ }
395
+ content={
396
+ <LayoutContent padding={6}>
397
+ <VStack gap={6}>
398
+ <VStack gap={4}>
399
+ <TextInput
400
+ label="Search"
401
+ isLabelHidden
402
+ placeholder="Search the stacks..."
403
+ value={search}
404
+ onChange={setSearch}
405
+ startIcon={Search}
406
+ size="lg"
407
+ />
408
+ <HStack vAlign="center" gap={4}>
409
+ <StackItem size="fill">
410
+ <VStack>
411
+ <ToggleButtonGroup
412
+ label="Filter by category"
413
+ value={activeTab}
414
+ onChange={v => setActiveTab(v ?? 'All')}>
415
+ <OverflowList
416
+ gap={1}
417
+ behavior="observeParent"
418
+ overflowRenderer={overflowItems => (
419
+ <DropdownMenu
420
+ button={{
421
+ label: `+${overflowItems.length}`,
422
+ variant: 'ghost',
423
+ size: 'lg',
424
+ }}
425
+ items={overflowItems.map(({index}) => ({
426
+ label: CATEGORIES[index],
427
+ onClick: () => setActiveTab(CATEGORIES[index]),
428
+ }))}
429
+ />
430
+ )}>
431
+ {CATEGORIES.map(cat => (
432
+ <ToggleButton
433
+ key={cat}
434
+ label={cat}
435
+ value={cat}
436
+ size="lg"
437
+ />
438
+ ))}
439
+ </OverflowList>
440
+ </ToggleButtonGroup>
441
+ </VStack>
442
+ </StackItem>
443
+ <DropdownMenu
444
+ button={{label: sortOrder, size: 'lg'}}
445
+ items={[
446
+ {label: 'A-Z', onClick: () => setSortOrder('A-Z')},
447
+ {label: 'Z-A', onClick: () => setSortOrder('Z-A')},
448
+ {label: 'Newest', onClick: () => setSortOrder('Newest')},
449
+ ]}
450
+ />
451
+ </HStack>
452
+ </VStack>
453
+
454
+ {filtered.length === 0 ? (
455
+ <EmptyState
456
+ icon={<Icon icon={Search} size="lg" color="secondary" />}
457
+ title="Nothing stirs in the stacks"
458
+ description="No entries match this search and filter. Clear them to browse the full library."
459
+ actions={
460
+ <Button
461
+ label="Clear search & filters"
462
+ variant="primary"
463
+ onClick={() => {
464
+ setSearch('');
465
+ setActiveTab('All');
466
+ }}
467
+ />
468
+ }
469
+ />
470
+ ) : (
471
+ <VStack gap={6}>
472
+ {(
473
+ groupedSections ?? [{category: activeTab, items: filtered}]
474
+ ).flatMap(section => [
475
+ <Divider key={`d-${section.category}`} />,
476
+ <LibrarySection
477
+ key={section.category}
478
+ category={section.category}
479
+ items={section.items}
480
+ />,
481
+ ])}
482
+ </VStack>
483
+ )}
484
+ </VStack>
485
+ </LayoutContent>
486
+ }
487
+ />
488
+ );
489
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Login Card',
4
+ description: 'Centered auth card with email and password',
5
+ category: 'auth',
6
+ };
@@ -0,0 +1,200 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState, type CSSProperties} from 'react';
4
+ import {Moon} from 'lucide-react';
5
+ import {VStack} from '@astryxdesign/core/Layout';
6
+ import {Center} from '@astryxdesign/core/Center';
7
+ import {Text, Heading} from '@astryxdesign/core/Text';
8
+ import {TextInput} from '@astryxdesign/core/TextInput';
9
+ import {Button} from '@astryxdesign/core/Button';
10
+ import {Card} from '@astryxdesign/core/Card';
11
+ import {Link} from '@astryxdesign/core/Link';
12
+ import {Divider} from '@astryxdesign/core/Divider';
13
+ import {Icon} from '@astryxdesign/core/Icon';
14
+
15
+ // Brand sign-in marks — bespoke inline glyphs, no icon-library equivalent.
16
+ const AppleIcon = (props: React.SVGProps<SVGSVGElement>) => (
17
+ <svg
18
+ viewBox="0 0 24 24"
19
+ fill="currentColor"
20
+ width={16}
21
+ height={16}
22
+ aria-hidden="true"
23
+ {...props}>
24
+ <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" />
25
+ </svg>
26
+ );
27
+
28
+ const GoogleIcon = (props: React.SVGProps<SVGSVGElement>) => (
29
+ <svg
30
+ viewBox="0 0 24 24"
31
+ fill="currentColor"
32
+ width={16}
33
+ height={16}
34
+ aria-hidden="true"
35
+ {...props}>
36
+ <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" />
37
+ <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" />
38
+ <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" />
39
+ <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" />
40
+ </svg>
41
+ );
42
+
43
+ // Standalone auth page paints its own body background (no host shell).
44
+ const pageStyle: CSSProperties = {
45
+ minHeight: '100%',
46
+ backgroundColor: 'var(--color-background-body)',
47
+ padding: 'var(--spacing-6)',
48
+ };
49
+ // Cap the column at 400px but let it shrink to fit narrow screens (Stack
50
+ // has no maxWidth prop, so it's set here).
51
+ const contentStyle: CSSProperties = {
52
+ width: '100%',
53
+ maxWidth: 400,
54
+ };
55
+
56
+ export default function LoginCard() {
57
+ const [email, setEmail] = useState('');
58
+ const [password, setPassword] = useState('');
59
+ const [loginFailed, setLoginFailed] = useState(false);
60
+ const [isLoading, setIsLoading] = useState(false);
61
+
62
+ const handleLogin = () => {
63
+ if (!email || !password) {
64
+ setLoginFailed(true);
65
+ return;
66
+ }
67
+ setIsLoading(true);
68
+ setLoginFailed(false);
69
+ setTimeout(() => {
70
+ setIsLoading(false);
71
+ setLoginFailed(true);
72
+ }, 2000);
73
+ };
74
+
75
+ return (
76
+ <Center axis="both" style={pageStyle}>
77
+ <VStack gap={4} hAlign="center" style={contentStyle}>
78
+ {/* Logo */}
79
+ <VStack gap={2} hAlign="center">
80
+ <Icon icon={Moon} size="lg" color="accent" />
81
+ <Text type="body" weight="bold" size="lg">
82
+ Castle Dracula
83
+ </Text>
84
+ </VStack>
85
+
86
+ {/* Card */}
87
+ <Card padding={8} width="100%">
88
+ <VStack gap={4} hAlign="stretch">
89
+ {/* Header */}
90
+ <VStack gap={1} hAlign="center">
91
+ <Heading level={2}>Welcome back to the night</Heading>
92
+ <Text type="body" color="secondary">
93
+ Sign in to your crypt
94
+ </Text>
95
+ </VStack>
96
+
97
+ {/* Form fields */}
98
+ <VStack gap={2}>
99
+ <TextInput
100
+ label="Email"
101
+ isLabelHidden
102
+ type="email"
103
+ placeholder="you@castle.dracula"
104
+ value={email}
105
+ onChange={setEmail}
106
+ size="lg"
107
+ />
108
+ <VStack gap={1}>
109
+ <TextInput
110
+ label="Password"
111
+ isLabelHidden
112
+ placeholder="Whisper your password"
113
+ type="password"
114
+ value={password}
115
+ onChange={(v: string) => {
116
+ setPassword(v);
117
+ setLoginFailed(false);
118
+ }}
119
+ size="lg"
120
+ status={
121
+ loginFailed
122
+ ? {
123
+ type: 'error',
124
+ message: 'Wrong incantation. Try again.',
125
+ }
126
+ : undefined
127
+ }
128
+ />
129
+ {loginFailed && (
130
+ <VStack hAlign="end">
131
+ <Link
132
+ href="#/templates/login-card"
133
+ size="sm"
134
+ color="secondary"
135
+ type="supporting">
136
+ Forgot password?
137
+ </Link>
138
+ </VStack>
139
+ )}
140
+ </VStack>
141
+ </VStack>
142
+
143
+ {/* Login button */}
144
+ <Button
145
+ label="Enter the night"
146
+ variant="primary"
147
+ size="lg"
148
+ isLoading={isLoading}
149
+ onClick={handleLogin}
150
+ />
151
+
152
+ {/* Divider */}
153
+ <Divider label="Or continue with" />
154
+
155
+ {/* Social buttons */}
156
+ <VStack gap={3} hAlign="stretch">
157
+ <Button
158
+ label="Login with Apple"
159
+ variant="secondary"
160
+ icon={<AppleIcon />}
161
+ size="lg"
162
+ />
163
+ <Button
164
+ label="Login with Google"
165
+ variant="secondary"
166
+ icon={<GoogleIcon />}
167
+ size="lg"
168
+ />
169
+ </VStack>
170
+
171
+ {/* Sign up link */}
172
+ <VStack hAlign="center">
173
+ <Text type="supporting" color="secondary">
174
+ New to the castle?{' '}
175
+ <Link href="#/templates/login-card" type="supporting">
176
+ Sign up
177
+ </Link>
178
+ </Text>
179
+ </VStack>
180
+ </VStack>
181
+ </Card>
182
+
183
+ {/* Terms */}
184
+ <VStack hAlign="center" width="100%">
185
+ <Text type="supporting" color="secondary" justify="center">
186
+ By clicking continue, you agree to our{' '}
187
+ <Link href="#/templates/login-card" type="supporting">
188
+ Terms of Service
189
+ </Link>{' '}
190
+ and{' '}
191
+ <Link href="#/templates/login-card" type="supporting">
192
+ Privacy Policy
193
+ </Link>
194
+ .
195
+ </Text>
196
+ </VStack>
197
+ </VStack>
198
+ </Center>
199
+ );
200
+ }