astryx-dracula 0.5.0 → 0.6.2

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 (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,156 +1,136 @@
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] > (C[p0] > Hd"Night Owl AI"[level=2]) + (G[c={min:200} g4] > (GS[c=3] > C > T) + (GS[c=1] > C > Hd"Revenue"[level=2])))
2
+ // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
+ // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
+ // human drift check — update it with the file, never alone):
5
+ // V[g=0] > (V[g=0] > (V[g=0] > (TN"Nocturne" > ((TNI"Shop" + TNI"New In" + TNI"Stories" + TNI"Help") + (H[g=2 a=center] > (H[g=0.5] > B.ghost"Search" + B.ghost"Account" + B.ghost"Cart") + B.primary"Sign in"))) + (S.transparent[p=6] > V[g=10 mw=880] > (Ctr > V[g=4 a=center mw=560] > Hd"Little haunts"[level=1 t=display-2] + Tx"We believe"[t=body]) + (G[c={min:200,max:3} g=4] > (C[p=0 h=100%] > (V[g=0 h=100%] > (AR[ratio=1] > Scene{SceneFrame}) + (V[g=2 a=center p=4] > Bd.cyan"New" + Hd"Moonphase Watch"[level=2] + Tx"Description"[t=body] + (H[g=2 a=center] > NI"Quantity" + B.secondary"Add to cart"))))*3)))) + (V[g=0 p=6] > (V[g=8] > ((C[p=0] > (H[g=3 j=between a=center] > Hd"Night Owl AI"[level=2] + (H[g=1 a=center] > B.ghost"Export conversation" + B.ghost"Close chat")) + D + (V[g=0] > ChML > (ChSM"Today" + (ChM > ChB{ChatMessageBubble}"Where's my order?") + (ChM > (V[g=3] > Tx"Body"[t=body] + Tx"Body"[t=body])) + (ChM > ChB{ChatMessageBubble}"Can you show me the full details?") + (ChM > (V[g=3] > Tx"Body"[t=body] + (C[p=3] > (V[g=1] > LI{Item}*4)))))) + (V[g=0] > (H[g=1 a=center wrap] > B.secondary*3)) + (V[g=0] > ChC"Ask Night Owl..."))) + (G[c={min:200} g=4] > (GS[c=3] > (C[p=0] > (H[g=3 j=between a=center] > Hd"Inventory"[level=2] + B.primary"Add item") + D + (H[g=3 j=between a=center] > (H[g=2 a=center] > TI"Search inventory" + (OFL[g=2] > SE{Selector}*3)) + (H[g=1 a=center] > B.ghost"List view" + B.ghost"Grid view")) + (V[g=0] > Bn.warning"2 items are running low") + (V[g=0] > T[density=spacious dv=rows hover] > (TR > THC + THC"Item" + THC"Available" + THC"Location" + THC"Tags" + THC) + (TR > (TC > CB) + (TC > (H[g=3 a=center] > Sw{ProductSwatch} + (V[g=0] > Tx"Moonphase Watch"[t=body] + Tx"Steel case, moonphase dial"[t=supporting]))) + TC"42" + TC"Roost 3" + (TC > Bd) + (TC > MM))*6))) + (GS[c=1] > (C[p=4 h=100%] > (V[g=4 h=100%] > Hd"Revenue"[level=2] + (G[c=2 g=3] > (V[g=0] > Tx"18K"[t=display-3] + Tx"Monthly revenue"[t=supporting]) + (V[g=0] > Tx"+12%"[t=display-3] + Tx"Order growth"[t=supporting])) + D + (H[g=3 j=between a=center] > Hd"Activity"[level=3] + Lk"See all") + (V[g=1] > LI{Item}*5)))))))
4
6
 
5
7
  /**
6
8
  * 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
9
  */
8
10
 
9
- import type {ReactNode} from 'react';
11
+ import { useAppShellMobile } from "@astryxdesign/core/AppShell";
12
+ import { AspectRatio } from "@astryxdesign/core/AspectRatio";
13
+ import { Badge } from "@astryxdesign/core/Badge";
14
+ import { Banner } from "@astryxdesign/core/Banner";
15
+ import { Button } from "@astryxdesign/core/Button";
16
+ import { Card } from "@astryxdesign/core/Card";
17
+ import { Center } from "@astryxdesign/core/Center";
10
18
  import {
11
- Plus,
12
- Search,
13
- Tag,
14
- Folder,
15
- MapPin,
16
- List,
17
- LayoutGrid,
18
- ShoppingBag,
19
- Banknote,
20
- Mic,
21
- X,
22
- Download,
23
- User,
24
- } from 'lucide-react';
25
- import {Text, Heading} from '@astryxdesign/core/Text';
26
- import {VStack, HStack} from '@astryxdesign/core/Layout';
27
- import {useAppShellMobile} from '@astryxdesign/core/AppShell';
28
- import {Grid, GridSpan} from '@astryxdesign/core/Grid';
29
- import {Card} from '@astryxdesign/core/Card';
30
- import {Button} from '@astryxdesign/core/Button';
31
- import {Link} from '@astryxdesign/core/Link';
32
- import {Badge} from '@astryxdesign/core/Badge';
33
- import {Banner} from '@astryxdesign/core/Banner';
34
- import {Divider} from '@astryxdesign/core/Divider';
35
- import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
36
- import {Item} from '@astryxdesign/core/Item';
37
- import {Table, proportional, pixel} from '@astryxdesign/core/Table';
38
- import type {TableColumn} from '@astryxdesign/core/Table';
39
- import {StatusDot} from '@astryxdesign/core/StatusDot';
40
- import {ProductSwatch} from 'astryx-dracula/shared/revenue-chart';
41
- import {useMediaQuery} from '@astryxdesign/core/hooks';
42
- import {TextInput} from '@astryxdesign/core/TextInput';
43
- import {Selector} from '@astryxdesign/core/Selector';
44
- import {MoreMenu} from '@astryxdesign/core/MoreMenu';
45
- import {Center} from '@astryxdesign/core/Center';
46
- import {Section} from '@astryxdesign/core/Section';
47
- import {AspectRatio} from '@astryxdesign/core/AspectRatio';
48
- import {NumberInput} from '@astryxdesign/core/NumberInput';
49
- import {OverflowList} from '@astryxdesign/core/OverflowList';
50
- import {TopNav, TopNavHeading, TopNavItem} from '@astryxdesign/core/TopNav';
19
+ ChatComposer,
20
+ ChatMessage,
21
+ ChatMessageBubble,
22
+ ChatMessageList,
23
+ ChatSystemMessage,
24
+ } from "@astryxdesign/core/Chat";
25
+ import { CheckboxInput } from "@astryxdesign/core/CheckboxInput";
26
+ import { Divider } from "@astryxdesign/core/Divider";
27
+ import { Grid, GridSpan } from "@astryxdesign/core/Grid";
28
+ import { useMediaQuery } from "@astryxdesign/core/hooks";
29
+ import { Item } from "@astryxdesign/core/Item";
30
+ import { HStack, VStack } from "@astryxdesign/core/Layout";
31
+ import { Link } from "@astryxdesign/core/Link";
32
+ import { MoreMenu } from "@astryxdesign/core/MoreMenu";
33
+ import { NumberInput } from "@astryxdesign/core/NumberInput";
34
+ import { OverflowList } from "@astryxdesign/core/OverflowList";
35
+ import { Section } from "@astryxdesign/core/Section";
36
+ import { Selector } from "@astryxdesign/core/Selector";
37
+ import { StatusDot } from "@astryxdesign/core/StatusDot";
38
+ import type { TableColumn } from "@astryxdesign/core/Table";
39
+ import { pixel, proportional, Table } from "@astryxdesign/core/Table";
40
+ import { Heading, Text } from "@astryxdesign/core/Text";
41
+ import { TextInput } from "@astryxdesign/core/TextInput";
42
+ import { TopNav, TopNavHeading, TopNavItem } from "@astryxdesign/core/TopNav";
43
+ import { ProductSwatch } from "astryx-dracula/shared/revenue-chart";
44
+ import { SceneFrame } from "astryx-dracula/shared/scene-frame";
45
+ import type { SceneHue } from "astryx-dracula/shared/scene-hues";
51
46
  import {
52
- ChatComposer,
53
- ChatMessage,
54
- ChatMessageBubble,
55
- ChatMessageList,
56
- ChatSystemMessage,
57
- } from '@astryxdesign/core/Chat';
58
- import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
59
- import {SceneFrame} from 'astryx-dracula/shared/scene-frame';
47
+ Banknote,
48
+ Download,
49
+ Folder,
50
+ LayoutGrid,
51
+ List,
52
+ MapPin,
53
+ Mic,
54
+ Plus,
55
+ Search,
56
+ ShoppingBag,
57
+ Tag,
58
+ User,
59
+ X,
60
+ } from "lucide-react";
61
+ import type { ReactNode } from "react";
60
62
 
