astryx-dracula 0.2.1 → 0.3.1

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 +30 -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 +181 -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 +102 -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 +433 -30
@@ -1,6 +1,10 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // TN"Nocturne" + (S[p6] > V[g10] > (Ctr > V[g4 a=center] > Tx"Little haunts"[t=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[c1] > C > Hd"Checkout"[level=2]) + (GS[c2] > C > Hd"Night Owl AI"[level=2])) + (G[c{min:200} g4] > (GS[c3] > C > T) + (GS[c1] > C > Hd"Revenue"[level=2])))
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
+ */
4
8
 
5
9
  import {type CSSProperties, type ReactNode} from 'react';
6
10
  import {
@@ -35,6 +39,10 @@ import {Divider} from '@astryxdesign/core/Divider';
35
39
  import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
36
40
  import {Item} from '@astryxdesign/core/Item';
37
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';
38
46
  import {TextInput} from '@astryxdesign/core/TextInput';
39
47
  import {Selector} from '@astryxdesign/core/Selector';
40
48
  import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
@@ -53,6 +61,7 @@ import {
53
61
  ChatMessageList,
54
62
  ChatSystemMessage,
55
63
  } from '@astryxdesign/core/Chat';
64
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
56
65
 
57
66
  // Styles passed to Astryx components via their `style` prop. Astryx components
58
67
  // forward the DOM `style` prop, so these work with no CSS compiler — in
@@ -100,6 +109,10 @@ const styles: Record<string, CSSProperties> = {
100
109
  minWidth: 0,
101
110
  maxWidth: 240,
102
111
  },
112
+ filterRowFill: {
113
+ flex: 1,
114
+ minWidth: 0,
115
+ },
103
116
  activityCard: {
104
117
  backgroundColor: 'var(--color-background-surface)',
105
118
  color: 'var(--color-text-primary)',
@@ -121,6 +134,9 @@ const styles: Record<string, CSSProperties> = {
121
134
  activityCardStack: {
122
135
  height: '100%',
123
136
  },
137
+ inventoryItemText: {
138
+ minWidth: 0,
139
+ },
124
140
  activityListFade: {
125
141
  flex: 1,
126
142
  minHeight: 0,
@@ -131,13 +147,11 @@ const styles: Record<string, CSSProperties> = {
131
147
  'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
132
148
  marginInline: 'calc(var(--spacing-2) * -1)',
133
149
  },
134
- content: {
135
- maxWidth: 960,
136
- marginInline: 'auto',
137
- minWidth: 0,
138
- },
150
+ // The hero column: centered, narrower than the grid below it.
139
151
  contentFluid: {
140
152
  maxWidth: 880,
153
+ marginInline: 'auto',
154
+ minWidth: 0,
141
155
  },
142
156
  heroText: {
143
157
  textAlign: 'center' as const,
@@ -151,6 +165,17 @@ const styles: Record<string, CSSProperties> = {
151
165
  },
152
166
  cardDescription: {
153
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)',
154
179
  },
155
180
  quantityInput: {
156
181
  // minWidth (not a hard width) so the field grows to fit the digit + the
@@ -175,7 +200,10 @@ const inlineStyles: Record<string, CSSProperties> = {
175
200
  chatBody: {
176
201
  flex: 1,
177
202
  minHeight: 0,
178
- 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,
179
207
  },
180
208
  chatSuggestions: {
181
209
  paddingInline: 'var(--spacing-4)',
@@ -185,22 +213,21 @@ const inlineStyles: Record<string, CSSProperties> = {
185
213
  paddingInline: 'var(--spacing-4)',
186
214
  paddingBottom: 'var(--spacing-4)',
187
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.
188
222
  activityIcon: {
189
- width: 32,
190
- height: 32,
191
- borderRadius: 'var(--radius-full)',
192
- backgroundColor: 'var(--color-background-muted)',
223
+ borderRadius: 'var(--radius-element)', // square chip: no circles except dots/avatars
224
+ backgroundColor: 'var(--color-background-surface)',
193
225
  color: 'var(--color-text-secondary)',
194
- display: 'flex',
195
- alignItems: 'center',
196
- justifyContent: 'center',
197
226
  flexShrink: 0,
198
227
  },
199
228
  cardBody: {
200
229
  padding: 'var(--spacing-4)',
201
230
  flex: 1,
202
- display: 'flex',
203
- flexDirection: 'column',
204
231
  },
205
232
  };
206
233
 
@@ -211,20 +238,23 @@ const artImage: CSSProperties = {
211
238
  height: '100%',
212
239
  display: 'block',
213
240
  };
214
- // Rounded inventory swatch. No radius prop where the swatch renders, so the
215
- // SVG carries its own rounded corners (rx=4).
216
- const thumbSwatch: CSSProperties = {
217
- flexShrink: 0,
218
- display: 'block',
219
- };
220
241
 
221
- // 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.
222
244
  const PRODUCT_HUES = [
223
- 'var(--dracula-purple)',
245
+ 'var(--dracula-green)',
224
246
  'var(--dracula-cyan)',
225
247
  'var(--dracula-yellow)',
226
248
  ];
227
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
+
228
258
  /** Categorical badge variants usable for showcase product/inventory tags. */
229
259
  export type ShowcaseBadgeVariant =
230
260
  | 'blue'
@@ -249,7 +279,7 @@ const DEFAULT_PRODUCTS: ProductSpec[] = [
249
279
  name: 'Moonphase Watch',
250
280
  description: 'Clean lines and lume that carry through the longest night.',
251
281
  badge: 'New',
252
- badgeVariant: 'blue',
282
+ badgeVariant: 'cyan',
253
283
  },
254
284
  {
255
285
  name: 'Night-Owl Headphones',
@@ -307,31 +337,6 @@ function ProductArt({hue, label}: {hue: string; label: string}) {
307
337
  );
308
338
  }
309
339
 
310
- // Rounded inventory swatch: Dracula surface with a per-row accent ring.
311
- function ThumbSwatch({hue, label}: {hue: string; label: string}) {
312
- return (
313
- <svg
314
- viewBox="0 0 40 40"
315
- width={40}
316
- height={40}
317
- style={thumbSwatch}
318
- role="img"
319
- aria-label={`${label} swatch`}>
320
- <rect width={40} height={40} rx={5} fill="var(--dracula-bg-light)" />
321
- <rect
322
- x={1}
323
- y={1}
324
- width={38}
325
- height={38}
326
- rx={4}
327
- fill="none"
328
- stroke="var(--color-widget-content-border)"
329
- />
330
- <circle cx={20} cy={20} r={8} fill="none" stroke={hue} strokeWidth={3} />
331
- </svg>
332
- );
333
- }
334
-
335
340
  export interface ThemeShowcaseProps {
336
341
  /** The three hero product cards. Defaults to the nocturne store products. */
337
342
  products?: ProductSpec[];
@@ -353,20 +358,12 @@ export function ThemeShowcaseStore({
353
358
  }: ThemeShowcaseProps = {}) {
354
359
  const {isMobile} = useAppShellMobile();
355
360
  return (
356
- <div
357
- style={{
358
- minHeight: '100%',
359
- backgroundColor: 'var(--color-background-body)',
360
- }}>
361
+ <VStack gap={0} style={styles.storeRoot}>
361
362
  <StorePreview products={products} isMobile={isMobile} />
362
- <div
363
- style={{
364
- padding: 'var(--spacing-6)',
365
- backgroundColor: 'var(--color-background-surface)',
366
- }}>
363
+ <VStack gap={0} style={styles.showcaseBand}>
367
364
  <CardShowcase inventory={inventory} isMobile={isMobile} />
368
- </div>
369
- </div>
365
+ </VStack>
366
+ </VStack>
370
367
  );
371
368
  }
372
369
 
@@ -377,7 +374,7 @@ function CardShowcase({
377
374
  inventory: InventoryRow[];
378
375
  isMobile: boolean;
379
376
  }) {
380
- const columns = isMobile ? 1 : ({minWidth: 200, repeat: 'fit'} as const);
377
+ const columns = isMobile ? 1 : SHOWCASE_COLUMNS;
381
378
 
382
379
  return (
383
380
  <VStack gap={8}>
@@ -408,14 +405,18 @@ function StorePreview({
408
405
  products: ProductSpec[];
409
406
  isMobile: boolean;
410
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;
411
412
  return (
412
- <div data-theme-preview="true">
413
+ <VStack gap={0} data-theme-preview="true">
413
414
  <VStack gap={0}>
414
415
  <TopNav
415
416
  label="Theme preview navigation"
416
417
  heading={<TopNavHeading heading="Nocturne" />}
417
418
  centerContent={
418
- isMobile ? undefined : (
419
+ collapseNav ? undefined : (
419
420
  <>
420
421
  <TopNavItem label="Shop" href="#/templates/theme-showcase" isSelected />
421
422
  <TopNavItem label="New In" href="#/templates/theme-showcase" />
@@ -425,47 +426,58 @@ function StorePreview({
425
426
  )
426
427
  }
427
428
  endContent={
428
- <HStack gap={2} vAlign="center">
429
- <HStack gap={0.5}>
430
- <Button
431
- label="Search"
432
- tooltip="Search"
433
- variant="ghost"
434
- isIconOnly
435
- icon={<Search size={20} />}
436
- href="#/templates/theme-showcase"
437
- />
438
- <Button
439
- label="Account"
440
- tooltip="Account"
441
- variant="ghost"
442
- isIconOnly
443
- icon={<User size={20} />}
444
- href="#/templates/theme-showcase"
445
- />
446
- <Button
447
- label="Cart"
448
- tooltip="Cart"
449
- variant="ghost"
450
- isIconOnly
451
- icon={<ShoppingBag size={20} />}
452
- href="#/templates/theme-showcase"
453
- />
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" />
454
467
  </HStack>
455
- <Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
456
- </HStack>
468
+ )
457
469
  }
458
470
  />
459
471
 
460
472
  <Section padding={6} variant="transparent">
461
- <VStack gap={10} style={{...styles.content, ...styles.contentFluid}}>
473
+ <VStack gap={10} style={styles.contentFluid}>
462
474
  <Center>
463
475
  <VStack gap={4} hAlign="center" style={styles.heroText}>
464
- <Text type="display-2" color="accent">
476
+ <Heading level={1} type="display-2">
465
477
  Little haunts,
466
478
  <br />
467
479
  everywhere you roam
468
- </Text>
480
+ </Heading>
469
481
  <Text type="body" color="secondary">
470
482
  We believe the smallest shadows are the ones that matter most.
471
483
  Turn an ordinary evening into something worth remembering.
@@ -473,7 +485,7 @@ function StorePreview({
473
485
  </VStack>
474
486
  </Center>
475
487
 
476
- <Grid columns={isMobile ? 1 : {minWidth: 200, max: 3}} gap={4}>
488
+ <Grid columns={isMobile ? 1 : PRODUCT_COLUMNS} gap={4}>
477
489
  {products.map((p, i) => (
478
490
  <Card key={p.name} padding={0} height="100%">
479
491
  <VStack gap={0} style={styles.cardStack}>
@@ -483,44 +495,42 @@ function StorePreview({
483
495
  label={p.name}
484
496
  />
485
497
  </AspectRatio>
486
- <div style={inlineStyles.cardBody}>
487
- <VStack gap={2} hAlign="center" style={styles.cardStack}>
488
- <HStack>
489
- <Badge label={p.badge} variant={p.badgeVariant} />
490
- </HStack>
491
- <Heading level={2} style={styles.centerText}>
492
- {p.name}
493
- </Heading>
494
- <Text
495
- type="body"
496
- color="secondary"
497
- style={{
498
- ...styles.cardDescription,
499
- ...styles.centerText,
500
- }}>
501
- {p.description}
502
- </Text>
503
- <HStack gap={2} vAlign="center" hAlign="center">
504
- <NumberInput
505
- label="Quantity"
506
- isLabelHidden
507
- value={1}
508
- onChange={() => {}}
509
- min={1}
510
- max={99}
511
- size="sm"
512
- style={styles.quantityInput}
513
- />
514
- <Button
515
- label="Add to cart"
516
- variant="secondary"
517
- size="sm"
518
- href="#/templates/theme-showcase"
519
- style={styles.cartButton}
520
- />
521
- </HStack>
522
- </VStack>
523
- </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>
524
534
  </VStack>
525
535
  </Card>
526
536
  ))}
@@ -528,13 +538,13 @@ function StorePreview({
528
538
  </VStack>
529
539
  </Section>
530
540
  </VStack>
531
- </div>
541
+ </VStack>
532
542
  );
533
543
  }
534
544
 
535
545
  function CheckoutCard({isMobile}: {isMobile: boolean}) {
536
546
  return (
537
- <Card padding={5} style={styles.card}>
547
+ <Card padding={4} style={styles.card}>
538
548
  <VStack gap={4} style={styles.checkoutStack}>
539
549
  <Heading level={2}>Checkout</Heading>
540
550
 
@@ -588,7 +598,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
588
598
  <Text type="label" weight="semibold">
589
599
  Payment method
590
600
  </Text>
591
- <Grid columns={isMobile ? 1 : {minWidth: 70, max: 3}} gap={2}>
601
+ <Grid columns={isMobile ? 1 : PAYMENT_COLUMNS} gap={2}>
592
602
  <SelectableCard
593
603
  label="Pay with card"
594
604
  isSelected={true}
@@ -646,7 +656,7 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
646
656
  size="lg"
647
657
  />
648
658
 
649
- <Grid columns={isMobile ? 1 : {minWidth: 90, max: 2}} gap={2}>
659
+ <Grid columns={isMobile ? 1 : EXPIRY_COLUMNS} gap={2}>
650
660
  <TextInput
651
661
  label="Expiry"
652
662
  placeholder="MM / YY"
@@ -735,26 +745,32 @@ function ChatCard() {
735
745
 
736
746
  <Divider variant="subtle" />
737
747
 
738
- <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">
739
755
  <ChatMessageList>
740
756
  <ChatSystemMessage>Today</ChatSystemMessage>
741
757
 
742
758
  <ChatMessage sender="user">
743
759
  <ChatMessageBubble variant="filled">
744
- Where’s my order?
760
+ Where's my order?
745
761
  </ChatMessageBubble>
746
762
  </ChatMessage>
747
763
 
748
764
  <ChatMessage sender="assistant">
749
765
  <VStack gap={3}>
750
766
  <Text type="body">
751
- Your order #1043 — the Moonphase Watch and Belfry Throw —
752
- shipped this morning from the Aisle 3 warehouse and is currently
753
- 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
754
770
  end of day tomorrow.
755
771
  </Text>
756
772
  <Text type="body">
757
- 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
758
774
  it to a pickup point, or start a return once it arrives.
759
775
  </Text>
760
776
  </VStack>
@@ -768,7 +784,7 @@ function ChatCard() {
768
784
 
769
785
  <ChatMessage sender="assistant">
770
786
  <VStack gap={3}>
771
- <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>
772
788
  <Card padding={3}>
773
789
  <VStack gap={1}>
774
790
  <Item
@@ -782,7 +798,7 @@ function ChatCard() {
782
798
  />
783
799
  <Item
784
800
  label="Shipping"
785
- description="UPS Ground"
801
+ description="Courier-raven ground"
786
802
  endContent={
787
803
  <Text type="body" weight="semibold" hasTabularNumbers>
788
804
  $12
@@ -792,21 +808,28 @@ function ChatCard() {
792
808
  <Item
793
809
  label="Estimated arrival"
794
810
  description="Tomorrow by 8pm"
795
- 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
+ }
796
819
  />
797
820
  <Item
798
821
  label="Tracking"
799
- description="UPS 1Z 999 AA1 0123 4567 84"
800
- 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>}
801
824
  />
802
825
  </VStack>
803
826
  </Card>
804
827
  </VStack>
805
828
  </ChatMessage>
806
829
  </ChatMessageList>
807
- </div>
830
+ </VStack>
808
831
 
809
- <div style={inlineStyles.chatSuggestions}>
832
+ <VStack gap={0} style={inlineStyles.chatSuggestions}>
810
833
  <HStack gap={1} hAlign="center" wrap="wrap">
811
834
  {SUGGESTED_QUESTIONS.map(question => (
812
835
  <Button
@@ -817,14 +840,14 @@ function ChatCard() {
817
840
  />
818
841
  ))}
819
842
  </HStack>
820
- </div>
843
+ </VStack>
821
844
 
822
- <div style={inlineStyles.chatComposer}>
845
+ <VStack gap={0} style={inlineStyles.chatComposer}>
823
846
  <ChatComposer
824
847
  value=""
825
848
  onChange={() => {}}
826
849
  onSubmit={() => {}}
827
- placeholder="Ask Night Owl…"
850
+ placeholder="Ask Night Owl..."
828
851
  footerActions={
829
852
  <Button
830
853
  variant="ghost"
@@ -846,7 +869,7 @@ function ChatCard() {
846
869
  />
847
870
  }
848
871
  />
849
- </div>
872
+ </VStack>
850
873
  </Card>
851
874
  );
852
875
  }
@@ -910,7 +933,7 @@ function formatAmount(amount: number): string {
910
933
 
911
934
  function LatestActivityCard({isMobile}: {isMobile: boolean}) {
912
935
  return (
913
- <Card padding={5} style={styles.activityCard}>
936
+ <Card padding={4} style={styles.activityCard}>
914
937
  <VStack gap={4} style={styles.activityCardStack}>
915
938
  <Heading level={2}>Revenue</Heading>
916
939
 
@@ -941,13 +964,31 @@ function LatestActivityCard({isMobile}: {isMobile: boolean}) {
941
964
  </HStack>
942
965
 
943
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. */}
944
981
  {ACTIVITY.map(item => (
945
982
  <Item
946
983
  key={item.id}
947
984
  startContent={
948
- <div style={inlineStyles.activityIcon} aria-hidden="true">
985
+ <Center
986
+ width={32}
987
+ height={32}
988
+ style={inlineStyles.activityIcon}
989
+ aria-hidden="true">
949
990
  {item.icon}
950
- </div>
991
+ </Center>
951
992
  }
952
993
  label={item.label}
953
994
  description={item.detail}
@@ -978,7 +1019,7 @@ export interface InventoryRow extends Record<string, unknown> {
978
1019
  available: number;
979
1020
  location: string;
980
1021
  tags: TagSpec[];
981
- hue: string;
1022
+ hue: SceneHue;
982
1023
  selected: boolean;
983
1024
  }
984
1025
 
@@ -988,9 +1029,9 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
988
1029
  name: 'Moonphase Watch',
989
1030
  meta: 'Steel case, moonphase dial',
990
1031
  available: 42,
991
- location: 'Aisle 3',
992
- tags: [{label: 'New', variant: 'blue'}],
993
- hue: 'var(--dracula-purple)',
1032
+ location: 'Roost 3',
1033
+ tags: [{label: 'New', variant: 'cyan'}],
1034
+ hue: 'var(--dracula-comment)',
994
1035
  selected: false,
995
1036
  },
996
1037
  {
@@ -998,7 +1039,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
998
1039
  name: 'Night-Owl Headphones',
999
1040
  meta: 'ANC, 30hr battery',
1000
1041
  available: 128,
1001
- location: 'Aisle 1',
1042
+ location: 'Roost 1',
1002
1043
  tags: [{label: 'Popular', variant: 'green'}],
1003
1044
  hue: 'var(--dracula-cyan)',
1004
1045
  selected: true,
@@ -1008,7 +1049,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1008
1049
  name: 'Coven Canvas Backpack',
1009
1050
  meta: 'Waxed canvas, 25L',
1010
1051
  available: 63,
1011
- location: 'Aisle 2',
1052
+ location: 'Roost 2',
1012
1053
  tags: [{label: 'Limited', variant: 'yellow'}],
1013
1054
  hue: 'var(--dracula-yellow)',
1014
1055
  selected: false,
@@ -1018,7 +1059,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1018
1059
  name: 'Night Market Wallet',
1019
1060
  meta: 'Full-grain, RFID blocking',
1020
1061
  available: 15,
1021
- location: 'Aisle 4',
1062
+ location: 'Roost 4',
1022
1063
  tags: [{label: 'Leather', variant: 'yellow'}],
1023
1064
  hue: 'var(--dracula-orange)',
1024
1065
  selected: true,
@@ -1028,8 +1069,8 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1028
1069
  name: 'Midnight Tumbler',
1029
1070
  meta: 'Vacuum insulated, 16oz',
1030
1071
  available: 87,
1031
- location: 'Aisle 5',
1032
- tags: [{label: 'Drinkware', variant: 'green'}],
1072
+ location: 'Roost 5',
1073
+ tags: [{label: 'Drinkware', variant: 'yellow'}],
1033
1074
  hue: 'var(--dracula-pink)',
1034
1075
  selected: false,
1035
1076
  },
@@ -1038,8 +1079,8 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1038
1079
  name: 'Belfry Throw',
1039
1080
  meta: 'Heavyweight, oat',
1040
1081
  available: 24,
1041
- location: 'Aisle 6',
1042
- tags: [{label: 'Home', variant: 'orange'}],
1082
+ location: 'Roost 6',
1083
+ tags: [{label: 'Home', variant: 'yellow'}],
1043
1084
  hue: 'var(--dracula-green)',
1044
1085
  selected: true,
1045
1086
  },
@@ -1061,8 +1102,8 @@ function SelectCell({row}: {row: InventoryRow}) {
1061
1102
  function ItemCell({row}: {row: InventoryRow}) {
1062
1103
  return (
1063
1104
  <HStack gap={3} vAlign="center">
1064
- <ThumbSwatch hue={row.hue} label={row.name} />
1065
- <VStack gap={0} style={{minWidth: 0}}>
1105
+ <ProductSwatch accent={row.hue} label={row.name} />
1106
+ <VStack gap={0} style={styles.inventoryItemText}>
1066
1107
  <Text type="body" weight="semibold">
1067
1108
  {row.name}
1068
1109
  </Text>
@@ -1100,7 +1141,79 @@ function ActionsCell() {
1100
1141
  );
1101
1142
  }
1102
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
+
1103
1215
  function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1216
+ const isNarrow = useMediaQuery(NARROW_TABLE_QUERY);
1104
1217
  const lowStockCount = inventory.filter(
1105
1218
  row => row.available < LOW_STOCK_THRESHOLD,
1106
1219
  ).length;
@@ -1111,7 +1224,6 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1111
1224
  <Button
1112
1225
  label="Add item"
1113
1226
  variant="primary"
1114
- size="sm"
1115
1227
  icon={<Plus size={16} />}
1116
1228
  />
1117
1229
  </HStack>
@@ -1123,7 +1235,7 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1123
1235
  vAlign="center"
1124
1236
  hAlign="between"
1125
1237
  style={styles.inventoryFilterRow}>
1126
- <HStack gap={2} vAlign="center" style={{flex: 1, minWidth: 0}}>
1238
+ <HStack gap={2} vAlign="center" style={styles.filterRowFill}>
1127
1239
  <TextInput
1128
1240
  label="Search inventory"
1129
1241
  isLabelHidden
@@ -1162,12 +1274,12 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1162
1274
  value={undefined}
1163
1275
  onChange={() => {}}
1164
1276
  options={[
1165
- 'Aisle 1',
1166
- 'Aisle 2',
1167
- 'Aisle 3',
1168
- 'Aisle 4',
1169
- 'Aisle 5',
1170
- 'Aisle 6',
1277
+ 'Roost 1',
1278
+ 'Roost 2',
1279
+ 'Roost 3',
1280
+ 'Roost 4',
1281
+ 'Roost 5',
1282
+ 'Roost 6',
1171
1283
  ]}
1172
1284
  />
1173
1285
  <Selector
@@ -1210,75 +1322,25 @@ function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
1210
1322
  </HStack>
1211
1323
 
1212
1324
  {lowStockCount > 0 && (
1213
- <div style={inlineStyles.inventoryBannerWrap}>
1325
+ <VStack gap={0} style={inlineStyles.inventoryBannerWrap}>
1214
1326
  <Banner
1215
1327
  status="warning"
1216
1328
  title={lowStockCount + ' items are running low'}
1217
1329
  />
1218
- </div>
1330
+ </VStack>
1219
1331
  )}
1220
1332
 
1221
- <div style={styles.inventoryTableWrap}>
1333
+ <VStack gap={0} style={styles.inventoryTableWrap}>
1222
1334
  <Table<InventoryRow>
1223
1335
  data={inventory}
1224
- columns={[
1225
- {
1226
- key: 'select',
1227
- header: '',
1228
- // Wide enough that the control + the theme's cell padding (up to
1229
- // --spacing-4 = 16px/side on spacious density) fit inside the cell,
1230
- // so the control's hover background doesn't overflow toward the
1231
- // card's clipped (rounded) edge on larger-padding themes.
1232
- width: pixel(64),
1233
- renderCell: row => <SelectCell row={row} />,
1234
- },
1235
- {
1236
- key: 'item',
1237
- header: 'Item',
1238
- // Lower min-width (default 120) so the table fits its container on
1239
- // larger-spacing themes instead of overflowing the actions column.
1240
- width: proportional(3, {minWidth: 80}),
1241
- renderCell: row => <ItemCell row={row} />,
1242
- },
1243
- {
1244
- key: 'available',
1245
- header: 'Available',
1246
- width: pixel(100),
1247
- renderCell: row => (
1248
- <Text type="body" hasTabularNumbers>
1249
- {row.available}
1250
- </Text>
1251
- ),
1252
- },
1253
- {
1254
- key: 'location',
1255
- header: 'Location',
1256
- width: pixel(100),
1257
- renderCell: row => <Text type="body">{row.location}</Text>,
1258
- },
1259
- {
1260
- key: 'tags',
1261
- header: 'Tags',
1262
- width: proportional(2, {minWidth: 80}),
1263
- align: 'end',
1264
- renderCell: row => <TagsCell row={row} />,
1265
- },
1266
- {
1267
- key: 'actions',
1268
- header: '',
1269
- // Match the select column: fit the sm more-menu button + cell
1270
- // padding so its hover background stays clear of the card's
1271
- // clipped rounded edge across themes.
1272
- width: pixel(64),
1273
- align: 'end',
1274
- renderCell: () => <ActionsCell />,
1275
- },
1276
- ]}
1336
+ columns={
1337
+ isNarrow ? NARROW_INVENTORY_COLUMNS : INVENTORY_COLUMNS
1338
+ }
1277
1339
  density="spacious"
1278
1340
  dividers="rows"
1279
1341
  hasHover
1280
1342
  />
1281
- </div>
1343
+ </VStack>
1282
1344
  </Card>
1283
1345
  );
1284
1346
  }