astryx-dracula 0.2.1 → 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 +17 -12
  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 +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  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 +299 -237
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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] > 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=label] + Tx"Kind"[t=supporting]) + (ML"Information" > MLI*3)))
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))))
4
4
 
5
5
  import {useState, useMemo, type CSSProperties} from 'react';
6
6
  import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -9,9 +9,11 @@ import {List, ListItem} from '@astryxdesign/core/List';
9
9
  import {HStack, VStack} from '@astryxdesign/core/Layout';
10
10
  import {useMediaQuery} from '@astryxdesign/core/hooks';
11
11
  import {Text} from '@astryxdesign/core/Text';
12
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
12
13
  import {Icon} from '@astryxdesign/core/Icon';
13
14
  import {IconButton} from '@astryxdesign/core/IconButton';
14
15
  import {Section} from '@astryxdesign/core/Section';
16
+ import {EmptyState} from '@astryxdesign/core/EmptyState';
15
17
  import {Avatar} from '@astryxdesign/core/Avatar';
16
18
  import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
17
19
  import {
@@ -264,13 +266,85 @@ const page: CSSProperties = {height: '100dvh'};
264
266
  const columnRow: CSSProperties = {overflowX: 'auto', overflowY: 'hidden'};
265
267
  const scrollable: CSSProperties = {overflowY: 'auto'};
266
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.
267
272
  const detailColumn: CSSProperties = {
268
273
  flexGrow: 1,
269
274
  flexShrink: 0,
270
- flexBasis: 320,
271
275
  };
272
276
  const controlsScroll: CSSProperties = {overflowX: 'auto'};
273
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
+
274
348
  function findItem(items: FileSystemItem[], id: string): FileSystemItem | null {
275
349
  for (const item of items) {
276
350
  if (item.id === id) {
@@ -299,16 +373,27 @@ export default function FileExplorer() {
299
373
  ]);
300
374
 
301
375
  const isNarrow = useMediaQuery('(max-width: 768px)');
302
- const isPhone = useMediaQuery('(max-width: 767px)');
376
+ const isPhone = isNarrow;
303
377
 
304
378
  const columns = useMemo(() => {
305
- const cols: {items: FileSystemItem[]; selectedId: string | null}[] = [];
306
- 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
+ });
307
391
  let currentItems: FileSystemItem[] = FILESYSTEM;
308
392
  for (let i = 0; i < selectedPath.length; i++) {
309
393
  const selected = currentItems.find(item => item.id === selectedPath[i]);
310
394
  if (selected?.children && selected.children.length > 0) {
311
395
  cols.push({
396
+ id: selectedPath[i],
312
397
  items: selected.children,
313
398
  selectedId: selectedPath[i + 1] ?? null,
314
399
  });
@@ -383,76 +468,6 @@ export default function FileExplorer() {
383
468
  </Text>
384
469
  </>
385
470
  );
386
- const viewSwitcher = (
387
- <SegmentedControl
388
- value="column"
389
- onChange={() => {}}
390
- label="View mode">
391
- <SegmentedControlItem
392
- value="grid"
393
- label="Grid"
394
- icon={<Icon icon={LayoutGrid} size="sm" />}
395
- isLabelHidden
396
- />
397
- <SegmentedControlItem
398
- value="list"
399
- label="List"
400
- icon={<Icon icon={Rows3} size="sm" />}
401
- isLabelHidden
402
- />
403
- <SegmentedControlItem
404
- value="column"
405
- label="Column"
406
- icon={<Icon icon={Columns} size="sm" />}
407
- isLabelHidden
408
- />
409
- <SegmentedControlItem
410
- value="gallery"
411
- label="Gallery"
412
- icon={<Icon icon={Table} size="sm" />}
413
- isLabelHidden
414
- />
415
- </SegmentedControl>
416
- );
417
- const fileActions = (
418
- <>
419
- <IconButton
420
- variant="ghost"
421
- size="sm"
422
- icon={<Icon icon={SlidersHorizontal} size="sm" />}
423
- label="Group"
424
- tooltip="Group"
425
- />
426
- <IconButton
427
- variant="ghost"
428
- size="sm"
429
- icon={<Icon icon={Share2} size="sm" />}
430
- label="Share"
431
- tooltip="Share"
432
- />
433
- <IconButton
434
- variant="ghost"
435
- size="sm"
436
- icon={<Icon icon={Tag} size="sm" />}
437
- label="Tags"
438
- tooltip="Tags"
439
- />
440
- <IconButton
441
- variant="ghost"
442
- size="sm"
443
- icon={<Icon icon={Ellipsis} size="sm" />}
444
- label="More"
445
- tooltip="More"
446
- />
447
- <IconButton
448
- variant="ghost"
449
- size="sm"
450
- icon={<Icon icon={Search} size="sm" />}
451
- label="Search"
452
- tooltip="Search"
453
- />
454
- </>
455
- );
456
471
 
457
472
  return (
458
473
  <Layout
@@ -473,7 +488,13 @@ export default function FileExplorer() {
473
488
  variant="transparent"
474
489
  padding={2}
475
490
  dividers={['bottom']}>
476
- <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}>
477
498
  {viewSwitcher}
478
499
  {fileActions}
479
500
  </HStack>
@@ -483,6 +504,9 @@ export default function FileExplorer() {
483
504
  }
484
505
  content={
485
506
  <LayoutContent padding={0} isScrollable={false}>
507
+ <VisuallyHidden as="h1">
508
+ File Explorer
509
+ </VisuallyHidden>
486
510
  <HStack height="100%" style={columnRow}>
487
511
  {visibleColumns.map((col, colIndex) => {
488
512
  const trueIndex = columnOffset + colIndex;
@@ -490,12 +514,22 @@ export default function FileExplorer() {
490
514
  trueIndex < columns.length - 1 || selectedFile != null;
491
515
  return (
492
516
  <Section
493
- key={trueIndex}
517
+ key={col.id}
494
518
  width={240}
495
519
  padding={2}
496
520
  variant="transparent"
497
521
  dividers={showDivider ? ['end'] : undefined}
498
- 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
+ ) : (
499
533
  <List density="compact" hasDividers={false}>
500
534
  {col.items.map(item => {
501
535
  const isSelected = col.selectedId === item.id;
@@ -539,18 +573,29 @@ export default function FileExplorer() {
539
573
  );
540
574
  })}
541
575
  </List>
576
+ )}
542
577
  </Section>
543
578
  );
544
579
  })}
545
580
  {selectedFile && (
546
581
  <Section
547
- 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}
548
590
  variant="transparent"
549
- style={{...scrollable, ...detailColumn}}>
591
+ style={{...scrollable, ...detailColumn}}
592
+ role="region"
593
+ aria-label={`Details for ${selectedFile.name}`}
594
+ tabIndex={0}>
550
595
  <VStack gap={4} hAlign="center">
551
596
  <Avatar name={selectedFile.name} size={96} />
552
597
  <VStack gap={1} hAlign="center">
553
- <Text type="label" maxLines={1}>
598
+ <Text type="body" weight="semibold" maxLines={1}>
554
599
  {selectedFile.name}
555
600
  </Text>
556
601
  <Text type="supporting" color="secondary">
@@ -559,13 +604,15 @@ export default function FileExplorer() {
559
604
  </VStack>
560
605
  <MetadataList title="Information">
561
606
  <MetadataListItem label="Created">
562
- March 28, 2026 at 2:15 PM
607
+ <Text type="body">March 28, 2026 at 2:15 PM</Text>
563
608
  </MetadataListItem>
564
609
  <MetadataListItem label="Modified">
565
- Yesterday, 10:27 PM
610
+ <Text type="body">Yesterday, 10:27 PM</Text>
566
611
  </MetadataListItem>
567
612
  <MetadataListItem label="Kind">
568
- {getFileExtension(selectedFile.name)} Document
613
+ <Text type="body">
614
+ {getFileExtension(selectedFile.name)} Document
615
+ </Text>
569
616
  </MetadataListItem>
570
617
  </MetadataList>
571
618
  </VStack>
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // Ctr > S.transparent[p=10] > V[g=10] > (G[c={min:320} 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=8] > V[g=4] > Tx"Your details"[t=label] + TI"Full name" + (G[c={min:180} g=3] > TI"Email" + 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"*3)) + 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))
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))
4
4
 
5
5
  import {useState, type CSSProperties} from 'react';
6
6
  import {VStack, HStack} from '@astryxdesign/core/Layout';
@@ -8,6 +8,7 @@ import {Center} from '@astryxdesign/core/Center';
8
8
  import {Section} from '@astryxdesign/core/Section';
9
9
  import {Grid} from '@astryxdesign/core/Grid';
10
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
11
+ import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
11
12
  import {Button} from '@astryxdesign/core/Button';
12
13
  import {Text, Heading} from '@astryxdesign/core/Text';
13
14
  import {TextInput} from '@astryxdesign/core/TextInput';
@@ -17,91 +18,10 @@ import {Link} from '@astryxdesign/core/Link';
17
18
  import {Divider} from '@astryxdesign/core/Divider';
18
19
  import {Card} from '@astryxdesign/core/Card';
19
20
  import {Selector} from '@astryxdesign/core/Selector';
20
-
21
- function CastleIllustration() {
22
- return (
23
- <svg
24
- style={illustrationImg}
25
- viewBox="0 0 400 300"
26
- role="img"
27
- aria-label="Moonlit castle where the night crew works">
28
- <title>Castle office after dark</title>
29
- <rect width="400" height="300" fill="var(--dracula-bg-dark)" />
30
- <g fill="var(--dracula-purple)">
31
- <circle cx="40" cy="40" r="2.5" />
32
- <circle cx="120" cy="70" r="2" />
33
- <circle cx="210" cy="36" r="2.5" />
34
- <circle cx="330" cy="60" r="2" />
35
- </g>
36
- <g fill="var(--dracula-cyan)">
37
- <circle cx="80" cy="55" r="2" />
38
- <circle cx="170" cy="60" r="2" />
39
- <circle cx="290" cy="36" r="2.5" />
40
- </g>
41
- <circle cx="315" cy="70" r="34" fill="var(--dracula-yellow)" />
42
- <circle
43
- cx="304"
44
- cy="61"
45
- r="6"
46
- fill="var(--dracula-orange)"
47
- opacity={0.5}
48
- />
49
- <g
50
- fill="none"
51
- stroke="var(--dracula-purple)"
52
- strokeWidth={3}
53
- strokeLinecap="round">
54
- <path d="M90 100 q9 -9 18 0 q9 -9 18 0" />
55
- <path d="M180 84 q9 -9 18 0 q9 -9 18 0" />
56
- </g>
57
- <path
58
- d="M0 220 Q140 180 260 210 T400 200 V300 H0 Z"
59
- fill="var(--dracula-selection)"
60
- />
61
- <path
62
- d="M0 255 Q160 225 320 250 T400 245 V300 H0 Z"
63
- fill="var(--dracula-bg-light)"
64
- />
65
- <g>
66
- <rect
67
- x="140"
68
- y="150"
69
- width="120"
70
- height="100"
71
- rx={4}
72
- fill="var(--dracula-bg-lighter)"
73
- />
74
- <rect
75
- x="122"
76
- y="122"
77
- width="38"
78
- height="128"
79
- rx={4}
80
- fill="var(--dracula-bg-lighter)"
81
- />
82
- <rect
83
- x="240"
84
- y="122"
85
- width="38"
86
- height="128"
87
- rx={4}
88
- fill="var(--dracula-bg-lighter)"
89
- />
90
- <g fill="var(--dracula-yellow)">
91
- <rect x="156" y="172" width="12" height="18" rx={2} />
92
- <rect x="186" y="172" width="12" height="18" rx={2} />
93
- <rect x="216" y="172" width="12" height="18" rx={2} />
94
- <rect x="132" y="146" width="9" height="12" rx={2} />
95
- <rect x="251" y="146" width="9" height="12" rx={2} />
96
- </g>
97
- <path
98
- d="M185 250 v-24 a15 15 0 0 1 30 0 v24 Z"
99
- fill="var(--dracula-bg-dark)"
100
- />
101
- </g>
102
- </svg>
103
- );
104
- }
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';
105
25
 
106
26
  const INQUIRY_REASONS = [
107
27
  'New business',
@@ -128,17 +48,12 @@ const CONTACT_COLUMNS = [
128
48
  {label: 'Press & partnerships', email: 'press@castle.dracula'},
129
49
  ];
130
50
 
131
- // AspectRatio has no radius prop and there's no Image primitive (#2582), so
132
- // 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).
133
54
  const pageStyle: CSSProperties = {
134
55
  minHeight: '100%',
135
56
  };
136
- const illustrationImg: CSSProperties = {
137
- width: '100%',
138
- height: '100%',
139
- display: 'block',
140
- borderRadius: 'var(--radius-container)',
141
- };
142
57
 
143
58
  /**
144
59
  * Form (Two-column) — marketing contact form template.
@@ -147,7 +62,12 @@ const illustrationImg: CSSProperties = {
147
62
  * Top: two-column — left has headline + description + illustration,
148
63
  * right has the contact form on a card.
149
64
  * Bottom: three-column contact info strip.
150
- * 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.
151
71
  */
152
72
  export default function TwoColumnForm() {
153
73
  const [fullName, setFullName] = useState('');
@@ -168,14 +88,18 @@ export default function TwoColumnForm() {
168
88
  }
169
89
  : {};
170
90
 
91
+ const isValid =
92
+ fullName.trim() !== '' &&
93
+ email.trim() !== '' &&
94
+ details.trim() !== '';
95
+
171
96
  const handleSubmit = () => setSubmitted(true);
172
97
 
173
98
  return (
174
99
  <Center style={pageStyle}>
175
100
  <Section maxWidth={1100} width="100%" padding={10} variant="transparent">
176
101
  <VStack gap={10}>
177
- {/* Two-column; stacks to one column below ~520px. */}
178
- <Grid columns={{minWidth: 320}} align="center" gap={10}>
102
+ <Grid columns={{minWidth: 280, max: 2}} align="center" gap={10}>
179
103
  <VStack gap={6}>
180
104
  <VStack gap={3}>
181
105
  <Heading level={1} type="display-1">
@@ -186,18 +110,18 @@ export default function TwoColumnForm() {
186
110
  the best path through the night.
187
111
  </Text>
188
112
  </VStack>
189
- <AspectRatio ratio={4 / 3}>
190
- <CastleIllustration />
113
+ <AspectRatio ratio={4 / 3} style={galleryImageClip}>
114
+ <SceneCastle variant="card" />
191
115
  </AspectRatio>
192
116
  </VStack>
193
117
 
194
- <Card padding={8}>
118
+ <Card padding={4}>
195
119
  <VStack gap={4}>
196
- <Text type="label">Your details</Text>
120
+ <Heading level={2}>Your details</Heading>
197
121
  <TextInput
198
122
  label="Full name"
199
- isLabelHidden
200
- placeholder="Full name*"
123
+ isRequired
124
+ placeholder="Full name"
201
125
  value={fullName}
202
126
  onChange={setFullName}
203
127
  status={
@@ -209,8 +133,8 @@ export default function TwoColumnForm() {
209
133
  <Grid columns={{minWidth: 180}} gap={3}>
210
134
  <TextInput
211
135
  label="Email"
212
- isLabelHidden
213
- placeholder="Email*"
136
+ isRequired
137
+ placeholder="Email"
214
138
  value={email}
215
139
  onChange={setEmail}
216
140
  status={
@@ -221,7 +145,6 @@ export default function TwoColumnForm() {
221
145
  />
222
146
  <TextInput
223
147
  label="Company name"
224
- isLabelHidden
225
148
  placeholder="Company name"
226
149
  value={company}
227
150
  onChange={setCompany}
@@ -230,14 +153,12 @@ export default function TwoColumnForm() {
230
153
  <Grid columns={{minWidth: 180}} gap={3}>
231
154
  <TextInput
232
155
  label="Job title"
233
- isLabelHidden
234
156
  placeholder="Job title"
235
157
  value={jobTitle}
236
158
  onChange={setJobTitle}
237
159
  />
238
160
  <TextInput
239
161
  label="Phone number"
240
- isLabelHidden
241
162
  placeholder="Phone number"
242
163
  value={phone}
243
164
  onChange={setPhone}
@@ -247,18 +168,27 @@ export default function TwoColumnForm() {
247
168
  <VStack gap={2}>
248
169
  <Text type="label">What are you reaching out about?</Text>
249
170
  <HStack gap={2} wrap="wrap">
250
- {INQUIRY_REASONS.map(reason => (
251
- <Token
252
- key={reason}
253
- label={reason}
254
- color={inquiryReason === reason ? 'yellow' : 'default'}
255
- onClick={() =>
256
- setInquiryReason(prev =>
257
- prev === reason ? '' : reason,
258
- )
259
- }
260
- />
261
- ))}
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
+ })}
262
192
  </HStack>
263
193
  </VStack>
264
194
  <Selector
@@ -266,12 +196,12 @@ export default function TwoColumnForm() {
266
196
  options={BUDGET_OPTIONS}
267
197
  value={budget}
268
198
  onChange={setBudget}
269
- placeholder="Select a budget range..."
199
+ placeholder="Select a budget range…"
270
200
  />
271
201
  <TextArea
272
202
  label="Project details"
273
- isLabelHidden
274
- placeholder="Project details*"
203
+ isRequired
204
+ placeholder="Project details"
275
205
  value={details}
276
206
  onChange={setDetails}
277
207
  status={
@@ -280,6 +210,13 @@ export default function TwoColumnForm() {
280
210
  : undefined
281
211
  }
282
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
+ )}
283
220
  {/* hAlign="stretch" = full-width button workaround; Button
284
221
  has no full-width prop (#2600). */}
285
222
  <VStack hAlign="stretch">
@@ -293,7 +230,6 @@ export default function TwoColumnForm() {
293
230
  </Card>
294
231
  </Grid>
295
232
 
296
- {/* Contact strip; stacks below ~440px. */}
297
233
  <VStack gap={6}>
298
234
  <Divider />
299
235
  <Grid columns={{minWidth: 200}} gap={6}>
@@ -302,7 +238,7 @@ export default function TwoColumnForm() {
302
238
  <Text type="supporting" color="secondary">
303
239
  {col.label}
304
240
  </Text>
305
- <Link href={`mailto:${col.email}`} type="body" size="sm">
241
+ <Link href={`mailto:${col.email}`}>
306
242
  {col.email}
307
243
  </Link>
308
244
  </VStack>