@astryxdesign/cli 0.7.11 → 0.7.12-canary.ccb4a6d

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 (68) hide show
  1. package/api/build/kit/rank.mjs +21 -4
  2. package/api/build/kit/words.d.mts +1 -0
  3. package/api/build/kit/words.mjs +1 -1
  4. package/api/icon/_adapter.d.mts +3 -2
  5. package/api/icon/_adapter.mjs +8 -2
  6. package/api/icon/_authoring.d.mts +1 -0
  7. package/api/icon/_authoring.mjs +586 -66
  8. package/api/icon/_default-look.d.mts +155 -0
  9. package/api/icon/_default-look.mjs +474 -0
  10. package/api/icon/_records.d.mts +1 -1
  11. package/api/icon/_records.mjs +1 -0
  12. package/api/icon/add/add.mjs +224 -38
  13. package/api/icon/icon.type.d.mts +22 -2
  14. package/api/icon/icon.type.mjs +6 -2
  15. package/api/icon/iconAdd.doc.mjs +2 -2
  16. package/api/icon/iconNewDefinition.doc.mjs +3 -2
  17. package/api/icon/iconNewSet.doc.mjs +4 -4
  18. package/api/icon/search/search.mjs +10 -9
  19. package/api/integration/add-helpers.d.mts +11 -1
  20. package/api/integration/add-helpers.mjs +5 -2
  21. package/api/integration/authoring-checks.d.mts +0 -2
  22. package/api/integration/authoring-checks.mjs +1 -6
  23. package/api/logger.mjs +2 -1
  24. package/api/swizzle/_adapter.d.mts +1 -0
  25. package/api/swizzle/_adapter.mjs +4 -1
  26. package/api/swizzle/_moduleGraph.d.mts +171 -0
  27. package/api/swizzle/_moduleGraph.mjs +795 -0
  28. package/api/swizzle/copy/copy.d.mts +2 -43
  29. package/api/swizzle/copy/copy.mjs +195 -314
  30. package/api/swizzle/swizzle.d.mts +0 -2
  31. package/api/swizzle/swizzle.doc.mjs +9 -4
  32. package/api/swizzle/swizzle.mjs +3 -7
  33. package/api/swizzle/swizzle.type.d.mts +2 -1
  34. package/api/swizzle/swizzle.type.mjs +3 -2
  35. package/assets/discover/official-packages.json +20 -0
  36. package/assets/docs/tree/icon-sets.doc.mjs +11 -5
  37. package/assets/templates/blocks/components/Table/TableRowIndexTable.doc.mjs +1 -1
  38. package/assets/templates/blocks/components/Table/TableRowIndexTable.tsx +2 -2
  39. package/assets/templates/pages/course-detail/page.tsx +699 -0
  40. package/assets/templates/pages/course-detail/template.doc.mjs +37 -0
  41. package/assets/templates/pages/file-explorer/page.tsx +103 -79
  42. package/assets/templates/pages/table-grouped/page.tsx +160 -232
  43. package/assets/templates/pages/table-grouped/template.doc.mjs +0 -2
  44. package/assets/templates/pages/theme-showcase/page.tsx +724 -792
  45. package/assets/templates/pages/theme-showcase/template.doc.mjs +4 -5
  46. package/authoring/doctypes/template/type.ts +1 -0
  47. package/clients/cli/commands/experimental/icon.mjs +31 -10
  48. package/clients/cli/commands/experimental-icon-add.doc.mjs +1 -1
  49. package/clients/cli/commands/experimental-icon-new-definition.doc.mjs +2 -1
  50. package/clients/cli/commands/experimental-icon-new-set.doc.mjs +2 -2
  51. package/clients/cli/commands/swizzle.doc.mjs +8 -3
  52. package/clients/cli/commands/swizzle.mjs +21 -9
  53. package/foundation/discovery/component-loader.d.mts +1 -1
  54. package/foundation/discovery/doc-types.mjs +9 -2
  55. package/foundation/discovery/icon-ranking.d.mts +27 -3
  56. package/foundation/discovery/icon-ranking.mjs +125 -8
  57. package/foundation/discovery/icon-set-checks.mjs +44 -35
  58. package/foundation/discovery/icon-set-discovery.d.mts +13 -0
  59. package/foundation/discovery/icon-set-discovery.mjs +25 -0
  60. package/foundation/discovery/icon-set-writer.d.mts +238 -16
  61. package/foundation/discovery/icon-set-writer.mjs +676 -79
  62. package/foundation/discovery/module-exports.d.mts +3 -2
  63. package/foundation/discovery/module-exports.mjs +167 -37
  64. package/foundation/fs/paths.d.mts +2 -1
  65. package/foundation/fs/paths.mjs +3 -4
  66. package/foundation/text/shell-arg.d.mts +14 -0
  67. package/foundation/text/shell-arg.mjs +17 -0
  68. package/package.json +6 -6
@@ -2,7 +2,14 @@
2
2
 
3
3
  'use client';
4
4
 
