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,10 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // TN"Nocturne" + (S[p6] > V[g10] > (Ctr > V[g4 a=center] > Hd"Little haunts"[level=1 type=display-2] + Tx"We believe"[t=body]) + (G[c={min:200,max:3} g4] > (C[p0] > AR + V[g2 a=center] > Bd + Hd"Product"[level=2] + Tx"Description"[t=body] + (H[g2] > NI + B"Add to cart"))*3)) + (V[g8] > (G[c={min:200} g4] > (GS[c=1] > C > Hd"Checkout"[level=2]) + (GS[c=2] > C > Hd"Night Owl AI"[level=2])) + (G[c={min:200} g4] > (GS[c=3] > C > T) + (GS[c=1] > C > Hd"Revenue"[level=2])))
4
+
5
+ /**
6
+ * Theme Showcase — the storefront page each theme is previewed against, and the only template that is deliberately shell-less: it paints its o (Frame/responsive/container: see XLE header above.)
7
+ */
2
8
 
3
9
  import {type CSSProperties, type ReactNode} from 'react';
4
10
  import {
@@ -33,6 +39,10 @@ import {Divider} from '@astryxdesign/core/Divider';
33
39
  import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
34
40
  import {Item} from '@astryxdesign/core/Item';
35
41
  import {Table, proportional, pixel} from '@astryxdesign/core/Table';
42
+ import type {TableColumn} from '@astryxdesign/core/Table';
43
+ import {StatusDot} from '@astryxdesign/core/StatusDot';
44
+ import {ProductSwatch} from 'astryx-dracula/shared/revenue-chart';
45
+ import {useMediaQuery} from '@astryxdesign/core/hooks';
36
46
  import {TextInput} from '@astryxdesign/core/TextInput';
37
47
  import {Selector} from '@astryxdesign/core/Selector';
38
48
  import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
@@ -51,6 +61,7 @@ import {
51
61
  ChatMessageList,
52
62
  ChatSystemMessage,
53
63
  } from '@astryxdesign/core/Chat';
64
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
54
65
 
55
66
  // Styles passed to Astryx components via their `style` prop. Astryx components
56
67
  // forward the DOM `style` prop, so these work with no CSS compiler — in
@@ -98,6 +109,10 @@ const styles: Record<string, CSSProperties> = {
98
109
  minWidth: 0,
99
110
  maxWidth: 240,
100
111
  },
112
+ filterRowFill: {
113
+ flex: 1,
114
+ minWidth: 0,
115
+ },
101
116
  activityCard: {
102
117
  backgroundColor: 'var(--color-background-surface)',
103
118
  color: 'var(--color-text-primary)',
@@ -119,6 +134,9 @@ const styles: Record<string, CSSProperties> = {
119
134
  activityCardStack: {
120
135
  height: '100%',
121
136
  },
137
+ inventoryItemText: {
138
+ minWidth: 0,
139
+ },
122
140
  activityListFade: {
123
141
  flex: 1,
124
142
  minHeight: 0,
@@ -129,13 +147,11 @@ const styles: Record<string, CSSProperties> = {
129
147
  'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
130
148
  marginInline: 'calc(var(--spacing-2) * -1)',
131
149
  },
132
- content: {
133
- maxWidth: 960,
134
- marginInline: 'auto',
135
- minWidth: 0,
136
- },
150
+ // The hero column: centered, narrower than the grid below it.
137
151
  contentFluid: {
138
152
  maxWidth: 880,
153
+ marginInline: 'auto',
154
+ minWidth: 0,
139
155
  },
140
156
  heroText: {
141
157
  textAlign: 'center' as const,
@@ -149,6 +165,17 @@ const styles: Record<string, CSSProperties> = {
149
165
  },
150
166
  cardDescription: {
151
167
  flex: 1,
168
+ textAlign: 'center' as const,
169
+ },
170
+ // Store surfaces: the shell is intentionally absent, so the page paints its
171
+ // own body and card-showcase bands.
172
+ storeRoot: {
173
+ minHeight: '100%',
174
+ backgroundColor: 'var(--color-background-body)',
175
+ },
176
+ showcaseBand: {
177
+ padding: 'var(--spacing-6)',
178
+ backgroundColor: 'var(--color-background-surface)',
152
179
  },
153
180
  quantityInput: {
154
181
  // minWidth (not a hard width) so the field grows to fit the digit + the
@@ -173,7 +200,10 @@ const inlineStyles: Record<string, CSSProperties> = {
173
200
  chatBody: {
174
201
  flex: 1,
175
202
  minHeight: 0,
176
- overflow: 'hidden',
203
+ // The message region is the scroll owner: the card is stretched to the
204
+ // grid row height, so longer conversations must scroll here rather than
205
+ // clip against the card.
206
+ overflowY: 'auto' as const,
177
207
  },
178
208
  chatSuggestions: {
179
209
  paddingInline: 'var(--spacing-4)',
@@ -183,22 +213,21 @@ const inlineStyles: Record<string, CSSProperties> = {
183
213
  paddingInline: 'var(--spacing-4)',
184
214
  paddingBottom: 'var(--spacing-4)',
185
215
  },
216
+ // Center supplies the 32px box and the centering; only the paint stays here.
217
+ // Surface, not muted: --color-text-secondary #9AA1BC is 3.57:1 on
218
+ // --color-background-muted #44475A, under the 4.5:1 text floor. The chip is
219
+ // aria-hidden decorative art so it clears the 3:1 non-text bar as-is, but the
220
+ // surface fill carries the same 4.60:1 pairing at no cost and cannot drop
221
+ // under the floor if anything smaller is ever put in the box.
186
222
  activityIcon: {
187
- width: 32,
188
- height: 32,
189
- borderRadius: 'var(--radius-full)',
190
- backgroundColor: 'var(--color-background-muted)',
223
+ borderRadius: 'var(--radius-element)', // square chip: no circles except dots/avatars
224
+ backgroundColor: 'var(--color-background-surface)',
191
225
  color: 'var(--color-text-secondary)',
192
- display: 'flex',
193
- alignItems: 'center',
194
- justifyContent: 'center',
195
226
  flexShrink: 0,
196
227
  },
197
228
  cardBody: {
198
229
  padding: 'var(--spacing-4)',
199
230
  flex: 1,
200
- display: 'flex',
201
- flexDirection: 'column',
202
231
  },
203
232
  };
204
233
 
@@ -209,20 +238,23 @@ const artImage: CSSProperties = {
209
238
  height: '100%',
210
239
  display: 'block',
211
240
  };
212
- // Rounded inventory swatch. No radius prop where the swatch renders, so the
213
- // SVG carries its own rounded corners (rx=4).
214
- const thumbSwatch: CSSProperties = {
215
- flexShrink: 0,
216
- display: 'block',
217
- };
218
241
 
219
- // One Dracula accent per hero product slot.
242
+ // One Dracula accent per hero product slot. Purple stays off decorative art:
243
+ // it reads as interactive, and nothing here is tappable.
220
244
  const PRODUCT_HUES = [
221
- 'var(--dracula-purple)',
245
+ 'var(--dracula-green)',
222
246
  'var(--dracula-cyan)',
223
247
  'var(--dracula-yellow)',
224
248
  ];
225
249
 
250
+ // Grid recipes the store renders at every width above the phone layout. Kept
251
+ // at module scope: an object literal rebuilt per render is a new value prop
252
+ // every time.
253
+ const SHOWCASE_COLUMNS = {minWidth: 200, repeat: 'fit'} as const;
254
+ const PRODUCT_COLUMNS = {minWidth: 200, max: 3} as const;
255
+ const PAYMENT_COLUMNS = {minWidth: 70, max: 3} as const;
256
+ const EXPIRY_COLUMNS = {minWidth: 90, max: 2} as const;
257
+
226
258
  /** Categorical badge variants usable for showcase product/inventory tags. */
227
259
  export type ShowcaseBadgeVariant =
228
260
  | 'blue'
@@ -247,7 +279,7 @@ const DEFAULT_PRODUCTS: ProductSpec[] = [
247
279
  name: 'Moonphase Watch',
248
280
  description: 'Clean lines and lume that carry through the longest night.',
249
281
  badge: 'New',
250
- badgeVariant: 'blue',
282
+ badgeVariant: 'cyan',
251
283
  },
252
284
  {
253
285
  name: 'Night-Owl Headphones',
@@ -305,31 +337,6 @@ function ProductArt({hue, label}: {hue: string; label: string}) {
305
337
  );
306
338
  }
307
339
 
308
- // Rounded inventory swatch: Dracula surface with a per-row accent ring.
309
- function ThumbSwatch({hue, label}: {hue: string; label: string}) {
310
- return (
311
- <svg
312
- viewBox="0 0 40 40"
313
- width={40}
314
- height={40}
315
- style={thumbSwatch}
316
- role="img"
317
- aria-label={`${label} swatch`}>
318
- <rect width={40} height={40} rx={5} fill="var(--dracula-bg-light)" />
319
- <rect
320
- x={1}
321
- y={1}
322
- width={38}
323
- height={38}
324
- rx={4}
325
- fill="none"
326
- stroke="var(--color-widget-content-border)"
327
- />
328
- <circle cx={20} cy={20} r={8} fill="none" stroke={hue} strokeWidth={3} />
329
- </svg>
330
- );
331
- }
332
-
333
340
  export interface ThemeShowcaseProps {
334
341
  /** The three hero product cards. Defaults to the nocturne store products. */
335
342
  products?: ProductSpec[];
@@ -351,20 +358,12 @@ export function ThemeShowcaseStore({
351
358
  }: ThemeShowcaseProps = {}) {
352
359
  const {isMobile} = useAppShellMobile();
353
360
  return (
354
- <div
355
- style={{
356
- minHeight: '100%',
357
- backgroundColor: 'var(--color-background-body)',
358
- }}>
361
+ <VStack gap={0} style={styles.storeRoot}>
359
362
  <StorePreview products={products} isMobile={isMobile} />
360
- <div
361
- style={{
362
- padding: 'var(--spacing-6)',
363
- backgroundColor: 'var(--color-background-surface)',
364
- }}>
363
+ <VStack gap={0} style={styles.showcaseBand}>
365
364
  <CardShowcase inventory={inventory} isMobile={isMobile} />
366
- </div>
367
- </div>
365
+ </VStack>
366
+ </VStack>
368
367
  );
369
368
  }
370
369
 
@@ -375,7 +374,7 @@ function CardShowcase({
375
374
  inventory: InventoryRow[];
376
375
  isMobile: boolean;
377
376
  }) {
378
- const columns = isMobile ? 1 : ({minWidth: 200, repeat: 'fit'} as const);
377
+ const columns = isMobile ? 1 : SHOWCASE_COLUMNS;
379
378
 
380
379
  return (
381
380
  <VStack gap={8}>
@@ -406,14 +405,18 @@ function StorePreview({
406
405
  products: ProductSpec[];
407
406
  isMobile: boolean;
408
407
  }) {
408
+ // Shell-less preview: no AppShell above means the host context always
409
+ // reports desktop, so the nav reads the real viewport instead.
410
+ const isNarrowViewport = useMediaQuery('(max-width: 640px)');
411
+ const collapseNav = isMobile || isNarrowViewport;
409
412
  return (
410
- <div data-theme-preview="true">
413
+ <VStack gap={0} data-theme-preview="true">
411
414
  <VStack gap={0}>
412
415
  <TopNav
413
416
  label="Theme preview navigation"
414
417
  heading={<TopNavHeading heading="Nocturne" />}
415
418
  centerContent={
416
- isMobile ? undefined : (
419
+ collapseNav ? undefined : (
417
420
  <>
418
421
  <TopNavItem label="Shop" href="#/templates/theme-showcase" isSelected />
419
422
  <TopNavItem label="New In" href="#/templates/theme-showcase" />
@@ -423,47 +426,58 @@ function StorePreview({
423
426
  )
424
427
  }
425
428
  endContent={
426
- <HStack gap={2} vAlign="center">
427
- <HStack gap={0.5}>
428
- <Button
429
- label="Search"
430
- tooltip="Search"
431
- variant="ghost"
432
- isIconOnly
433
- icon={<Search size={20} />}
434
- href="#/templates/theme-showcase"
435
- />
436
- <Button
437
- label="Account"
438
- tooltip="Account"
439
- variant="ghost"
440
- isIconOnly
441
- icon={<User size={20} />}
442
- href="#/templates/theme-showcase"
443
- />
444
- <Button
445
- label="Cart"
446
- tooltip="Cart"
447
- variant="ghost"
448
- isIconOnly
449
- icon={<ShoppingBag size={20} />}
450
- href="#/templates/theme-showcase"
451
- />
429
+ collapseNav ? (
430
+ <Button
431
+ label="Cart"
432
+ tooltip="Cart"
433
+ variant="ghost"
434
+ isIconOnly
435
+ icon={<ShoppingBag size={20} />}
436
+ href="#/templates/theme-showcase"
437
+ />
438
+ ) : (
439
+ <HStack gap={2} vAlign="center">
440
+ <HStack gap={0.5}>
441
+ <Button
442
+ label="Search"
443
+ tooltip="Search"
444
+ variant="ghost"
445
+ isIconOnly
446
+ icon={<Search size={20} />}
447
+ href="#/templates/theme-showcase"
448
+ />
449
+ <Button
450
+ label="Account"
451
+ tooltip="Account"
452
+ variant="ghost"
453
+ isIconOnly
454
+ icon={<User size={20} />}
455
+ href="#/templates/theme-showcase"
456
+ />
457
+ <Button
458
+ label="Cart"
459
+ tooltip="Cart"
460
+ variant="ghost"
461
+ isIconOnly
462
+ icon={<ShoppingBag size={20} />}
463
+ href="#/templates/theme-showcase"
464
+ />
465
+ </HStack>
466
+ <Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
452
467
  </HStack>
453
- <Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
454
- </HStack>
468
+ )
455
469
  }
456
470
  />
457
471
 
458
472
  <Section padding={6} variant="transparent">
459
- <VStack gap={10} style={{...styles.content, ...styles.contentFluid}}>
473
+ <VStack gap={10} style={styles.contentFluid}>
460
474
  <Center>
461
475
  <VStack gap={4} hAlign="center" style={styles.heroText}>
462
- <Text type="display-2" color="accent">
476
+ <Heading level={1} type="display-2">
463
477
  Little haunts,
464
478
  <br />
465
479
  everywhere you roam
466
- </Text>
480
+ </Heading>
467
481
  <Text type="body" color="secondary">
468
482
  We believe the smallest shadows are the ones that matter most.
469
483
  Turn an ordinary evening into something worth remembering.
@@ -471,7 +485,7 @@ function StorePreview({
471
485
  </VStack>
472
486
  </Center>
473
487
 
474
- <Grid columns={isMobile ? 1 : {minWidth: 200, max: 3}} gap={4}>
488
+ <Grid columns={isMobile ? 1 : PRODUCT_COLUMNS} gap={4}>
475
489
  {products.map((p, i) => (
476
490
  <Card key={p.name} padding={0} height="100%">
477
491
  <VStack gap={0} style={styles.cardStack}>
@@ -481,44 +495,42 @@ function StorePreview({
481
495
  label={p.name}
482
496
  />
483
497
  </AspectRatio>
484
- <div style={inlineStyles.cardBody}>
485
- <VStack gap={2} hAlign="center" style={styles.cardStack}>
486
- <HStack>
487
- <Badge label={p.badge} variant={p.badgeVariant} />
488
- </HStack>
489
- <Heading level={2} style={styles.centerText}>
490
- {p.name}
491
- </Heading>
492
- <Text
493
- type="body"
494
- color="secondary"
495
- style={{
496
- ...styles.cardDescription,
497
- ...styles.centerText,
498
- }}>
499
- {p.description}
500
- </Text>
501
- <HStack gap={2} vAlign="center" hAlign="center">
502
- <NumberInput
503
- label="Quantity"
504
- isLabelHidden
505
- value={1}
506
- onChange={() => {}}
507
- min={1}
508
- max={99}
509
- size="sm"
510
- style={styles.quantityInput}
511
- />
512
- <Button
513
- label="Add to cart"
514
- variant="secondary"
515
- size="sm"
516
- href="#/templates/theme-showcase"
517
- style={styles.cartButton}
518
- />
519
- </HStack>
520
- </VStack>
521
- </div>
498
+ <VStack
499
+ gap={2}
500
+ hAlign="center"
501
+ style={inlineStyles.cardBody}>
502
+ <HStack>
503
+ <Badge label={p.badge} variant={p.badgeVariant} />
504
+ </HStack>
505
+ <Heading level={2} style={styles.centerText}>
506
+ {p.name}
507
+ </Heading>
508
+ <Text
509
+ type="body"
510
+ color="secondary"
511
+ style={styles.cardDescription}>
512
+ {p.description}
513
+ </Text>
514
+ <HStack gap={2} vAlign="center" hAlign="center">
515
+ <NumberInput
516
+ label="Quantity"
517
+ isLabelHidden
518
+ value={1}
519
+ onChange={() => {}}
520
+ min={1}
521
+ max={99}
522
+ size="sm"
523
+ style={styles.quantityInput}
524
+ />
525
+ <Button
526
+ label="Add to cart"
527
+ variant="secondary"
528
+ size="sm"
529
+ href="#/templates/theme-showcase"
530
+ style={styles.cartButton}
531
+ />
532
+ </HStack>
533
+ </VStack>
522
534
  </VStack>
523
535
  </Card>
524
536
  ))}
@@ -526,13 +538,13 @@ function StorePreview({
526
538
  </VStack>
527
539
  </Section>
528
540
  </VStack>
529
- </div>
541
+ </VStack>
530
542
  );
531
543
  }
532
544
 
533
545
  function CheckoutCard({isMobile}: {isMobile: boolean}) {
534
546
  return (
535
- <Card padding={5} style={styles.card}>
547
+ <Card padding={4} style={styles.card}>
536
548
  <VStack gap={4} style={styles.checkoutStack}>
537
549
  <Heading level={2}>Checkout</Heading>
538
550
 
@@ -555,7 +567,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
555
567
  label="Economy Shipping"
556
568
  description="Delivered in 5–7 business days"
557
569
  endContent={
558
- <Text type="body" weight="bold" hasTabularNumbers>
570
+ <Text type="body" weight="semibold" hasTabularNumbers>
559
571
  $12.00
560
572
  </Text>
561
573
  }
@@ -565,7 +577,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
565
577
  label="Standard Shipping"
566
578
  description="Delivered in 3–5 business days"
567
579
  endContent={
568
- <Text type="body" weight="bold" hasTabularNumbers>
580
+ <Text type="body" weight="semibold" hasTabularNumbers>
569
581
  $16.00
570
582
  </Text>
571
583
  }
@@ -575,7 +587,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
575
587
  label="Express Shipping"
576
588
  description="Delivered in 1–2 business days"
577
589
  endContent={
578
- <Text type="body" weight="bold" hasTabularNumbers>
590
+ <Text type="body" weight="semibold" hasTabularNumbers>
579
591
  $24.00
580
592
  </Text>
581
593
  }
@@ -583,10 +595,10 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
583
595
  </RadioList>
584
596
 
585
597
  <VStack gap={2} style={styles.checkoutStack}>
586
- <Text type="label" weight="bold">
598
+ <Text type="label" weight="semibold">
587
599
  Payment method
588
600
  </Text>
589
- <Grid columns={isMobile ? 1 : {minWidth: 70, max: 3}} gap={2}>
601
+ <Grid columns={isMobile ? 1 : PAYMENT_COLUMNS} gap={2}>
590
602
  <SelectableCard
591
603
  label="Pay with card"
592
604
  isSelected={true}
@@ -597,7 +609,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
597
609
  hAlign="center"
598
610
  style={styles.paymentCardContent}>
599
611
  <CreditCard size={20} />
600
- <Text type="supporting" weight="bold">
612
+ <Text type="supporting" weight="semibold">
601
613
  Card
602
614
  </Text>
603
615
  </VStack>
@@ -612,7 +624,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
612
624
  hAlign="center"
613
625
  style={styles.paymentCardContent}>
614
626
  <Smartphone size={20} />
615
- <Text type="supporting" weight="bold">
627
+ <Text type="supporting" weight="semibold">
616
628
  Apple Pay
617
629
  </Text>
618
630
  </VStack>
@@ -627,7 +639,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
627
639
  hAlign="center"
628
640
  style={styles.paymentCardContent}>
629
641
  <Wallet size={20} />
630
- <Text type="supporting" weight="bold">
642
+ <Text type="supporting" weight="semibold">
631
643
  Google Pay
632
644
  </Text>
633
645
  </VStack>
@@ -644,7 +656,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
644
656
  size="lg"
645
657
  />
646
658
 
647
- <Grid columns={isMobile ? 1 : {minWidth: 90, max: 2}} gap={2}>
659
+ <Grid columns={isMobile ? 1 : EXPIRY_COLUMNS} gap={2}>
648
660
  <TextInput
649
661
  label="Expiry"
650
662
  placeholder="MM / YY"
@@ -733,26 +745,32 @@ function ChatCard() {
733
745
 
734
746
  <Divider variant="subtle" />
735
747
 
736
- <div style={inlineStyles.chatBody}>
748
+ <VStack
749
+ gap={0}
750
+ style={inlineStyles.chatBody}
751
+ isScrollable={false}
752
+ tabIndex={0}
753
+ role="region"
754
+ aria-label="Night Owl chat messages">
737
755
  <ChatMessageList>
738
756
  <ChatSystemMessage>Today</ChatSystemMessage>
739
757
 
740
758
  <ChatMessage sender="user">
741
759
  <ChatMessageBubble variant="filled">
742
- Where’s my order?
760
+ Where's my order?
743
761
  </ChatMessageBubble>
744
762
  </ChatMessage>
745
763
 
746
764
  <ChatMessage sender="assistant">
747
765
  <VStack gap={3}>
748
766
  <Text type="body">
749
- Your order #1043 — the Moonphase Watch and Belfry Throw —
750
- shipped this morning from the Aisle 3 warehouse and is currently
751
- in transit with UPS. It’s on track to arrive at your address by
767
+ Your order #1043 (the Moonphase Watch and Belfry Throw)
768
+ shipped this morning from the courier-raven roost and is currently
769
+ in transit with courier-raven. It's on track to arrive at your address by
752
770
  end of day tomorrow.
753
771
  </Text>
754
772
  <Text type="body">
755
- Let me know if you’d like to reschedule the delivery, redirect
773
+ Let me know if you'd like to reschedule the delivery, redirect
756
774
  it to a pickup point, or start a return once it arrives.
757
775
  </Text>
758
776
  </VStack>
@@ -766,23 +784,23 @@ function ChatCard() {
766
784
 
767
785
  <ChatMessage sender="assistant">
768
786
  <VStack gap={3}>
769
- <Text type="body">Here’s everything I have on order #1043:</Text>
787
+ <Text type="body">Here's everything I have on order #1043:</Text>
770
788
  <Card padding={3}>
771
789
  <VStack gap={1}>
772
790
  <Item
773
791
  label="Items"
774
792
  description="Moonphase Watch · Belfry Throw"
775
793
  endContent={
776
- <Text type="body" weight="bold" hasTabularNumbers>
794
+ <Text type="body" weight="semibold" hasTabularNumbers>
777
795
  $248
778
796
  </Text>
779
797
  }
780
798
  />
781
799
  <Item
782
800
  label="Shipping"
783
- description="UPS Ground"
801
+ description="Courier-raven ground"
784
802
  endContent={
785
- <Text type="body" weight="bold" hasTabularNumbers>
803
+ <Text type="body" weight="semibold" hasTabularNumbers>
786
804
  $12
787
805
  </Text>
788
806
  }
@@ -790,21 +808,28 @@ function ChatCard() {
790
808
  <Item
791
809
  label="Estimated arrival"
792
810
  description="Tomorrow by 8pm"
793
- endContent={<Badge variant="green" label="On time" />}
811
+ endContent={
812
+ <HStack gap={1} vAlign="center">
813
+ <StatusDot variant="success" label="On time" />
814
+ <Text type="supporting" color="secondary">
815
+ On time
816
+ </Text>
817
+ </HStack>
818
+ }
794
819
  />
795
820
  <Item
796
821
  label="Tracking"
797
- description="UPS 1Z 999 AA1 0123 4567 84"
798
- endContent={<Link href="#/templates/theme-showcase">Track →</Link>}
822
+ description="RVN 1Z 999 AA1 0123 4567 84"
823
+ endContent={<Link href="#/templates/theme-showcase">Track</Link>}
799
824
  />
800
825
  </VStack>
801
826
  </Card>
802
827
  </VStack>
803
828
  </ChatMessage>
804
829
  </ChatMessageList>
805
- </div>
830
+ </VStack>
806
831
 
807
- <div style={inlineStyles.chatSuggestions}>
832
+ <VStack gap={0} style={inlineStyles.chatSuggestions}>
808
833
  <HStack gap={1} hAlign="center" wrap="wrap">
809
834
  {SUGGESTED_QUESTIONS.map(question => (
810
835
  <Button
@@ -815,14 +840,14 @@ function ChatCard() {
815
840
  />
816
841
  ))}
817
842
  </HStack>
818
- </div>
843
+ </VStack>
819
844
 
820
- <div style={inlineStyles.chatComposer}>
845
+ <VStack gap={0} style={inlineStyles.chatComposer}>
821
846
  <ChatComposer
822
847
  value=""
823
848
  onChange={() => {}}
824
849
  onSubmit={() => {}}
825
- placeholder="Ask Night Owl…"
850
+ placeholder="Ask Night Owl..."
826
851
  footerActions={
827
852
  <Button
828
853
  variant="ghost"
@@ -844,7 +869,7 @@ function ChatCard() {
844
869
  />
845
870
  }
846
871
  />
847
- </div>
872
+ </VStack>
848
873
  </Card>
849
874
  );
850
875
  }
@@ -908,13 +933,13 @@ function formatAmount(amount: number): string {
908
933
 
909
934
  function LatestActivityCard({isMobile}: {isMobile: boolean}) {
910
935
  return (
911
- <Card padding={5} style={styles.activityCard}>
936
+ <Card padding={4} style={styles.activityCard}>
912
937
  <VStack gap={4} style={styles.activityCardStack}>
913
938
  <Heading level={2}>Revenue</Heading>
914
939
 
915
940
  <Grid columns={isMobile ? 1 : 2} gap={3}>
916
941
  <VStack gap={0}>
917
- <Text type="display-3" weight="bold" hasTabularNumbers>
942
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
918
943
  18K
919
944
  </Text>
920
945
  <Text type="supporting" color="secondary">
@@ -922,7 +947,7 @@ function LatestActivityCard({isMobile}: {isMobile: boolean}) {
922
947
  </Text>
923
948
  </VStack>
924
949
  <VStack gap={0}>
925
- <Text type="display-3" weight="bold" hasTabularNumbers>
950
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
926
951
  +12%
927
952
  </Text>
928
953
  <Text type="supporting" color="secondary">
@@ -939,20 +964,38 @@ function LatestActivityCard({isMobile}: {isMobile: boolean}) {
939
964
  </HStack>
940
965
 
941
966
  <VStack gap={1} style={styles.activityListFade}>
967
+ {/* The sign travels in the string (formatAmount prefixes U+2212), so
968
+ direction is never colour-alone and 1.4.1 holds without a tint.
969
+ A negative text role is not available here, and the reason is
970
+ worth recording rather than working around:
971
+ - TextColorMap is six keys (primary, secondary, disabled,
972
+ placeholder, accent, inherit). No negative.
973
+ - `accent` is Purple in this theme, and principle 2 makes
974
+ purple tappable-only, so a refund would read as a link.
975
+ - --color-negative #FF5555 measures 3.75:1 on this card
976
+ #343746, under the 4.5:1 floor, so a style-prop tint would
977
+ trade a weak signal for a real contrast failure.
978
+ secondary therefore stays and the minus sign is the signal. A
979
+ kit-wide negative text role needs a new token AND a colour that
980
+ clears 4.5:1 on a card — a human call, not a call-site one. */}
942
981
  {ACTIVITY.map(item => (
943
982
  <Item
944
983
  key={item.id}
945
984
  startContent={
946
- <div style={inlineStyles.activityIcon} aria-hidden="true">
985
+ <Center
986
+ width={32}
987
+ height={32}
988
+ style={inlineStyles.activityIcon}
989
+ aria-hidden="true">
947
990
  {item.icon}
948
- </div>
991
+ </Center>
949
992
  }
950
993
  label={item.label}
951
994
  description={item.detail}
952
995
  endContent={
953
996
  <Text
954
997
  type="body"
955
- weight="bold"
998
+ weight="semibold"
956
999
  hasTabularNumbers
957
1000
  color={item.amount < 0 ? 'secondary' : 'primary'}>
958
1001
  {formatAmount(item.amount)}
@@ -976,7 +1019,7 @@ export interface InventoryRow extends Record<string, unknown> {
976
1019
  available: number;
977
1020
  location: string;
978
1021
  tags: TagSpec[];
979
- hue: string;
1022
+ hue: SceneHue;
980
1023
  selected: boolean;
981
1024
  }
982
1025
 
@@ -986,9 +1029,9 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
986
1029
  name: 'Moonphase Watch',
987
1030
  meta: 'Steel case, moonphase dial',
988
1031
  available: 42,
989
- location: 'Aisle 3',
990
- tags: [{label: 'New', variant: 'blue'}],
991
- hue: 'var(--dracula-purple)',
1032
+ location: 'Roost 3',
1033
+ tags: [{label: 'New', variant: 'cyan'}],
1034
+ hue: 'var(--dracula-comment)',
992
1035
  selected: false,
993
1036
  },
994
1037
  {
@@ -996,7 +1039,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
996
1039
  name: 'Night-Owl Headphones',
997
1040
  meta: 'ANC, 30hr battery',
998
1041
  available: 128,
999
- location: 'Aisle 1',
1042
+ location: 'Roost 1',
1000
1043
  tags: [{label: 'Popular', variant: 'green'}],
1001
1044
  hue: 'var(--dracula-cyan)',
1002
1045
  selected: true,
@@ -1006,7 +1049,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1006
1049
  name: 'Coven Canvas Backpack',
1007
1050
  meta: 'Waxed canvas, 25L',
1008
1051
  available: 63,
1009
- location: 'Aisle 2',
1052
+ location: 'Roost 2',
1010
1053
  tags: [{label: 'Limited', variant: 'yellow'}],
1011
1054
  hue: 'var(--dracula-yellow)',
1012
1055
  selected: false,
@@ -1016,7 +1059,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1016
1059
  name: 'Night Market Wallet',
1017
1060
  meta: 'Full-grain, RFID blocking',
1018
1061
  available: 15,
1019
- location: 'Aisle 4',
1062
+ location: 'Roost 4',
1020
1063
  tags: [{label: 'Leather', variant: 'yellow'}],
1021
1064
  hue: 'var(--dracula-orange)',
1022
1065
  selected: true,
@@ -1026,8 +1069,8 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1026
1069
  name: 'Midnight Tumbler',
1027
1070
  meta: 'Vacuum insulated, 16oz',
1028
1071
  available: 87,
1029
- location: 'Aisle 5',
1030
- tags: [{label: 'Drinkware', variant: 'green'}],
1072
+ location: 'Roost 5',
1073
+ tags: [{label: 'Drinkware', variant: 'yellow'}],
1031
1074
  hue: 'var(--dracula-pink)',
1032
1075
  selected: false,
1033
1076
  },
@@ -1036,8 +1079,8 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1036
1079
  name: 'Belfry Throw',
1037
1080
  meta: 'Heavyweight, oat',
1038
1081
  available: 24,
1039
- location: 'Aisle 6',
1040
- tags: [{label: 'Home', variant: 'orange'}],
1082
+ location: 'Roost 6',
1083
+ tags: [{label: 'Home', variant: 'yellow'}],
1041
1084
  hue: 'var(--dracula-green)',
1042
1085
  selected: true,
1043
1086
  },
@@ -1059,9 +1102,9 @@ function SelectCell({row}: {row: InventoryRow}) {
1059
1102
  function ItemCell({row}: {row: InventoryRow}) {
1060
1103
  return (
1061
1104
  <HStack gap={3} vAlign="center">
1062
- <ThumbSwatch hue={row.hue} label={row.name} />
1063
- <VStack gap={0} style={{minWidth: 0}}>
1064
- <Text type="body" weight="bold">
1105
+ <ProductSwatch accent={row.hue} label={row.name} />
1106
+ <VStack gap={0} style={styles.inventoryItemText}>
1107
+ <Text type="body" weight="semibold">
1065
1108
  {row.name}
1066
1109
  </Text>
1067
1110
  <Text type="supporting" color="secondary">
@@ -1098,7 +1141,79 @@ function ActionsCell() {
1098
1141
  );
1099
1142
  }
1100
1143
 
1144
+ // The full column set needs 64+80+100+100+80+64 = 488px of grid and the card
1145
+ // insets cost 2 × --spacing-6 on top, so it stops fitting below ~600px. A 375px
1146
+ // phone leaves ~280px inside the card: the phone layout drops the selection
1147
+ // control (no bulk action bar acts on it) and the two metadata columns, which
1148
+ // needs 244px. Table keeps its own scroll wrapper either way; the point is that
1149
+ // a row should not have to be read sideways.
1150
+ const NARROW_TABLE_QUERY = '(max-width: 600px)';
1151
+ const NARROW_COLUMN_KEYS: Record<string, true> = {
1152
+ select: true,
1153
+ location: true,
1154
+ tags: true,
1155
+ };
1156
+
1157
+ const INVENTORY_COLUMNS: TableColumn<InventoryRow>[] = [
1158
+ {
1159
+ key: 'select',
1160
+ header: '',
1161
+ // Wide enough that the control + the theme's cell padding (up to
1162
+ // --spacing-4 = 16px/side on spacious density) fit inside the cell,
1163
+ // so the control's hover background doesn't overflow toward the
1164
+ // card's clipped (rounded) edge on larger-padding themes.
1165
+ width: pixel(64),
1166
+ renderCell: row => <SelectCell row={row} />,
1167
+ },
1168
+ {
1169
+ key: 'item',
1170
+ header: 'Item',
1171
+ // Lower min-width (default 120) so the table fits its container on
1172
+ // larger-spacing themes instead of overflowing the actions column.
1173
+ width: proportional(3, {minWidth: 80}),
1174
+ renderCell: row => <ItemCell row={row} />,
1175
+ },
1176
+ {
1177
+ key: 'available',
1178
+ header: 'Available',
1179
+ width: pixel(100),
1180
+ renderCell: row => (
1181
+ <Text type="body" hasTabularNumbers>
1182
+ {row.available}
1183
+ </Text>
1184
+ ),
1185
+ },
1186
+ {
1187
+ key: 'location',
1188
+ header: 'Location',
1189
+ width: pixel(100),
1190
+ renderCell: row => <Text type="body">{row.location}</Text>,
1191
+ },
1192
+ {
1193
+ key: 'tags',
1194
+ header: 'Tags',
1195
+ width: proportional(2, {minWidth: 80}),
1196
+ align: 'end',
1197
+ renderCell: row => <TagsCell row={row} />,
1198
+ },
1199
+ {
1200
+ key: 'actions',
1201
+ header: '',
1202
+ // Match the select column: fit the sm more-menu button + cell
1203
+ // padding so its hover background stays clear of the card's
1204
+ // clipped rounded edge across themes.
1205
+ width: pixel(64),
1206
+ align: 'end',
1207
+ renderCell: () => <ActionsCell />,
1208
+ },
1209
+ ];
1210
+
1211
+ const NARROW_INVENTORY_COLUMNS = INVENTORY_COLUMNS.filter(
1212
+ column => !NARROW_COLUMN_KEYS[column.key],
1213
+ );
1214
+
1101
1215
  function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1216
+ const isNarrow = useMediaQuery(NARROW_TABLE_QUERY);
1102
1217
  const lowStockCount = inventory.filter(
1103
1218
  row => row.available < LOW_STOCK_THRESHOLD,
1104
1219
  ).length;
@@ -1109,7 +1224,6 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1109
1224
  <Button
1110
1225
  label="Add item"
1111
1226
  variant="primary"
1112
- size="sm"
1113
1227
  icon={<Plus size={16} />}
1114
1228
  />
1115
1229
  </HStack>
@@ -1121,7 +1235,7 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1121
1235
  vAlign="center"
1122
1236
  hAlign="between"
1123
1237
  style={styles.inventoryFilterRow}>
1124
- <HStack gap={2} vAlign="center" style={{flex: 1, minWidth: 0}}>
1238
+ <HStack gap={2} vAlign="center" style={styles.filterRowFill}>
1125
1239
  <TextInput
1126
1240
  label="Search inventory"
1127
1241
  isLabelHidden
@@ -1160,12 +1274,12 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1160
1274
  value={undefined}
1161
1275
  onChange={() => {}}
1162
1276
  options={[
1163
- 'Aisle 1',
1164
- 'Aisle 2',
1165
- 'Aisle 3',
1166
- 'Aisle 4',
1167
- 'Aisle 5',
1168
- 'Aisle 6',
1277
+ 'Roost 1',
1278
+ 'Roost 2',
1279
+ 'Roost 3',
1280
+ 'Roost 4',
1281
+ 'Roost 5',
1282
+ 'Roost 6',
1169
1283
  ]}
1170
1284
  />
1171
1285
  <Selector
@@ -1208,75 +1322,25 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1208
1322
  </HStack>
1209
1323
 
1210
1324
  {lowStockCount > 0 && (
1211
- <div style={inlineStyles.inventoryBannerWrap}>
1325
+ <VStack gap={0} style={inlineStyles.inventoryBannerWrap}>
1212
1326
  <Banner
1213
1327
  status="warning"
1214
1328
  title={lowStockCount + ' items are running low'}
1215
1329
  />
1216
- </div>
1330
+ </VStack>
1217
1331
  )}
1218
1332
 
1219
- <div style={styles.inventoryTableWrap}>
1333
+ <VStack gap={0} style={styles.inventoryTableWrap}>
1220
1334
  <Table<InventoryRow>
1221
1335
  data={inventory}
1222
- columns={[
1223
- {
1224
- key: 'select',
1225
- header: '',
1226
- // Wide enough that the control + the theme's cell padding (up to
1227
- // --spacing-4 = 16px/side on spacious density) fit inside the cell,
1228
- // so the control's hover background doesn't overflow toward the
1229
- // card's clipped (rounded) edge on larger-padding themes.
1230
- width: pixel(64),
1231
- renderCell: row => <SelectCell row={row} />,
1232
- },
1233
- {
1234
- key: 'item',
1235
- header: 'Item',
1236
- // Lower min-width (default 120) so the table fits its container on
1237
- // larger-spacing themes instead of overflowing the actions column.
1238
- width: proportional(3, {minWidth: 80}),
1239
- renderCell: row => <ItemCell row={row} />,
1240
- },
1241
- {
1242
- key: 'available',
1243
- header: 'Available',
1244
- width: pixel(100),
1245
- renderCell: row => (
1246
- <Text type="body" hasTabularNumbers>
1247
- {row.available}
1248
- </Text>
1249
- ),
1250
- },
1251
- {
1252
- key: 'location',
1253
- header: 'Location',
1254
- width: pixel(100),
1255
- renderCell: row => <Text type="body">{row.location}</Text>,
1256
- },
1257
- {
1258
- key: 'tags',
1259
- header: 'Tags',
1260
- width: proportional(2, {minWidth: 80}),
1261
- align: 'end',
1262
- renderCell: row => <TagsCell row={row} />,
1263
- },
1264
- {
1265
- key: 'actions',
1266
- header: '',
1267
- // Match the select column: fit the sm more-menu button + cell
1268
- // padding so its hover background stays clear of the card's
1269
- // clipped rounded edge across themes.
1270
- width: pixel(64),
1271
- align: 'end',
1272
- renderCell: () => <ActionsCell />,
1273
- },
1274
- ]}
1336
+ columns={
1337
+ isNarrow ? NARROW_INVENTORY_COLUMNS : INVENTORY_COLUMNS
1338
+ }
1275
1339
  density="spacious"
1276
1340
  dividers="rows"
1277
1341
  hasHover
1278
1342
  />
1279
- </div>
1343
+ </VStack>
1280
1344
  </Card>
1281
1345
  );
1282
1346
  }