61
63
  // One Dracula accent per hero product slot. Purple stays off decorative art:
62
64
  // it reads as interactive, and nothing here is tappable.
63
- const PRODUCT_HUES = [
64
- 'var(--dracula-green)',
65
- 'var(--dracula-cyan)',
66
- 'var(--dracula-yellow)',
67
- ];
65
+ const PRODUCT_HUES = ["var(--dracula-green)", "var(--dracula-cyan)", "var(--dracula-yellow)"];
68
66
 
69
67
  // Grid recipes the store renders at every width above the phone layout. Kept
70
68
  // at module scope: an object literal rebuilt per render is a new value prop
71
69
  // every time.
72
- const SHOWCASE_COLUMNS = {minWidth: 200, repeat: 'fit'} as const;
73
- const PRODUCT_COLUMNS = {minWidth: 200, max: 3} as const;
70
+ const SHOWCASE_COLUMNS = { minWidth: 200, repeat: "fit" } as const;
71
+ const PRODUCT_COLUMNS = { minWidth: 200, max: 3 } as const;
74
72
 
75
73
  /** Categorical badge variants usable for showcase product/inventory tags. */
76
74
  export type ShowcaseBadgeVariant =
77
- | 'blue'
78
- | 'cyan'
79
- | 'green'
80
- | 'orange'
81
- | 'pink'
82
- | 'purple'
83
- | 'red'
84
- | 'teal'
85
- | 'yellow';
75
+ "blue" | "cyan" | "green" | "orange" | "pink" | "purple" | "red" | "teal" | "yellow";
86
76
 
87
77
  export interface ProductSpec {
88
- name: string;
89
- description: string;
90
- badge: string;
91
- badgeVariant: ShowcaseBadgeVariant;
78
+ name: string;
79
+ description: string;
80
+ badge: string;
81
+ badgeVariant: ShowcaseBadgeVariant;
92
82
  }
93
83
 
94
84
  const DEFAULT_PRODUCTS: ProductSpec[] = [
95
- {
96
- name: 'Moonphase Watch',
97
- description: 'Clean lines and lume that carry through the longest night.',
98
- badge: 'New',
99
- badgeVariant: 'cyan',
100
- },
101
- {
102
- name: 'Night-Owl Headphones',
103
- description: 'Deep sound and soft cushions for all-night listening.',
104
- badge: 'Popular',
105
- badgeVariant: 'green',
106
- },
107
- {
108
- name: 'Coven Canvas Backpack',
109
- description: 'Waxed canvas with a quiet, moonlit profile.',
110
- badge: 'Limited',
111
- badgeVariant: 'yellow',
112
- },
85
+ {
86
+ name: "Moonphase Watch",
87
+ description: "Clean lines and lume that carry through the longest night.",
88
+ badge: "New",
89
+ badgeVariant: "cyan",
90
+ },
91
+ {
92
+ name: "Night-Owl Headphones",
93
+ description: "Deep sound and soft cushions for all-night listening.",
94
+ badge: "Popular",
95
+ badgeVariant: "green",
96
+ },
97
+ {
98
+ name: "Coven Canvas Backpack",
99
+ description: "Waxed canvas with a quiet, moonlit profile.",
100
+ badge: "Limited",
101
+ badgeVariant: "yellow",
102
+ },
113
103
  ];
114
104
 
115
105
  // Dracula product scene: harvest moon over sleeping hills with a per-product
116
106
  // accent glyph. All fills are brand vars, corners rx=4.
