astryx-dracula 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {type CSSProperties, type ReactNode} from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Plus,
|
|
6
|
+
Search,
|
|
7
|
+
Tag,
|
|
8
|
+
Folder,
|
|
9
|
+
MapPin,
|
|
10
|
+
List,
|
|
11
|
+
LayoutGrid,
|
|
12
|
+
ShoppingBag,
|
|
13
|
+
Banknote,
|
|
14
|
+
Mic,
|
|
15
|
+
CreditCard,
|
|
16
|
+
Lock,
|
|
17
|
+
X,
|
|
18
|
+
Download,
|
|
19
|
+
Smartphone,
|
|
20
|
+
Wallet,
|
|
21
|
+
User,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
24
|
+
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
25
|
+
import {useAppShellMobile} from '@astryxdesign/core/AppShell';
|
|
26
|
+
import {Grid, GridSpan} from '@astryxdesign/core/Grid';
|
|
27
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
28
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
29
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
30
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
31
|
+
import {Banner} from '@astryxdesign/core/Banner';
|
|
32
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
33
|
+
import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
|
|
34
|
+
import {Item} from '@astryxdesign/core/Item';
|
|
35
|
+
import {Table, proportional, pixel} from '@astryxdesign/core/Table';
|
|
36
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
37
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
38
|
+
import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
|
|
39
|
+
import {SelectableCard} from '@astryxdesign/core/SelectableCard';
|
|
40
|
+
import {MoreMenu} from '@astryxdesign/core/MoreMenu';
|
|
41
|
+
import {Center} from '@astryxdesign/core/Center';
|
|
42
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
43
|
+
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
44
|
+
import {NumberInput} from '@astryxdesign/core/NumberInput';
|
|
45
|
+
import {OverflowList} from '@astryxdesign/core/OverflowList';
|
|
46
|
+
import {TopNav, TopNavHeading, TopNavItem} from '@astryxdesign/core/TopNav';
|
|
47
|
+
import {
|
|
48
|
+
ChatComposer,
|
|
49
|
+
ChatMessage,
|
|
50
|
+
ChatMessageBubble,
|
|
51
|
+
ChatMessageList,
|
|
52
|
+
ChatSystemMessage,
|
|
53
|
+
} from '@astryxdesign/core/Chat';
|
|
54
|
+
|
|
55
|
+
// Styles passed to Astryx components via their `style` prop. Astryx components
|
|
56
|
+
// forward the DOM `style` prop, so these work with no CSS compiler — in
|
|
57
|
+
// compiled builds and in the live playground preview alike.
|
|
58
|
+
const styles: Record<string, CSSProperties> = {
|
|
59
|
+
card: {
|
|
60
|
+
backgroundColor: 'var(--color-background-body)',
|
|
61
|
+
color: 'var(--color-text-primary)',
|
|
62
|
+
minWidth: 0,
|
|
63
|
+
borderColor: 'transparent',
|
|
64
|
+
},
|
|
65
|
+
checkoutStack: {
|
|
66
|
+
minWidth: 0,
|
|
67
|
+
width: '100%',
|
|
68
|
+
},
|
|
69
|
+
paymentCardContent: {
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
width: '100%',
|
|
72
|
+
textAlign: 'center',
|
|
73
|
+
wordBreak: 'break-word',
|
|
74
|
+
},
|
|
75
|
+
inventoryCard: {
|
|
76
|
+
backgroundColor: 'var(--color-background-surface)',
|
|
77
|
+
color: 'var(--color-text-primary)',
|
|
78
|
+
overflow: 'hidden',
|
|
79
|
+
},
|
|
80
|
+
inventoryHeader: {
|
|
81
|
+
paddingBlock: 'var(--spacing-6)',
|
|
82
|
+
paddingInline: 'var(--spacing-6)',
|
|
83
|
+
},
|
|
84
|
+
inventoryFilterRow: {
|
|
85
|
+
paddingBlock: 'var(--spacing-4)',
|
|
86
|
+
paddingInline: 'var(--spacing-6)',
|
|
87
|
+
width: '100%',
|
|
88
|
+
overflowX: 'auto' as const,
|
|
89
|
+
},
|
|
90
|
+
// Inset the table by --spacing-6 (the card is padding={0}) so its edge lines
|
|
91
|
+
// up with the header/filter row in every theme's spacing scale.
|
|
92
|
+
inventoryTableWrap: {
|
|
93
|
+
paddingInline: 'var(--spacing-6)',
|
|
94
|
+
paddingBlockEnd: 'var(--spacing-2)',
|
|
95
|
+
},
|
|
96
|
+
searchInput: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
minWidth: 0,
|
|
99
|
+
maxWidth: 240,
|
|
100
|
+
},
|
|
101
|
+
activityCard: {
|
|
102
|
+
backgroundColor: 'var(--color-background-surface)',
|
|
103
|
+
color: 'var(--color-text-primary)',
|
|
104
|
+
minWidth: 0,
|
|
105
|
+
height: '100%',
|
|
106
|
+
},
|
|
107
|
+
chatCard: {
|
|
108
|
+
backgroundColor: 'var(--color-background-surface)',
|
|
109
|
+
color: 'var(--color-text-primary)',
|
|
110
|
+
minWidth: 0,
|
|
111
|
+
overflow: 'hidden',
|
|
112
|
+
display: 'flex',
|
|
113
|
+
flexDirection: 'column' as const,
|
|
114
|
+
},
|
|
115
|
+
chatHeader: {
|
|
116
|
+
paddingBlock: 'var(--spacing-4)',
|
|
117
|
+
paddingInline: 'var(--spacing-4)',
|
|
118
|
+
},
|
|
119
|
+
activityCardStack: {
|
|
120
|
+
height: '100%',
|
|
121
|
+
},
|
|
122
|
+
activityListFade: {
|
|
123
|
+
flex: 1,
|
|
124
|
+
minHeight: 0,
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
maskImage:
|
|
127
|
+
'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
|
|
128
|
+
WebkitMaskImage:
|
|
129
|
+
'linear-gradient(to bottom, black calc(100% - 48px), transparent)',
|
|
130
|
+
marginInline: 'calc(var(--spacing-2) * -1)',
|
|
131
|
+
},
|
|
132
|
+
content: {
|
|
133
|
+
maxWidth: 960,
|
|
134
|
+
marginInline: 'auto',
|
|
135
|
+
minWidth: 0,
|
|
136
|
+
},
|
|
137
|
+
contentFluid: {
|
|
138
|
+
maxWidth: 880,
|
|
139
|
+
},
|
|
140
|
+
heroText: {
|
|
141
|
+
textAlign: 'center' as const,
|
|
142
|
+
maxWidth: 560,
|
|
143
|
+
},
|
|
144
|
+
centerText: {
|
|
145
|
+
textAlign: 'center',
|
|
146
|
+
},
|
|
147
|
+
cardStack: {
|
|
148
|
+
height: '100%',
|
|
149
|
+
},
|
|
150
|
+
cardDescription: {
|
|
151
|
+
flex: 1,
|
|
152
|
+
},
|
|
153
|
+
quantityInput: {
|
|
154
|
+
// minWidth (not a hard width) so the field grows to fit the digit + the
|
|
155
|
+
// theme's input padding. A fixed 40px was too tight on themes with larger
|
|
156
|
+
// padding / bigger type scale (e.g. Matcha, Y2K), clipping the value.
|
|
157
|
+
minWidth: 64,
|
|
158
|
+
flexShrink: 0,
|
|
159
|
+
},
|
|
160
|
+
cartButton: {
|
|
161
|
+
flex: 1,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Styles applied directly to plain DOM elements via the `style` prop.
|
|
166
|
+
// Plain inline styles so they render with no CSS compiler. All are static
|
|
167
|
+
// (no media/pseudo variants), so inline styles reproduce them exactly.
|
|
168
|
+
const inlineStyles: Record<string, CSSProperties> = {
|
|
169
|
+
inventoryBannerWrap: {
|
|
170
|
+
paddingInline: 'var(--spacing-6)',
|
|
171
|
+
paddingBottom: 'var(--spacing-4)',
|
|
172
|
+
},
|
|
173
|
+
chatBody: {
|
|
174
|
+
flex: 1,
|
|
175
|
+
minHeight: 0,
|
|
176
|
+
overflow: 'hidden',
|
|
177
|
+
},
|
|
178
|
+
chatSuggestions: {
|
|
179
|
+
paddingInline: 'var(--spacing-4)',
|
|
180
|
+
paddingBottom: 'var(--spacing-2)',
|
|
181
|
+
},
|
|
182
|
+
chatComposer: {
|
|
183
|
+
paddingInline: 'var(--spacing-4)',
|
|
184
|
+
paddingBottom: 'var(--spacing-4)',
|
|
185
|
+
},
|
|
186
|
+
activityIcon: {
|
|
187
|
+
width: 32,
|
|
188
|
+
height: 32,
|
|
189
|
+
borderRadius: 'var(--radius-full)',
|
|
190
|
+
backgroundColor: 'var(--color-background-muted)',
|
|
191
|
+
color: 'var(--color-text-secondary)',
|
|
192
|
+
display: 'flex',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center',
|
|
195
|
+
flexShrink: 0,
|
|
196
|
+
},
|
|
197
|
+
cardBody: {
|
|
198
|
+
padding: 'var(--spacing-4)',
|
|
199
|
+
flex: 1,
|
|
200
|
+
display: 'flex',
|
|
201
|
+
flexDirection: 'column',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Fills the AspectRatio box with a Dracula product scene. No Image primitive
|
|
206
|
+
// in Astryx (#2582), so product tiles are inline SVG on brand tokens.
|
|
207
|
+
const artImage: CSSProperties = {
|
|
208
|
+
width: '100%',
|
|
209
|
+
height: '100%',
|
|
210
|
+
display: 'block',
|
|
211
|
+
};
|
|
212
|
+
// Rounded inventory swatch. No radius prop where the swatch renders, so the
|
|
213
|
+
// SVG carries its own rounded corners (rx=4).
|
|
214
|
+
const thumbSwatch: CSSProperties = {
|
|
215
|
+
flexShrink: 0,
|
|
216
|
+
display: 'block',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// One Dracula accent per hero product slot.
|
|
220
|
+
const PRODUCT_HUES = [
|
|
221
|
+
'var(--dracula-purple)',
|
|
222
|
+
'var(--dracula-cyan)',
|
|
223
|
+
'var(--dracula-yellow)',
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
/** Categorical badge variants usable for showcase product/inventory tags. */
|
|
227
|
+
export type ShowcaseBadgeVariant =
|
|
228
|
+
| 'blue'
|
|
229
|
+
| 'cyan'
|
|
230
|
+
| 'green'
|
|
231
|
+
| 'orange'
|
|
232
|
+
| 'pink'
|
|
233
|
+
| 'purple'
|
|
234
|
+
| 'red'
|
|
235
|
+
| 'teal'
|
|
236
|
+
| 'yellow';
|
|
237
|
+
|
|
238
|
+
export interface ProductSpec {
|
|
239
|
+
name: string;
|
|
240
|
+
description: string;
|
|
241
|
+
badge: string;
|
|
242
|
+
badgeVariant: ShowcaseBadgeVariant;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const DEFAULT_PRODUCTS: ProductSpec[] = [
|
|
246
|
+
{
|
|
247
|
+
name: 'Moonphase Watch',
|
|
248
|
+
description: 'Clean lines and lume that carry through the longest night.',
|
|
249
|
+
badge: 'New',
|
|
250
|
+
badgeVariant: 'blue',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'Night-Owl Headphones',
|
|
254
|
+
description: 'Deep sound and soft cushions for all-night listening.',
|
|
255
|
+
badge: 'Popular',
|
|
256
|
+
badgeVariant: 'green',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'Coven Canvas Backpack',
|
|
260
|
+
description: 'Waxed canvas with a quiet, moonlit profile.',
|
|
261
|
+
badge: 'Limited',
|
|
262
|
+
badgeVariant: 'yellow',
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
// Dracula product scene: harvest moon over sleeping hills with a per-product
|
|
267
|
+
// accent glyph. All fills are brand vars, corners rx=4.
|
|
268
|
+
function ProductArt({hue, label}: {hue: string; label: string}) {
|
|
269
|
+
return (
|
|
270
|
+
<svg
|
|
271
|
+
viewBox="0 0 400 300"
|
|
272
|
+
preserveAspectRatio="xMidYMid slice"
|
|
273
|
+
style={artImage}
|
|
274
|
+
role="img"
|
|
275
|
+
aria-label={`${label} artwork`}>
|
|
276
|
+
<rect width="400" height="300" fill="var(--dracula-bg-light)" />
|
|
277
|
+
<circle cx="200" cy="110" r="52" fill={hue} opacity={0.9} />
|
|
278
|
+
<circle
|
|
279
|
+
cx="184"
|
|
280
|
+
cy="100"
|
|
281
|
+
r={44}
|
|
282
|
+
fill="var(--dracula-bg-light)"
|
|
283
|
+
opacity={0.55}
|
|
284
|
+
/>
|
|
285
|
+
<path
|
|
286
|
+
d="M0 210 Q120 170 240 200 T400 190 V300 H0 Z"
|
|
287
|
+
fill="var(--dracula-current-line)"
|
|
288
|
+
/>
|
|
289
|
+
<path
|
|
290
|
+
d="M0 245 Q140 215 300 240 T400 235 V300 H0 Z"
|
|
291
|
+
fill="var(--dracula-bg)"
|
|
292
|
+
/>
|
|
293
|
+
<g
|
|
294
|
+
transform="translate(200 232)"
|
|
295
|
+
fill="none"
|
|
296
|
+
stroke={hue}
|
|
297
|
+
strokeWidth="5"
|
|
298
|
+
strokeLinecap="round"
|
|
299
|
+
strokeLinejoin="round">
|
|
300
|
+
<rect x="-30" y="-30" width="60" height="60" rx={4} />
|
|
301
|
+
<circle cx="13" cy="-13" r="2.5" fill={hue} stroke="none" />
|
|
302
|
+
<path d="M-22 20 L-5 2 L7 12 L14 5 L23 15" />
|
|
303
|
+
</g>
|
|
304
|
+
</svg>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Rounded inventory swatch: Dracula surface with a per-row accent ring.
|
|
309
|
+
function ThumbSwatch({hue, label}: {hue: string; label: string}) {
|
|
310
|
+
return (
|
|
311
|
+
<svg
|
|
312
|
+
viewBox="0 0 40 40"
|
|
313
|
+
width={40}
|
|
314
|
+
height={40}
|
|
315
|
+
style={thumbSwatch}
|
|
316
|
+
role="img"
|
|
317
|
+
aria-label={`${label} swatch`}>
|
|
318
|
+
<rect width={40} height={40} rx={5} fill="var(--dracula-bg-light)" />
|
|
319
|
+
<rect
|
|
320
|
+
x={1}
|
|
321
|
+
y={1}
|
|
322
|
+
width={38}
|
|
323
|
+
height={38}
|
|
324
|
+
rx={4}
|
|
325
|
+
fill="none"
|
|
326
|
+
stroke="var(--color-widget-content-border)"
|
|
327
|
+
/>
|
|
328
|
+
<circle cx={20} cy={20} r={8} fill="none" stroke={hue} strokeWidth={3} />
|
|
329
|
+
</svg>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface ThemeShowcaseProps {
|
|
334
|
+
/** The three hero product cards. Defaults to the nocturne store products. */
|
|
335
|
+
products?: ProductSpec[];
|
|
336
|
+
/** Inventory table rows. Defaults to the nocturne store inventory. */
|
|
337
|
+
inventory?: InventoryRow[];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Default export is the route page (sandbox renders this as a Next.js page, so
|
|
341
|
+
// it must take no props / satisfy PageProps). It renders the store with the
|
|
342
|
+
// nocturne defaults. Consumers that need per-theme content import the named
|
|
343
|
+
// `ThemeShowcaseStore` below and pass products/inventory.
|
|
344
|
+
export default function ThemeShowcase() {
|
|
345
|
+
return <ThemeShowcaseStore />;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function ThemeShowcaseStore({
|
|
349
|
+
products = DEFAULT_PRODUCTS,
|
|
350
|
+
inventory = DEFAULT_INVENTORY,
|
|
351
|
+
}: ThemeShowcaseProps = {}) {
|
|
352
|
+
const {isMobile} = useAppShellMobile();
|
|
353
|
+
return (
|
|
354
|
+
<div
|
|
355
|
+
style={{
|
|
356
|
+
minHeight: '100%',
|
|
357
|
+
backgroundColor: 'var(--color-background-body)',
|
|
358
|
+
}}>
|
|
359
|
+
<StorePreview products={products} isMobile={isMobile} />
|
|
360
|
+
<div
|
|
361
|
+
style={{
|
|
362
|
+
padding: 'var(--spacing-6)',
|
|
363
|
+
backgroundColor: 'var(--color-background-surface)',
|
|
364
|
+
}}>
|
|
365
|
+
<CardShowcase inventory={inventory} isMobile={isMobile} />
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function CardShowcase({
|
|
372
|
+
inventory,
|
|
373
|
+
isMobile,
|
|
374
|
+
}: {
|
|
375
|
+
inventory: InventoryRow[];
|
|
376
|
+
isMobile: boolean;
|
|
377
|
+
}) {
|
|
378
|
+
const columns = isMobile ? 1 : ({minWidth: 200, repeat: 'fit'} as const);
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<VStack gap={8}>
|
|
382
|
+
<Grid columns={columns} gap={4}>
|
|
383
|
+
<GridSpan columns={1}>
|
|
384
|
+
<CheckoutCard isMobile={isMobile} />
|
|
385
|
+
</GridSpan>
|
|
386
|
+
<GridSpan columns={isMobile ? 1 : 2}>
|
|
387
|
+
<ChatCard />
|
|
388
|
+
</GridSpan>
|
|
389
|
+
</Grid>
|
|
390
|
+
<Grid columns={columns} gap={4}>
|
|
391
|
+
<GridSpan columns={isMobile ? 1 : 3}>
|
|
392
|
+
<InventoryCard inventory={inventory} />
|
|
393
|
+
</GridSpan>
|
|
394
|
+
<GridSpan columns={1}>
|
|
395
|
+
<LatestActivityCard isMobile={isMobile} />
|
|
396
|
+
</GridSpan>
|
|
397
|
+
</Grid>
|
|
398
|
+
</VStack>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function StorePreview({
|
|
403
|
+
products,
|
|
404
|
+
isMobile,
|
|
405
|
+
}: {
|
|
406
|
+
products: ProductSpec[];
|
|
407
|
+
isMobile: boolean;
|
|
408
|
+
}) {
|
|
409
|
+
return (
|
|
410
|
+
<div data-theme-preview="true">
|
|
411
|
+
<VStack gap={0}>
|
|
412
|
+
<TopNav
|
|
413
|
+
label="Theme preview navigation"
|
|
414
|
+
heading={<TopNavHeading heading="Nocturne" />}
|
|
415
|
+
centerContent={
|
|
416
|
+
isMobile ? undefined : (
|
|
417
|
+
<>
|
|
418
|
+
<TopNavItem label="Shop" href="#/templates/theme-showcase" isSelected />
|
|
419
|
+
<TopNavItem label="New In" href="#/templates/theme-showcase" />
|
|
420
|
+
<TopNavItem label="Stories" href="#/templates/theme-showcase" />
|
|
421
|
+
<TopNavItem label="Help" href="#/templates/theme-showcase" />
|
|
422
|
+
</>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
endContent={
|
|
426
|
+
<HStack gap={2} vAlign="center">
|
|
427
|
+
<HStack gap={0.5}>
|
|
428
|
+
<Button
|
|
429
|
+
label="Search"
|
|
430
|
+
tooltip="Search"
|
|
431
|
+
variant="ghost"
|
|
432
|
+
isIconOnly
|
|
433
|
+
icon={<Search size={20} />}
|
|
434
|
+
href="#/templates/theme-showcase"
|
|
435
|
+
/>
|
|
436
|
+
<Button
|
|
437
|
+
label="Account"
|
|
438
|
+
tooltip="Account"
|
|
439
|
+
variant="ghost"
|
|
440
|
+
isIconOnly
|
|
441
|
+
icon={<User size={20} />}
|
|
442
|
+
href="#/templates/theme-showcase"
|
|
443
|
+
/>
|
|
444
|
+
<Button
|
|
445
|
+
label="Cart"
|
|
446
|
+
tooltip="Cart"
|
|
447
|
+
variant="ghost"
|
|
448
|
+
isIconOnly
|
|
449
|
+
icon={<ShoppingBag size={20} />}
|
|
450
|
+
href="#/templates/theme-showcase"
|
|
451
|
+
/>
|
|
452
|
+
</HStack>
|
|
453
|
+
<Button label="Sign in" variant="primary" href="#/templates/theme-showcase" />
|
|
454
|
+
</HStack>
|
|
455
|
+
}
|
|
456
|
+
/>
|
|
457
|
+
|
|
458
|
+
<Section padding={6} variant="transparent">
|
|
459
|
+
<VStack gap={10} style={{...styles.content, ...styles.contentFluid}}>
|
|
460
|
+
<Center>
|
|
461
|
+
<VStack gap={4} hAlign="center" style={styles.heroText}>
|
|
462
|
+
<Text type="display-2" color="accent">
|
|
463
|
+
Little haunts,
|
|
464
|
+
<br />
|
|
465
|
+
everywhere you roam
|
|
466
|
+
</Text>
|
|
467
|
+
<Text type="body" color="secondary">
|
|
468
|
+
We believe the smallest shadows are the ones that matter most.
|
|
469
|
+
Turn an ordinary evening into something worth remembering.
|
|
470
|
+
</Text>
|
|
471
|
+
</VStack>
|
|
472
|
+
</Center>
|
|
473
|
+
|
|
474
|
+
<Grid columns={isMobile ? 1 : {minWidth: 200, max: 3}} gap={4}>
|
|
475
|
+
{products.map((p, i) => (
|
|
476
|
+
<Card key={p.name} padding={0} height="100%">
|
|
477
|
+
<VStack gap={0} style={styles.cardStack}>
|
|
478
|
+
<AspectRatio ratio={1}>
|
|
479
|
+
<ProductArt
|
|
480
|
+
hue={PRODUCT_HUES[i % PRODUCT_HUES.length]}
|
|
481
|
+
label={p.name}
|
|
482
|
+
/>
|
|
483
|
+
</AspectRatio>
|
|
484
|
+
<div style={inlineStyles.cardBody}>
|
|
485
|
+
<VStack gap={2} hAlign="center" style={styles.cardStack}>
|
|
486
|
+
<HStack>
|
|
487
|
+
<Badge label={p.badge} variant={p.badgeVariant} />
|
|
488
|
+
</HStack>
|
|
489
|
+
<Heading level={2} style={styles.centerText}>
|
|
490
|
+
{p.name}
|
|
491
|
+
</Heading>
|
|
492
|
+
<Text
|
|
493
|
+
type="body"
|
|
494
|
+
color="secondary"
|
|
495
|
+
style={{
|
|
496
|
+
...styles.cardDescription,
|
|
497
|
+
...styles.centerText,
|
|
498
|
+
}}>
|
|
499
|
+
{p.description}
|
|
500
|
+
</Text>
|
|
501
|
+
<HStack gap={2} vAlign="center" hAlign="center">
|
|
502
|
+
<NumberInput
|
|
503
|
+
label="Quantity"
|
|
504
|
+
isLabelHidden
|
|
505
|
+
value={1}
|
|
506
|
+
onChange={() => {}}
|
|
507
|
+
min={1}
|
|
508
|
+
max={99}
|
|
509
|
+
size="sm"
|
|
510
|
+
style={styles.quantityInput}
|
|
511
|
+
/>
|
|
512
|
+
<Button
|
|
513
|
+
label="Add to cart"
|
|
514
|
+
variant="secondary"
|
|
515
|
+
size="sm"
|
|
516
|
+
href="#/templates/theme-showcase"
|
|
517
|
+
style={styles.cartButton}
|
|
518
|
+
/>
|
|
519
|
+
</HStack>
|
|
520
|
+
</VStack>
|
|
521
|
+
</div>
|
|
522
|
+
</VStack>
|
|
523
|
+
</Card>
|
|
524
|
+
))}
|
|
525
|
+
</Grid>
|
|
526
|
+
</VStack>
|
|
527
|
+
</Section>
|
|
528
|
+
</VStack>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function CheckoutCard({isMobile}: {isMobile: boolean}) {
|
|
534
|
+
return (
|
|
535
|
+
<Card padding={5} style={styles.card}>
|
|
536
|
+
<VStack gap={4} style={styles.checkoutStack}>
|
|
537
|
+
<Heading level={2}>Checkout</Heading>
|
|
538
|
+
|
|
539
|
+
<VStack gap={3} style={styles.checkoutStack}>
|
|
540
|
+
<TextInput
|
|
541
|
+
label="Email"
|
|
542
|
+
placeholder="you@nocturne.shop"
|
|
543
|
+
value=""
|
|
544
|
+
onChange={() => {}}
|
|
545
|
+
size="lg"
|
|
546
|
+
/>
|
|
547
|
+
|
|
548
|
+
<RadioList
|
|
549
|
+
label="Shipping method"
|
|
550
|
+
description="Delivery time may vary based on location and availability."
|
|
551
|
+
value="economy"
|
|
552
|
+
onChange={() => {}}>
|
|
553
|
+
<RadioListItem
|
|
554
|
+
value="economy"
|
|
555
|
+
label="Economy Shipping"
|
|
556
|
+
description="Delivered in 5–7 business days"
|
|
557
|
+
endContent={
|
|
558
|
+
<Text type="body" weight="bold" hasTabularNumbers>
|
|
559
|
+
$12.00
|
|
560
|
+
</Text>
|
|
561
|
+
}
|
|
562
|
+
/>
|
|
563
|
+
<RadioListItem
|
|
564
|
+
value="standard"
|
|
565
|
+
label="Standard Shipping"
|
|
566
|
+
description="Delivered in 3–5 business days"
|
|
567
|
+
endContent={
|
|
568
|
+
<Text type="body" weight="bold" hasTabularNumbers>
|
|
569
|
+
$16.00
|
|
570
|
+
</Text>
|
|
571
|
+
}
|
|
572
|
+
/>
|
|
573
|
+
<RadioListItem
|
|
574
|
+
value="express"
|
|
575
|
+
label="Express Shipping"
|
|
576
|
+
description="Delivered in 1–2 business days"
|
|
577
|
+
endContent={
|
|
578
|
+
<Text type="body" weight="bold" hasTabularNumbers>
|
|
579
|
+
$24.00
|
|
580
|
+
</Text>
|
|
581
|
+
}
|
|
582
|
+
/>
|
|
583
|
+
</RadioList>
|
|
584
|
+
|
|
585
|
+
<VStack gap={2} style={styles.checkoutStack}>
|
|
586
|
+
<Text type="label" weight="bold">
|
|
587
|
+
Payment method
|
|
588
|
+
</Text>
|
|
589
|
+
<Grid columns={isMobile ? 1 : {minWidth: 70, max: 3}} gap={2}>
|
|
590
|
+
<SelectableCard
|
|
591
|
+
label="Pay with card"
|
|
592
|
+
isSelected={true}
|
|
593
|
+
onChange={() => {}}
|
|
594
|
+
padding={3}>
|
|
595
|
+
<VStack
|
|
596
|
+
gap={1}
|
|
597
|
+
hAlign="center"
|
|
598
|
+
style={styles.paymentCardContent}>
|
|
599
|
+
<CreditCard size={20} />
|
|
600
|
+
<Text type="supporting" weight="bold">
|
|
601
|
+
Card
|
|
602
|
+
</Text>
|
|
603
|
+
</VStack>
|
|
604
|
+
</SelectableCard>
|
|
605
|
+
<SelectableCard
|
|
606
|
+
label="Pay with Apple Pay"
|
|
607
|
+
isSelected={false}
|
|
608
|
+
onChange={() => {}}
|
|
609
|
+
padding={3}>
|
|
610
|
+
<VStack
|
|
611
|
+
gap={1}
|
|
612
|
+
hAlign="center"
|
|
613
|
+
style={styles.paymentCardContent}>
|
|
614
|
+
<Smartphone size={20} />
|
|
615
|
+
<Text type="supporting" weight="bold">
|
|
616
|
+
Apple Pay
|
|
617
|
+
</Text>
|
|
618
|
+
</VStack>
|
|
619
|
+
</SelectableCard>
|
|
620
|
+
<SelectableCard
|
|
621
|
+
label="Pay with Google Pay"
|
|
622
|
+
isSelected={false}
|
|
623
|
+
onChange={() => {}}
|
|
624
|
+
padding={3}>
|
|
625
|
+
<VStack
|
|
626
|
+
gap={1}
|
|
627
|
+
hAlign="center"
|
|
628
|
+
style={styles.paymentCardContent}>
|
|
629
|
+
<Wallet size={20} />
|
|
630
|
+
<Text type="supporting" weight="bold">
|
|
631
|
+
Google Pay
|
|
632
|
+
</Text>
|
|
633
|
+
</VStack>
|
|
634
|
+
</SelectableCard>
|
|
635
|
+
</Grid>
|
|
636
|
+
</VStack>
|
|
637
|
+
|
|
638
|
+
<TextInput
|
|
639
|
+
label="Card number"
|
|
640
|
+
placeholder="1234 1234 1234 1234"
|
|
641
|
+
value=""
|
|
642
|
+
onChange={() => {}}
|
|
643
|
+
startIcon={<CreditCard size={16} />}
|
|
644
|
+
size="lg"
|
|
645
|
+
/>
|
|
646
|
+
|
|
647
|
+
<Grid columns={isMobile ? 1 : {minWidth: 90, max: 2}} gap={2}>
|
|
648
|
+
<TextInput
|
|
649
|
+
label="Expiry"
|
|
650
|
+
placeholder="MM / YY"
|
|
651
|
+
value=""
|
|
652
|
+
onChange={() => {}}
|
|
653
|
+
size="lg"
|
|
654
|
+
/>
|
|
655
|
+
<TextInput
|
|
656
|
+
label="CVC"
|
|
657
|
+
placeholder="123"
|
|
658
|
+
value=""
|
|
659
|
+
onChange={() => {}}
|
|
660
|
+
size="lg"
|
|
661
|
+
/>
|
|
662
|
+
</Grid>
|
|
663
|
+
|
|
664
|
+
<Selector
|
|
665
|
+
label="Country"
|
|
666
|
+
value="us"
|
|
667
|
+
onChange={() => {}}
|
|
668
|
+
size="lg"
|
|
669
|
+
options={[
|
|
670
|
+
{value: 'us', label: 'United States'},
|
|
671
|
+
{value: 'ca', label: 'Canada'},
|
|
672
|
+
{value: 'uk', label: 'United Kingdom'},
|
|
673
|
+
{value: 'de', label: 'Germany'},
|
|
674
|
+
{value: 'jp', label: 'Japan'},
|
|
675
|
+
{value: 'au', label: 'Australia'},
|
|
676
|
+
]}
|
|
677
|
+
/>
|
|
678
|
+
</VStack>
|
|
679
|
+
|
|
680
|
+
<CheckboxInput
|
|
681
|
+
label="Securely save my information for 1-click checkout"
|
|
682
|
+
description="Pay faster on Nocturne and everywhere Link is accepted."
|
|
683
|
+
value={true}
|
|
684
|
+
onChange={() => {}}
|
|
685
|
+
/>
|
|
686
|
+
|
|
687
|
+
<Button
|
|
688
|
+
variant="primary"
|
|
689
|
+
size="lg"
|
|
690
|
+
label="Pay now"
|
|
691
|
+
icon={<Lock size={16} />}
|
|
692
|
+
/>
|
|
693
|
+
</VStack>
|
|
694
|
+
</Card>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const SUGGESTED_QUESTIONS = [
|
|
699
|
+
'Reschedule delivery',
|
|
700
|
+
'Update shipping address',
|
|
701
|
+
'Start a return',
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
function ChatCard() {
|
|
705
|
+
return (
|
|
706
|
+
<Card padding={0} style={styles.chatCard}>
|
|
707
|
+
<HStack
|
|
708
|
+
hAlign="between"
|
|
709
|
+
vAlign="center"
|
|
710
|
+
gap={3}
|
|
711
|
+
style={styles.chatHeader}>
|
|
712
|
+
<Heading level={2}>Night Owl AI</Heading>
|
|
713
|
+
|
|
714
|
+
<HStack gap={1} vAlign="center">
|
|
715
|
+
<Button
|
|
716
|
+
variant="ghost"
|
|
717
|
+
size="sm"
|
|
718
|
+
isIconOnly
|
|
719
|
+
label="Export conversation"
|
|
720
|
+
tooltip="Export conversation"
|
|
721
|
+
icon={<Download size={16} />}
|
|
722
|
+
/>
|
|
723
|
+
<Button
|
|
724
|
+
variant="ghost"
|
|
725
|
+
size="sm"
|
|
726
|
+
isIconOnly
|
|
727
|
+
label="Close chat"
|
|
728
|
+
tooltip="Close chat"
|
|
729
|
+
icon={<X size={16} />}
|
|
730
|
+
/>
|
|
731
|
+
</HStack>
|
|
732
|
+
</HStack>
|
|
733
|
+
|
|
734
|
+
<Divider variant="subtle" />
|
|
735
|
+
|
|
736
|
+
<div style={inlineStyles.chatBody}>
|
|
737
|
+
<ChatMessageList>
|
|
738
|
+
<ChatSystemMessage>Today</ChatSystemMessage>
|
|
739
|
+
|
|
740
|
+
<ChatMessage sender="user">
|
|
741
|
+
<ChatMessageBubble variant="filled">
|
|
742
|
+
Where’s my order?
|
|
743
|
+
</ChatMessageBubble>
|
|
744
|
+
</ChatMessage>
|
|
745
|
+
|
|
746
|
+
<ChatMessage sender="assistant">
|
|
747
|
+
<VStack gap={3}>
|
|
748
|
+
<Text type="body">
|
|
749
|
+
Your order #1043 — the Moonphase Watch and Belfry Throw —
|
|
750
|
+
shipped this morning from the Aisle 3 warehouse and is currently
|
|
751
|
+
in transit with UPS. It’s on track to arrive at your address by
|
|
752
|
+
end of day tomorrow.
|
|
753
|
+
</Text>
|
|
754
|
+
<Text type="body">
|
|
755
|
+
Let me know if you’d like to reschedule the delivery, redirect
|
|
756
|
+
it to a pickup point, or start a return once it arrives.
|
|
757
|
+
</Text>
|
|
758
|
+
</VStack>
|
|
759
|
+
</ChatMessage>
|
|
760
|
+
|
|
761
|
+
<ChatMessage sender="user">
|
|
762
|
+
<ChatMessageBubble variant="filled">
|
|
763
|
+
Can you show me the full details?
|
|
764
|
+
</ChatMessageBubble>
|
|
765
|
+
</ChatMessage>
|
|
766
|
+
|
|
767
|
+
<ChatMessage sender="assistant">
|
|
768
|
+
<VStack gap={3}>
|
|
769
|
+
<Text type="body">Here’s everything I have on order #1043:</Text>
|
|
770
|
+
<Card padding={3}>
|
|
771
|
+
<VStack gap={1}>
|
|
772
|
+
<Item
|
|
773
|
+
label="Items"
|
|
774
|
+
description="Moonphase Watch · Belfry Throw"
|
|
775
|
+
endContent={
|
|
776
|
+
<Text type="body" weight="bold" hasTabularNumbers>
|
|
777
|
+
$248
|
|
778
|
+
</Text>
|
|
779
|
+
}
|
|
780
|
+
/>
|
|
781
|
+
<Item
|
|
782
|
+
label="Shipping"
|
|
783
|
+
description="UPS Ground"
|
|
784
|
+
endContent={
|
|
785
|
+
<Text type="body" weight="bold" hasTabularNumbers>
|
|
786
|
+
$12
|
|
787
|
+
</Text>
|
|
788
|
+
}
|
|
789
|
+
/>
|
|
790
|
+
<Item
|
|
791
|
+
label="Estimated arrival"
|
|
792
|
+
description="Tomorrow by 8pm"
|
|
793
|
+
endContent={<Badge variant="green" label="On time" />}
|
|
794
|
+
/>
|
|
795
|
+
<Item
|
|
796
|
+
label="Tracking"
|
|
797
|
+
description="UPS 1Z 999 AA1 0123 4567 84"
|
|
798
|
+
endContent={<Link href="#/templates/theme-showcase">Track →</Link>}
|
|
799
|
+
/>
|
|
800
|
+
</VStack>
|
|
801
|
+
</Card>
|
|
802
|
+
</VStack>
|
|
803
|
+
</ChatMessage>
|
|
804
|
+
</ChatMessageList>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<div style={inlineStyles.chatSuggestions}>
|
|
808
|
+
<HStack gap={1} hAlign="center" wrap="wrap">
|
|
809
|
+
{SUGGESTED_QUESTIONS.map(question => (
|
|
810
|
+
<Button
|
|
811
|
+
key={question}
|
|
812
|
+
variant="secondary"
|
|
813
|
+
size="sm"
|
|
814
|
+
label={question}
|
|
815
|
+
/>
|
|
816
|
+
))}
|
|
817
|
+
</HStack>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
<div style={inlineStyles.chatComposer}>
|
|
821
|
+
<ChatComposer
|
|
822
|
+
value=""
|
|
823
|
+
onChange={() => {}}
|
|
824
|
+
onSubmit={() => {}}
|
|
825
|
+
placeholder="Ask Night Owl…"
|
|
826
|
+
footerActions={
|
|
827
|
+
<Button
|
|
828
|
+
variant="ghost"
|
|
829
|
+
size="md"
|
|
830
|
+
isIconOnly
|
|
831
|
+
label="Attach"
|
|
832
|
+
tooltip="Attach"
|
|
833
|
+
icon={<Plus size={16} />}
|
|
834
|
+
/>
|
|
835
|
+
}
|
|
836
|
+
sendActions={
|
|
837
|
+
<Button
|
|
838
|
+
variant="ghost"
|
|
839
|
+
size="md"
|
|
840
|
+
isIconOnly
|
|
841
|
+
label="Voice input"
|
|
842
|
+
tooltip="Voice input"
|
|
843
|
+
icon={<Mic size={16} />}
|
|
844
|
+
/>
|
|
845
|
+
}
|
|
846
|
+
/>
|
|
847
|
+
</div>
|
|
848
|
+
</Card>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
interface ActivityRow {
|
|
853
|
+
id: string;
|
|
854
|
+
icon: ReactNode;
|
|
855
|
+
label: string;
|
|
856
|
+
detail: string;
|
|
857
|
+
time: string;
|
|
858
|
+
amount: number;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const ACTIVITY: ActivityRow[] = [
|
|
862
|
+
{
|
|
863
|
+
id: '1',
|
|
864
|
+
icon: <ShoppingBag size={16} />,
|
|
865
|
+
label: 'Order #1043',
|
|
866
|
+
detail: 'Placed · 1:59 pm',
|
|
867
|
+
time: '1:59 pm',
|
|
868
|
+
amount: 248,
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
id: '2',
|
|
872
|
+
icon: <Banknote size={16} />,
|
|
873
|
+
label: 'Order #1041',
|
|
874
|
+
detail: 'Refunded · 12:40 pm',
|
|
875
|
+
time: '12:40 pm',
|
|
876
|
+
amount: -89,
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: '3',
|
|
880
|
+
icon: <ShoppingBag size={16} />,
|
|
881
|
+
label: 'Order #1040',
|
|
882
|
+
detail: 'Placed · 10:30 am',
|
|
883
|
+
time: '10:30 am',
|
|
884
|
+
amount: 156,
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: '4',
|
|
888
|
+
icon: <ShoppingBag size={16} />,
|
|
889
|
+
label: 'Order #1038',
|
|
890
|
+
detail: 'Placed · 9:11 am',
|
|
891
|
+
time: '9:11 am',
|
|
892
|
+
amount: 412,
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: '5',
|
|
896
|
+
icon: <ShoppingBag size={16} />,
|
|
897
|
+
label: 'Order #1037',
|
|
898
|
+
detail: 'Placed · 8:42 am',
|
|
899
|
+
time: '8:42 am',
|
|
900
|
+
amount: 95,
|
|
901
|
+
},
|
|
902
|
+
];
|
|
903
|
+
|
|
904
|
+
function formatAmount(amount: number): string {
|
|
905
|
+
const sign = amount < 0 ? '−' : '+';
|
|
906
|
+
return sign + '$' + Math.abs(amount).toLocaleString();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function LatestActivityCard({isMobile}: {isMobile: boolean}) {
|
|
910
|
+
return (
|
|
911
|
+
<Card padding={5} style={styles.activityCard}>
|
|
912
|
+
<VStack gap={4} style={styles.activityCardStack}>
|
|
913
|
+
<Heading level={2}>Revenue</Heading>
|
|
914
|
+
|
|
915
|
+
<Grid columns={isMobile ? 1 : 2} gap={3}>
|
|
916
|
+
<VStack gap={0}>
|
|
917
|
+
<Text type="display-3" weight="bold" hasTabularNumbers>
|
|
918
|
+
18K
|
|
919
|
+
</Text>
|
|
920
|
+
<Text type="supporting" color="secondary">
|
|
921
|
+
Monthly revenue
|
|
922
|
+
</Text>
|
|
923
|
+
</VStack>
|
|
924
|
+
<VStack gap={0}>
|
|
925
|
+
<Text type="display-3" weight="bold" hasTabularNumbers>
|
|
926
|
+
+12%
|
|
927
|
+
</Text>
|
|
928
|
+
<Text type="supporting" color="secondary">
|
|
929
|
+
Order growth
|
|
930
|
+
</Text>
|
|
931
|
+
</VStack>
|
|
932
|
+
</Grid>
|
|
933
|
+
|
|
934
|
+
<Divider variant="subtle" />
|
|
935
|
+
|
|
936
|
+
<HStack hAlign="between" vAlign="center">
|
|
937
|
+
<Heading level={3}>Activity</Heading>
|
|
938
|
+
<Link href="#/templates/theme-showcase">See all</Link>
|
|
939
|
+
</HStack>
|
|
940
|
+
|
|
941
|
+
<VStack gap={1} style={styles.activityListFade}>
|
|
942
|
+
{ACTIVITY.map(item => (
|
|
943
|
+
<Item
|
|
944
|
+
key={item.id}
|
|
945
|
+
startContent={
|
|
946
|
+
<div style={inlineStyles.activityIcon} aria-hidden="true">
|
|
947
|
+
{item.icon}
|
|
948
|
+
</div>
|
|
949
|
+
}
|
|
950
|
+
label={item.label}
|
|
951
|
+
description={item.detail}
|
|
952
|
+
endContent={
|
|
953
|
+
<Text
|
|
954
|
+
type="body"
|
|
955
|
+
weight="bold"
|
|
956
|
+
hasTabularNumbers
|
|
957
|
+
color={item.amount < 0 ? 'secondary' : 'primary'}>
|
|
958
|
+
{formatAmount(item.amount)}
|
|
959
|
+
</Text>
|
|
960
|
+
}
|
|
961
|
+
href="#/templates/theme-showcase"
|
|
962
|
+
/>
|
|
963
|
+
))}
|
|
964
|
+
</VStack>
|
|
965
|
+
</VStack>
|
|
966
|
+
</Card>
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
type TagSpec = {label: string; variant: ShowcaseBadgeVariant};
|
|
971
|
+
|
|
972
|
+
export interface InventoryRow extends Record<string, unknown> {
|
|
973
|
+
id: string;
|
|
974
|
+
name: string;
|
|
975
|
+
meta: string;
|
|
976
|
+
available: number;
|
|
977
|
+
location: string;
|
|
978
|
+
tags: TagSpec[];
|
|
979
|
+
hue: string;
|
|
980
|
+
selected: boolean;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const DEFAULT_INVENTORY: InventoryRow[] = [
|
|
984
|
+
{
|
|
985
|
+
id: 'a',
|
|
986
|
+
name: 'Moonphase Watch',
|
|
987
|
+
meta: 'Steel case, moonphase dial',
|
|
988
|
+
available: 42,
|
|
989
|
+
location: 'Aisle 3',
|
|
990
|
+
tags: [{label: 'New', variant: 'blue'}],
|
|
991
|
+
hue: 'var(--dracula-purple)',
|
|
992
|
+
selected: false,
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
id: 'b',
|
|
996
|
+
name: 'Night-Owl Headphones',
|
|
997
|
+
meta: 'ANC, 30hr battery',
|
|
998
|
+
available: 128,
|
|
999
|
+
location: 'Aisle 1',
|
|
1000
|
+
tags: [{label: 'Popular', variant: 'green'}],
|
|
1001
|
+
hue: 'var(--dracula-cyan)',
|
|
1002
|
+
selected: true,
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
id: 'c',
|
|
1006
|
+
name: 'Coven Canvas Backpack',
|
|
1007
|
+
meta: 'Waxed canvas, 25L',
|
|
1008
|
+
available: 63,
|
|
1009
|
+
location: 'Aisle 2',
|
|
1010
|
+
tags: [{label: 'Limited', variant: 'yellow'}],
|
|
1011
|
+
hue: 'var(--dracula-yellow)',
|
|
1012
|
+
selected: false,
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
id: 'd',
|
|
1016
|
+
name: 'Night Market Wallet',
|
|
1017
|
+
meta: 'Full-grain, RFID blocking',
|
|
1018
|
+
available: 15,
|
|
1019
|
+
location: 'Aisle 4',
|
|
1020
|
+
tags: [{label: 'Leather', variant: 'yellow'}],
|
|
1021
|
+
hue: 'var(--dracula-orange)',
|
|
1022
|
+
selected: true,
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
id: 'e',
|
|
1026
|
+
name: 'Midnight Tumbler',
|
|
1027
|
+
meta: 'Vacuum insulated, 16oz',
|
|
1028
|
+
available: 87,
|
|
1029
|
+
location: 'Aisle 5',
|
|
1030
|
+
tags: [{label: 'Drinkware', variant: 'green'}],
|
|
1031
|
+
hue: 'var(--dracula-pink)',
|
|
1032
|
+
selected: false,
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'f',
|
|
1036
|
+
name: 'Belfry Throw',
|
|
1037
|
+
meta: 'Heavyweight, oat',
|
|
1038
|
+
available: 24,
|
|
1039
|
+
location: 'Aisle 6',
|
|
1040
|
+
tags: [{label: 'Home', variant: 'orange'}],
|
|
1041
|
+
hue: 'var(--dracula-green)',
|
|
1042
|
+
selected: true,
|
|
1043
|
+
},
|
|
1044
|
+
];
|
|
1045
|
+
|
|
1046
|
+
const LOW_STOCK_THRESHOLD = 25;
|
|
1047
|
+
|
|
1048
|
+
function SelectCell({row}: {row: InventoryRow}) {
|
|
1049
|
+
return (
|
|
1050
|
+
<CheckboxInput
|
|
1051
|
+
label={'Select ' + row.name}
|
|
1052
|
+
isLabelHidden
|
|
1053
|
+
value={row.selected}
|
|
1054
|
+
onChange={() => {}}
|
|
1055
|
+
/>
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function ItemCell({row}: {row: InventoryRow}) {
|
|
1060
|
+
return (
|
|
1061
|
+
<HStack gap={3} vAlign="center">
|
|
1062
|
+
<ThumbSwatch hue={row.hue} label={row.name} />
|
|
1063
|
+
<VStack gap={0} style={{minWidth: 0}}>
|
|
1064
|
+
<Text type="body" weight="bold">
|
|
1065
|
+
{row.name}
|
|
1066
|
+
</Text>
|
|
1067
|
+
<Text type="supporting" color="secondary">
|
|
1068
|
+
{row.meta}
|
|
1069
|
+
</Text>
|
|
1070
|
+
</VStack>
|
|
1071
|
+
</HStack>
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function TagsCell({row}: {row: InventoryRow}) {
|
|
1076
|
+
return (
|
|
1077
|
+
<HStack gap={1} wrap="wrap" hAlign="end">
|
|
1078
|
+
{row.tags.map(tag => (
|
|
1079
|
+
<Badge key={tag.label} label={tag.label} variant={tag.variant} />
|
|
1080
|
+
))}
|
|
1081
|
+
</HStack>
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function ActionsCell() {
|
|
1086
|
+
return (
|
|
1087
|
+
<MoreMenu
|
|
1088
|
+
label="Row actions"
|
|
1089
|
+
size="sm"
|
|
1090
|
+
items={[
|
|
1091
|
+
{label: 'Edit'},
|
|
1092
|
+
{label: 'Duplicate'},
|
|
1093
|
+
{label: 'Move to…'},
|
|
1094
|
+
{type: 'divider'},
|
|
1095
|
+
{label: 'Delete'},
|
|
1096
|
+
]}
|
|
1097
|
+
/>
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function InventoryCard({inventory}: {inventory: InventoryRow[]}) {
|
|
1102
|
+
const lowStockCount = inventory.filter(
|
|
1103
|
+
row => row.available < LOW_STOCK_THRESHOLD,
|
|
1104
|
+
).length;
|
|
1105
|
+
return (
|
|
1106
|
+
<Card padding={0} style={styles.inventoryCard}>
|
|
1107
|
+
<HStack hAlign="between" vAlign="center" style={styles.inventoryHeader}>
|
|
1108
|
+
<Heading level={2}>Inventory</Heading>
|
|
1109
|
+
<Button
|
|
1110
|
+
label="Add item"
|
|
1111
|
+
variant="primary"
|
|
1112
|
+
size="sm"
|
|
1113
|
+
icon={<Plus size={16} />}
|
|
1114
|
+
/>
|
|
1115
|
+
</HStack>
|
|
1116
|
+
|
|
1117
|
+
<Divider variant="subtle" />
|
|
1118
|
+
|
|
1119
|
+
<HStack
|
|
1120
|
+
gap={3}
|
|
1121
|
+
vAlign="center"
|
|
1122
|
+
hAlign="between"
|
|
1123
|
+
style={styles.inventoryFilterRow}>
|
|
1124
|
+
<HStack gap={2} vAlign="center" style={{flex: 1, minWidth: 0}}>
|
|
1125
|
+
<TextInput
|
|
1126
|
+
label="Search inventory"
|
|
1127
|
+
isLabelHidden
|
|
1128
|
+
placeholder="Type and hit enter…"
|
|
1129
|
+
value=""
|
|
1130
|
+
onChange={() => {}}
|
|
1131
|
+
startIcon={<Search size={16} />}
|
|
1132
|
+
style={styles.searchInput}
|
|
1133
|
+
/>
|
|
1134
|
+
<OverflowList
|
|
1135
|
+
gap={2}
|
|
1136
|
+
overflowRenderer={() => (
|
|
1137
|
+
<Button
|
|
1138
|
+
label="Filters"
|
|
1139
|
+
variant="ghost"
|
|
1140
|
+
size="sm"
|
|
1141
|
+
icon={<Tag size={16} />}
|
|
1142
|
+
/>
|
|
1143
|
+
)}>
|
|
1144
|
+
<Selector
|
|
1145
|
+
label="Categories"
|
|
1146
|
+
isLabelHidden
|
|
1147
|
+
placeholder="Categories"
|
|
1148
|
+
size="sm"
|
|
1149
|
+
startIcon={<Folder size={16} />}
|
|
1150
|
+
value={undefined}
|
|
1151
|
+
onChange={() => {}}
|
|
1152
|
+
options={['Wearables', 'Audio', 'Bags', 'Drinkware', 'Home']}
|
|
1153
|
+
/>
|
|
1154
|
+
<Selector
|
|
1155
|
+
label="Locations"
|
|
1156
|
+
isLabelHidden
|
|
1157
|
+
placeholder="Locations"
|
|
1158
|
+
size="sm"
|
|
1159
|
+
startIcon={<MapPin size={16} />}
|
|
1160
|
+
value={undefined}
|
|
1161
|
+
onChange={() => {}}
|
|
1162
|
+
options={[
|
|
1163
|
+
'Aisle 1',
|
|
1164
|
+
'Aisle 2',
|
|
1165
|
+
'Aisle 3',
|
|
1166
|
+
'Aisle 4',
|
|
1167
|
+
'Aisle 5',
|
|
1168
|
+
'Aisle 6',
|
|
1169
|
+
]}
|
|
1170
|
+
/>
|
|
1171
|
+
<Selector
|
|
1172
|
+
label="Tags"
|
|
1173
|
+
isLabelHidden
|
|
1174
|
+
placeholder="Tags"
|
|
1175
|
+
size="sm"
|
|
1176
|
+
startIcon={<Tag size={16} />}
|
|
1177
|
+
value={undefined}
|
|
1178
|
+
onChange={() => {}}
|
|
1179
|
+
options={[
|
|
1180
|
+
'New',
|
|
1181
|
+
'Popular',
|
|
1182
|
+
'Limited',
|
|
1183
|
+
'Leather',
|
|
1184
|
+
'Drinkware',
|
|
1185
|
+
'Home',
|
|
1186
|
+
]}
|
|
1187
|
+
/>
|
|
1188
|
+
</OverflowList>
|
|
1189
|
+
</HStack>
|
|
1190
|
+
<HStack gap={1} vAlign="center">
|
|
1191
|
+
<Button
|
|
1192
|
+
variant="ghost"
|
|
1193
|
+
size="sm"
|
|
1194
|
+
isIconOnly
|
|
1195
|
+
label="List view"
|
|
1196
|
+
tooltip="List view"
|
|
1197
|
+
icon={<List size={18} />}
|
|
1198
|
+
/>
|
|
1199
|
+
<Button
|
|
1200
|
+
variant="ghost"
|
|
1201
|
+
size="sm"
|
|
1202
|
+
isIconOnly
|
|
1203
|
+
label="Grid view"
|
|
1204
|
+
tooltip="Grid view"
|
|
1205
|
+
icon={<LayoutGrid size={18} />}
|
|
1206
|
+
/>
|
|
1207
|
+
</HStack>
|
|
1208
|
+
</HStack>
|
|
1209
|
+
|
|
1210
|
+
{lowStockCount > 0 && (
|
|
1211
|
+
<div style={inlineStyles.inventoryBannerWrap}>
|
|
1212
|
+
<Banner
|
|
1213
|
+
status="warning"
|
|
1214
|
+
title={lowStockCount + ' items are running low'}
|
|
1215
|
+
/>
|
|
1216
|
+
</div>
|
|
1217
|
+
)}
|
|
1218
|
+
|
|
1219
|
+
<div style={styles.inventoryTableWrap}>
|
|
1220
|
+
<Table<InventoryRow>
|
|
1221
|
+
data={inventory}
|
|
1222
|
+
columns={[
|
|
1223
|
+
{
|
|
1224
|
+
key: 'select',
|
|
1225
|
+
header: '',
|
|
1226
|
+
// Wide enough that the control + the theme's cell padding (up to
|
|
1227
|
+
// --spacing-4 = 16px/side on spacious density) fit inside the cell,
|
|
1228
|
+
// so the control's hover background doesn't overflow toward the
|
|
1229
|
+
// card's clipped (rounded) edge on larger-padding themes.
|
|
1230
|
+
width: pixel(64),
|
|
1231
|
+
renderCell: row => <SelectCell row={row} />,
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
key: 'item',
|
|
1235
|
+
header: 'Item',
|
|
1236
|
+
// Lower min-width (default 120) so the table fits its container on
|
|
1237
|
+
// larger-spacing themes instead of overflowing the actions column.
|
|
1238
|
+
width: proportional(3, {minWidth: 80}),
|
|
1239
|
+
renderCell: row => <ItemCell row={row} />,
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
key: 'available',
|
|
1243
|
+
header: 'Available',
|
|
1244
|
+
width: pixel(100),
|
|
1245
|
+
renderCell: row => (
|
|
1246
|
+
<Text type="body" hasTabularNumbers>
|
|
1247
|
+
{row.available}
|
|
1248
|
+
</Text>
|
|
1249
|
+
),
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
key: 'location',
|
|
1253
|
+
header: 'Location',
|
|
1254
|
+
width: pixel(100),
|
|
1255
|
+
renderCell: row => <Text type="body">{row.location}</Text>,
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
key: 'tags',
|
|
1259
|
+
header: 'Tags',
|
|
1260
|
+
width: proportional(2, {minWidth: 80}),
|
|
1261
|
+
align: 'end',
|
|
1262
|
+
renderCell: row => <TagsCell row={row} />,
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
key: 'actions',
|
|
1266
|
+
header: '',
|
|
1267
|
+
// Match the select column: fit the sm more-menu button + cell
|
|
1268
|
+
// padding so its hover background stays clear of the card's
|
|
1269
|
+
// clipped rounded edge across themes.
|
|
1270
|
+
width: pixel(64),
|
|
1271
|
+
align: 'end',
|
|
1272
|
+
renderCell: () => <ActionsCell />,
|
|
1273
|
+
},
|
|
1274
|
+
]}
|
|
1275
|
+
density="spacious"
|
|
1276
|
+
dividers="rows"
|
|
1277
|
+
hasHover
|
|
1278
|
+
/>
|
|
1279
|
+
</div>
|
|
1280
|
+
</Card>
|
|
1281
|
+
);
|
|
1282
|
+
}
|