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,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 & 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,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
|
+
}
|