117
- function ProductArt({hue, label}: {hue: string; label: string}) {
118
- return (
119
- <SceneFrame
120
- label={`${label} artwork`}
121
- viewBox="0 0 400 300"
122
- fill="var(--dracula-bg-light)"
123
- hasBackdrop={false}>
124
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
125
- <circle cx="200" cy="110" r="52" fill={hue} opacity={0.9} />
126
- <circle
127
- cx="184"
128
- cy="100"
129
- r={44}
130
- fill="var(--dracula-bg-light)"
131
- opacity={0.55}
132
- />
133
- <path
134
- d="M0 210 Q120 170 240 200 T400 190 V300 H0 Z"
135
- fill="var(--dracula-current-line)"
136
- />
137
- <path
138
- d="M0 245 Q140 215 300 240 T400 235 V300 H0 Z"
139
- fill="var(--dracula-bg)"
140
- />
141
- <g
142
- transform="translate(200 232)"
143
- fill="none"
144
- stroke={hue}
145
- strokeWidth="5"
146
- strokeLinecap="round"
147
- strokeLinejoin="round">
148
- <rect x="-30" y="-30" width="60" height="60" rx={4} />
149
- <circle cx="13" cy="-13" r="2.5" fill={hue} stroke="none" />
150
- <path d="M-22 20 L-5 2 L7 12 L14 5 L23 15" />
151
- </g>
152
- </SceneFrame>
153
- );
107
+ function ProductArt({ hue, label }: { hue: string; label: string }) {
108
+ return (
109
+ <SceneFrame
110
+ label={`${label} artwork`}
111
+ viewBox="0 0 400 300"
112
+ fill="var(--dracula-bg-light)"
113
+ hasBackdrop={false}
114
+ >
115
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
116
+ <circle cx="200" cy="110" r="52" fill={hue} opacity={0.9} />
117
+ <circle cx="184" cy="100" r={44} fill="var(--dracula-bg-light)" opacity={0.55} />
118
+ <path d="M0 210 Q120 170 240 200 T400 190 V300 H0 Z" fill="var(--dracula-current-line)" />
119
+ <path d="M0 245 Q140 215 300 240 T400 235 V300 H0 Z" fill="var(--dracula-bg)" />
120
+ <g
121
+ transform="translate(200 232)"
122
+ fill="none"
123
+ stroke={hue}
124
+ strokeWidth="5"
125
+ strokeLinecap="round"
126
+ strokeLinejoin="round"
127
+ >
128
+ <rect x="-30" y="-30" width="60" height="60" rx={4} />
129
+ <circle cx="13" cy="-13" r="2.5" fill={hue} stroke="none" />
130
+ <path d="M-22 20 L-5 2 L7 12 L14 5 L23 15" />
131
+ </g>
132
+ </SceneFrame>
133
+ );
154
134
  }
155
135
 
156
136
  // Default export is the route page (sandbox renders this as a Next.js page, so
@@ -159,495 +139,456 @@ function ProductArt({hue, label}: {hue: string; label: string}) {
159
139
  // or inventory in, so the props/store split bought indirection for zero
160
140
  // callers.
161
141
  export default function ThemeShowcase() {
162
- const {isMobile} = useAppShellMobile();
163
- return (
164
- <VStack
165
- gap={0}
166
- minHeight="100%"
167
- style={{backgroundColor: 'var(--color-background-body)'}}>
168
- <StorePreview products={DEFAULT_PRODUCTS} isMobile={isMobile} />
169
- <VStack
170
- gap={0}
171
- padding={6}
172
- style={{backgroundColor: 'var(--color-background-surface)'}}>
173
- <CardShowcase inventory={DEFAULT_INVENTORY} isMobile={isMobile} />
174
- </VStack>
175
- </VStack>
176
- );
142
+ const { isMobile } = useAppShellMobile();
143
+ return (
144
+ <VStack gap={0} minHeight="100%" style={{ backgroundColor: "var(--color-background-body)" }}>
145
+ <StorePreview products={DEFAULT_PRODUCTS} isMobile={isMobile} />
146
+ <VStack gap={0} padding={6} style={{ backgroundColor: "var(--color-background-surface)" }}>
147
+ <CardShowcase inventory={DEFAULT_INVENTORY} isMobile={isMobile} />
148
+ </VStack>
149
+ </VStack>
150
+ );
177
151
  }
178
152
 
179
- function CardShowcase({
180
- inventory,
181
- isMobile,
182
- }: {
183
- inventory: InventoryRow[];
184
- isMobile: boolean;
185
- }) {
186
- const columns = isMobile ? 1 : SHOWCASE_COLUMNS;
187
-
188
- return (
189
- <VStack gap={8}>
190
- <ChatCard />
191
- <Grid columns={columns} gap={4}>
192
- <GridSpan columns={isMobile ? 1 : 3}>
193
- <InventoryCard inventory={inventory} />
194
- </GridSpan>
195
- <GridSpan columns={1}>
196
- <LatestActivityCard isMobile={isMobile} />
197
- </GridSpan>
198
- </Grid>
199
- </VStack>
200
- );
153
+ function CardShowcase({ inventory, isMobile }: { inventory: InventoryRow[]; isMobile: boolean }) {
154
+ const columns = isMobile ? 1 : SHOWCASE_COLUMNS;
155
+
156
+ return (
157
+ <VStack gap={8}>
158
+ <ChatCard />
159
+ <Grid columns={columns} gap={4}>
160
+ <GridSpan columns={isMobile ? 1 : 3}>
161
+ <InventoryCard inventory={inventory} />
162
+ </GridSpan>
163
+ <GridSpan columns={1}>
164
+ <LatestActivityCard isMobile={isMobile} />
165
+ </GridSpan>
166
+ </Grid>
167
+ </VStack>
168
+ );
201
169
  }
202
170
 
203
- function StorePreview({
204
- products,
205
- isMobile,
206
- }: {
207
- products: ProductSpec[];
208
- isMobile: boolean;
209
- }) {
210
- // Shell-less preview: no AppShell above means the host context always
211
- // reports desktop, so the nav reads the real viewport instead.
212
- const isNarrowViewport = useMediaQuery('(max-width: 640px)');
213
- const collapseNav = isMobile || isNarrowViewport;
214
- return (
215
- <VStack gap={0} data-theme-preview="true">
216
- <VStack gap={0}>
217
- <TopNav
218
- label="Theme preview navigation"
219
- heading={<TopNavHeading heading="Nocturne" />}
220
- centerContent={
221
- collapseNav ? undefined : (
222
- <>
223
- <TopNavItem label="Shop" href="#/templates/theme-showcase" isSelected />
224
- <TopNavItem label="New In" href="#/templates/theme-showcase" />
225
- <TopNavItem label="Stories" href="#/templates/theme-showcase" />
226
- <TopNavItem label="Help" href="#/templates/theme-showcase" />
227
- </>
228
- )
229
- }
230
- endContent={
231
- collapseNav ? (
232
- <Button
233
- label="Cart"
234
- tooltip="Cart"
235
- variant="ghost"
236
- isIconOnly
237
- icon={<ShoppingBag size={20} />}
238
- href="#/templates/theme-showcase"
239
- />
240
- ) : (
241
- <HStack gap={2} vAlign="center">
242
- <HStack gap={0.5}>
243
- <Button
244
- label="Search"
245
- tooltip="Search"
246
- variant="ghost"
247
- isIconOnly
248
- icon={<Search size={20} />}
249
- href="#/templates/theme-showcase"
250
- />
251
- <Button
252
- label="Account"
253
- tooltip="Account"
254
- variant="ghost"
255
- isIconOnly
256
- icon={<User size={20} />}
257
- href="#/templates/theme-showcase"
258
- />
259
- <Button
260
- label="Cart"
261
- tooltip="Cart"
262
- variant="ghost"
263
- isIconOnly
264
- icon={<ShoppingBag size={20} />}
265
- href="#/templates/theme-showcase"
266
- />
267
- </HStack>
268
- <Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
269
- </HStack>
270
- )
271
- }
272
- />
273
-
274
- <Section padding={6} variant="transparent">
275
- <VStack
276
- gap={10}
277
- maxWidth={880}
278
- width="100%"
279
- style={{marginInline: 'auto', minWidth: 0}}>
280
- <Center>
281
- <VStack gap={4} hAlign="center" maxWidth={560}>
282
- <Heading level={1} type="display-2" justify="center">
283
- Little haunts,
284
- <br />
285
- everywhere you roam
286
- </Heading>
287
- <Text type="body" color="secondary" justify="center">
288
- We believe the smallest shadows are the ones that matter most.
289
- Turn an ordinary evening into something worth remembering.
290
- </Text>
291
- </VStack>
292
- </Center>
293
-
294
- <Grid columns={isMobile ? 1 : PRODUCT_COLUMNS} gap={4}>
295
- {products.map((p, i) => (
296
- <Card key={p.name} padding={0} height="100%">
297
- <VStack gap={0} height="100%">
298
- <AspectRatio ratio={1}>
299
- <ProductArt
300
- hue={PRODUCT_HUES[i % PRODUCT_HUES.length]}
301
- label={p.name}
302
- />
303
- </AspectRatio>
304
- <VStack
305
- gap={2}
306
- hAlign="center"
307
- padding={4}
308
- style={{flex: 1}}>
309
- <HStack>
310
- <Badge label={p.badge} variant={p.badgeVariant} />
311
- </HStack>
312
- <Heading level={2} justify="center">
313
- {p.name}
314
- </Heading>
315
- <Text
316
- type="body"
317
- color="secondary"
318
- justify="center"
319
- style={{flex: 1}}>
320
- {p.description}
321
- </Text>
322
- <HStack gap={2} vAlign="center" hAlign="center">
323
- <NumberInput
324
- label="Quantity"
325
- isLabelHidden
326
- value={1}
327
- onChange={() => {}}
328
- min={1}
329
- max={99}
330
- size="sm"
331
- // minWidth (not a hard width) so the field grows to
332
- // fit the digit + the theme's input padding. A fixed
333
- // 40px was too tight on themes with larger padding /
334
- // bigger type scale (e.g. Matcha, Y2K), clipping the
335
- // value.
336
- style={{minWidth: 64, flexShrink: 0}}
337
- />
338
- <Button
339
- label="Add to cart"
340
- variant="secondary"
341
- size="sm"
342
- href="#/templates/theme-showcase"
343
- style={{flex: 1}}
344
- />
345
- </HStack>
346
- </VStack>
347
- </VStack>
348
- </Card>
349
- ))}
350
- </Grid>
351
- </VStack>
352
- </Section>
353
- </VStack>
354
- </VStack>
355
- );
171
+ function StorePreview({ products, isMobile }: { products: ProductSpec[]; isMobile: boolean }) {
172
+ // Shell-less preview: no AppShell above means the host context always
173
+ // reports desktop, so the nav reads the real viewport instead.
174
+ const isNarrowViewport = useMediaQuery("(max-width: 640px)");
175
+ const collapseNav = isMobile || isNarrowViewport;
176
+ return (
177
+ <VStack gap={0} data-theme-preview="true">
178
+ <VStack gap={0}>
179
+ <TopNav
180
+ label="Theme preview navigation"
181
+ heading={<TopNavHeading heading="Nocturne" />}
182
+ centerContent={
183
+ collapseNav ? undefined : (
184
+ <>
185
+ <TopNavItem label="Shop" href="#/templates/theme-showcase" isSelected />
186
+ <TopNavItem label="New In" href="#/templates/theme-showcase" />
187
+ <TopNavItem label="Stories" href="#/templates/theme-showcase" />
188
+ <TopNavItem label="Help" href="#/templates/theme-showcase" />
189
+ </>
190
+ )
191
+ }
192
+ endContent={
193
+ collapseNav ? (
194
+ <Button
195
+ label="Cart"
196
+ tooltip="Cart"
197
+ variant="ghost"
198
+ isIconOnly
199
+ icon={<ShoppingBag size={20} />}
200
+ href="#/templates/theme-showcase"
201
+ />
202
+ ) : (
203
+ <HStack gap={2} vAlign="center">
204
+ <HStack gap={0.5}>
205
+ <Button
206
+ label="Search"
207
+ tooltip="Search"
208
+ variant="ghost"
209
+ isIconOnly
210
+ icon={<Search size={20} />}
211
+ href="#/templates/theme-showcase"
212
+ />
213
+ <Button
214
+ label="Account"
215
+ tooltip="Account"
216
+ variant="ghost"
217
+ isIconOnly
218
+ icon={<User size={20} />}
219
+ href="#/templates/theme-showcase"
220
+ />
221
+ <Button
222
+ label="Cart"
223
+ tooltip="Cart"
224
+ variant="ghost"
225
+ isIconOnly
226
+ icon={<ShoppingBag size={20} />}
227
+ href="#/templates/theme-showcase"
228
+ />
229
+ </HStack>
230
+ <Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
231
+ </HStack>
232
+ )
233
+ }
234
+ />
235
+
236
+ <Section padding={6} variant="transparent">
237
+ <VStack
238
+ gap={10}
239
+ maxWidth={880}
240
+ width="100%"
241
+ style={{ marginInline: "auto", minWidth: 0 }}
242
+ >
243
+ <Center>
244
+ <VStack gap={4} hAlign="center" maxWidth={560}>
245
+ <Heading level={1} type="display-2" justify="center">
246
+ Little haunts,
247
+ <br />
248
+ everywhere you roam
249
+ </Heading>
250
+ <Text type="body" color="secondary" justify="center">
251
+ We believe the smallest shadows are the ones that matter most. Turn an ordinary
252
+ evening into something worth remembering.
253
+ </Text>
254
+ </VStack>
255
+ </Center>
256
+
257
+ <Grid columns={isMobile ? 1 : PRODUCT_COLUMNS} gap={4}>
258
+ {products.map((p, i) => (
259
+ <Card key={p.name} padding={0} height="100%">
260
+ <VStack gap={0} height="100%">
261
+ <AspectRatio ratio={1}>
262
+ <ProductArt hue={PRODUCT_HUES[i % PRODUCT_HUES.length]} label={p.name} />
263
+ </AspectRatio>
264
+ <VStack gap={2} hAlign="center" padding={4} style={{ flex: 1 }}>
265
+ <HStack>
266
+ <Badge label={p.badge} variant={p.badgeVariant} />
267
+ </HStack>
268
+ <Heading level={2} justify="center">
269
+ {p.name}
270
+ </Heading>
271
+ <Text type="body" color="secondary" justify="center" style={{ flex: 1 }}>
272
+ {p.description}
273
+ </Text>
274
+ <HStack gap={2} vAlign="center" hAlign="center">
275
+ <NumberInput
276
+ label="Quantity"
277
+ isLabelHidden
278
+ value={1}
279
+ onChange={() => {}}
280
+ min={1}
281
+ max={99}
282
+ size="sm"
283
+ // minWidth (not a hard width) so the field grows to
284
+ // fit the digit + the theme's input padding. A fixed
285
+ // 40px was too tight on themes with larger padding /
286
+ // bigger type scale (e.g. Matcha, Y2K), clipping the
287
+ // value.
288
+ controlStyle={{ minWidth: 64, flexShrink: 0 }}
289
+ />
290
+ <Button
291
+ label="Add to cart"
292
+ variant="secondary"
293
+ size="sm"
294
+ href="#/templates/theme-showcase"
295
+ style={{ flex: 1 }}
296
+ />
297
+ </HStack>
298
+ </VStack>
299
+ </VStack>
300
+ </Card>
301
+ ))}
302
+ </Grid>
303
+ </VStack>
304
+ </Section>
305
+ </VStack>
306
+ </VStack>
307
+ );
356
308
  }
357
309
 
358
- const SUGGESTED_QUESTIONS = [
359
- 'Reschedule delivery',
360
- 'Update shipping address',
361
- 'Start a return',
362
- ];
310
+ const SUGGESTED_QUESTIONS = ["Reschedule delivery", "Update shipping address", "Start a return"];
363
311
 
364
312
  function ChatCard() {
365
- return (
366
- <Card
367
- padding={0}
368
- style={{
369
- backgroundColor: 'var(--color-background-surface)',
370
- color: 'var(--color-text-primary)',
371
- minWidth: 0,
372
- overflow: 'hidden',
373
- display: 'flex',
374
- flexDirection: 'column',
375
- }}>
376
- <HStack
377
- hAlign="between"
378
- vAlign="center"
379
- gap={3}
380
- paddingBlock={4}
381
- paddingInline={4}>
382
- <Heading level={2}>Night Owl AI</Heading>
383
-
384
- <HStack gap={1} vAlign="center">
385
- <Button
386
- variant="ghost"
387
- size="sm"
388
- isIconOnly
389
- label="Export conversation"
390
- tooltip="Export conversation"
391
- icon={<Download size={16} />}
392
- />
393
- <Button
394
- variant="ghost"
395
- size="sm"
396
- isIconOnly
397
- label="Close chat"
398
- tooltip="Close chat"
399
- icon={<X size={16} />}
400
- />
401
- </HStack>
402
- </HStack>
403
-
404
- <Divider variant="subtle" />
405
-
406
- <VStack
407
- gap={0}
408
- isScrollable={false}
409
- tabIndex={0}
410
- role="region"
411
- aria-label="Night Owl chat messages"
412
- style={{
413
- flex: 1,
414
- minHeight: 0,
415
- // The message region is the scroll owner: the card is stretched to the
416
- // grid row height, so longer conversations must scroll here rather than
417
- // clip against the card.
418
- overflowY: 'auto',
419
- }}>
420
- <ChatMessageList>
421
- <ChatSystemMessage>Today</ChatSystemMessage>
422
-
423
- <ChatMessage sender="user">
424
- <ChatMessageBubble variant="filled">
425
- Where's my order?
426
- </ChatMessageBubble>
427
- </ChatMessage>
428
-
429
- <ChatMessage sender="assistant">
430
- <VStack gap={3}>
431
- <Text type="body">
432
- Your order #1043 (the Moonphase Watch and Belfry Throw)
433
- shipped this morning from the courier-raven roost and is currently
434
- in transit with courier-raven. It's on track to arrive at your address by
435
- end of day tomorrow.
436
- </Text>
437
- <Text type="body">
438
- Let me know if you'd like to reschedule the delivery, redirect
439
- it to a pickup point, or start a return once it arrives.
440
- </Text>
441
- </VStack>
442
- </ChatMessage>
443
-
444
- <ChatMessage sender="user">
445
- <ChatMessageBubble variant="filled">
446
- Can you show me the full details?
447
- </ChatMessageBubble>
448
- </ChatMessage>
449
-
450
- <ChatMessage sender="assistant">
451
- <VStack gap={3}>
452
- <Text type="body">Here's everything I have on order #1043:</Text>
453
- <Card padding={3}>
454
- <VStack gap={1}>
455
- <Item
456
- label="Items"
457
- description="Moonphase Watch · Belfry Throw"
458
- endContent={
459
- <Text type="body" weight="semibold" hasTabularNumbers>
460
- $248
461
- </Text>
462
- }
463
- />
464
- <Item
465
- label="Shipping"
466
- description="Courier-raven ground"
467
- endContent={
468
- <Text type="body" weight="semibold" hasTabularNumbers>
469
- $12
470
- </Text>
471
- }
472
- />
473
- <Item
474
- label="Estimated arrival"
475
- description="Tomorrow by 8pm"
476
- endContent={
477
- <HStack gap={1} vAlign="center">
478
- <StatusDot variant="success" label="On time" />
479
- <Text type="supporting" color="secondary">
480
- On time
481
- </Text>
482
- </HStack>
483
- }
484
- />
485
- <Item
486
- label="Tracking"
487
- description="RVN 1Z 999 AA1 0123 4567 84"
488
- endContent={<Link href="#/templates/theme-showcase">Track</Link>}
489
- />
490
- </VStack>
491
- </Card>
492
- </VStack>
493
- </ChatMessage>
494
- </ChatMessageList>
495
- </VStack>
496
-
497
- <VStack gap={0} paddingInline={4} paddingBlockEnd={2}>
498
- <HStack gap={1} hAlign="center" wrap="wrap">
499
- {SUGGESTED_QUESTIONS.map(question => (
500
- <Button
501
- key={question}
502
- variant="secondary"
503
- size="sm"
504
- label={question}
505
- />
506
- ))}
507
- </HStack>
508
- </VStack>
509
-
510
- <VStack gap={0} paddingInline={4} paddingBlockEnd={4}>
511
- <ChatComposer
512
- value=""
513
- onChange={() => {}}
514
- onSubmit={() => {}}
515
- placeholder="Ask Night Owl..."
516
- footerActions={
517
- <Button
518
- variant="ghost"
519
- size="md"
520
- isIconOnly
521
- label="Attach"
522
- tooltip="Attach"
523
- icon={<Plus size={16} />}
524
- />
525
- }
526
- sendActions={
527
- <Button
528
- variant="ghost"
529
- size="md"
530
- isIconOnly
531
- label="Voice input"
532
- tooltip="Voice input"
533
- icon={<Mic size={16} />}
534
- />
535
- }
536
- />
537
- </VStack>
538
- </Card>
539
- );
313
+ return (
314
+ <Card
315
+ padding={0}
316
+ style={{
317
+ backgroundColor: "var(--color-background-surface)",
318
+ color: "var(--color-text-primary)",
319
+ minWidth: 0,
320
+ overflow: "hidden",
321
+ display: "flex",
322
+ flexDirection: "column",
323
+ }}
324
+ >
325
+ <HStack hAlign="between" vAlign="center" gap={3} paddingBlock={4} paddingInline={4}>
326
+ <Heading level={2}>Night Owl AI</Heading>
327
+
328
+ <HStack gap={1} vAlign="center">
329
+ <Button
330
+ variant="ghost"
331
+ size="sm"
332
+ isIconOnly
333
+ label="Export conversation"
334
+ tooltip="Export conversation"
335
+ icon={<Download size={16} />}
336
+ />
337
+ <Button
338
+ variant="ghost"
339
+ size="sm"
340
+ isIconOnly
341
+ label="Close chat"
342
+ tooltip="Close chat"
343
+ icon={<X size={16} />}
344
+ />
345
+ </HStack>
346
+ </HStack>
347
+
348
+ <Divider variant="subtle" />
349
+
350
+ <VStack
351
+ gap={0}
352
+ isScrollable={false}
353
+ tabIndex={0}
354
+ role="region"
355
+ aria-label="Night Owl chat messages"
356
+ style={{
357
+ flex: 1,
358
+ minHeight: 0,
359
+ // The message region is the scroll owner: the card is stretched to the
360
+ // grid row height, so longer conversations must scroll here rather than
361
+ // clip against the card.
362
+ overflowY: "auto",
363
+ }}
364
+ >
365
+ <ChatMessageList>
366
+ <ChatSystemMessage>Today</ChatSystemMessage>
367
+
368
+ <ChatMessage sender="user">
369
+ <ChatMessageBubble variant="filled">Where's my order?</ChatMessageBubble>
370
+ </ChatMessage>
371
+
372
+ <ChatMessage sender="assistant">
373
+ <VStack gap={3}>
374
+ <Text type="body">
375
+ Your order #1043 (the Moonphase Watch and Belfry Throw) shipped this morning from
376
+ the courier-raven roost and is currently in transit with courier-raven. It's on
377
+ track to arrive at your address by end of day tomorrow.
378
+ </Text>
379
+ <Text type="body">
380
+ Let me know if you'd like to reschedule the delivery, redirect it to a pickup point,
381
+ or start a return once it arrives.
382
+ </Text>
383
+ </VStack>
384
+ </ChatMessage>
385
+
386
+ <ChatMessage sender="user">
387
+ <ChatMessageBubble variant="filled">
388
+ Can you show me the full details?
389
+ </ChatMessageBubble>
390
+ </ChatMessage>
391
+
392
+ <ChatMessage sender="assistant">
393
+ <VStack gap={3}>
394
+ <Text type="body">Here's everything I have on order #1043:</Text>
395
+ <Card padding={3}>
396
+ <VStack gap={1}>
397
+ <Item
398
+ label="Items"
399
+ description="Moonphase Watch · Belfry Throw"
400
+ endContent={
401
+ <Text type="body" weight="semibold" hasTabularNumbers>
402
+ $248
403
+ </Text>
404
+ }
405
+ />
406
+ <Item
407
+ label="Shipping"
408
+ description="Courier-raven ground"
409
+ endContent={
410
+ <Text type="body" weight="semibold" hasTabularNumbers>
411
+ $12
412
+ </Text>
413
+ }
414
+ />
415
+ <Item
416
+ label="Estimated arrival"
417
+ description="Tomorrow by 8pm"
418
+ endContent={
419
+ <HStack gap={1} vAlign="center">
420
+ <StatusDot variant="success" label="On time" />
421
+ <Text type="supporting" color="secondary">
422
+ On time
423
+ </Text>
424
+ </HStack>
425
+ }
426
+ />
427
+ <Item
428
+ label="Tracking"
429
+ description="RVN 1Z 999 AA1 0123 4567 84"
430
+ endContent={<Link href="#/templates/theme-showcase">Track</Link>}
431
+ />
432
+ </VStack>
433
+ </Card>
434
+ </VStack>
435
+ </ChatMessage>
436
+ </ChatMessageList>
437
+ </VStack>
438
+
439
+ <VStack gap={0} paddingInline={4} paddingBlockEnd={2}>
440
+ <HStack gap={1} hAlign="center" wrap="wrap">
441
+ {SUGGESTED_QUESTIONS.map((question) => (
442
+ <Button key={question} variant="secondary" size="sm" label={question} />
443
+ ))}
444
+ </HStack>
445
+ </VStack>
446
+
447
+ <VStack gap={0} paddingInline={4} paddingBlockEnd={4}>
448
+ <ChatComposer
449
+ value=""
450
+ onChange={() => {}}
451
+ onSubmit={() => {}}
452
+ placeholder="Ask Night Owl..."
453
+ footerActions={
454
+ <Button
455
+ variant="ghost"
456
+ size="md"
457
+ isIconOnly
458
+ label="Attach"
459
+ tooltip="Attach"
460
+ icon={<Plus size={16} />}
461
+ />
462
+ }
463
+ sendActions={
464
+ <Button
465
+ variant="ghost"
466
+ size="md"
467
+ isIconOnly
468
+ label="Voice input"
469
+ tooltip="Voice input"
470
+ icon={<Mic size={16} />}
471
+ />
472
+ }
473
+ />
474
+ </VStack>
475
+ </Card>
476
+ );
540
477
  }
541
478
 
542
479
  interface ActivityRow {
543
- id: string;
544
- icon: ReactNode;
545
- label: string;
546
- detail: string;
547
- time: string;
548
- amount: number;
480
+ id: string;
481
+ icon: ReactNode;
482
+ label: string;
483
+ detail: string;
484
+ time: string;
485
+ amount: number;
549
486
  }
550
487
 
551
488
  const ACTIVITY: ActivityRow[] = [
552
- {
553
- id: '1',
554
- icon: <ShoppingBag size={16} />,
555
- label: 'Order #1043',
556
- detail: 'Placed · 1:59 pm',
557
- time: '1:59 pm',
558
- amount: 248,
559
- },
560
- {
561
- id: '2',
562
- icon: <Banknote size={16} />,
563
- label: 'Order #1041',
564
- detail: 'Refunded · 12:40 pm',
565
- time: '12:40 pm',
566
- amount: -89,
567
- },
568
- {
569
- id: '3',
570
- icon: <ShoppingBag size={16} />,
571
- label: 'Order #1040',
572
- detail: 'Placed · 10:30 am',
573
- time: '10:30 am',
574
- amount: 156,
575
- },
576
- {
577
- id: '4',
578
- icon: <ShoppingBag size={16} />,
579
- label: 'Order #1038',
580
- detail: 'Placed · 9:11 am',
581
- time: '9:11 am',
582
- amount: 412,
583
- },
584
- {
585
- id: '5',
586
- icon: <ShoppingBag size={16} />,
587
- label: 'Order #1037',
588
- detail: 'Placed · 8:42 am',
589
- time: '8:42 am',
590
- amount: 95,
591
- },
489
+ {
490
+ id: "1",
491
+ icon: <ShoppingBag size={16} />,
492
+ label: "Order #1043",
493
+ detail: "Placed · 1:59 pm",
494
+ time: "1:59 pm",
495
+ amount: 248,
496
+ },
497
+ {
498
+ id: "2",
499
+ icon: <Banknote size={16} />,
500
+ label: "Order #1041",
501
+ detail: "Refunded · 12:40 pm",
502
+ time: "12:40 pm",
503
+ amount: -89,
504
+ },
505
+ {
506
+ id: "3",
507
+ icon: <ShoppingBag size={16} />,
508
+ label: "Order #1040",
509
+ detail: "Placed · 10:30 am",
510
+ time: "10:30 am",
511
+ amount: 156,
512
+ },
513
+ {
514
+ id: "4",
515
+ icon: <ShoppingBag size={16} />,
516
+ label: "Order #1038",
517
+ detail: "Placed · 9:11 am",
518
+ time: "9:11 am",
519
+ amount: 412,
520
+ },
521
+ {
522
+ id: "5",
523
+ icon: <ShoppingBag size={16} />,
524
+ label: "Order #1037",
525
+ detail: "Placed · 8:42 am",
526
+ time: "8:42 am",
527
+ amount: 95,
528
+ },
592
529
  ];
593
530
 
594
531
  function formatAmount(amount: number): string {
595
- const sign = amount < 0 ? '−' : '+';
596
- return sign + '$' + Math.abs(amount).toLocaleString();
532
+ const sign = amount < 0 ? "−" : "+";
533
+ return `${sign}$${Math.abs(amount).toLocaleString()}`;
597
534
  }
598
535
 
599
- function LatestActivityCard({isMobile}: {isMobile: boolean}) {
600
- return (
601
- <Card
602
- padding={4}
603
- height="100%"
604
- style={{
605
- backgroundColor: 'var(--color-background-surface)',
606
- color: 'var(--color-text-primary)',
607
- minWidth: 0,
608
- }}>
609
- <VStack gap={4} height="100%">
610
- <Heading level={2}>Revenue</Heading>
611
-
612
- <Grid columns={isMobile ? 1 : 2} gap={3}>
613
- <VStack gap={0}>
614
- <Text type="display-3" weight="semibold" hasTabularNumbers>
615
- 18K
616
- </Text>
617
- <Text type="supporting" color="secondary">
618
- Monthly revenue
619
- </Text>
620
- </VStack>
621
- <VStack gap={0}>
622
- <Text type="display-3" weight="semibold" hasTabularNumbers>
623
- +12%
624
- </Text>
625
- <Text type="supporting" color="secondary">
626
- Order growth
627
- </Text>
628
- </VStack>
629
- </Grid>
630
-
631
- <Divider variant="subtle" />
632
-
633
- <HStack hAlign="between" vAlign="center">
634
- <Heading level={3}>Activity</Heading>
635
- <Link href="#/templates/theme-showcase">See all</Link>
636
- </HStack>
637
-
638
- <VStack
639
- gap={1}
640
- style={{
641
- flex: 1,
642
- minHeight: 0,
643
- overflow: 'hidden',
644
- maskImage:
645
- 'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
646
- WebkitMaskImage:
647
- 'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
648
- marginInline: 'calc(var(--spacing-2) * -1)',
649
- }}>
650
- {/* The sign travels in the string (formatAmount prefixes U+2212), so
536
+ function LatestActivityCard({ isMobile }: { isMobile: boolean }) {
537
+ return (
538
+ <Card
539
+ padding={4}
540
+ height="100%"
541
+ style={{
542
+ backgroundColor: "var(--color-background-surface)",
543
+ color: "var(--color-text-primary)",
544
+ minWidth: 0,
545
+ }}
546
+ >
547
+ <VStack gap={4} height="100%">
548
+ <Heading level={2}>Revenue</Heading>
549
+
550
+ <Grid columns={isMobile ? 1 : 2} gap={3}>
551
+ <VStack gap={0}>
552
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
553
+ 18K
554
+ </Text>
555
+ <Text type="supporting" color="secondary">
556
+ Monthly revenue
557
+ </Text>
558
+ </VStack>
559
+ <VStack gap={0}>
560
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
561
+ +12%
562
+ </Text>
563
+ <Text type="supporting" color="secondary">
564
+ Order growth
565
+ </Text>
566
+ </VStack>
567
+ </Grid>
568
+
569
+ <Divider variant="subtle" />
570
+
571
+ <HStack hAlign="between" vAlign="center">
572
+ <Heading level={3}>Activity</Heading>
573
+ <Link href="#/templates/theme-showcase">See all</Link>
574
+ </HStack>
575
+
576
+ <VStack
577
+ gap={1}
578
+ style={{
579
+ flex: 1,
580
+ minHeight: 0,
581
+ overflow: "hidden",
582
+ maskImage: "linear-gradient(to bottom, black calc(100% - 48px), transparent)",
583
+ WebkitMaskImage: "linear-gradient(to bottom, black calc(100% - 48px), transparent)",
584
+ // Cancel the Item inline inset with the inset ITSELF: core
585
+ // publishes it as --_item-inset-inline (0.6.7), and it
586
+ // tracks density and theme overrides. A literal spacing
587
+ // step silently stops matching when either moves.
588
+ marginInline: "calc(-1 * var(--_item-inset-inline))",
589
+ }}
590
+ >
591
+ {/* The sign travels in the string (formatAmount prefixes U+2212), so
651
592
  direction is never colour-alone and 1.4.1 holds without a tint.
652
593
  A negative text role is not available here, and the reason is
653
594
  worth recording rather than working around:
@@ -661,178 +602,180 @@ function LatestActivityCard({isMobile}: {isMobile: boolean}) {
661
602
  secondary therefore stays and the minus sign is the signal. A
662
603
  kit-wide negative text role needs a new token AND a colour that
663
604
  clears 4.5:1 on a card — a human call, not a call-site one. */}
664
- {ACTIVITY.map(item => (
665
- <Item
666
- key={item.id}
667
- startContent={
668
- <Center
669
- width={32}
670
- height={32}
671
- aria-hidden="true"
672
- style={{
673
- // Center supplies the 32px box and the centering; only the
674
- // paint stays here. Surface, not muted: secondary
675
- // #9AA1BC is 3.57:1 on muted #44475A, under the 4.5:1 text
676
- // floor. The chip is aria-hidden decorative art so it
677
- // clears the 3:1 non-text bar as-is, but the surface fill
678
- // carries the same 4.60:1 pairing at no cost.
679
- borderRadius: 'var(--radius-element)',
680
- backgroundColor: 'var(--color-background-surface)',
681
- color: 'var(--color-text-secondary)',
682
- flexShrink: 0,
683
- }}>
684
- {item.icon}
685
- </Center>
686
- }
687
- label={item.label}
688
- description={item.detail}
689
- endContent={
690
- <Text
691
- type="body"
692
- weight="semibold"
693
- hasTabularNumbers
694
- color={item.amount < 0 ? 'secondary' : 'primary'}>
695
- {formatAmount(item.amount)}
696
- </Text>
697
- }
698
- href="#/templates/theme-showcase"
699
- />
700
- ))}
701
- </VStack>
702
- </VStack>
703
- </Card>
704
- );
605
+ {ACTIVITY.map((item) => (
606
+ <Item
607
+ key={item.id}
608
+ startContent={
609
+ <Center
610
+ width={32}
611
+ height={32}
612
+ aria-hidden="true"
613
+ style={{
614
+ // Center supplies the 32px box and the centering; only the
615
+ // paint stays here. Surface, not muted: secondary
616
+ // #9AA1BC is 3.57:1 on muted #44475A, under the 4.5:1 text
617
+ // floor. The chip is aria-hidden decorative art so it
618
+ // clears the 3:1 non-text bar as-is, but the surface fill
619
+ // carries the same 4.60:1 pairing at no cost.
620
+ borderRadius: "var(--radius-element)",
621
+ backgroundColor: "var(--color-background-surface)",
622
+ color: "var(--color-text-secondary)",
623
+ flexShrink: 0,
624
+ }}
625
+ >
626
+ {item.icon}
627
+ </Center>
628
+ }
629
+ label={item.label}
630
+ description={item.detail}
631
+ endContent={
632
+ <Text
633
+ type="body"
634
+ weight="semibold"
635
+ hasTabularNumbers
636
+ color={item.amount < 0 ? "secondary" : "primary"}
637
+ >
638
+ {formatAmount(item.amount)}
639
+ </Text>
640
+ }
641
+ href="#/templates/theme-showcase"
642
+ />
643
+ ))}
644
+ </VStack>
645
+ </VStack>
646
+ </Card>
647
+ );
705
648
  }
706
649
 
707
- type TagSpec = {label: string; variant: ShowcaseBadgeVariant};
650
+ type TagSpec = { label: string; variant: ShowcaseBadgeVariant };
708
651
 
709
652
  export interface InventoryRow extends Record<string, unknown> {
710
- id: string;
711
- name: string;
712
- meta: string;
713
- available: number;
714
- location: string;
715
- tags: TagSpec[];
716
- hue: SceneHue;
717
- selected: boolean;
653
+ id: string;
654
+ name: string;
655
+ meta: string;
656
+ available: number;
657
+ location: string;
658
+ tags: TagSpec[];
659
+ hue: SceneHue;
660
+ selected: boolean;
718
661
  }
719
662
 
720
663
  const DEFAULT_INVENTORY: InventoryRow[] = [
721
- {
722
- id: 'a',
723
- name: 'Moonphase Watch',
724
- meta: 'Steel case, moonphase dial',
725
- available: 42,
726
- location: 'Roost 3',
727
- tags: [{label: 'New', variant: 'cyan'}],
728
- hue: 'var(--dracula-comment)',
729
- selected: false,
730
- },
731
- {
732
- id: 'b',
733
- name: 'Night-Owl Headphones',
734
- meta: 'ANC, 30hr battery',
735
- available: 128,
736
- location: 'Roost 1',
737
- tags: [{label: 'Popular', variant: 'green'}],
738
- hue: 'var(--dracula-cyan)',
739
- selected: true,
740
- },
741
- {
742
- id: 'c',
743
- name: 'Coven Canvas Backpack',
744
- meta: 'Waxed canvas, 25L',
745
- available: 63,
746
- location: 'Roost 2',
747
- tags: [{label: 'Limited', variant: 'yellow'}],
748
- hue: 'var(--dracula-yellow)',
749
- selected: false,
750
- },
751
- {
752
- id: 'd',
753
- name: 'Night Market Wallet',
754
- meta: 'Full-grain, RFID blocking',
755
- available: 15,
756
- location: 'Roost 4',
757
- tags: [{label: 'Leather', variant: 'yellow'}],
758
- hue: 'var(--dracula-orange)',
759
- selected: true,
760
- },
761
- {
762
- id: 'e',
763
- name: 'Midnight Tumbler',
764
- meta: 'Vacuum insulated, 16oz',
765
- available: 87,
766
- location: 'Roost 5',
767
- tags: [{label: 'Drinkware', variant: 'yellow'}],
768
- hue: 'var(--dracula-pink)',
769
- selected: false,
770
- },
771
- {
772
- id: 'f',
773
- name: 'Belfry Throw',
774
- meta: 'Heavyweight, oat',
775
- available: 24,
776
- location: 'Roost 6',
777
- tags: [{label: 'Home', variant: 'yellow'}],
778
- hue: 'var(--dracula-green)',
779
- selected: true,
780
- },
664
+ {
665
+ id: "a",
666
+ name: "Moonphase Watch",
667
+ meta: "Steel case, moonphase dial",
668
+ available: 42,
669
+ location: "Roost 3",
670
+ tags: [{ label: "New", variant: "cyan" }],
671
+ hue: "var(--dracula-comment)",
672
+ selected: false,
673
+ },
674
+ {
675
+ id: "b",
676
+ name: "Night-Owl Headphones",
677
+ meta: "ANC, 30hr battery",
678
+ available: 128,
679
+ location: "Roost 1",
680
+ tags: [{ label: "Popular", variant: "green" }],
681
+ hue: "var(--dracula-cyan)",
682
+ selected: true,
683
+ },
684
+ {
685
+ id: "c",
686
+ name: "Coven Canvas Backpack",
687
+ meta: "Waxed canvas, 25L",
688
+ available: 63,
689
+ location: "Roost 2",
690
+ tags: [{ label: "Limited", variant: "yellow" }],
691
+ hue: "var(--dracula-yellow)",
692
+ selected: false,
693
+ },
694
+ {
695
+ id: "d",
696
+ name: "Night Market Wallet",
697
+ meta: "Full-grain, RFID blocking",
698
+ available: 15,
699
+ location: "Roost 4",
700
+ tags: [{ label: "Leather", variant: "yellow" }],
701
+ hue: "var(--dracula-orange)",
702
+ selected: true,
703
+ },
704
+ {
705
+ id: "e",
706
+ name: "Midnight Tumbler",
707
+ meta: "Vacuum insulated, 16oz",
708
+ available: 87,
709
+ location: "Roost 5",
710
+ tags: [{ label: "Drinkware", variant: "yellow" }],
711
+ hue: "var(--dracula-pink)",
712
+ selected: false,
713
+ },
714
+ {
715
+ id: "f",
716
+ name: "Belfry Throw",
717
+ meta: "Heavyweight, oat",
718
+ available: 24,
719
+ location: "Roost 6",
720
+ tags: [{ label: "Home", variant: "yellow" }],
721
+ hue: "var(--dracula-green)",
722
+ selected: true,
723
+ },
781
724
  ];
782
725
 
783
726
  const LOW_STOCK_THRESHOLD = 25;
784
727
 
785
- function SelectCell({row}: {row: InventoryRow}) {
786
- return (
787
- <CheckboxInput
788
- label={'Select ' + row.name}
789
- isLabelHidden
790
- value={row.selected}
791
- onChange={() => {}}
792
- />
793
- );
728
+ function SelectCell({ row }: { row: InventoryRow }) {
729
+ return (
730
+ <CheckboxInput
731
+ label={`Select ${row.name}`}
732
+ isLabelHidden
733
+ value={row.selected}
734
+ onChange={() => {}}
735
+ />
736
+ );
794
737
  }
795
738
 
796
- function ItemCell({row}: {row: InventoryRow}) {
797
- return (
798
- <HStack gap={3} vAlign="center">
799
- <ProductSwatch accent={row.hue} label={row.name} />
800
- <VStack gap={0} style={{minWidth: 0}}>
801
- <Text type="body" weight="semibold">
802
- {row.name}
803
- </Text>
804
- <Text type="supporting" color="secondary">
805
- {row.meta}
806
- </Text>
807
- </VStack>
808
- </HStack>
809
- );
739
+ function ItemCell({ row }: { row: InventoryRow }) {
740
+ return (
741
+ <HStack gap={3} vAlign="center">
742
+ <ProductSwatch accent={row.hue} label={row.name} />
743
+ <VStack gap={0} style={{ minWidth: 0 }}>
744
+ <Text type="body" weight="semibold">
745
+ {row.name}
746
+ </Text>
747
+ <Text type="supporting" color="secondary">
748
+ {row.meta}
749
+ </Text>
750
+ </VStack>
751
+ </HStack>
752
+ );
810
753
  }
811
754
 
812
- function TagsCell({row}: {row: InventoryRow}) {
813
- return (
814
- <HStack gap={1} wrap="wrap" hAlign="end">
815
- {row.tags.map(tag => (
816
- <Badge key={tag.label} label={tag.label} variant={tag.variant} />
817
- ))}
818
- </HStack>
819
- );
755
+ function TagsCell({ row }: { row: InventoryRow }) {
756
+ return (
757
+ <HStack gap={1} wrap="wrap" hAlign="end">
758
+ {row.tags.map((tag) => (
759
+ <Badge key={tag.label} label={tag.label} variant={tag.variant} />
760
+ ))}
761
+ </HStack>
762
+ );
820
763
  }
821
764
 
822
765
  function ActionsCell() {
823
- return (
824
- <MoreMenu
825
- label="Row actions"
826
- size="sm"
827
- items={[
828
- {label: 'Edit'},
829
- {label: 'Duplicate'},
830
- {label: 'Move to…'},
831
- {type: 'divider'},
832
- {label: 'Delete'},
833
- ]}
834
- />
835
- );
766
+ return (
767
+ <MoreMenu
768
+ label="Row actions"
769
+ size="sm"
770
+ items={[
771
+ { label: "Edit" },
772
+ { label: "Duplicate" },
773
+ { label: "Move to…" },
774
+ { type: "divider" },
775
+ { label: "Delete" },
776
+ ]}
777
+ />
778
+ );
836
779
  }
837
780
 
838
781
  // The full column set needs 64+80+100+100+80+64 = 488px of grid and the card
@@ -841,215 +784,192 @@ function ActionsCell() {
841
784
  // control (no bulk action bar acts on it) and the two metadata columns, which
842
785
  // needs 244px. Table keeps its own scroll wrapper either way; the point is that
843
786
  // a row should not have to be read sideways.
844
- const NARROW_TABLE_QUERY = '(max-width: 600px)';
787
+ const NARROW_TABLE_QUERY = "(max-width: 600px)";
845
788
  const NARROW_COLUMN_KEYS: Record<string, true> = {
846
- select: true,
847
- location: true,
848
- tags: true,
789
+ select: true,
790
+ location: true,
791
+ tags: true,
849
792
  };
850
793
 
851
794
  const INVENTORY_COLUMNS: TableColumn<InventoryRow>[] = [
852
- {
853
- key: 'select',
854
- header: '',
855
- // Wide enough that the control + the theme's cell padding (up to
856
- // --spacing-4 = 16px/side on spacious density) fit inside the cell,
857
- // so the control's hover background doesn't overflow toward the
858
- // card's clipped (rounded) edge on larger-padding themes.
859
- width: pixel(64),
860
- renderCell: row => <SelectCell row={row} />,
861
- },
862
- {
863
- key: 'item',
864
- header: 'Item',
865
- // Lower min-width (default 120) so the table fits its container on
866
- // larger-spacing themes instead of overflowing the actions column.
867
- width: proportional(3, {minWidth: 80}),
868
- renderCell: row => <ItemCell row={row} />,
869
- },
870
- {
871
- key: 'available',
872
- header: 'Available',
873
- width: pixel(100),
874
- renderCell: row => (
875
- <Text type="body" hasTabularNumbers>
876
- {row.available}
877
- </Text>
878
- ),
879
- },
880
- {
881
- key: 'location',
882
- header: 'Location',
883
- width: pixel(100),
884
- renderCell: row => <Text type="body">{row.location}</Text>,
885
- },
886
- {
887
- key: 'tags',
888
- header: 'Tags',
889
- width: proportional(2, {minWidth: 80}),
890
- align: 'end',
891
- renderCell: row => <TagsCell row={row} />,
892
- },
893
- {
894
- key: 'actions',
895
- header: '',
896
- // Match the select column: fit the sm more-menu button + cell
897
- // padding so its hover background stays clear of the card's
898
- // clipped rounded edge across themes.
899
- width: pixel(64),
900
- align: 'end',
901
- renderCell: () => <ActionsCell />,
902
- },
795
+ {
796
+ key: "select",
797
+ header: "",
798
+ // Wide enough that the control + the theme's cell padding (up to
799
+ // --spacing-4 = 16px/side on spacious density) fit inside the cell,
800
+ // so the control's hover background doesn't overflow toward the
801
+ // card's clipped (rounded) edge on larger-padding themes.
802
+ width: pixel(64),
803
+ renderCell: (row) => <SelectCell row={row} />,
804
+ },
805
+ {
806
+ key: "item",
807
+ header: "Item",
808
+ // Lower min-width (default 120) so the table fits its container on
809
+ // larger-spacing themes instead of overflowing the actions column.
810
+ width: proportional(3, { minWidth: 80 }),
811
+ renderCell: (row) => <ItemCell row={row} />,
812
+ },
813
+ {
814
+ key: "available",
815
+ header: "Available",
816
+ width: pixel(100),
817
+ renderCell: (row) => (
818
+ <Text type="body" hasTabularNumbers>
819
+ {row.available}
820
+ </Text>
821
+ ),
822
+ },
823
+ {
824
+ key: "location",
825
+ header: "Location",
826
+ width: pixel(100),
827
+ renderCell: (row) => <Text type="body">{row.location}</Text>,
828
+ },
829
+ {
830
+ key: "tags",
831
+ header: "Tags",
832
+ width: proportional(2, { minWidth: 80 }),
833
+ align: "end",
834
+ renderCell: (row) => <TagsCell row={row} />,
835
+ },
836
+ {
837
+ key: "actions",
838
+ header: "",
839
+ // Match the select column: fit the sm more-menu button + cell
840
+ // padding so its hover background stays clear of the card's
841
+ // clipped rounded edge across themes.
842
+ width: pixel(64),
843
+ align: "end",
844
+ renderCell: () => <ActionsCell />,
845
+ },
903
846
  ];
904
847
 
905
848
  const NARROW_INVENTORY_COLUMNS = INVENTORY_COLUMNS.filter(
906
- column => !NARROW_COLUMN_KEYS[column.key],
849
+ (column) => !NARROW_COLUMN_KEYS[column.key],
907
850
  );
908
851
 
909
- function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
910
- const isNarrow = useMediaQuery(NARROW_TABLE_QUERY);
911
- const lowStockCount = inventory.filter(
912
- row => row.available < LOW_STOCK_THRESHOLD,
913
- ).length;
914
- return (
915
- <Card
916
- padding={0}
917
- style={{
918
- backgroundColor: 'var(--color-background-surface)',
919
- color: 'var(--color-text-primary)',
920
- overflow: 'hidden',
921
- }}>
922
- <HStack
923
- hAlign="between"
924
- vAlign="center"
925
- paddingBlock={6}
926
- paddingInline={6}>
927
- <Heading level={2}>Inventory</Heading>
928
- <Button
929
- label="Add item"
930
- variant="primary"
931
- icon={<Plus size={16} />}
932
- />
933
- </HStack>
934
-
935
- <Divider variant="subtle" />
936
-
937
- <HStack
938
- gap={3}
939
- vAlign="center"
940
- hAlign="between"
941
- width="100%"
942
- paddingBlock={4}
943
- paddingInline={6}
944
- style={{overflowX: 'auto'}}>
945
- <HStack gap={2} vAlign="center" style={{flex: 1, minWidth: 0}}>
946
- <TextInput
947
- label="Search inventory"
948
- isLabelHidden
949
- placeholder="Type and hit enter…"
950
- value=""
951
- onChange={() => {}}
952
- startIcon={<Search size={16} />}
953
- style={{flex: 1, minWidth: 0, maxWidth: 240}}
954
- />
955
- <OverflowList
956
- gap={2}
957
- overflowRenderer={() => (
958
- <Button
959
- label="Filters"
960
- variant="ghost"
961
- size="sm"
962
- icon={<Tag size={16} />}
963
- />
964
- )}>
965
- <Selector
966
- label="Categories"
967
- isLabelHidden
968
- placeholder="Categories"
969
- size="sm"
970
- startIcon={<Folder size={16} />}
971
- value={undefined}
972
- onChange={() => {}}
973
- options={['Wearables', 'Audio', 'Bags', 'Drinkware', 'Home']}
974
- />
975
- <Selector
976
- label="Locations"
977
- isLabelHidden
978
- placeholder="Locations"
979
- size="sm"
980
- startIcon={<MapPin size={16} />}
981
- value={undefined}
982
- onChange={() => {}}
983
- options={[
984
- 'Roost 1',
985
- 'Roost 2',
986
- 'Roost 3',
987
- 'Roost 4',
988
- 'Roost 5',
989
- 'Roost 6',
990
- ]}
991
- />
992
- <Selector
993
- label="Tags"
994
- isLabelHidden
995
- placeholder="Tags"
996
- size="sm"
997
- startIcon={<Tag size={16} />}
998
- value={undefined}
999
- onChange={() => {}}
1000
- options={[
1001
- 'New',
1002
- 'Popular',
1003
- 'Limited',
1004
- 'Leather',
1005
- 'Drinkware',
1006
- 'Home',
1007
- ]}
1008
- />
1009
- </OverflowList>
1010
- </HStack>
1011
- <HStack gap={1} vAlign="center">
1012
- <Button
1013
- variant="ghost"
1014
- size="sm"
1015
- isIconOnly
1016
- label="List view"
1017
- tooltip="List view"
1018
- icon={<List size={18} />}
1019
- />
1020
- <Button
1021
- variant="ghost"
1022
- size="sm"
1023
- isIconOnly
1024
- label="Grid view"
1025
- tooltip="Grid view"
1026
- icon={<LayoutGrid size={18} />}
1027
- />
1028
- </HStack>
1029
- </HStack>
1030
-
1031
- {lowStockCount > 0 && (
1032
- <VStack gap={0} paddingInline={6} paddingBlockEnd={4}>
1033
- <Banner
1034
- status="warning"
1035
- title={lowStockCount + ' items are running low'}
1036
- />
1037
- </VStack>
1038
- )}
1039
-
1040
- {/* Inset the table by --spacing-6 (the card is padding={0}) so its edge
852
+ function InventoryCard({ inventory }: { inventory: InventoryRow[] }) {
853
+ const isNarrow = useMediaQuery(NARROW_TABLE_QUERY);
854
+ const lowStockCount = inventory.filter((row) => row.available < LOW_STOCK_THRESHOLD).length;
855
+ return (
856
+ <Card
857
+ padding={0}
858
+ style={{
859
+ backgroundColor: "var(--color-background-surface)",
860
+ color: "var(--color-text-primary)",
861
+ overflow: "hidden",
862
+ }}
863
+ >
864
+ <HStack hAlign="between" vAlign="center" paddingBlock={6} paddingInline={6}>
865
+ <Heading level={2}>Inventory</Heading>
866
+ <Button label="Add item" variant="primary" icon={<Plus size={16} />} />
867
+ </HStack>
868
+
869
+ <Divider variant="subtle" />
870
+
871
+ <HStack
872
+ gap={3}
873
+ vAlign="center"
874
+ hAlign="between"
875
+ width="100%"
876
+ paddingBlock={4}
877
+ paddingInline={6}
878
+ style={{ overflowX: "auto" }}
879
+ >
880
+ <HStack gap={2} vAlign="center" style={{ flex: 1, minWidth: 0 }}>
881
+ <TextInput
882
+ label="Search inventory"
883
+ isLabelHidden
884
+ placeholder="Type and hit enter…"
885
+ value=""
886
+ onChange={() => {}}
887
+ startIcon={<Search size={16} />}
888
+ controlStyle={{ flex: 1, minWidth: 0, maxWidth: 240 }}
889
+ />
890
+ <OverflowList
891
+ gap={2}
892
+ overflowRenderer={() => (
893
+ <Button label="Filters" variant="ghost" size="sm" icon={<Tag size={16} />} />
894
+ )}
895
+ >
896
+ <Selector
897
+ label="Categories"
898
+ isLabelHidden
899
+ placeholder="Categories"
900
+ size="sm"
901
+ startIcon={<Folder size={16} />}
902
+ value={undefined}
903
+ onChange={() => {}}
904
+ options={["Wearables", "Audio", "Bags", "Drinkware", "Home"]}
905
+ />
906
+ <Selector
907
+ label="Locations"
908
+ isLabelHidden
909
+ placeholder="Locations"
910
+ size="sm"
911
+ startIcon={<MapPin size={16} />}
912
+ value={undefined}
913
+ onChange={() => {}}
914
+ options={["Roost 1", "Roost 2", "Roost 3", "Roost 4", "Roost 5", "Roost 6"]}
915
+ />
916
+ <Selector
917
+ label="Tags"
918
+ isLabelHidden
919
+ placeholder="Tags"
920
+ size="sm"
921
+ startIcon={<Tag size={16} />}
922
+ value={undefined}
923
+ onChange={() => {}}
924
+ options={["New", "Popular", "Limited", "Leather", "Drinkware", "Home"]}
925
+ />
926
+ </OverflowList>
927
+ </HStack>
928
+ <HStack gap={1} vAlign="center">
929
+ <Button
930
+ variant="ghost"
931
+ size="sm"
932
+ isIconOnly
933
+ label="List view"
934
+ tooltip="List view"
935
+ icon={<List size={18} />}
936
+ />
937
+ <Button
938
+ variant="ghost"
939
+ size="sm"
940
+ isIconOnly
941
+ label="Grid view"
942
+ tooltip="Grid view"
943
+ icon={<LayoutGrid size={18} />}
944
+ />
945
+ </HStack>
946
+ </HStack>
947
+
948
+ {lowStockCount > 0 && (
949
+ <VStack gap={0} paddingInline={6} paddingBlockEnd={4}>
950
+ <Banner
951
+ status="warning"
952
+ // Core 0.7.0 stopped deriving the live-region role from
953
+ // `status`, so this banner states its own. It appears when
954
+ // the count crosses the threshold, and a screen reader must
955
+ // hear it.
956
+ role="status"
957
+ title={`${lowStockCount} items are running low`}
958
+ />
959
+ </VStack>
960
+ )}
961
+
962
+ {/* Inset the table by --spacing-6 (the card is padding={0}) so its edge
1041
963
  lines up with the header/filter row in every theme's spacing scale. */}
1042
- <VStack gap={0} paddingInline={6} paddingBlockEnd={2}>
1043
- <Table<InventoryRow>
1044
- data={inventory}
1045
- columns={
1046
- isNarrow ? NARROW_INVENTORY_COLUMNS : INVENTORY_COLUMNS
1047
- }
1048
- density="spacious"
1049
- dividers="rows"
1050
- hasHover
1051
- />
1052
- </VStack>
1053
- </Card>
1054
- );
964
+ <VStack gap={0} paddingInline={6} paddingBlockEnd={2}>
965
+ <Table<InventoryRow>
966
+ data={inventory}
967
+ columns={isNarrow ? NARROW_INVENTORY_COLUMNS : INVENTORY_COLUMNS}
968
+ density="spacious"
969
+ dividers="rows"
970
+ hasHover
971
+ />
972
+ </VStack>
973
+ </Card>
974
+ );
1055
975
  }