5
- import {type CSSProperties, type ReactNode} from 'react';
5
+ /**
6
+ * @file Theme comparison shell and embeddable preview.
7
+ * @input Optional product images, product descriptions, and inventory fixtures
8
+ * @output One responsive showcase built from Astryx components
9
+ * @position Theme preview shared by the sandbox, docsite, and playground
10
+ */
11
+
12
+ import {type CSSProperties, useRef, useState} from 'react';
6
13
  import {
7
14
  Plus,
8
15
  Search,
@@ -23,11 +30,20 @@ import {
23
30
  User,
24
31
  } from 'lucide-react';
25
32
  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';
33
+ import {
34
+ VStack,
35
+ HStack,
36
+ Layout,
37
+ LayoutContent,
38
+ LayoutHeader,
39
+ LayoutFooter,
40
+ } from '@astryxdesign/core/Layout';
41
+ import {StackItem} from '@astryxdesign/core/Stack';
42
+ import {AppShell, useAppShellMobile} from '@astryxdesign/core/AppShell';
43
+ import {Grid} from '@astryxdesign/core/Grid';
29
44
  import {Card} from '@astryxdesign/core/Card';
30
45
  import {Button} from '@astryxdesign/core/Button';
46
+ import {Icon} from '@astryxdesign/core/Icon';
31
47
  import {Link} from '@astryxdesign/core/Link';
32
48
  import {Badge} from '@astryxdesign/core/Badge';
33
49
  import {Banner} from '@astryxdesign/core/Banner';
@@ -44,7 +60,7 @@ import {Center} from '@astryxdesign/core/Center';
44
60
  import {Section} from '@astryxdesign/core/Section';
45
61
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
46
62
  import {NumberInput} from '@astryxdesign/core/NumberInput';
47
- import {OverflowList} from '@astryxdesign/core/OverflowList';
63
+ import {ScrollableArea} from '@astryxdesign/core/ScrollableArea';
48
64
  import {TopNav, TopNavHeading, TopNavItem} from '@astryxdesign/core/TopNav';
49
65
  import {
50
66
  ChatComposer,
@@ -52,204 +68,17 @@ import {
52
68
  ChatMessageBubble,
53
69
  ChatMessageList,
54
70
  ChatSystemMessage,
71
+ useChatNewMessages,
72
+ useChatStreamScroll,
55
73
  } from '@astryxdesign/core/Chat';
56
74
 
57
- // Styles passed to Astryx components via their `style` prop. Astryx components
58
- // forward the DOM `style` prop, so these work with no CSS compiler — in
59
- // compiled builds and in the live playground preview alike.
75
+ // StackItem has no wrapping basis; Stack has no body variant.
76
+ // LayoutContent has no height cap; keep the message viewport bounded.
60
77
  const styles: Record<string, CSSProperties> = {
61
- card: {
62
- backgroundColor: 'var(--color-background-body)',
63
- color: 'var(--color-text-primary)',
64
- minWidth: 0,
65
- borderColor: 'transparent',
66
- },
67
- checkoutStack: {
68
- minWidth: 0,
69
- width: '100%',
70
- },
71
- paymentCardContent: {
72
- minWidth: 0,
73
- width: '100%',
74
- textAlign: 'center',
75
- wordBreak: 'break-word',
76
- },
77
- inventoryCard: {
78
- backgroundColor: 'var(--color-background-surface)',
79
- color: 'var(--color-text-primary)',
80
- overflow: 'hidden',
81
- },
82
- inventoryHeader: {
83
- paddingBlock: 'var(--spacing-6)',
84
- paddingInline: 'var(--spacing-6)',
85
- },
86
- inventoryFilterRow: {
87
- paddingBlock: 'var(--spacing-4)',
88
- paddingInline: 'var(--spacing-6)',
89
- width: '100%',
90
- overflowX: 'auto' as const,
91
- },
92
- // Inset the table by --spacing-6 (the card is padding={0}) so its edge lines
93
- // up with the header/filter row in every theme's spacing scale.
94
- inventoryTableWrap: {
95
- paddingInline: 'var(--spacing-6)',
96
- paddingBlockEnd: 'var(--spacing-2)',
97
- },
98
- searchInput: {
99
- flex: 1,
100
- minWidth: 0,
101
- maxWidth: 240,
102
- },
103
- activityCard: {
104
- backgroundColor: 'var(--color-background-surface)',
105
- color: 'var(--color-text-primary)',
106
- minWidth: 0,
107
- height: '100%',
108
- },
109
- chatCard: {
110
- backgroundColor: 'var(--color-background-surface)',
111
- color: 'var(--color-text-primary)',
112
- minWidth: 0,
113
- overflow: 'hidden',
114
- display: 'flex',
115
- flexDirection: 'column' as const,
116
- },
117
- chatHeader: {
118
- paddingBlock: 'var(--spacing-4)',
119
- paddingInline: 'var(--spacing-4)',
120
- },
121
- activityCardStack: {
122
- height: '100%',
123
- },
124
- activityListFade: {
125
- flex: 1,
126
- minHeight: 0,
127
- overflow: 'hidden',
128
- maskImage:
129
- 'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
130
- WebkitMaskImage:
131
- 'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
132
- marginInline: 'calc(var(--spacing-2) * -1)',
133
- },
134
- content: {
135
- maxWidth: 960,
136
- marginInline: 'auto',
137
- minWidth: 0,
138
- },
139
- contentFluid: {
140
- maxWidth: 880,
141
- },
142
- heroText: {
143
- textAlign: 'center' as const,
144
- maxWidth: 560,
145
- },
146
- centerText: {
147
- textAlign: 'center',
148
- },
149
- cardStack: {
150
- height: '100%',
151
- },
152
- cardDescription: {
153
- flex: 1,
154
- },
155
- quantityInput: {
156
- flexShrink: 0,
157
- },
158
- // `auto` basis so the button wraps to its own line rather than ellipsizing.
159
- cartButton: {
160
- flex: '1 1 auto',
161
- minWidth: 0,
162
- },
163
- cartRow: {
164
- width: '100%',
165
- minWidth: 0,
166
- },
167
- };
168
-
169
- // Styles applied directly to plain DOM elements via the `style` prop.
170
- // Plain inline styles so they render with no CSS compiler. All are static
171
- // (no media/pseudo variants), so inline styles reproduce them exactly.
172
- const inlineStyles: Record<string, CSSProperties> = {
173
- inventoryBannerWrap: {
174
- paddingInline: 'var(--spacing-6)',
175
- paddingBottom: 'var(--spacing-4)',
176
- },
177
- thumbnail: {
178
- width: 40,
179
- height: 40,
180
- borderRadius: 'var(--radius-element)',
181
- objectFit: 'cover',
182
- display: 'block',
183
- flexShrink: 0,
184
- },
185
- thumbnailFallback: {
186
- width: 40,
187
- height: 40,
188
- borderRadius: 'var(--radius-element)',
189
- backgroundColor: 'var(--color-background-muted)',
190
- color: 'var(--color-text-secondary)',
191
- display: 'flex',
192
- alignItems: 'center',
193
- justifyContent: 'center',
194
- fontSize: 'var(--font-size-sm)',
195
- fontWeight: 600,
196
- flexShrink: 0,
197
- },
198
- kpiValue: {
199
- fontSize: 24,
200
- fontWeight: 700,
201
- lineHeight: 1.1,
202
- color: 'var(--color-text-primary)',
203
- fontFamily: 'var(--font-family-heading)',
204
- letterSpacing: '-0.01em',
205
- },
206
- chatBody: {
207
- flex: 1,
208
- minHeight: 0,
209
- overflow: 'hidden',
210
- },
211
- chatSuggestions: {
212
- paddingInline: 'var(--spacing-4)',
213
- paddingBottom: 'var(--spacing-2)',
214
- },
215
- chatComposer: {
216
- paddingInline: 'var(--spacing-4)',
217
- paddingBottom: 'var(--spacing-4)',
218
- },
219
- activityIcon: {
220
- width: 32,
221
- height: 32,
222
- borderRadius: 'var(--radius-full)',
223
- backgroundColor: 'var(--color-background-muted)',
224
- color: 'var(--color-text-secondary)',
225
- display: 'flex',
226
- alignItems: 'center',
227
- justifyContent: 'center',
228
- flexShrink: 0,
229
- },
230
- productImage: {
231
- width: '100%',
232
- height: '100%',
233
- objectFit: 'cover',
234
- },
235
- cardBody: {
236
- padding: 'var(--spacing-4)',
237
- flex: 1,
238
- display: 'flex',
239
- flexDirection: 'column',
240
- },
241
- // Flex bases, not grid tracks: the checkout form needs a width floor, and the
242
- // chat wraps to its own row rather than squeezing it under one.
243
- checkoutColumn: {
244
- flex: '1 1 300px',
245
- minWidth: 0,
246
- display: 'grid',
247
- },
248
- chatColumn: {
249
- flex: '2 1 520px',
250
- minWidth: 0,
251
- display: 'grid',
252
- },
78
+ preview: {backgroundColor: 'var(--color-background-body)'},
79
+ checkoutColumn: {flex: '1 1 300px'},
80
+ chatColumn: {flex: '2 1 520px'},
81
+ chatViewport: {maxHeight: 720},
253
82
  };
254
83
 
255
84
  const PRODUCT_IMAGE_KEYS = ['watch', 'headphones', 'backpack'];
@@ -294,8 +123,6 @@ const DEFAULT_PRODUCTS: ProductSpec[] = [
294
123
  },
295
124
  ];
296
125
 
297
- // Neutral product photos, served from the shared astryx asset CDN so the
298
- // scaffolded template renders real imagery without needing local public assets.
299
126
  const DEFAULT_IMAGES: Record<string, string> = {
300
127
  watch: '/template-assets/Neutral-Watch.png',
301
128
  headphones: '/template-assets/Neutral-Headphones.png',
@@ -314,39 +141,106 @@ export interface ThemeShowcaseProps {
314
141
  inventory?: InventoryRow[];
315
142
  }
316
143
 
317
- // Default export is the route page (sandbox renders this as a Next.js page, so
318
- // it must take no props / satisfy PageProps). It renders the store with the
319
- // neutral defaults. Consumers that need per-theme content import the named
320
- // `ThemeShowcaseStore` below and pass images/products/inventory.
321
144
  export default function ThemeShowcase() {
322
- return <ThemeShowcaseStore />;
145
+ return (
146
+ <AppShell
147
+ height="auto"
148
+ variant="surface"
149
+ contentPadding={0}
150
+ data-theme-preview="true"
151
+ topNav={<ShowcaseNavigation />}>
152
+ <ShowcaseContent headingLevel={1} />
153
+ </AppShell>
154
+ );
323
155
  }
324
156
 
325
- export function ThemeShowcaseStore({
157
+ /** Embeddable comparison surface: the host owns its application shell. */
158
+ export function ThemeShowcaseStore(props: ThemeShowcaseProps = {}) {
159
+ return (
160
+ <VStack gap={0} data-theme-preview="true" style={styles.preview}>
161
+ <ShowcaseNavigation />
162
+ <ShowcaseContent {...props} />
163
+ </VStack>
164
+ );
165
+ }
166
+
167
+ function ShowcaseContent({
326
168
  images = DEFAULT_IMAGES,
327
169
  products = DEFAULT_PRODUCTS,
328
170
  inventory = DEFAULT_INVENTORY,
329
- }: ThemeShowcaseProps = {}) {
171
+ headingLevel = 2,
172
+ }: ThemeShowcaseProps & {headingLevel?: 1 | 2} = {}) {
330
173
  const {isMobile} = useAppShellMobile();
331
174
  return (
332
- <div
333
- style={{
334
- minHeight: '100%',
335
- backgroundColor: 'var(--color-background-body)',
336
- }}>
337
- <StorePreview images={images} products={products} isMobile={isMobile} />
338
- <div
339
- style={{
340
- padding: 'var(--spacing-6)',
341
- backgroundColor: 'var(--color-background-surface)',
342
- }}>
343
- <CardShowcase
344
- images={images}
345
- inventory={inventory}
346
- isMobile={isMobile}
347
- />
348
- </div>
349
- </div>
175
+ <Layout height="auto" padding={0}>
176
+ <LayoutContent padding={0} isScrollable={false}>
177
+ <VStack gap={0}>
178
+ <StorePreview
179
+ images={images}
180
+ products={products}
181
+ headingLevel={headingLevel}
182
+ />
183
+ <Section padding={6} variant="transparent">
184
+ <CardShowcase
185
+ images={images}
186
+ inventory={inventory}
187
+ isMobile={isMobile}
188
+ />
189
+ </Section>
190
+ </VStack>
191
+ </LayoutContent>
192
+ </Layout>
193
+ );
194
+ }
195
+
196
+ function ShowcaseNavigation() {
197
+ const {isMobile} = useAppShellMobile();
198
+ return (
199
+ <TopNav
200
+ label="Theme preview navigation"
201
+ heading={<TopNavHeading heading="Studio" />}
202
+ centerContent={
203
+ isMobile ? undefined : (
204
+ <>
205
+ <TopNavItem label="Shop" href="#" isSelected />
206
+ <TopNavItem label="New In" href="#" />
207
+ <TopNavItem label="Stories" href="#" />
208
+ <TopNavItem label="Help" href="#" />
209
+ </>
210
+ )
211
+ }
212
+ endContent={
213
+ <HStack gap={2} vAlign="center">
214
+ <HStack gap={0.5}>
215
+ <Button
216
+ label="Search"
217
+ tooltip="Search"
218
+ variant="ghost"
219
+ isIconOnly
220
+ icon={<Icon icon={Search} size="md" />}
221
+ href="#"
222
+ />
223
+ <Button
224
+ label="Account"
225
+ tooltip="Account"
226
+ variant="ghost"
227
+ isIconOnly
228
+ icon={<Icon icon={User} size="md" />}
229
+ href="#"
230
+ />
231
+ <Button
232
+ label="Cart"
233
+ tooltip="Cart"
234
+ variant="ghost"
235
+ isIconOnly
236
+ icon={<Icon icon={ShoppingBag} size="md" />}
237
+ href="#"
238
+ />
239
+ </HStack>
240
+ <Button label="Sign in" variant="primary" href="#" />
241
+ </HStack>
242
+ }
243
+ />
350
244
  );
351
245
  }
352
246
 
@@ -359,26 +253,32 @@ function CardShowcase({
359
253
  inventory: InventoryRow[];
360
254
  isMobile: boolean;
361
255
  }) {
362
- const columns = isMobile ? 1 : ({minWidth: 200, repeat: 'fit'} as const);
363
-
256
+ const activity = <LatestActivityCard />;
364
257
  return (
365
258
  <VStack gap={8}>
366
259
  <HStack gap={4} wrap="wrap">
367
- <div style={inlineStyles.checkoutColumn}>
368
- <CheckoutCard isMobile={isMobile} />
369
- </div>
370
- <div style={inlineStyles.chatColumn}>
260
+ <StackItem size="fill" style={styles.checkoutColumn}>
261
+ <CheckoutCard />
262
+ </StackItem>
263
+ <StackItem size="fill" style={styles.chatColumn}>
371
264
  <ChatCard />
372
- </div>
265
+ </StackItem>
266
+ </HStack>
267
+ <HStack gap={isMobile ? 4 : 0} wrap={isMobile ? 'wrap' : 'nowrap'}>
268
+ <StackItem size="fill">
269
+ <InventoryCard
270
+ key={inventory.map(row => row.name).join('|')}
271
+ images={images}
272
+ inventory={inventory}
273
+ />
274
+ </StackItem>
275
+ <VStack
276
+ as={StackItem}
277
+ width={isMobile ? '100%' : 280}
278
+ paddingInlineStart={isMobile ? 0 : 4}>
279
+ {activity}
280
+ </VStack>
373
281
  </HStack>
374
- <Grid columns={columns} gap={4}>
375
- <GridSpan columns={isMobile ? 1 : 3}>
376
- <InventoryCard images={images} inventory={inventory} />
377
- </GridSpan>
378
- <GridSpan columns={1}>
379
- <LatestActivityCard isMobile={isMobile} />
380
- </GridSpan>
381
- </Grid>
382
282
  </VStack>
383
283
  );
384
284
  }
@@ -386,163 +286,137 @@ function CardShowcase({
386
286
  function StorePreview({
387
287
  images,
388
288
  products,
389
- isMobile,
289
+ headingLevel,
390
290
  }: {
391
291
  images: Record<string, string>;
392
292
  products: ProductSpec[];
393
- isMobile: boolean;
293
+ headingLevel: 1 | 2;
394
294
  }) {
395
295
  return (
396
- <div data-theme-preview="true">
397
- <VStack gap={0}>
398
- <TopNav
399
- label="Theme preview navigation"
400
- heading={<TopNavHeading heading="Studio" />}
401
- centerContent={
402
- isMobile ? undefined : (
403
- <>
404
- <TopNavItem label="Shop" href="#" isSelected />
405
- <TopNavItem label="New In" href="#" />
406
- <TopNavItem label="Stories" href="#" />
407
- <TopNavItem label="Help" href="#" />
408
- </>
409
- )
410
- }
411
- endContent={
412
- <HStack gap={2} vAlign="center">
413
- <HStack gap={0.5}>
414
- <Button
415
- label="Search"
416
- tooltip="Search"
417
- variant="ghost"
418
- isIconOnly
419
- icon={<Search size={20} />}
420
- href="#"
421
- />
422
- <Button
423
- label="Account"
424
- tooltip="Account"
425
- variant="ghost"
426
- isIconOnly
427
- icon={<User size={20} />}
428
- href="#"
429
- />
430
- <Button
431
- label="Cart"
432
- tooltip="Cart"
433
- variant="ghost"
434
- isIconOnly
435
- icon={<ShoppingBag size={20} />}
436
- href="#"
437
- />
438
- </HStack>
439
- <Button label="Sign in" variant="primary" href="#" />
440
- </HStack>
441
- }
442
- />
443
-
444
- <Section padding={6} variant="transparent">
445
- <VStack gap={10} style={{...styles.content, ...styles.contentFluid}}>
446
- <Center>
447
- <VStack gap={4} hAlign="center" style={styles.heroText}>
448
- <Text type="display-2" color="accent">
296
+ <Section padding={6} variant="transparent">
297
+ <Center>
298
+ <VStack gap={10} width="100%" maxWidth={880}>
299
+ <Center>
300
+ <VStack gap={4} hAlign="center" maxWidth={560}>
301
+ <Heading
302
+ level={1}
303
+ accessibilityLevel={headingLevel}
304
+ type="display-2"
305
+ color="accent"
306
+ justify="center">
307
+ <Text type="inherit" display="block">
449
308
  Little joys,
450
- <br />
451
- everywhere you go
452
309
  </Text>
453
- <Text type="body" color="secondary">
454
- We believe the smallest details are the ones that matter most.
455
- Turn an ordinary day into something worth remembering.
310
+ <Text type="inherit" display="block">
311
+ everywhere you go
456
312
  </Text>
457
- </VStack>
458
- </Center>
313
+ </Heading>
314
+ <Text type="body" color="secondary" justify="center">
315
+ We believe the smallest details are the ones that matter most.
316
+ Turn an ordinary day into something worth remembering.
317
+ </Text>
318
+ </VStack>
319
+ </Center>
320
+ <Grid columns={{minWidth: 200, max: 3}} gap={4}>
321
+ {products.map((product, index) => (
322
+ <ProductCard
323
+ key={product.name}
324
+ product={product}
325
+ image={images[PRODUCT_IMAGE_KEYS[index]]}
326
+ />
327
+ ))}
328
+ </Grid>
329
+ </VStack>
330
+ </Center>
331
+ </Section>
332
+ );
333
+ }
459
334
 
460
- <Grid columns={isMobile ? 1 : {minWidth: 200, max: 3}} gap={4}>
461
- {products.map((p, i) => (
462
- <Card key={p.name} padding={0} height="100%">
463
- <VStack gap={0} style={styles.cardStack}>
464
- <AspectRatio ratio={1}>
465
- <img
466
- src={images[PRODUCT_IMAGE_KEYS[i]]}
467
- alt={p.name}
468
- style={inlineStyles.productImage}
469
- />
470
- </AspectRatio>
471
- <div style={inlineStyles.cardBody}>
472
- <VStack gap={2} hAlign="center" style={styles.cardStack}>
473
- <HStack>
474
- <Badge label={p.badge} variant={p.badgeVariant} />
475
- </HStack>
476
- <Heading level={2} style={styles.centerText}>
477
- {p.name}
478
- </Heading>
479
- <Text
480
- type="supporting"
481
- color="secondary"
482
- style={{
483
- ...styles.cardDescription,
484
- ...styles.centerText,
485
- }}>
486
- {p.description}
487
- </Text>
488
- <HStack
489
- gap={2}
490
- vAlign="center"
491
- hAlign="center"
492
- wrap="wrap"
493
- style={styles.cartRow}>
494
- <NumberInput
495
- label="Quantity"
496
- isLabelHidden
497
- value={1}
498
- onChange={() => {}}
499
- min={1}
500
- max={99}
501
- size="sm"
502
- width={72}
503
- controlStyle={styles.quantityInput}
504
- />
505
- <Button
506
- label="Add to cart"
507
- variant="secondary"
508
- size="sm"
509
- href="#"
510
- style={styles.cartButton}
511
- />
512
- </HStack>
513
- </VStack>
514
- </div>
515
- </VStack>
516
- </Card>
517
- ))}
518
- </Grid>
335
+ function ProductCard({product, image}: {product: ProductSpec; image?: string}) {
336
+ const [quantity, setQuantity] = useState<number | null>(1);
337
+ const [isAdded, setIsAdded] = useState(false);
338
+ return (
339
+ <Card padding={0} height="100%">
340
+ <VStack gap={0} height="100%">
341
+ <AspectRatio ratio={1} fit="cover">
342
+ <img src={image} alt={product.name} />
343
+ </AspectRatio>
344
+ <StackItem size="fill">
345
+ <VStack padding={4} gap={2} hAlign="center" height="100%">
346
+ <Badge label={product.badge} variant={product.badgeVariant} />
347
+ <Heading level={2} justify="center">
348
+ {product.name}
349
+ </Heading>
350
+ <StackItem size="fill">
351
+ <Text
352
+ type="supporting"
353
+ color="secondary"
354
+ justify="center"
355
+ display="block">
356
+ {product.description}
357
+ </Text>
358
+ </StackItem>
359
+ <HStack
360
+ gap={2}
361
+ vAlign="center"
362
+ hAlign="center"
363
+ wrap="wrap"
364
+ width="100%">
365
+ <NumberInput
366
+ label="Quantity"
367
+ isLabelHidden
368
+ value={quantity}
369
+ onChange={value => {
370
+ setQuantity(value);
371
+ setIsAdded(false);
372
+ }}
373
+ min={1}
374
+ max={99}
375
+ size="sm"
376
+ width={72}
377
+ />
378
+ <Button
379
+ label={isAdded ? 'Added to cart' : 'Add to cart'}
380
+ variant="secondary"
381
+ size="sm"
382
+ onClick={() => setIsAdded(true)}
383
+ />
384
+ </HStack>
519
385
  </VStack>
520
- </Section>
386
+ </StackItem>
521
387
  </VStack>
522
- </div>
388
+ </Card>
523
389
  );
524
390
  }
525
391
 
526
- function CheckoutCard({isMobile}: {isMobile: boolean}) {
392
+ function CheckoutCard() {
393
+ const {isMobile} = useAppShellMobile();
394
+ const [email, setEmail] = useState('');
395
+ const [shipping, setShipping] = useState('economy');
396
+ const [payment, setPayment] = useState('card');
397
+ const [cardNumber, setCardNumber] = useState('');
398
+ const [expiry, setExpiry] = useState('');
399
+ const [cvc, setCvc] = useState('');
400
+ const [country, setCountry] = useState<string | undefined>('us');
401
+ const [saveInformation, setSaveInformation] = useState(true);
402
+ const [showPaymentPreview, setShowPaymentPreview] = useState(false);
527
403
  return (
528
- <Card padding={isMobile ? 4 : 5} style={styles.card}>
529
- <VStack gap={4} style={styles.checkoutStack}>
404
+ <Card height="100%" padding={isMobile ? 4 : 5}>
405
+ <VStack gap={4}>
530
406
  <Heading level={2}>Checkout</Heading>
531
-
532
- <VStack gap={3} style={styles.checkoutStack}>
407
+ <VStack gap={3}>
533
408
  <TextInput
534
409
  label="Email"
535
410
  placeholder="you@studio.com"
536
- value=""
537
- onChange={() => {}}
411
+ value={email}
412
+ onChange={setEmail}
538
413
  size="lg"
539
414
  />
540
-
541
415
  <RadioList
542
416
  label="Shipping method"
543
417
  description="Delivery time may vary based on location and availability."
544
- value="economy"
545
- onChange={() => {}}>
418
+ value={shipping}
419
+ onChange={setShipping}>
546
420
  <RadioListItem
547
421
  value="economy"
548
422
  label="Economy Shipping"
@@ -574,90 +448,85 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
574
448
  }
575
449
  />
576
450
  </RadioList>
577
-
578
- <VStack gap={2} style={styles.checkoutStack}>
451
+ <VStack gap={2}>
579
452
  <Text type="supporting" weight="bold">
580
453
  Payment method
581
454
  </Text>
582
- <Grid columns={isMobile ? 1 : {minWidth: 80, max: 3}} gap={2}>
455
+ <Grid
456
+ columns={
457
+ isMobile ? {minWidth: 80, max: 1} : {minWidth: 80, max: 3}
458
+ }
459
+ gap={2}>
583
460
  <SelectableCard
584
461
  label="Pay with card"
585
- isSelected={true}
586
- onChange={() => {}}
462
+ isSelected={payment === 'card'}
463
+ onChange={() => setPayment('card')}
587
464
  padding={3}>
588
- <VStack
589
- gap={1}
590
- hAlign="center"
591
- style={styles.paymentCardContent}>
592
- <CreditCard size={20} />
593
- <Text type="supporting" weight="bold">
465
+ <VStack gap={1} hAlign="center">
466
+ <Icon icon={CreditCard} size="md" />
467
+ <Text type="supporting" weight="bold" justify="center">
594
468
  Card
595
469
  </Text>
596
470
  </VStack>
597
471
  </SelectableCard>
598
472
  <SelectableCard
599
473
  label="Pay with Apple Pay"
600
- isSelected={false}
601
- onChange={() => {}}
474
+ isSelected={payment === 'apple'}
475
+ onChange={() => setPayment('apple')}
602
476
  padding={3}>
603
- <VStack
604
- gap={1}
605
- hAlign="center"
606
- style={styles.paymentCardContent}>
607
- <Smartphone size={20} />
608
- <Text type="supporting" weight="bold">
477
+ <VStack gap={1} hAlign="center">
478
+ <Icon icon={Smartphone} size="md" />
479
+ <Text type="supporting" weight="bold" justify="center">
609
480
  Apple Pay
610
481
  </Text>
611
482
  </VStack>
612
483
  </SelectableCard>
613
484
  <SelectableCard
614
485
  label="Pay with Google Pay"
615
- isSelected={false}
616
- onChange={() => {}}
486
+ isSelected={payment === 'google'}
487
+ onChange={() => setPayment('google')}
617
488
  padding={3}>
618
- <VStack
619
- gap={1}
620
- hAlign="center"
621
- style={styles.paymentCardContent}>
622
- <Wallet size={20} />
623
- <Text type="supporting" weight="bold">
489
+ <VStack gap={1} hAlign="center">
490
+ <Icon icon={Wallet} size="md" />
491
+ <Text type="supporting" weight="bold" justify="center">
624
492
  Google Pay
625
493
  </Text>
626
494
  </VStack>
627
495
  </SelectableCard>
628
496
  </Grid>
629
497
  </VStack>
630
-
631
498
  <TextInput
632
499
  label="Card number"
633
500
  placeholder="1234 1234 1234 1234"
634
- value=""
635
- onChange={() => {}}
636
- startIcon={isMobile ? undefined : <CreditCard size={16} />}
501
+ value={cardNumber}
502
+ onChange={setCardNumber}
503
+ startIcon={
504
+ isMobile ? undefined : <Icon icon={CreditCard} size="sm" />
505
+ }
637
506
  size="lg"
638
507
  />
639
-
640
- <Grid columns={isMobile ? 1 : {minWidth: 90, max: 2}} gap={2}>
508
+ <Grid
509
+ columns={isMobile ? {minWidth: 90, max: 1} : {minWidth: 90, max: 2}}
510
+ gap={2}>
641
511
  <TextInput
642
512
  label="Expiry"
643
513
  placeholder="MM / YY"
644
- value=""
645
- onChange={() => {}}
514
+ value={expiry}
515
+ onChange={setExpiry}
646
516
  size="lg"
647
517
  />
648
518
  <TextInput
649
519
  label="CVC"
650
520
  placeholder="123"
651
- value=""
652
- onChange={() => {}}
521
+ value={cvc}
522
+ onChange={setCvc}
653
523
  size="lg"
654
524
  />
655
525
  </Grid>
656
-
657
526
  <Selector
658
527
  label="Country"
659
- value="us"
660
- onChange={() => {}}
528
+ value={country}
529
+ onChange={setCountry}
661
530
  size="lg"
662
531
  options={[
663
532
  {value: 'us', label: 'United States'},
@@ -669,19 +538,25 @@ function CheckoutCard({isMobile}: {isMobile: boolean}) {
669
538
  ]}
670
539
  />
671
540
  </VStack>
672
-
673
541
  <CheckboxInput
674
542
  label="Securely save my information for 1-click checkout"
675
543
  description="Pay faster on Studio and everywhere Link is accepted."
676
- value={true}
677
- onChange={() => {}}
544
+ value={saveInformation}
545
+ onChange={setSaveInformation}
678
546
  />
679
-
547
+ {showPaymentPreview && (
548
+ <Banner
549
+ status="info"
550
+ title="Payment preview only"
551
+ description="No payment is submitted or saved."
552
+ />
553
+ )}
680
554
  <Button
681
555
  variant="primary"
682
556
  size="lg"
683
557
  label="Pay now"
684
- icon={<Lock size={16} />}
558
+ icon={<Icon icon={Lock} size="sm" />}
559
+ onClick={() => setShowPaymentPreview(true)}
685
560
  />
686
561
  </VStack>
687
562
  </Card>
@@ -695,201 +570,224 @@ const SUGGESTED_QUESTIONS = [
695
570
  ];
696
571
 
697
572
  function ChatCard() {
573
+ const scrollRef = useRef<HTMLDivElement>(null);
574
+ const {lock, isLocked, scrollIfLocked} = useChatStreamScroll({scrollRef});
575
+ const {contentRef} = useChatNewMessages({isLocked, onResize: scrollIfLocked});
576
+ const [draft, setDraft] = useState('');
577
+ const [messages, setMessages] = useState<Array<{id: number; text: string}>>(
578
+ [],
579
+ );
580
+ function submitMessage() {
581
+ const message = draft.trim();
582
+ if (!message) {
583
+ return;
584
+ }
585
+ lock();
586
+ setMessages(previous => [
587
+ ...previous,
588
+ {id: (previous.at(-1)?.id ?? 0) + 1, text: message},
589
+ ]);
590
+ setDraft('');
591
+ }
698
592
  return (
699
- <Card padding={0} style={styles.chatCard}>
700
- <HStack
701
- hAlign="between"
702
- vAlign="center"
703
- gap={3}
704
- style={styles.chatHeader}>
705
- <Heading level={2}>Studio AI</Heading>
706
-
707
- <HStack gap={1} vAlign="center">
708
- <Button
709
- variant="ghost"
710
- size="sm"
711
- isIconOnly
712
- label="Export conversation"
713
- tooltip="Export conversation"
714
- icon={<Download size={16} />}
715
- />
716
- <Button
717
- variant="ghost"
718
- size="sm"
719
- isIconOnly
720
- label="Close chat"
721
- tooltip="Close chat"
722
- icon={<X size={16} />}
723
- />
724
- </HStack>
725
- </HStack>
726
-
727
- <Divider variant="subtle" />
728
-
729
- <div style={inlineStyles.chatBody}>
730
- <ChatMessageList>
731
- <ChatSystemMessage>Today</ChatSystemMessage>
732
-
733
- <ChatMessage sender="user">
734
- <ChatMessageBubble variant="filled">
735
- Where’s my order?
736
- </ChatMessageBubble>
737
- </ChatMessage>
738
-
739
- <ChatMessage sender="assistant">
740
- <VStack gap={3}>
741
- <Text type="body">
742
- Your order #1043 — the Minimalist Watch and Linen Throw —
743
- shipped this morning from the Aisle 3 warehouse and is currently
744
- in transit with UPS. It’s on track to arrive at your address by
745
- end of day tomorrow.
746
- </Text>
747
- <Text type="body">
748
- Let me know if you’d like to reschedule the delivery, redirect
749
- it to a pickup point, or start a return once it arrives.
750
- </Text>
751
- </VStack>
752
- </ChatMessage>
753
-
754
- <ChatMessage sender="user">
755
- <ChatMessageBubble variant="filled">
756
- Can you show me the full details?
757
- </ChatMessageBubble>
758
- </ChatMessage>
759
-
760
- <ChatMessage sender="assistant">
761
- <VStack gap={3}>
762
- <Text type="body">Here’s everything I have on order #1043:</Text>
763
- <Card padding={3}>
764
- <VStack gap={1}>
765
- <Item
766
- label="Items"
767
- description="Minimalist Watch · Linen Throw"
768
- endContent={
769
- <Text type="body" weight="bold">
770
- $248
771
- </Text>
772
- }
773
- />
774
- <Item
775
- label="Shipping"
776
- description="UPS Ground"
777
- endContent={
778
- <Text type="body" weight="bold">
779
- $12
780
- </Text>
781
- }
593
+ <Card padding={0} height="100%">
594
+ <Layout
595
+ height="fill"
596
+ padding={4}
597
+ header={
598
+ <LayoutHeader hasDivider>
599
+ <HStack hAlign="between" vAlign="center" gap={3}>
600
+ <Heading level={2}>Studio AI</Heading>
601
+ <HStack gap={1} vAlign="center">
602
+ <Button
603
+ variant="ghost"
604
+ size="sm"
605
+ isIconOnly
606
+ label="Export conversation"
607
+ tooltip="Export conversation"
608
+ icon={<Icon icon={Download} size="sm" />}
609
+ />
610
+ <Button
611
+ variant="ghost"
612
+ size="sm"
613
+ isIconOnly
614
+ label="Close chat"
615
+ tooltip="Close chat"
616
+ icon={<Icon icon={X} size="sm" />}
617
+ />
618
+ </HStack>
619
+ </HStack>
620
+ </LayoutHeader>
621
+ }
622
+ footer={
623
+ <LayoutFooter>
624
+ <VStack gap={2}>
625
+ <HStack gap={1} hAlign="center" wrap="wrap">
626
+ {SUGGESTED_QUESTIONS.map(question => (
627
+ <Button
628
+ key={question}
629
+ variant="secondary"
630
+ size="sm"
631
+ label={question}
632
+ onClick={() => setDraft(question)}
782
633
  />
783
- <Item
784
- label="Estimated arrival"
785
- description="Tomorrow by 8pm"
786
- endContent={<Badge variant="green" label="On time" />}
634
+ ))}
635
+ </HStack>
636
+ <ChatComposer
637
+ value={draft}
638
+ onChange={setDraft}
639
+ onSubmit={submitMessage}
640
+ placeholder="Ask Studio AI…"
641
+ footerActions={
642
+ <Button
643
+ variant="ghost"
644
+ size="md"
645
+ isIconOnly
646
+ label="Attach"
647
+ tooltip="Attach"
648
+ icon={<Icon icon={Plus} size="sm" />}
787
649
  />
788
- <Item
789
- label="Tracking"
790
- description="UPS 1Z 999 AA1 0123 4567 84"
791
- endContent={<Link href="#">Track →</Link>}
650
+ }
651
+ sendActions={
652
+ <Button
653
+ variant="ghost"
654
+ size="md"
655
+ isIconOnly
656
+ label="Voice input"
657
+ tooltip="Voice input"
658
+ icon={<Icon icon={Mic} size="sm" />}
792
659
  />
793
- </VStack>
794
- </Card>
660
+ }
661
+ />
795
662
  </VStack>
796
- </ChatMessage>
797
- </ChatMessageList>
798
- </div>
799
-
800
- <div style={inlineStyles.chatSuggestions}>
801
- <HStack gap={1} hAlign="center" wrap="wrap">
802
- {SUGGESTED_QUESTIONS.map(question => (
803
- <Button
804
- key={question}
805
- variant="secondary"
806
- size="sm"
807
- label={question}
808
- />
809
- ))}
810
- </HStack>
811
- </div>
812
-
813
- <div style={inlineStyles.chatComposer}>
814
- <ChatComposer
815
- value=""
816
- onChange={() => {}}
817
- onSubmit={() => {}}
818
- placeholder="Ask Studio AI…"
819
- footerActions={
820
- <Button
821
- variant="ghost"
822
- size="md"
823
- isIconOnly
824
- label="Attach"
825
- tooltip="Attach"
826
- icon={<Plus size={16} />}
827
- />
828
- }
829
- sendActions={
830
- <Button
831
- variant="ghost"
832
- size="md"
833
- isIconOnly
834
- label="Voice input"
835
- tooltip="Voice input"
836
- icon={<Mic size={16} />}
837
- />
838
- }
839
- />
840
- </div>
663
+ </LayoutFooter>
664
+ }>
665
+ <LayoutContent
666
+ ref={scrollRef}
667
+ style={styles.chatViewport}
668
+ padding={0}
669
+ label="Studio AI conversation">
670
+ <ChatMessageList ref={contentRef}>
671
+ <ChatSystemMessage>Today</ChatSystemMessage>
672
+ <ChatMessage sender="user">
673
+ <ChatMessageBubble variant="filled">
674
+ Where’s my order?
675
+ </ChatMessageBubble>
676
+ </ChatMessage>
677
+ <ChatMessage sender="assistant">
678
+ <VStack gap={3}>
679
+ <Text type="body">
680
+ Your order #1043 — the Minimalist Watch and Linen Throw —
681
+ shipped this morning from the Aisle 3 warehouse and is
682
+ currently in transit with UPS. It’s on track to arrive at your
683
+ address by end of day tomorrow.
684
+ </Text>
685
+ <Text type="body">
686
+ Let me know if you’d like to reschedule the delivery, redirect
687
+ it to a pickup point, or start a return once it arrives.
688
+ </Text>
689
+ </VStack>
690
+ </ChatMessage>
691
+ <ChatMessage sender="user">
692
+ <ChatMessageBubble variant="filled">
693
+ Can you show me the full details?
694
+ </ChatMessageBubble>
695
+ </ChatMessage>
696
+ <ChatMessage sender="assistant">
697
+ <VStack gap={3}>
698
+ <Text type="body">
699
+ Here’s everything I have on order #1043:
700
+ </Text>
701
+ <Card padding={3}>
702
+ <VStack gap={1}>
703
+ <Item
704
+ label="Items"
705
+ description="Minimalist Watch · Linen Throw"
706
+ endContent={
707
+ <Text type="body" weight="bold">
708
+ $248
709
+ </Text>
710
+ }
711
+ />
712
+ <Item
713
+ label="Shipping"
714
+ description="UPS Ground"
715
+ endContent={
716
+ <Text type="body" weight="bold">
717
+ $12
718
+ </Text>
719
+ }
720
+ />
721
+ <Item
722
+ label="Estimated arrival"
723
+ description="Tomorrow by 8pm"
724
+ endContent={<Badge variant="green" label="On time" />}
725
+ />
726
+ <Item
727
+ label="Tracking"
728
+ description="UPS 1Z 999 AA1 0123 4567 84"
729
+ endContent={<Link href="#">Track</Link>}
730
+ />
731
+ </VStack>
732
+ </Card>
733
+ </VStack>
734
+ </ChatMessage>
735
+ {messages.map(message => (
736
+ <ChatMessage key={message.id} sender="user">
737
+ <ChatMessageBubble variant="filled">
738
+ {message.text}
739
+ </ChatMessageBubble>
740
+ </ChatMessage>
741
+ ))}
742
+ </ChatMessageList>
743
+ </LayoutContent>
744
+ </Layout>
841
745
  </Card>
842
746
  );
843
747
  }
844
748
 
845
749
  interface ActivityRow {
846
750
  id: string;
847
- icon: ReactNode;
751
+ kind: 'order' | 'refund';
848
752
  label: string;
849
753
  detail: string;
850
- time: string;
851
754
  amount: number;
852
755
  }
853
756
 
854
757
  const ACTIVITY: ActivityRow[] = [
855
758
  {
856
759
  id: '1',
857
- icon: <ShoppingBag size={16} />,
760
+ kind: 'order',
858
761
  label: 'Order #1043',
859
762
  detail: 'Placed · 1:59 pm',
860
- time: '1:59 pm',
861
763
  amount: 248,
862
764
  },
863
765
  {
864
766
  id: '2',
865
- icon: <Banknote size={16} />,
767
+ kind: 'refund',
866
768
  label: 'Order #1041',
867
769
  detail: 'Refunded · 12:40 pm',
868
- time: '12:40 pm',
869
770
  amount: -89,
870
771
  },
871
772
  {
872
773
  id: '3',
873
- icon: <ShoppingBag size={16} />,
774
+ kind: 'order',
874
775
  label: 'Order #1040',
875
776
  detail: 'Placed · 10:30 am',
876
- time: '10:30 am',
877
777
  amount: 156,
878
778
  },
879
779
  {
880
780
  id: '4',
881
- icon: <ShoppingBag size={16} />,
781
+ kind: 'order',
882
782
  label: 'Order #1038',
883
783
  detail: 'Placed · 9:11 am',
884
- time: '9:11 am',
885
784
  amount: 412,
886
785
  },
887
786
  {
888
787
  id: '5',
889
- icon: <ShoppingBag size={16} />,
788
+ kind: 'order',
890
789
  label: 'Order #1037',
891
790
  detail: 'Placed · 8:42 am',
892
- time: '8:42 am',
893
791
  amount: 95,
894
792
  },
895
793
  ];
@@ -899,57 +797,68 @@ function formatAmount(amount: number): string {
899
797
  return sign + '$' + Math.abs(amount).toLocaleString();
900
798
  }
901
799
 
902
- function LatestActivityCard({isMobile}: {isMobile: boolean}) {
800
+ function LatestActivityCard() {
801
+ const {isMobile} = useAppShellMobile();
903
802
  return (
904
- <Card padding={5} style={styles.activityCard}>
905
- <VStack gap={4} style={styles.activityCardStack}>
803
+ <Card height="100%" padding={5}>
804
+ <VStack gap={4} height="100%">
906
805
  <Heading level={2}>Revenue</Heading>
907
-
908
- <Grid columns={isMobile ? 1 : 2} gap={3}>
806
+ <Grid
807
+ columns={isMobile ? {minWidth: 80, max: 1} : {minWidth: 80, max: 2}}
808
+ gap={3}>
909
809
  <VStack gap={0}>
910
- <span style={inlineStyles.kpiValue}>18K</span>
810
+ <Text type="large" weight="bold" hasTabularNumbers>
811
+ 18K
812
+ </Text>
911
813
  <Text type="supporting" color="secondary">
912
814
  Monthly revenue
913
815
  </Text>
914
816
  </VStack>
915
817
  <VStack gap={0}>
916
- <span style={inlineStyles.kpiValue}>+12%</span>
818
+ <Text type="large" weight="bold" hasTabularNumbers>
819
+ +12%
820
+ </Text>
917
821
  <Text type="supporting" color="secondary">
918
822
  Order growth
919
823
  </Text>
920
824
  </VStack>
921
825
  </Grid>
922
-
923
826
  <Divider variant="subtle" />
924
-
925
827
  <HStack hAlign="between" vAlign="center">
926
828
  <Heading level={3}>Activity</Heading>
927
829
  <Link href="#">See all</Link>
928
830
  </HStack>
929
-
930
- <VStack gap={1} style={styles.activityListFade}>
931
- {ACTIVITY.map(item => (
932
- <Item
933
- key={item.id}
934
- startContent={
935
- <div style={inlineStyles.activityIcon} aria-hidden="true">
936
- {item.icon}
937
- </div>
938
- }
939
- label={item.label}
940
- description={item.detail}
941
- endContent={
942
- <Text
943
- type="body"
944
- weight="bold"
945
- color={item.amount < 0 ? 'secondary' : 'primary'}>
946
- {formatAmount(item.amount)}
947
- </Text>
948
- }
949
- href="#"
950
- />
951
- ))}
952
- </VStack>
831
+ <StackItem size="fill">
832
+ <ScrollableArea label="Recent order activity" height="100%">
833
+ <VStack gap={1}>
834
+ {ACTIVITY.map(item => (
835
+ <Item
836
+ key={item.id}
837
+ startContent={
838
+ <Center width={32} height={32}>
839
+ <Icon
840
+ icon={item.kind === 'refund' ? Banknote : ShoppingBag}
841
+ size="sm"
842
+ color="secondary"
843
+ />
844
+ </Center>
845
+ }
846
+ label={item.label}
847
+ description={item.detail}
848
+ endContent={
849
+ <Text
850
+ type="body"
851
+ weight="bold"
852
+ color={item.amount < 0 ? 'secondary' : 'primary'}>
853
+ {formatAmount(item.amount)}
854
+ </Text>
855
+ }
856
+ href="#"
857
+ />
858
+ ))}
859
+ </VStack>
860
+ </ScrollableArea>
861
+ </StackItem>
953
862
  </VStack>
954
863
  </Card>
955
864
  );
@@ -963,6 +872,8 @@ export interface InventoryRow extends Record<string, unknown> {
963
872
  meta: string;
964
873
  available: number;
965
874
  location: string;
875
+ /** Optional category supplied by a preview's inventory fixtures. */
876
+ category?: string;
966
877
  tags: TagSpec[];
967
878
  imageKey?: string;
968
879
  thumbnailFallback: string;
@@ -976,6 +887,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
976
887
  meta: 'Stainless steel, sapphire crystal',
977
888
  available: 42,
978
889
  location: 'Aisle 3',
890
+ category: 'Wearables',
979
891
  tags: [{label: 'New', variant: 'blue'}],
980
892
  imageKey: 'watch',
981
893
  thumbnailFallback: 'M',
@@ -987,6 +899,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
987
899
  meta: 'ANC, 30hr battery',
988
900
  available: 128,
989
901
  location: 'Aisle 1',
902
+ category: 'Audio',
990
903
  tags: [{label: 'Popular', variant: 'green'}],
991
904
  imageKey: 'headphones',
992
905
  thumbnailFallback: 'W',
@@ -998,6 +911,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
998
911
  meta: 'Water-resistant, 25L',
999
912
  available: 63,
1000
913
  location: 'Aisle 2',
914
+ category: 'Bags',
1001
915
  tags: [{label: 'Limited', variant: 'yellow'}],
1002
916
  imageKey: 'backpack',
1003
917
  thumbnailFallback: 'C',
@@ -1009,6 +923,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1009
923
  meta: 'Full-grain, RFID blocking',
1010
924
  available: 15,
1011
925
  location: 'Aisle 4',
926
+ category: 'Wearables',
1012
927
  tags: [{label: 'Leather', variant: 'yellow'}],
1013
928
  imageKey: 'wallet',
1014
929
  thumbnailFallback: 'L',
@@ -1020,6 +935,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1020
935
  meta: 'Vacuum insulated, 16oz',
1021
936
  available: 87,
1022
937
  location: 'Aisle 5',
938
+ category: 'Drinkware',
1023
939
  tags: [{label: 'Drinkware', variant: 'green'}],
1024
940
  imageKey: 'tumbler',
1025
941
  thumbnailFallback: 'T',
@@ -1031,6 +947,7 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1031
947
  meta: 'Heavyweight, oat',
1032
948
  available: 24,
1033
949
  location: 'Aisle 6',
950
+ category: 'Home',
1034
951
  tags: [{label: 'Home', variant: 'orange'}],
1035
952
  imageKey: 'throw_',
1036
953
  thumbnailFallback: 'L',
@@ -1040,17 +957,6 @@ const DEFAULT_INVENTORY: InventoryRow[] = [
1040
957
 
1041
958
  const LOW_STOCK_THRESHOLD = 25;
1042
959
 
1043
- function SelectCell({row}: {row: InventoryRow}) {
1044
- return (
1045
- <CheckboxInput
1046
- label={'Select ' + row.name}
1047
- isLabelHidden
1048
- value={row.selected}
1049
- onChange={() => {}}
1050
- />
1051
- );
1052
- }
1053
-
1054
960
  function ItemCell({
1055
961
  row,
1056
962
  images,
@@ -1061,21 +967,31 @@ function ItemCell({
1061
967
  const thumbnailSrc = row.imageKey ? images[row.imageKey] : undefined;
1062
968
  return (
1063
969
  <HStack gap={3} vAlign="center">
1064
- {thumbnailSrc ? (
1065
- <img src={thumbnailSrc} alt="" style={inlineStyles.thumbnail} />
1066
- ) : (
1067
- <div style={inlineStyles.thumbnailFallback} aria-hidden="true">
1068
- {row.thumbnailFallback}
1069
- </div>
1070
- )}
1071
- <VStack gap={0} style={{minWidth: 0}}>
1072
- <Text type="body" weight="bold">
1073
- {row.name}
1074
- </Text>
1075
- <Text type="supporting" color="secondary">
1076
- {row.meta}
1077
- </Text>
1078
- </VStack>
970
+ <StackItem>
971
+ <VStack width={40}>
972
+ {thumbnailSrc ? (
973
+ <AspectRatio ratio={1} fit="cover">
974
+ <img src={thumbnailSrc} alt="" />
975
+ </AspectRatio>
976
+ ) : (
977
+ <Center width={40} height={40} aria-hidden="true">
978
+ <Text type="supporting" weight="bold">
979
+ {row.thumbnailFallback}
980
+ </Text>
981
+ </Center>
982
+ )}
983
+ </VStack>
984
+ </StackItem>
985
+ <StackItem size="fill">
986
+ <VStack gap={0}>
987
+ <Text type="body" weight="bold">
988
+ {row.name}
989
+ </Text>
990
+ <Text type="supporting" color="secondary">
991
+ {row.meta}
992
+ </Text>
993
+ </VStack>
994
+ </StackItem>
1079
995
  </HStack>
1080
996
  );
1081
997
  }
@@ -1113,180 +1029,196 @@ function InventoryCard({
1113
1029
  images: Record<string, string>;
1114
1030
  inventory: InventoryRow[];
1115
1031
  }) {
1032
+ const [query, setQuery] = useState('');
1033
+ const [category, setCategory] = useState<string | null>(null);
1034
+ const [location, setLocation] = useState<string | null>(null);
1035
+ const [tag, setTag] = useState<string | null>(null);
1036
+ const [selectedIds, setSelectedIds] = useState(
1037
+ () => new Set(inventory.filter(row => row.selected).map(row => row.id)),
1038
+ );
1116
1039
  const lowStockCount = inventory.filter(
1117
1040
  row => row.available < LOW_STOCK_THRESHOLD,
1118
1041
  ).length;
1042
+ const categories = [
1043
+ ...new Set(inventory.flatMap(row => (row.category ? [row.category] : []))),
1044
+ ];
1045
+ const rows = inventory.filter(
1046
+ row =>
1047
+ (!query ||
1048
+ `${row.name} ${row.meta}`
1049
+ .toLowerCase()
1050
+ .includes(query.toLowerCase())) &&
1051
+ (!category || row.category === category) &&
1052
+ (!location || row.location === location) &&
1053
+ (!tag || row.tags.some(value => value.label === tag)),
1054
+ );
1055
+ function selectRow(id: string, selected: boolean) {
1056
+ setSelectedIds(previous => {
1057
+ const next = new Set(previous);
1058
+ if (selected) {
1059
+ next.add(id);
1060
+ } else {
1061
+ next.delete(id);
1062
+ }
1063
+ return next;
1064
+ });
1065
+ }
1119
1066
  return (
1120
- <Card padding={0} style={styles.inventoryCard}>
1121
- <HStack hAlign="between" vAlign="center" style={styles.inventoryHeader}>
1122
- <Heading level={2}>Inventory</Heading>
1123
- <Button
1124
- label="Add item"
1125
- variant="primary"
1126
- size="sm"
1127
- icon={<Plus size={16} />}
1128
- />
1129
- </HStack>
1130
-
1131
- <Divider variant="subtle" />
1132
-
1133
- <HStack
1134
- gap={3}
1135
- vAlign="center"
1136
- hAlign="between"
1137
- style={styles.inventoryFilterRow}>
1138
- <HStack gap={2} vAlign="center" style={{flex: 1, minWidth: 0}}>
1139
- <TextInput
1140
- label="Search inventory"
1141
- isLabelHidden
1142
- placeholder="Type and hit enter…"
1143
- value=""
1144
- onChange={() => {}}
1145
- startIcon={<Search size={16} />}
1146
- controlStyle={styles.searchInput}
1067
+ <Card padding={0}>
1068
+ <VStack gap={0}>
1069
+ <HStack hAlign="between" vAlign="center" padding={6}>
1070
+ <Heading level={2}>Inventory</Heading>
1071
+ <Button
1072
+ label="Add item"
1073
+ variant="primary"
1074
+ size="sm"
1075
+ icon={<Icon icon={Plus} size="sm" />}
1147
1076
  />
1148
- <OverflowList
1149
- gap={2}
1150
- overflowRenderer={() => (
1077
+ </HStack>
1078
+ <Divider variant="subtle" />
1079
+ <ScrollableArea
1080
+ label="Inventory filters"
1081
+ axis="inline"
1082
+ paddingBlock={4}
1083
+ paddingInline={6}>
1084
+ <HStack gap={3} vAlign="center" hAlign="between">
1085
+ <HStack gap={2} vAlign="center">
1086
+ <TextInput
1087
+ label="Search inventory"
1088
+ isLabelHidden
1089
+ placeholder="Type and hit enter…"
1090
+ value={query}
1091
+ onChange={setQuery}
1092
+ startIcon={<Icon icon={Search} size="sm" />}
1093
+ width={240}
1094
+ />
1095
+ <HStack gap={2} wrap="wrap" vAlign="center">
1096
+ <Selector
1097
+ label="Categories"
1098
+ isLabelHidden
1099
+ placeholder="Categories"
1100
+ size="sm"
1101
+ startIcon={<Icon icon={Folder} size="sm" />}
1102
+ value={category}
1103
+ hasClear
1104
+ onChange={setCategory}
1105
+ options={categories}
1106
+ isDisabled={categories.length === 0}
1107
+ />
1108
+ <Selector
1109
+ label="Locations"
1110
+ isLabelHidden
1111
+ placeholder="Locations"
1112
+ size="sm"
1113
+ startIcon={<Icon icon={MapPin} size="sm" />}
1114
+ value={location}
1115
+ hasClear
1116
+ onChange={setLocation}
1117
+ options={[...new Set(inventory.map(row => row.location))]}
1118
+ />
1119
+ <Selector
1120
+ label="Tags"
1121
+ isLabelHidden
1122
+ placeholder="Tags"
1123
+ size="sm"
1124
+ startIcon={<Icon icon={Tag} size="sm" />}
1125
+ value={tag}
1126
+ hasClear
1127
+ onChange={setTag}
1128
+ options={[
1129
+ ...new Set(
1130
+ inventory.flatMap(row =>
1131
+ row.tags.map(value => value.label),
1132
+ ),
1133
+ ),
1134
+ ]}
1135
+ />
1136
+ </HStack>
1137
+ </HStack>
1138
+ <HStack gap={1} vAlign="center">
1151
1139
  <Button
1152
- label="Filters"
1153
1140
  variant="ghost"
1154
1141
  size="sm"
1155
- icon={<Tag size={16} />}
1142
+ isIconOnly
1143
+ label="List view"
1144
+ tooltip="List view"
1145
+ icon={<List size={18} />}
1156
1146
  />
1157
- )}>
1158
- <Selector
1159
- label="Categories"
1160
- isLabelHidden
1161
- placeholder="Categories"
1162
- size="sm"
1163
- startIcon={<Folder size={16} />}
1164
- value={undefined}
1165
- onChange={() => {}}
1166
- options={['Wearables', 'Audio', 'Bags', 'Drinkware', 'Home']}
1167
- />
1168
- <Selector
1169
- label="Locations"
1170
- isLabelHidden
1171
- placeholder="Locations"
1172
- size="sm"
1173
- startIcon={<MapPin size={16} />}
1174
- value={undefined}
1175
- onChange={() => {}}
1176
- options={[
1177
- 'Aisle 1',
1178
- 'Aisle 2',
1179
- 'Aisle 3',
1180
- 'Aisle 4',
1181
- 'Aisle 5',
1182
- 'Aisle 6',
1183
- ]}
1184
- />
1185
- <Selector
1186
- label="Tags"
1187
- isLabelHidden
1188
- placeholder="Tags"
1189
- size="sm"
1190
- startIcon={<Tag size={16} />}
1191
- value={undefined}
1192
- onChange={() => {}}
1193
- options={[
1194
- 'New',
1195
- 'Popular',
1196
- 'Limited',
1197
- 'Leather',
1198
- 'Drinkware',
1199
- 'Home',
1200
- ]}
1147
+ <Button
1148
+ variant="ghost"
1149
+ size="sm"
1150
+ isIconOnly
1151
+ label="Grid view"
1152
+ tooltip="Grid view"
1153
+ icon={<LayoutGrid size={18} />}
1154
+ />
1155
+ </HStack>
1156
+ </HStack>
1157
+ </ScrollableArea>
1158
+ {lowStockCount > 0 && (
1159
+ <VStack paddingInline={6} paddingBlockEnd={4}>
1160
+ <Banner
1161
+ status="warning"
1162
+ title={lowStockCount + ' items are running low'}
1201
1163
  />
1202
- </OverflowList>
1203
- </HStack>
1204
- <HStack gap={1} vAlign="center">
1205
- <Button
1206
- variant="ghost"
1207
- size="sm"
1208
- isIconOnly
1209
- label="List view"
1210
- tooltip="List view"
1211
- icon={<List size={18} />}
1212
- />
1213
- <Button
1214
- variant="ghost"
1215
- size="sm"
1216
- isIconOnly
1217
- label="Grid view"
1218
- tooltip="Grid view"
1219
- icon={<LayoutGrid size={18} />}
1220
- />
1221
- </HStack>
1222
- </HStack>
1223
-
1224
- {lowStockCount > 0 && (
1225
- <div style={inlineStyles.inventoryBannerWrap}>
1226
- <Banner
1227
- status="warning"
1228
- title={lowStockCount + ' items are running low'}
1164
+ </VStack>
1165
+ )}
1166
+ <VStack paddingInline={6} paddingBlockEnd={2}>
1167
+ <Table<InventoryRow>
1168
+ data={rows}
1169
+ columns={[
1170
+ {
1171
+ key: 'select',
1172
+ header: '',
1173
+ width: pixel(64),
1174
+ renderCell: row => (
1175
+ <CheckboxInput
1176
+ label={'Select ' + row.name}
1177
+ isLabelHidden
1178
+ value={selectedIds.has(row.id)}
1179
+ onChange={value => selectRow(row.id, value)}
1180
+ />
1181
+ ),
1182
+ },
1183
+ {
1184
+ key: 'item',
1185
+ header: 'Item',
1186
+ width: proportional(3, {minWidth: 80}),
1187
+ renderCell: row => <ItemCell row={row} images={images} />,
1188
+ },
1189
+ {
1190
+ key: 'available',
1191
+ header: 'Available',
1192
+ width: pixel(100),
1193
+ renderCell: row => <Text type="body">{row.available}</Text>,
1194
+ },
1195
+ {
1196
+ key: 'location',
1197
+ header: 'Location',
1198
+ width: pixel(100),
1199
+ renderCell: row => <Text type="body">{row.location}</Text>,
1200
+ },
1201
+ {
1202
+ key: 'tags',
1203
+ header: 'Tags',
1204
+ width: proportional(2, {minWidth: 80}),
1205
+ align: 'end',
1206
+ renderCell: row => <TagsCell row={row} />,
1207
+ },
1208
+ {
1209
+ key: 'actions',
1210
+ header: '',
1211
+ width: pixel(64),
1212
+ align: 'end',
1213
+ renderCell: () => <ActionsCell />,
1214
+ },
1215
+ ]}
1216
+ density="spacious"
1217
+ dividers="rows"
1218
+ hasHover
1229
1219
  />
1230
- </div>
1231
- )}
1232
-
1233
- <div style={styles.inventoryTableWrap}>
1234
- <Table<InventoryRow>
1235
- data={inventory}
1236
- columns={[
1237
- {
1238
- key: 'select',
1239
- header: '',
1240
- // Wide enough that the control + the theme's cell padding (up to
1241
- // --spacing-4 = 16px/side on spacious density) fit inside the cell,
1242
- // so the control's hover background doesn't overflow toward the
1243
- // card's clipped (rounded) edge on larger-padding themes.
1244
- width: pixel(64),
1245
- renderCell: row => <SelectCell row={row} />,
1246
- },
1247
- {
1248
- key: 'item',
1249
- header: 'Item',
1250
- // Lower min-width (default 120) so the table fits its container on
1251
- // larger-spacing themes instead of overflowing the actions column.
1252
- width: proportional(3, {minWidth: 80}),
1253
- renderCell: row => <ItemCell row={row} images={images} />,
1254
- },
1255
- {
1256
- key: 'available',
1257
- header: 'Available',
1258
- width: pixel(100),
1259
- renderCell: row => <Text type="body">{row.available}</Text>,
1260
- },
1261
- {
1262
- key: 'location',
1263
- header: 'Location',
1264
- width: pixel(100),
1265
- renderCell: row => <Text type="body">{row.location}</Text>,
1266
- },
1267
- {
1268
- key: 'tags',
1269
- header: 'Tags',
1270
- width: proportional(2, {minWidth: 80}),
1271
- align: 'end',
1272
- renderCell: row => <TagsCell row={row} />,
1273
- },
1274
- {
1275
- key: 'actions',
1276
- header: '',
1277
- // Match the select column: fit the sm more-menu button + cell
1278
- // padding so its hover background stays clear of the card's
1279
- // clipped rounded edge across themes.
1280
- width: pixel(64),
1281
- align: 'end',
1282
- renderCell: () => <ActionsCell />,
1283
- },
1284
- ]}
1285
- density="spacious"
1286
- dividers="rows"
1287
- hasHover
1288
- />
1289
- </div>
1220
+ </VStack>
1221
+ </VStack>
1290
1222
  </Card>
1291
1223
  );
1292
1224
  }