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.
Files changed (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Product Detail',
4
+ description: 'Product page with gallery and collapsible sections',
5
+ category: 'commerce',
6
+ };
@@ -0,0 +1,366 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState} from 'react';
4
+ import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
5
+ import {Center} from '@astryxdesign/core/Center';
6
+ import {Grid} from '@astryxdesign/core/Grid';
7
+ import {Text, Heading} from '@astryxdesign/core/Text';
8
+ import {Button} from '@astryxdesign/core/Button';
9
+ import {NumberInput} from '@astryxdesign/core/NumberInput';
10
+ import {Icon} from '@astryxdesign/core/Icon';
11
+ import {
12
+ SegmentedControl,
13
+ SegmentedControlItem,
14
+ } from '@astryxdesign/core/SegmentedControl';
15
+ import {Badge} from '@astryxdesign/core/Badge';
16
+ import {Banner} from '@astryxdesign/core/Banner';
17
+ import {Divider} from '@astryxdesign/core/Divider';
18
+ import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
19
+ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
20
+ import {SelectableCard} from '@astryxdesign/core/SelectableCard';
21
+ import type {CSSProperties} from 'react';
22
+
23
+ // Custom CSS here is limited to what Astryx components can't express today:
24
+ // - image fill + corner radius (no Image primitive — #2582)
25
+ // - the sticky info column (no sticky prop on Astryx layout primitives — #2613)
26
+ // Keeps the info column in view while the gallery scrolls. No sticky prop on
27
+ // Astryx layout primitives.
28
+ const stickyInfo: CSSProperties = {
29
+ position: 'sticky',
30
+ top: 'var(--spacing-8)',
31
+ alignSelf: 'start',
32
+ };
33
+ // Product scenes are inline Dracula SVG — there is no Image primitive in
34
+ // Astryx (#2582). Fills the AspectRatio box + rounds hero corners. No
35
+ // objectFit/radius props on AspectRatio.
36
+ const heroImage: CSSProperties = {
37
+ borderRadius: 'var(--radius-container)',
38
+ overflow: 'clip',
39
+ };
40
+ // Fills the thumbnail card. Corner radius + selection ring come from
41
+ // SelectableCard; the scene only needs to fill the box (#2582).
42
+ const thumbImage: CSSProperties = {
43
+ width: '100%',
44
+ height: '100%',
45
+ display: 'block',
46
+ };
47
+
48
+ // One Dracula placeholder scene per view, drawn from the fixed badge
49
+ // vocabulary. Resolves to theme tokens so the gallery stays on-brand in the
50
+ // dark-only theme.
51
+ function ProductScene({hue, label}: {hue: string; label: string}) {
52
+ return (
53
+ <svg
54
+ viewBox="0 0 400 300"
55
+ preserveAspectRatio="xMidYMid slice"
56
+ style={thumbImage}
57
+ role="img"
58
+ aria-label={label}>
59
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
60
+ <g
61
+ transform="translate(200 150)"
62
+ fill="none"
63
+ stroke="var(--dracula-comment)"
64
+ strokeWidth="5"
65
+ strokeLinecap="round"
66
+ strokeLinejoin="round">
67
+ <rect x="-44" y="-44" width="88" height="88" rx="5" />
68
+ <circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
69
+ <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
70
+ </g>
71
+ </svg>
72
+ );
73
+ }
74
+
75
+ import {Minus, Plus, Star} from 'lucide-react';
76
+
77
+ // ─── Star Rating ─────────────────────────────────────────────────────────────
78
+ function StarRating({rating, count}: {rating: number; count: number}) {
79
+ const filled = Math.round(rating);
80
+ const empty = 5 - filled;
81
+
82
+ return (
83
+ <HStack gap={1} vAlign="center">
84
+ {Array.from({length: filled}, (_, i) => (
85
+ <Icon key={`full-${i}`} icon={Star} size="sm" color="warning" />
86
+ ))}
87
+ {Array.from({length: empty}, (_, i) => (
88
+ <Icon key={`empty-${i}`} icon={Star} size="sm" />
89
+ ))}
90
+ <Text type="body" color="secondary" hasTabularNumbers>
91
+ {rating} ({count})
92
+ </Text>
93
+ </HStack>
94
+ );
95
+ }
96
+
97
+ // ─── Image hues ─────────────────────────────────────────────────────────────
98
+ // IMAGES[selected] is the hero; all six double as thumbnails so the 3-column
99
+ // grid closes into two full rows (a slice(1) subset left a ragged 3+2 row and
100
+ // a fallback entry that never rendered).
101
+ const IMAGES = [
102
+ 'var(--dracula-purple)',
103
+ 'var(--dracula-cyan)',
104
+ 'var(--dracula-pink)',
105
+ 'var(--dracula-yellow)',
106
+ 'var(--dracula-green)',
107
+ 'var(--dracula-orange)',
108
+ ];
109
+
110
+ // ─── Product Data ───────────────────────────────────────────────────────────
111
+ const PRODUCT = {
112
+ name: 'Midnight Ceremony Mug & Plate Set',
113
+ price: 89.0,
114
+ originalPrice: 119.0,
115
+ description:
116
+ 'A hand-thrown mug and plate set that brings quiet warmth to every midnight meal. The mug sits easy in the hand with a generous 12 oz capacity, while the 8-inch plate works for everything from toast to tapas. Each piece is kiln-fired at 2,300\u00B0F for a finish that resists chips and stains. Subtle variations in the reactive glaze pool like night mist, so no two sets are exactly alike. Dishwasher and microwave safe.',
117
+ composition:
118
+ 'High-fire stoneware clay, wheel-thrown and trimmed by hand. Reactive glaze applied by dipping \u2014 color pools and breaks naturally over the clay body. Lead-free and food-safe. Unglazed foot ring reveals the raw clay underneath. Each piece is bisque-fired, glazed, then fired again to cone 10 in a gas reduction kiln.',
119
+ deliveryReturns:
120
+ 'Free shipping on all ceramics orders over $75. Each piece is individually wrapped in recycled kraft paper and cushioned for transit. Returns accepted within 30 days \u2014 items must be unused and in original packaging. Replacement pieces available individually.',
121
+ dimensions:
122
+ 'Mug height: 9.5 cm / 3.75 in. Mug diameter: 8.5 cm / 3.35 in. Capacity: 350 ml / 12 oz. Plate diameter: 20 cm / 8 in. Plate height: 2 cm / 0.75 in. Weight: 680 g / 1.5 lb (set).',
123
+ };
124
+
125
+ const COLORS = [
126
+ {value: 'midnight', label: 'Midnight'},
127
+ {value: 'crypt', label: 'Crypt'},
128
+ {value: 'moonlight', label: 'Moonlight'},
129
+ ];
130
+
131
+ const FINISHES = [
132
+ {value: 'matte', label: 'Matte'},
133
+ {value: 'satin', label: 'Satin'},
134
+ {value: 'speckled', label: 'Speckled'},
135
+ ];
136
+
137
+ const fmt = (n: number) => `$${n.toFixed(2)}`;
138
+
139
+ // ─── Image Gallery ──────────────────────────────────────────────────────────
140
+ function ImageGallery({
141
+ selected,
142
+ onSelect,
143
+ }: {
144
+ selected: number;
145
+ onSelect: (i: number) => void;
146
+ }) {
147
+ const heroHue = IMAGES[selected];
148
+ const thumbnails = IMAGES;
149
+
150
+ return (
151
+ <VStack gap={3}>
152
+ <AspectRatio ratio={4 / 5} style={heroImage}>
153
+ <ProductScene hue={heroHue} label={PRODUCT.name} />
154
+ </AspectRatio>
155
+ <Grid columns={3} gap={2}>
156
+ {thumbnails.map((hue, i) => (
157
+ <AspectRatio key={i} ratio={1}>
158
+ <SelectableCard
159
+ label={`Product image ${i + 1}`}
160
+ isSelected={selected === i}
161
+ onChange={() => onSelect(i)}
162
+ variant="transparent"
163
+ padding={0}
164
+ width="100%"
165
+ height="100%">
166
+ <ProductScene hue={hue} label={`Product image ${i + 1}`} />
167
+ </SelectableCard>
168
+ </AspectRatio>
169
+ ))}
170
+ </Grid>
171
+ </VStack>
172
+ );
173
+ }
174
+
175
+ // ─── Product Info ───────────────────────────────────────────────────────────
176
+ function ProductInfo() {
177
+ const [color, setColor] = useState('midnight');
178
+ const [finish, setFinish] = useState('matte');
179
+ const [quantity, setQuantity] = useState<number | null>(1);
180
+ const [notice, setNotice] = useState<{
181
+ status: 'success' | 'info';
182
+ title: string;
183
+ } | null>(null);
184
+
185
+ const decrement = () => setQuantity(q => Math.max(1, (q ?? 1) - 1));
186
+ const increment = () => setQuantity(q => Math.min(10, (q ?? 1) + 1));
187
+
188
+ return (
189
+ <VStack gap={5}>
190
+ <VStack gap={2}>
191
+ <Heading level={1} type="display-2">
192
+ {PRODUCT.name}
193
+ </Heading>
194
+ <StarRating rating={4.3} count={128} />
195
+ <HStack gap={2} vAlign="center">
196
+ <Text type="large" weight="bold" hasTabularNumbers>
197
+ {fmt(PRODUCT.price)}
198
+ </Text>
199
+ <Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
200
+ {fmt(PRODUCT.originalPrice)}
201
+ </Text>
202
+ <Badge variant="error" label="Sale" />
203
+ </HStack>
204
+ </VStack>
205
+ <Text type="large" weight="normal">
206
+ {PRODUCT.description}
207
+ </Text>
208
+ <VStack gap={2}>
209
+ <Text type="label">Glaze</Text>
210
+ <VStack hAlign="start">
211
+ <SegmentedControl value={color} onChange={setColor} label="Glaze">
212
+ {COLORS.map(c => (
213
+ <SegmentedControlItem
214
+ key={c.value}
215
+ value={c.value}
216
+ label={c.label}
217
+ />
218
+ ))}
219
+ </SegmentedControl>
220
+ </VStack>
221
+ </VStack>
222
+ <VStack gap={2}>
223
+ <Text type="label">Finish</Text>
224
+ <VStack hAlign="start">
225
+ <SegmentedControl value={finish} onChange={setFinish} label="Finish">
226
+ {FINISHES.map(f => (
227
+ <SegmentedControlItem
228
+ key={f.value}
229
+ value={f.value}
230
+ label={f.label}
231
+ />
232
+ ))}
233
+ </SegmentedControl>
234
+ </VStack>
235
+ </VStack>
236
+ <VStack gap={2}>
237
+ <Text type="label">Quantity</Text>
238
+ <HStack gap={1} vAlign="center">
239
+ <Button
240
+ label="Decrease quantity"
241
+ variant="ghost"
242
+ icon={<Icon icon={Minus} size="sm" />}
243
+ clickAction={decrement}
244
+ isDisabled={(quantity ?? 1) <= 1}
245
+ isIconOnly
246
+ tooltip="Decrease quantity"
247
+ />
248
+ <Center width={100}>
249
+ <NumberInput
250
+ label="Quantity"
251
+ isLabelHidden
252
+ value={quantity}
253
+ onChange={setQuantity}
254
+ min={1}
255
+ max={10}
256
+ isIntegerOnly
257
+ />
258
+ </Center>
259
+ <Button
260
+ label="Increase quantity"
261
+ variant="ghost"
262
+ icon={<Icon icon={Plus} size="sm" />}
263
+ clickAction={increment}
264
+ isDisabled={(quantity ?? 1) >= 10}
265
+ isIconOnly
266
+ tooltip="Increase quantity"
267
+ />
268
+ </HStack>
269
+ </VStack>
270
+ <VStack gap={2}>
271
+ {notice && (
272
+ <Banner
273
+ status={notice.status}
274
+ title={notice.title}
275
+ container="card"
276
+ isDismissable
277
+ onDismiss={() => setNotice(null)}
278
+ />
279
+ )}
280
+ <Button
281
+ label="Add to Cart"
282
+ variant="primary"
283
+ size="lg"
284
+ clickAction={() =>
285
+ setNotice({
286
+ status: 'success',
287
+ title: `Added ${quantity ?? 1} × ${PRODUCT.name} to your cart.`,
288
+ })
289
+ }
290
+ />
291
+ <Button
292
+ label="Buy it now"
293
+ size="lg"
294
+ clickAction={() =>
295
+ setNotice({
296
+ status: 'info',
297
+ title: 'Checkout is disabled in this preview.',
298
+ })
299
+ }
300
+ />
301
+ </VStack>
302
+ <CollapsibleGroup type="multiple" defaultValue={['composition']}>
303
+ <Divider />
304
+ <Collapsible
305
+ value="composition"
306
+ trigger={
307
+ <Heading level={3} accessibilityLevel={2}>
308
+ Composition
309
+ </Heading>
310
+ }>
311
+ <Text type="body">{PRODUCT.composition}</Text>
312
+ </Collapsible>
313
+ <Divider />
314
+ <Collapsible
315
+ value="delivery"
316
+ defaultIsOpen={false}
317
+ trigger={
318
+ <Heading level={3} accessibilityLevel={2}>
319
+ Delivery &amp; Returns
320
+ </Heading>
321
+ }>
322
+ <Text type="body">{PRODUCT.deliveryReturns}</Text>
323
+ </Collapsible>
324
+ <Divider />
325
+ <Collapsible
326
+ value="dimensions"
327
+ defaultIsOpen={false}
328
+ trigger={
329
+ <Heading level={3} accessibilityLevel={2}>
330
+ Dimensions
331
+ </Heading>
332
+ }>
333
+ <Text type="body" hasTabularNumbers>
334
+ {PRODUCT.dimensions}
335
+ </Text>
336
+ </Collapsible>
337
+ <Divider />
338
+ </CollapsibleGroup>
339
+ </VStack>
340
+ );
341
+ }
342
+
343
+ // ─── Main Page ──────────────────────────────────────────────────────────────
344
+ export default function ProductDetail() {
345
+ const [selectedThumb, setSelectedThumb] = useState(0);
346
+
347
+ return (
348
+ <Layout
349
+ height="fill"
350
+ contentWidth={1200}
351
+ content={
352
+ <LayoutContent padding={6}>
353
+ <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={5}>
354
+ <ImageGallery
355
+ selected={selectedThumb}
356
+ onSelect={setSelectedThumb}
357
+ />
358
+ <VStack gap={0} style={stickyInfo}>
359
+ <ProductInfo />
360
+ </VStack>
361
+ </Grid>
362
+ </LayoutContent>
363
+ }
364
+ />
365
+ );
366
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Product Gallery',
4
+ description: 'Card grid with images, prices, and descriptions',
5
+ category: 'commerce',
6
+ };
@@ -0,0 +1,173 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
4
+ import {Text, Heading} from '@astryxdesign/core/Text';
5
+ import {Button} from '@astryxdesign/core/Button';
6
+ import {Grid} from '@astryxdesign/core/Grid';
7
+ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
8
+ import {Card} from '@astryxdesign/core/Card';
9
+ import {Icon} from '@astryxdesign/core/Icon';
10
+ import {ArrowRight} from 'lucide-react';
11
+ import type {CSSProperties} from 'react';
12
+
13
+ // ─── Styles ─────────────────────────────────────────────────────────────────
14
+ // Product tiles are inline Dracula SVG — there is no Image primitive to fill
15
+ // the AspectRatio box with `object-fit` (#2582).
16
+
17
+ const svgImage: CSSProperties = {
18
+ width: '100%',
19
+ height: '100%',
20
+ display: 'block',
21
+ };
22
+
23
+ // ─── Product Data ───────────────────────────────────────────────────────────
24
+
25
+ interface Product {
26
+ id: number;
27
+ name: string;
28
+ description: string;
29
+ price: number;
30
+ hue: string;
31
+ }
32
+
33
+ // One accent per product from the fixed badge vocabulary.
34
+ const PRODUCTS: Product[] = [
35
+ {
36
+ id: 1,
37
+ name: 'Nightfall Stoneware Mug',
38
+ description:
39
+ 'A hand-thrown mug that sits easy in the hand through the longest night watch.',
40
+ price: 75.0,
41
+ hue: 'var(--dracula-purple)',
42
+ },
43
+ {
44
+ id: 2,
45
+ name: 'Coven Gathering Candle',
46
+ description:
47
+ 'Smoked amber and clove poured for slow evenings around the table.',
48
+ price: 80.0,
49
+ hue: 'var(--dracula-orange)',
50
+ },
51
+ {
52
+ id: 3,
53
+ name: 'Belfry Wool Throw',
54
+ description:
55
+ 'A heavy weave that keeps the chill of the tower off your shoulders.',
56
+ price: 75.0,
57
+ hue: 'var(--dracula-cyan)',
58
+ },
59
+ {
60
+ id: 4,
61
+ name: 'Crypt Archive Journal',
62
+ description:
63
+ 'Stitched pages that take ink well for field notes and midnight lists.',
64
+ price: 75.0,
65
+ hue: 'var(--dracula-yellow)',
66
+ },
67
+ {
68
+ id: 5,
69
+ name: 'Moonrise Enamel Pin',
70
+ description:
71
+ 'A small harvest moon for the lapel of your favourite coat.',
72
+ price: 60.0,
73
+ hue: 'var(--dracula-pink)',
74
+ },
75
+ {
76
+ id: 6,
77
+ name: 'Transylvanian Tea Blend',
78
+ description:
79
+ 'Black tea with midnight berries, packed for the cold months.',
80
+ price: 80.0,
81
+ hue: 'var(--dracula-green)',
82
+ },
83
+ ];
84
+
85
+ const fmt = (n: number) => `$${n.toFixed(2)}`;
86
+
87
+ // ─── Product Card ───────────────────────────────────────────────────────────
88
+
89
+ function ProductCard({product}: {product: Product}) {
90
+ return (
91
+ <VStack gap={3}>
92
+ <Card padding={0}>
93
+ <AspectRatio ratio={1}>
94
+ <svg
95
+ viewBox="0 0 400 300"
96
+ preserveAspectRatio="xMidYMid slice"
97
+ style={svgImage}
98
+ role="img"
99
+ aria-label={`${product.name} thumbnail`}>
100
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
101
+ <g
102
+ transform="translate(200 150)"
103
+ fill="none"
104
+ stroke="var(--dracula-comment)"
105
+ strokeWidth="5"
106
+ strokeLinecap="round"
107
+ strokeLinejoin="round">
108
+ <rect x="-44" y="-44" width="88" height="88" rx="5" />
109
+ <circle cx="18" cy="-18" r="2.5" fill={product.hue} stroke="none" />
110
+ <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
111
+ </g>
112
+ </svg>
113
+ </AspectRatio>
114
+ </Card>
115
+ <VStack gap={1}>
116
+ <Heading level={3}>{product.name}</Heading>
117
+ <Text type="body" color="secondary" maxLines={2}>
118
+ {product.description}
119
+ </Text>
120
+ <Text type="large" weight="bold" hasTabularNumbers>
121
+ {fmt(product.price)}
122
+ </Text>
123
+ </VStack>
124
+ </VStack>
125
+ );
126
+ }
127
+
128
+ // ─── Main Page ──────────────────────────────────────────────────────────────
129
+
130
+ export default function ProductGallery() {
131
+ return (
132
+ <Layout
133
+ height="fill"
134
+ contentWidth={1200}
135
+ content={
136
+ <LayoutContent padding={6}>
137
+ <VStack gap={6}>
138
+ {/* Header — Grid handles responsive stacking */}
139
+ <Grid columns={{minWidth: 280}} gap={4} align="start">
140
+ <Heading level={1}>
141
+ Small comforts for the midnight hours.
142
+ </Heading>
143
+ <VStack gap={3} hAlign="start">
144
+ <Text type="body">
145
+ Provisions from the castle workshops: kiln-fired, hand-poured,
146
+ and stitched by the coven. Each piece is made in small batches
147
+ and restocked with every full moon.
148
+ </Text>
149
+ <Button
150
+ label="Browse the collection"
151
+ variant="primary"
152
+ endContent={<Icon icon={ArrowRight} color="inherit" />}
153
+ clickAction={() =>
154
+ document
155
+ .getElementById('products')
156
+ ?.scrollIntoView({behavior: 'smooth'})
157
+ }
158
+ />
159
+ </VStack>
160
+ </Grid>
161
+
162
+ {/* Product Grid — reflows 3 → 2 → 1 columns as width narrows */}
163
+ <Grid columns={{minWidth: 300}} gap={6} id="products">
164
+ {PRODUCTS.map(product => (
165
+ <ProductCard key={product.id} product={product} />
166
+ ))}
167
+ </Grid>
168
+ </VStack>
169
+ </LayoutContent>
170
+ }
171
+ />
172
+ );
173
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Settings Dialog',
4
+ description: 'Account settings dialog with sidebar sections',
5
+ category: 'forms',
6
+ };