astryx-dracula 0.2.0 → 0.3.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 (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +22 -11
  3. package/USAGE.md +29 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +174 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +67 -32
  31. package/templates/ai-chat.tsx +41 -20
  32. package/templates/blank.tsx +5 -2
  33. package/templates/centered-hero.tsx +15 -124
  34. package/templates/classic-gallery.tsx +21 -65
  35. package/templates/contact-form.tsx +62 -40
  36. package/templates/dashboard-portfolio.tsx +146 -227
  37. package/templates/dashboard.tsx +123 -121
  38. package/templates/detail-page.tsx +101 -95
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +85 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +33 -21
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +54 -74
  45. package/templates/editor.tsx +119 -89
  46. package/templates/file-explorer.tsx +133 -84
  47. package/templates/form-two-column.tsx +62 -124
  48. package/templates/gallery-hero.tsx +17 -171
  49. package/templates/ide.tsx +27 -21
  50. package/templates/incident-console.tsx +105 -69
  51. package/templates/kanban-board.tsx +100 -41
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +38 -41
  54. package/templates/login-card.tsx +59 -86
  55. package/templates/login-split.tsx +88 -128
  56. package/templates/login-sso.tsx +107 -68
  57. package/templates/login.tsx +108 -52
  58. package/templates/messaging-shell.tsx +172 -143
  59. package/templates/mixed-gallery.tsx +31 -130
  60. package/templates/payment-form.tsx +369 -289
  61. package/templates/product-detail.tsx +40 -57
  62. package/templates/product-gallery.tsx +31 -40
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +145 -286
  66. package/templates/settings-sidebar.tsx +179 -267
  67. package/templates/settings.tsx +42 -13
  68. package/templates/shell-nav.tsx +32 -23
  69. package/templates/shell-side-nav.tsx +30 -19
  70. package/templates/shell-top-nav.tsx +81 -43
  71. package/templates/side-gallery.tsx +39 -93
  72. package/templates/table-grouped.tsx +63 -33
  73. package/templates/table-page-chart.tsx +24 -297
  74. package/templates/table-page-heatmap-status.tsx +157 -91
  75. package/templates/table-page-shoe-store-heatmap.tsx +19 -288
  76. package/templates/table-page.tsx +24 -6
  77. package/templates/table.tsx +24 -7
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +313 -249
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,4 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > (Tbar[size=sm] > (H > IB"Go back" + IB"Go forward" + Tx"Folder"[t=label]) + (SG"View mode" > SGI*4) + (H > IB"Group" + IB"Share" + IB"Tags" + IB"More" + IB"Search")) + (LC[p=0 !scroll] > (Hd"File Explorer"[level=1] + H[h=fill] > (S.transparent[p=2 w=240 dv=[end]] > UL[density=compact !hasDividers] > (LI > Ic + Tx"Item"[t=body])*3)*3 + (S.transparent[p=6] > V[g=4 a=center] > Av[size=96] + (V[g=1 a=center] > Tx"Name"[t=body] + Tx"Kind"[t=supporting]) + (ML"Information" > MLI*3))))
2
4
 
3
5
  import {useState, useMemo, type CSSProperties} from 'react';
4
6
  import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -7,9 +9,11 @@ import {List, ListItem} from '@astryxdesign/core/List';
7
9
  import {HStack, VStack} from '@astryxdesign/core/Layout';
8
10
  import {useMediaQuery} from '@astryxdesign/core/hooks';
9
11
  import {Text} from '@astryxdesign/core/Text';
12
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
10
13
  import {Icon} from '@astryxdesign/core/Icon';
11
14
  import {IconButton} from '@astryxdesign/core/IconButton';
12
15
  import {Section} from '@astryxdesign/core/Section';
16
+ import {EmptyState} from '@astryxdesign/core/EmptyState';
13
17
  import {Avatar} from '@astryxdesign/core/Avatar';
14
18
  import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
15
19
  import {
@@ -262,13 +266,85 @@ const page: CSSProperties = {height: '100dvh'};
262
266
  const columnRow: CSSProperties = {overflowX: 'auto', overflowY: 'hidden'};
263
267
  const scrollable: CSSProperties = {overflowY: 'auto'};
264
268
  const fixedColumn: CSSProperties = {flexShrink: 0};
269
+ // The column's 320px resting width rides on Section's `width` prop; grow
270
+ // lets it absorb whatever the fixed columns leave. Inline styles are for
271
+ // what props cannot express.
265
272
  const detailColumn: CSSProperties = {
266
273
  flexGrow: 1,
267
274
  flexShrink: 0,
268
- flexBasis: 320,
269
275
  };
270
276
  const controlsScroll: CSSProperties = {overflowX: 'auto'};
271
277
 
278
+ // Static toolbar content, hoisted out of the render path: neither element
279
+ // reads component state, so rebuilding them per render buys nothing.
280
+ const viewSwitcher = (
281
+ <SegmentedControl value="column" onChange={() => {}} label="View mode">
282
+ <SegmentedControlItem
283
+ value="grid"
284
+ label="Grid"
285
+ icon={<Icon icon={LayoutGrid} size="sm" />}
286
+ isLabelHidden
287
+ />
288
+ <SegmentedControlItem
289
+ value="list"
290
+ label="List"
291
+ icon={<Icon icon={Rows3} size="sm" />}
292
+ isLabelHidden
293
+ />
294
+ <SegmentedControlItem
295
+ value="column"
296
+ label="Column"
297
+ icon={<Icon icon={Columns} size="sm" />}
298
+ isLabelHidden
299
+ />
300
+ <SegmentedControlItem
301
+ value="gallery"
302
+ label="Gallery"
303
+ icon={<Icon icon={Table} size="sm" />}
304
+ isLabelHidden
305
+ />
306
+ </SegmentedControl>
307
+ );
308
+ const fileActions = (
309
+ <>
310
+ <IconButton
311
+ variant="ghost"
312
+ size="sm"
313
+ icon={<Icon icon={SlidersHorizontal} size="sm" />}
314
+ label="Group"
315
+ tooltip="Group"
316
+ />
317
+ <IconButton
318
+ variant="ghost"
319
+ size="sm"
320
+ icon={<Icon icon={Share2} size="sm" />}
321
+ label="Share"
322
+ tooltip="Share"
323
+ />
324
+ <IconButton
325
+ variant="ghost"
326
+ size="sm"
327
+ icon={<Icon icon={Tag} size="sm" />}
328
+ label="Tags"
329
+ tooltip="Tags"
330
+ />
331
+ <IconButton
332
+ variant="ghost"
333
+ size="sm"
334
+ icon={<Icon icon={Ellipsis} size="sm" />}
335
+ label="More"
336
+ tooltip="More"
337
+ />
338
+ <IconButton
339
+ variant="ghost"
340
+ size="sm"
341
+ icon={<Icon icon={Search} size="sm" />}
342
+ label="Search"
343
+ tooltip="Search"
344
+ />
345
+ </>
346
+ );
347
+
272
348
  function findItem(items: FileSystemItem[], id: string): FileSystemItem | null {
273
349
  for (const item of items) {
274
350
  if (item.id === id) {
@@ -297,16 +373,27 @@ export default function FileExplorer() {
297
373
  ]);
298
374
 
299
375
  const isNarrow = useMediaQuery('(max-width: 768px)');
300
- const isPhone = useMediaQuery('(max-width: 767px)');
376
+ const isPhone = isNarrow;
301
377
 
302
378
  const columns = useMemo(() => {
303
- const cols: {items: FileSystemItem[]; selectedId: string | null}[] = [];
304
- cols.push({items: FILESYSTEM, selectedId: selectedPath[0] ?? null});
379
+ // `id` names the folder a column lists, so the React key stays put when
380
+ // narrow mode slices the row down to the last visible column.
381
+ const cols: {
382
+ id: string;
383
+ items: FileSystemItem[];
384
+ selectedId: string | null;
385
+ }[] = [];
386
+ cols.push({
387
+ id: 'root',
388
+ items: FILESYSTEM,
389
+ selectedId: selectedPath[0] ?? null,
390
+ });
305
391
  let currentItems: FileSystemItem[] = FILESYSTEM;
306
392
  for (let i = 0; i < selectedPath.length; i++) {
307
393
  const selected = currentItems.find(item => item.id === selectedPath[i]);
308
394
  if (selected?.children && selected.children.length > 0) {
309
395
  cols.push({
396
+ id: selectedPath[i],
310
397
  items: selected.children,
311
398
  selectedId: selectedPath[i + 1] ?? null,
312
399
  });
@@ -381,76 +468,6 @@ export default function FileExplorer() {
381
468
  </Text>
382
469
  </>
383
470
  );
384
- const viewSwitcher = (
385
- <SegmentedControl
386
- value="column"
387
- onChange={() => {}}
388
- label="View mode">
389
- <SegmentedControlItem
390
- value="grid"
391
- label="Grid"
392
- icon={<Icon icon={LayoutGrid} size="sm" />}
393
- isLabelHidden
394
- />
395
- <SegmentedControlItem
396
- value="list"
397
- label="List"
398
- icon={<Icon icon={Rows3} size="sm" />}
399
- isLabelHidden
400
- />
401
- <SegmentedControlItem
402
- value="column"
403
- label="Column"
404
- icon={<Icon icon={Columns} size="sm" />}
405
- isLabelHidden
406
- />
407
- <SegmentedControlItem
408
- value="gallery"
409
- label="Gallery"
410
- icon={<Icon icon={Table} size="sm" />}
411
- isLabelHidden
412
- />
413
- </SegmentedControl>
414
- );
415
- const fileActions = (
416
- <>
417
- <IconButton
418
- variant="ghost"
419
- size="sm"
420
- icon={<Icon icon={SlidersHorizontal} size="sm" />}
421
- label="Group"
422
- tooltip="Group"
423
- />
424
- <IconButton
425
- variant="ghost"
426
- size="sm"
427
- icon={<Icon icon={Share2} size="sm" />}
428
- label="Share"
429
- tooltip="Share"
430
- />
431
- <IconButton
432
- variant="ghost"
433
- size="sm"
434
- icon={<Icon icon={Tag} size="sm" />}
435
- label="Tags"
436
- tooltip="Tags"
437
- />
438
- <IconButton
439
- variant="ghost"
440
- size="sm"
441
- icon={<Icon icon={Ellipsis} size="sm" />}
442
- label="More"
443
- tooltip="More"
444
- />
445
- <IconButton
446
- variant="ghost"
447
- size="sm"
448
- icon={<Icon icon={Search} size="sm" />}
449
- label="Search"
450
- tooltip="Search"
451
- />
452
- </>
453
- );
454
471
 
455
472
  return (
456
473
  <Layout
@@ -471,7 +488,13 @@ export default function FileExplorer() {
471
488
  variant="transparent"
472
489
  padding={2}
473
490
  dividers={['bottom']}>
474
- <HStack gap={2} vAlign="center" style={controlsScroll}>
491
+ <HStack
492
+ gap={2}
493
+ vAlign="center"
494
+ style={controlsScroll}
495
+ role="region"
496
+ aria-label="View and file actions"
497
+ tabIndex={0}>
475
498
  {viewSwitcher}
476
499
  {fileActions}
477
500
  </HStack>
@@ -481,6 +504,9 @@ export default function FileExplorer() {
481
504
  }
482
505
  content={
483
506
  <LayoutContent padding={0} isScrollable={false}>
507
+ <VisuallyHidden as="h1">
508
+ File Explorer
509
+ </VisuallyHidden>
484
510
  <HStack height="100%" style={columnRow}>
485
511
  {visibleColumns.map((col, colIndex) => {
486
512
  const trueIndex = columnOffset + colIndex;
@@ -488,12 +514,22 @@ export default function FileExplorer() {
488
514
  trueIndex < columns.length - 1 || selectedFile != null;
489
515
  return (
490
516
  <Section
491
- key={trueIndex}
517
+ key={col.id}
492
518
  width={240}
493
519
  padding={2}
494
520
  variant="transparent"
495
521
  dividers={showDivider ? ['end'] : undefined}
496
- style={{...scrollable, ...fixedColumn}}>
522
+ style={{...scrollable, ...fixedColumn}}
523
+ role="region"
524
+ aria-label={`Folder column ${trueIndex + 1}`}
525
+ tabIndex={0}>
526
+ {col.items.length === 0 ? (
527
+ <EmptyState
528
+ title="Quiet in the crypt"
529
+ description="This folder holds no scrolls yet."
530
+ isCompact
531
+ />
532
+ ) : (
497
533
  <List density="compact" hasDividers={false}>
498
534
  {col.items.map(item => {
499
535
  const isSelected = col.selectedId === item.id;
@@ -537,33 +573,46 @@ export default function FileExplorer() {
537
573
  );
538
574
  })}
539
575
  </List>
576
+ )}
540
577
  </Section>
541
578
  );
542
579
  })}
543
580
  {selectedFile && (
544
581
  <Section
545
- padding={6}
582
+ width={320}
583
+ // 16px, not 24. The sibling column at :519 is padding={2}
584
+ // (8px) because it holds a dense compact List, and these two
585
+ // sit in one HStack (:510) as columns of the same view --
586
+ // 24 beside 8 made the compact column look broken rather
587
+ // than compact. 16 is the outer-inset value this repo already
588
+ // uses for exactly this (scaffolds.md new-site recipe).
589
+ padding={4}
546
590
  variant="transparent"
547
- style={{...scrollable, ...detailColumn}}>
591
+ style={{...scrollable, ...detailColumn}}
592
+ role="region"
593
+ aria-label={`Details for ${selectedFile.name}`}
594
+ tabIndex={0}>
548
595
  <VStack gap={4} hAlign="center">
549
596
  <Avatar name={selectedFile.name} size={96} />
550
597
  <VStack gap={1} hAlign="center">
551
- <Text type="label" maxLines={1}>
598
+ <Text type="body" weight="semibold" maxLines={1}>
552
599
  {selectedFile.name}
553
600
  </Text>
554
- <Text type="supporting">
601
+ <Text type="supporting" color="secondary">
555
602
  {getFileExtension(selectedFile.name)} Document
556
603
  </Text>
557
604
  </VStack>
558
605
  <MetadataList title="Information">
559
606
  <MetadataListItem label="Created">
560
- March 28, 2026 at 2:15 PM
607
+ <Text type="body">March 28, 2026 at 2:15 PM</Text>
561
608
  </MetadataListItem>
562
609
  <MetadataListItem label="Modified">
563
- Yesterday, 10:27 PM
610
+ <Text type="body">Yesterday, 10:27 PM</Text>
564
611
  </MetadataListItem>
565
612
  <MetadataListItem label="Kind">
566
- {getFileExtension(selectedFile.name)} Document
613
+ <Text type="body">
614
+ {getFileExtension(selectedFile.name)} Document
615
+ </Text>
567
616
  </MetadataListItem>
568
617
  </MetadataList>
569
618
  </VStack>
@@ -1,4 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // Ctr > S.transparent[p=10] > V[g=10] > (G[c={min:280,max:2} g=10] > (V[g=6] > (V[g=3] > Hd"Let's conjure together"[level=1 t=display-1] + Tx"Tell us what you're brewing"[t=body]) + AR) + (C[p=4] > V[g=4] > Hd"Your details"[level=2] + TI"Full name"[req] + (G[c={min:180} g=3] > TI"Email"[req] + TI"Company") + (G[c={min:180} g=3] > TI"Job title" + TI"Phone") + (V[g=2] > Tx"Reason"[t=label] + (H[g=2] > Tk"Reason"*7)) + SE"Budget" + TA"Project details" + B.primary"Send it into the night")) + (V[g=6] > D + (G[c={min:200} g=6] > (V[g=1 a=center] > Tx"General"[t=supporting] + Lk"hello@castle.dracula")*3))
2
4
 
3
5
  import {useState, type CSSProperties} from 'react';
4
6
  import {VStack, HStack} from '@astryxdesign/core/Layout';
@@ -6,6 +8,7 @@ import {Center} from '@astryxdesign/core/Center';
6
8
  import {Section} from '@astryxdesign/core/Section';
7
9
  import {Grid} from '@astryxdesign/core/Grid';
8
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
11
+ import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
9
12
  import {Button} from '@astryxdesign/core/Button';
10
13
  import {Text, Heading} from '@astryxdesign/core/Text';
11
14
  import {TextInput} from '@astryxdesign/core/TextInput';
@@ -15,91 +18,10 @@ import {Link} from '@astryxdesign/core/Link';
15
18
  import {Divider} from '@astryxdesign/core/Divider';
16
19
  import {Card} from '@astryxdesign/core/Card';
17
20
  import {Selector} from '@astryxdesign/core/Selector';
18
-
19
- function CastleIllustration() {
20
- return (
21
- <svg
22
- style={illustrationImg}
23
- viewBox="0 0 400 300"
24
- role="img"
25
- aria-label="Moonlit castle where the night crew works">
26
- <title>Castle office after dark</title>
27
- <rect width="400" height="300" fill="var(--dracula-bg-dark)" />
28
- <g fill="var(--dracula-purple)">
29
- <circle cx="40" cy="40" r="2.5" />
30
- <circle cx="120" cy="70" r="2" />
31
- <circle cx="210" cy="36" r="2.5" />
32
- <circle cx="330" cy="60" r="2" />
33
- </g>
34
- <g fill="var(--dracula-cyan)">
35
- <circle cx="80" cy="55" r="2" />
36
- <circle cx="170" cy="60" r="2" />
37
- <circle cx="290" cy="36" r="2.5" />
38
- </g>
39
- <circle cx="315" cy="70" r="34" fill="var(--dracula-yellow)" />
40
- <circle
41
- cx="304"
42
- cy="61"
43
- r="6"
44
- fill="var(--dracula-orange)"
45
- opacity={0.5}
46
- />
47
- <g
48
- fill="none"
49
- stroke="var(--dracula-purple)"
50
- strokeWidth={3}
51
- strokeLinecap="round">
52
- <path d="M90 100 q9 -9 18 0 q9 -9 18 0" />
53
- <path d="M180 84 q9 -9 18 0 q9 -9 18 0" />
54
- </g>
55
- <path
56
- d="M0 220 Q140 180 260 210 T400 200 V300 H0 Z"
57
- fill="var(--dracula-selection)"
58
- />
59
- <path
60
- d="M0 255 Q160 225 320 250 T400 245 V300 H0 Z"
61
- fill="var(--dracula-bg-light)"
62
- />
63
- <g>
64
- <rect
65
- x="140"
66
- y="150"
67
- width="120"
68
- height="100"
69
- rx={4}
70
- fill="var(--dracula-bg-lighter)"
71
- />
72
- <rect
73
- x="122"
74
- y="122"
75
- width="38"
76
- height="128"
77
- rx={4}
78
- fill="var(--dracula-bg-lighter)"
79
- />
80
- <rect
81
- x="240"
82
- y="122"
83
- width="38"
84
- height="128"
85
- rx={4}
86
- fill="var(--dracula-bg-lighter)"
87
- />
88
- <g fill="var(--dracula-yellow)">
89
- <rect x="156" y="172" width="12" height="18" rx={2} />
90
- <rect x="186" y="172" width="12" height="18" rx={2} />
91
- <rect x="216" y="172" width="12" height="18" rx={2} />
92
- <rect x="132" y="146" width="9" height="12" rx={2} />
93
- <rect x="251" y="146" width="9" height="12" rx={2} />
94
- </g>
95
- <path
96
- d="M185 250 v-24 a15 15 0 0 1 30 0 v24 Z"
97
- fill="var(--dracula-bg-dark)"
98
- />
99
- </g>
100
- </svg>
101
- );
102
- }
21
+ import {Banner} from '@astryxdesign/core/Banner';
22
+ import {Icon} from '@astryxdesign/core/Icon';
23
+ import {Check} from 'lucide-react';
24
+ import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
103
25
 
104
26
  const INQUIRY_REASONS = [
105
27
  'New business',
@@ -126,17 +48,12 @@ const CONTACT_COLUMNS = [
126
48
  {label: 'Press & partnerships', email: 'press@castle.dracula'},
127
49
  ];
128
50
 
129
- // AspectRatio has no radius prop and there's no Image primitive (#2582), so
130
- // the illustration carries its own corner radius directly.
51
+ // Castle illustration lives in shared/scene-castle (`card` variant:
52
+ // harvest-yellow moon, fg-stroke bats). Fill + radius-clip live in
53
+ // shared/gallery-image (no Image primitive, #2582).
131
54
  const pageStyle: CSSProperties = {
132
55
  minHeight: '100%',
133
56
  };
134
- const illustrationImg: CSSProperties = {
135
- width: '100%',
136
- height: '100%',
137
- display: 'block',
138
- borderRadius: 'var(--radius-container)',
139
- };
140
57
 
141
58
  /**
142
59
  * Form (Two-column) — marketing contact form template.
@@ -145,7 +62,12 @@ const illustrationImg: CSSProperties = {
145
62
  * Top: two-column — left has headline + description + illustration,
146
63
  * right has the contact form on a card.
147
64
  * Bottom: three-column contact info strip.
148
- * Mobile (<768px): single column stack.
65
+ *
66
+ * Responsive contract (both grids reflow on their own; no JS breakpoints):
67
+ * Form grid (min 280px tracks, capped at 2 columns): two columns from a
68
+ * 680px viewport up, one column below that.
69
+ * Contact strip (min 200px tracks): three columns from 728px, two columns
70
+ * from 504px, one column below.
149
71
  */
150
72
  export default function TwoColumnForm() {
151
73
  const [fullName, setFullName] = useState('');
@@ -166,14 +88,18 @@ export default function TwoColumnForm() {
166
88
  }
167
89
  : {};
168
90
 
91
+ const isValid =
92
+ fullName.trim() !== '' &&
93
+ email.trim() !== '' &&
94
+ details.trim() !== '';
95
+
169
96
  const handleSubmit = () => setSubmitted(true);
170
97
 
171
98
  return (
172
99
  <Center style={pageStyle}>
173
100
  <Section maxWidth={1100} width="100%" padding={10} variant="transparent">
174
101
  <VStack gap={10}>
175
- {/* Two-column; stacks to one column below ~520px. */}
176
- <Grid columns={{minWidth: 320}} align="center" gap={10}>
102
+ <Grid columns={{minWidth: 280, max: 2}} align="center" gap={10}>
177
103
  <VStack gap={6}>
178
104
  <VStack gap={3}>
179
105
  <Heading level={1} type="display-1">
@@ -184,18 +110,18 @@ export default function TwoColumnForm() {
184
110
  the best path through the night.
185
111
  </Text>
186
112
  </VStack>
187
- <AspectRatio ratio={4 / 3}>
188
- <CastleIllustration />
113
+ <AspectRatio ratio={4 / 3} style={galleryImageClip}>
114
+ <SceneCastle variant="card" />
189
115
  </AspectRatio>
190
116
  </VStack>
191
117
 
192
- <Card padding={8}>
118
+ <Card padding={4}>
193
119
  <VStack gap={4}>
194
- <Text type="label">Your details</Text>
120
+ <Heading level={2}>Your details</Heading>
195
121
  <TextInput
196
122
  label="Full name"
197
- isLabelHidden
198
- placeholder="Full name*"
123
+ isRequired
124
+ placeholder="Full name"
199
125
  value={fullName}
200
126
  onChange={setFullName}
201
127
  status={
@@ -207,8 +133,8 @@ export default function TwoColumnForm() {
207
133
  <Grid columns={{minWidth: 180}} gap={3}>
208
134
  <TextInput
209
135
  label="Email"
210
- isLabelHidden
211
- placeholder="Email*"
136
+ isRequired
137
+ placeholder="Email"
212
138
  value={email}
213
139
  onChange={setEmail}
214
140
  status={
@@ -219,7 +145,6 @@ export default function TwoColumnForm() {
219
145
  />
220
146
  <TextInput
221
147
  label="Company name"
222
- isLabelHidden
223
148
  placeholder="Company name"
224
149
  value={company}
225
150
  onChange={setCompany}
@@ -228,14 +153,12 @@ export default function TwoColumnForm() {
228
153
  <Grid columns={{minWidth: 180}} gap={3}>
229
154
  <TextInput
230
155
  label="Job title"
231
- isLabelHidden
232
156
  placeholder="Job title"
233
157
  value={jobTitle}
234
158
  onChange={setJobTitle}
235
159
  />
236
160
  <TextInput
237
161
  label="Phone number"
238
- isLabelHidden
239
162
  placeholder="Phone number"
240
163
  value={phone}
241
164
  onChange={setPhone}
@@ -245,18 +168,27 @@ export default function TwoColumnForm() {
245
168
  <VStack gap={2}>
246
169
  <Text type="label">What are you reaching out about?</Text>
247
170
  <HStack gap={2} wrap="wrap">
248
- {INQUIRY_REASONS.map(reason => (
249
- <Token
250
- key={reason}
251
- label={reason}
252
- color={inquiryReason === reason ? 'blue' : 'default'}
253
- onClick={() =>
254
- setInquiryReason(prev =>
255
- prev === reason ? '' : reason,
256
- )
257
- }
258
- />
259
- ))}
171
+ {INQUIRY_REASONS.map(reason => {
172
+ const isSelected = inquiryReason === reason;
173
+ return (
174
+ <Token
175
+ key={reason}
176
+ label={reason}
177
+ color={isSelected ? 'yellow' : 'default'}
178
+ icon={
179
+ isSelected ? (
180
+ <Icon icon={Check} size="xsm" color="inherit" />
181
+ ) : undefined
182
+ }
183
+ description={isSelected ? 'Selected' : undefined}
184
+ onClick={() =>
185
+ setInquiryReason(prev =>
186
+ prev === reason ? '' : reason,
187
+ )
188
+ }
189
+ />
190
+ );
191
+ })}
260
192
  </HStack>
261
193
  </VStack>
262
194
  <Selector
@@ -264,12 +196,12 @@ export default function TwoColumnForm() {
264
196
  options={BUDGET_OPTIONS}
265
197
  value={budget}
266
198
  onChange={setBudget}
267
- placeholder="Select a budget range..."
199
+ placeholder="Select a budget range…"
268
200
  />
269
201
  <TextArea
270
202
  label="Project details"
271
- isLabelHidden
272
- placeholder="Project details*"
203
+ isRequired
204
+ placeholder="Project details"
273
205
  value={details}
274
206
  onChange={setDetails}
275
207
  status={
@@ -278,6 +210,13 @@ export default function TwoColumnForm() {
278
210
  : undefined
279
211
  }
280
212
  />
213
+ {submitted && isValid && (
214
+ <Banner
215
+ status="success"
216
+ title="Sent into the night"
217
+ description="The coven received your message and will answer after dark."
218
+ />
219
+ )}
281
220
  {/* hAlign="stretch" = full-width button workaround; Button
282
221
  has no full-width prop (#2600). */}
283
222
  <VStack hAlign="stretch">
@@ -291,7 +230,6 @@ export default function TwoColumnForm() {
291
230
  </Card>
292
231
  </Grid>
293
232
 
294
- {/* Contact strip; stacks below ~440px. */}
295
233
  <VStack gap={6}>
296
234
  <Divider />
297
235
  <Grid columns={{minWidth: 200}} gap={6}>
@@ -300,7 +238,7 @@ export default function TwoColumnForm() {
300
238
  <Text type="supporting" color="secondary">
301
239
  {col.label}
302
240
  </Text>
303
- <Link href={`mailto:${col.email}`} type="body" size="sm">
241
+ <Link href={`mailto:${col.email}`}>
304
242
  {col.email}
305
243
  </Link>
306
244
  </VStack>