astryx-dracula 0.2.1 → 0.3.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 +30 -18
- package/README.md +17 -12
- package/USAGE.md +29 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +174 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +94 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +66 -33
- package/templates/ai-chat.tsx +36 -19
- package/templates/blank.tsx +4 -3
- package/templates/centered-hero.tsx +14 -125
- package/templates/classic-gallery.tsx +20 -66
- package/templates/contact-form.tsx +59 -39
- package/templates/dashboard-portfolio.tsx +143 -226
- package/templates/dashboard.tsx +122 -122
- package/templates/detail-page.tsx +95 -91
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +83 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +29 -19
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +52 -74
- package/templates/editor.tsx +112 -84
- package/templates/file-explorer.tsx +131 -84
- package/templates/form-two-column.tsx +61 -125
- package/templates/gallery-hero.tsx +15 -171
- package/templates/ide.tsx +22 -20
- package/templates/incident-console.tsx +104 -70
- package/templates/kanban-board.tsx +99 -42
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +37 -42
- package/templates/login-card.tsx +58 -87
- package/templates/login-split.tsx +88 -126
- package/templates/login-sso.tsx +105 -64
- package/templates/login.tsx +107 -53
- package/templates/messaging-shell.tsx +171 -144
- package/templates/mixed-gallery.tsx +29 -130
- package/templates/payment-form.tsx +368 -290
- package/templates/product-detail.tsx +39 -58
- package/templates/product-gallery.tsx +30 -41
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +135 -282
- package/templates/settings-sidebar.tsx +178 -268
- package/templates/settings.tsx +41 -14
- package/templates/shell-nav.tsx +31 -24
- package/templates/shell-side-nav.tsx +29 -20
- package/templates/shell-top-nav.tsx +80 -44
- package/templates/side-gallery.tsx +38 -94
- package/templates/table-grouped.tsx +62 -34
- package/templates/table-page-chart.tsx +23 -302
- package/templates/table-page-heatmap-status.tsx +156 -92
- package/templates/table-page-shoe-store-heatmap.tsx +18 -293
- package/templates/table-page.tsx +23 -7
- package/templates/table.tsx +23 -8
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +299 -237
- package/theme.css +76 -14
- package/tokens.css +425 -30
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC[p=6] > G[c={min:
|
|
3
|
+
// L > LC[p=6] > G[c={min:280} g=8] > (V[g=3] > AR[ratio=4/5] + (G[c=3 g=2] > (AR[ratio=1] > SelectableCard)*6)) + (V[g=8] > (V[g=3] > Hd"Midnight Ceremony Mug & Plate Set"[level=1 t=display-2] + (H[g=1] > Ic*5 + Tx"4.3 (128)") + (H[g=2] > Tx"$89.00"[t=large] + Tx"$119.00"[t=body] + Tk"Sale")) + Tx"A hand-thrown mug and plate set"[t=large] + (V[g=3] > Tx"Glaze"[t=label] + SG) + (V[g=3] > Tx"Finish"[t=label] + SG) + (V[g=3] > Tx"Quantity"[t=label] + (H[g=1] > B.ghost"-" + TI"1" + B.ghost"+")) + (V[g=3] > B.primary"Add to cart" + B.secondary"Buy it now") + (ColG > Col"Composition" + Col"Delivery & Returns" + Col"Dimensions"))
|
|
4
4
|
|
|
5
5
|
import {useState} from 'react';
|
|
6
6
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
@@ -14,16 +14,18 @@ import {
|
|
|
14
14
|
SegmentedControl,
|
|
15
15
|
SegmentedControlItem,
|
|
16
16
|
} from '@astryxdesign/core/SegmentedControl';
|
|
17
|
-
import {
|
|
17
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
18
18
|
import {Banner} from '@astryxdesign/core/Banner';
|
|
19
19
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
20
20
|
import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
|
|
21
21
|
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
22
22
|
import {SelectableCard} from '@astryxdesign/core/SelectableCard';
|
|
23
23
|
import type {CSSProperties} from 'react';
|
|
24
|
+
import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
|
|
25
|
+
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
24
26
|
|
|
25
27
|
// Custom CSS here is limited to what Astryx components can't express today:
|
|
26
|
-
// - image
|
|
28
|
+
// - image radius-clip (no Image primitive — #2582, see shared/gallery-image)
|
|
27
29
|
// - the sticky info column (no sticky prop on Astryx layout primitives — #2613)
|
|
28
30
|
// Keeps the info column in view while the gallery scrolls. No sticky prop on
|
|
29
31
|
// Astryx layout primitives.
|
|
@@ -32,49 +34,16 @@ const stickyInfo: CSSProperties = {
|
|
|
32
34
|
top: 'var(--spacing-8)',
|
|
33
35
|
alignSelf: 'start',
|
|
34
36
|
};
|
|
35
|
-
// Product scenes are inline Dracula SVG — there is no Image primitive in
|
|
36
|
-
// Astryx (#2582). Fills the AspectRatio box + rounds hero corners. No
|
|
37
|
-
// objectFit/radius props on AspectRatio.
|
|
38
|
-
const heroImage: CSSProperties = {
|
|
39
|
-
borderRadius: 'var(--radius-container)',
|
|
40
|
-
overflow: 'clip',
|
|
41
|
-
};
|
|
42
|
-
// Fills the thumbnail card. Corner radius + selection ring come from
|
|
43
|
-
// SelectableCard; the scene only needs to fill the box (#2582).
|
|
44
|
-
const thumbImage: CSSProperties = {
|
|
45
|
-
width: '100%',
|
|
46
|
-
height: '100%',
|
|
47
|
-
display: 'block',
|
|
48
|
-
};
|
|
49
37
|
|
|
50
38
|
// One Dracula placeholder scene per view, drawn from the fixed badge
|
|
51
|
-
// vocabulary. Resolves to theme tokens so the
|
|
52
|
-
// dark-only theme.
|
|
53
|
-
function ProductScene({hue, label}: {hue:
|
|
54
|
-
return
|
|
55
|
-
<svg
|
|
56
|
-
viewBox="0 0 400 300"
|
|
57
|
-
preserveAspectRatio="xMidYMid slice"
|
|
58
|
-
style={thumbImage}
|
|
59
|
-
role="img"
|
|
60
|
-
aria-label={label}>
|
|
61
|
-
<rect width="400" height="300" fill="var(--dracula-bg-light)" />
|
|
62
|
-
<g
|
|
63
|
-
transform="translate(200 150)"
|
|
64
|
-
fill="none"
|
|
65
|
-
stroke="var(--dracula-comment)"
|
|
66
|
-
strokeWidth="5"
|
|
67
|
-
strokeLinecap="round"
|
|
68
|
-
strokeLinejoin="round">
|
|
69
|
-
<rect x="-44" y="-44" width="88" height="88" rx="5" />
|
|
70
|
-
<circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
|
|
71
|
-
<path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
|
|
72
|
-
</g>
|
|
73
|
-
</svg>
|
|
74
|
-
);
|
|
39
|
+
// vocabulary (shared/scene-tile large fork). Resolves to theme tokens so the
|
|
40
|
+
// gallery stays on-brand in the dark-only theme.
|
|
41
|
+
function ProductScene({hue, label}: {hue: SceneHue; label: string}) {
|
|
42
|
+
return <SceneTile label={label} hue={hue} size="lg" />;
|
|
75
43
|
}
|
|
76
44
|
|
|
77
45
|
import {Minus, Plus, Star} from 'lucide-react';
|
|
46
|
+
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
78
47
|
|
|
79
48
|
// ─── Star Rating ─────────────────────────────────────────────────────────────
|
|
80
49
|
function StarRating({rating, count}: {rating: number; count: number}) {
|
|
@@ -84,10 +53,15 @@ function StarRating({rating, count}: {rating: number; count: number}) {
|
|
|
84
53
|
return (
|
|
85
54
|
<HStack gap={1} vAlign="center">
|
|
86
55
|
{Array.from({length: filled}, (_, i) => (
|
|
87
|
-
<Icon key={`full-${i}`} icon={Star} size="sm" color="
|
|
56
|
+
<Icon key={`full-${i}`} icon={Star} size="sm" color="yellow" />
|
|
88
57
|
))}
|
|
58
|
+
{/* secondary, not disabled: --color-icon-disabled #6272A4 is 2.51:1 on
|
|
59
|
+
the card #343746, under the 3:1 non-text floor, and reads as "this
|
|
60
|
+
control is unavailable" — but a star is a data mark, not a control.
|
|
61
|
+
--color-icon-secondary #9AA1BC is 4.60:1, one step under the filled
|
|
62
|
+
yellow (7.4:1), so the two halves still read as one rating. */}
|
|
89
63
|
{Array.from({length: empty}, (_, i) => (
|
|
90
|
-
<Icon key={`empty-${i}`} icon={Star} size="sm" color="
|
|
64
|
+
<Icon key={`empty-${i}`} icon={Star} size="sm" color="secondary" />
|
|
91
65
|
))}
|
|
92
66
|
<Text type="body" color="secondary" hasTabularNumbers>
|
|
93
67
|
{rating} ({count})
|
|
@@ -100,13 +74,19 @@ function StarRating({rating, count}: {rating: number; count: number}) {
|
|
|
100
74
|
// IMAGES[selected] is the hero; all six double as thumbnails so the 3-column
|
|
101
75
|
// grid closes into two full rows (a slice(1) subset left a ragged 3+2 row and
|
|
102
76
|
// a fallback entry that never rendered).
|
|
103
|
-
const IMAGES = [
|
|
104
|
-
'var(--dracula-purple)',
|
|
77
|
+
const IMAGES: readonly SceneHue[] = [
|
|
105
78
|
'var(--dracula-cyan)',
|
|
106
79
|
'var(--dracula-pink)',
|
|
107
80
|
'var(--dracula-yellow)',
|
|
108
81
|
'var(--dracula-green)',
|
|
109
82
|
'var(--dracula-orange)',
|
|
83
|
+
// The sixth is `muted` (Comment): a legal / neutral / out-of-stock
|
|
84
|
+
// register, which is exactly what a product photo set needs an extra slot
|
|
85
|
+
// for. Cyan, pink, yellow, green and orange are all saturated identity
|
|
86
|
+
// colours; a sixth saturated one would read as a sixth product variant
|
|
87
|
+
// rather than as "this one is the plain shot". Keeps the 3-column grid
|
|
88
|
+
// closing on two full rows, which is the reason the set exists.
|
|
89
|
+
'var(--dracula-comment)',
|
|
110
90
|
];
|
|
111
91
|
|
|
112
92
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
@@ -146,17 +126,17 @@ function ImageGallery({
|
|
|
146
126
|
selected: number;
|
|
147
127
|
onSelect: (i: number) => void;
|
|
148
128
|
}) {
|
|
149
|
-
const heroHue = IMAGES[selected];
|
|
129
|
+
const heroHue: SceneHue = IMAGES[selected];
|
|
150
130
|
const thumbnails = IMAGES;
|
|
151
131
|
|
|
152
132
|
return (
|
|
153
133
|
<VStack gap={3}>
|
|
154
|
-
<AspectRatio ratio={4 / 5} style={
|
|
134
|
+
<AspectRatio ratio={4 / 5} style={galleryImageClip}>
|
|
155
135
|
<ProductScene hue={heroHue} label={PRODUCT.name} />
|
|
156
136
|
</AspectRatio>
|
|
157
137
|
<Grid columns={3} gap={2}>
|
|
158
138
|
{thumbnails.map((hue, i) => (
|
|
159
|
-
<AspectRatio key={
|
|
139
|
+
<AspectRatio key={hue} ratio={1}>
|
|
160
140
|
<SelectableCard
|
|
161
141
|
label={`Product image ${i + 1}`}
|
|
162
142
|
isSelected={selected === i}
|
|
@@ -188,26 +168,26 @@ function ProductInfo() {
|
|
|
188
168
|
const increment = () => setQuantity(q => Math.min(10, (q ?? 1) + 1));
|
|
189
169
|
|
|
190
170
|
return (
|
|
191
|
-
<VStack gap={
|
|
192
|
-
<VStack gap={
|
|
171
|
+
<VStack gap={8}>
|
|
172
|
+
<VStack gap={3}>
|
|
193
173
|
<Heading level={1} type="display-2">
|
|
194
174
|
{PRODUCT.name}
|
|
195
175
|
</Heading>
|
|
196
176
|
<StarRating rating={4.3} count={128} />
|
|
197
177
|
<HStack gap={2} vAlign="center">
|
|
198
|
-
<Text type="large"
|
|
178
|
+
<Text type="large" hasTabularNumbers>
|
|
199
179
|
{fmt(PRODUCT.price)}
|
|
200
180
|
</Text>
|
|
201
181
|
<Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
|
|
202
182
|
{fmt(PRODUCT.originalPrice)}
|
|
203
183
|
</Text>
|
|
204
|
-
<
|
|
184
|
+
<Token label="Sale" color="yellow" />
|
|
205
185
|
</HStack>
|
|
206
186
|
</VStack>
|
|
207
187
|
<Text type="large" weight="normal">
|
|
208
188
|
{PRODUCT.description}
|
|
209
189
|
</Text>
|
|
210
|
-
<VStack gap={
|
|
190
|
+
<VStack gap={3}>
|
|
211
191
|
<Text type="label">Glaze</Text>
|
|
212
192
|
<VStack hAlign="start">
|
|
213
193
|
<SegmentedControl value={color} onChange={setColor} label="Glaze">
|
|
@@ -221,7 +201,7 @@ function ProductInfo() {
|
|
|
221
201
|
</SegmentedControl>
|
|
222
202
|
</VStack>
|
|
223
203
|
</VStack>
|
|
224
|
-
<VStack gap={
|
|
204
|
+
<VStack gap={3}>
|
|
225
205
|
<Text type="label">Finish</Text>
|
|
226
206
|
<VStack hAlign="start">
|
|
227
207
|
<SegmentedControl value={finish} onChange={setFinish} label="Finish">
|
|
@@ -235,7 +215,7 @@ function ProductInfo() {
|
|
|
235
215
|
</SegmentedControl>
|
|
236
216
|
</VStack>
|
|
237
217
|
</VStack>
|
|
238
|
-
<VStack gap={
|
|
218
|
+
<VStack gap={3}>
|
|
239
219
|
<Text type="label">Quantity</Text>
|
|
240
220
|
<HStack gap={1} vAlign="center">
|
|
241
221
|
<Button
|
|
@@ -269,7 +249,7 @@ function ProductInfo() {
|
|
|
269
249
|
/>
|
|
270
250
|
</HStack>
|
|
271
251
|
</VStack>
|
|
272
|
-
<VStack gap={
|
|
252
|
+
<VStack gap={3}>
|
|
273
253
|
{notice && (
|
|
274
254
|
<Banner
|
|
275
255
|
status={notice.status}
|
|
@@ -280,7 +260,7 @@ function ProductInfo() {
|
|
|
280
260
|
/>
|
|
281
261
|
)}
|
|
282
262
|
<Button
|
|
283
|
-
label="Add to
|
|
263
|
+
label="Add to cart"
|
|
284
264
|
variant="primary"
|
|
285
265
|
size="lg"
|
|
286
266
|
clickAction={() =>
|
|
@@ -292,6 +272,7 @@ function ProductInfo() {
|
|
|
292
272
|
/>
|
|
293
273
|
<Button
|
|
294
274
|
label="Buy it now"
|
|
275
|
+
variant="secondary"
|
|
295
276
|
size="lg"
|
|
296
277
|
clickAction={() =>
|
|
297
278
|
setNotice({
|
|
@@ -352,7 +333,7 @@ export default function ProductDetail() {
|
|
|
352
333
|
contentWidth={1200}
|
|
353
334
|
content={
|
|
354
335
|
<LayoutContent padding={6}>
|
|
355
|
-
<Grid columns={{minWidth:
|
|
336
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={8}>
|
|
356
337
|
<ImageGallery
|
|
357
338
|
selected={selectedThumb}
|
|
358
339
|
onSelect={setSelectedThumb}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC > V[g=
|
|
3
|
+
// L > LC[p=6] > V[g=8] > (G[c={min:280} g=4 a=start] > Hd"Small comforts for the midnight hours."[level=1 t=display-2] + (V[g=3 a=start] > Tx"Provisions from the castle workshops"[t=body] + B.primary"Browse the collection")) + (V[g=8] > (V[g=2] > Hd"The collection"[level=2] + Tx"Each piece is made in small batches"[t=body]) + (G[c={min:280} g=8] > (V[g=3] > (C[p=0] > AR[ratio=1]) + (V[g=1] > Hd"Nightfall Stoneware Mug"[level=3] + Tx"A hand-thrown mug that sits easy"[t=body color=secondary] + Tx.lg"$75.00"))*6))
|
|
4
4
|
|
|
5
5
|
import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
6
6
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
@@ -10,17 +10,12 @@ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
|
10
10
|
import {Card} from '@astryxdesign/core/Card';
|
|
11
11
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
12
12
|
import {ArrowRight} from 'lucide-react';
|
|
13
|
-
import
|
|
13
|
+
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
14
|
+
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
14
15
|
|
|
15
16
|
// ─── Styles ─────────────────────────────────────────────────────────────────
|
|
16
|
-
// Product
|
|
17
|
-
// the AspectRatio box with `object-fit` (#2582).
|
|
18
|
-
|
|
19
|
-
const svgImage: CSSProperties = {
|
|
20
|
-
width: '100%',
|
|
21
|
-
height: '100%',
|
|
22
|
-
display: 'block',
|
|
23
|
-
};
|
|
17
|
+
// Product tile art lives in shared/scene-tile (large fork); there is no Image
|
|
18
|
+
// primitive to fill the AspectRatio box with `object-fit` (#2582).
|
|
24
19
|
|
|
25
20
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
26
21
|
|
|
@@ -29,7 +24,7 @@ interface Product {
|
|
|
29
24
|
name: string;
|
|
30
25
|
description: string;
|
|
31
26
|
price: number;
|
|
32
|
-
hue:
|
|
27
|
+
hue: SceneHue;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
// One accent per product from the fixed badge vocabulary.
|
|
@@ -40,7 +35,7 @@ const PRODUCTS: Product[] = [
|
|
|
40
35
|
description:
|
|
41
36
|
'A hand-thrown mug that sits easy in the hand through the longest night watch.',
|
|
42
37
|
price: 75.0,
|
|
43
|
-
hue: 'var(--dracula-
|
|
38
|
+
hue: 'var(--dracula-orange)',
|
|
44
39
|
},
|
|
45
40
|
{
|
|
46
41
|
id: 2,
|
|
@@ -93,25 +88,11 @@ function ProductCard({product}: {product: Product}) {
|
|
|
93
88
|
<VStack gap={3}>
|
|
94
89
|
<Card padding={0}>
|
|
95
90
|
<AspectRatio ratio={1}>
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
aria-label={`${product.name} thumbnail`}>
|
|
102
|
-
<rect width="400" height="300" fill="var(--dracula-bg-light)" />
|
|
103
|
-
<g
|
|
104
|
-
transform="translate(200 150)"
|
|
105
|
-
fill="none"
|
|
106
|
-
stroke="var(--dracula-comment)"
|
|
107
|
-
strokeWidth="5"
|
|
108
|
-
strokeLinecap="round"
|
|
109
|
-
strokeLinejoin="round">
|
|
110
|
-
<rect x="-44" y="-44" width="88" height="88" rx="5" />
|
|
111
|
-
<circle cx="18" cy="-18" r="2.5" fill={product.hue} stroke="none" />
|
|
112
|
-
<path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
|
|
113
|
-
</g>
|
|
114
|
-
</svg>
|
|
91
|
+
<SceneTile
|
|
92
|
+
label={`${product.name} thumbnail`}
|
|
93
|
+
hue={product.hue}
|
|
94
|
+
size="lg"
|
|
95
|
+
/>
|
|
115
96
|
</AspectRatio>
|
|
116
97
|
</Card>
|
|
117
98
|
<VStack gap={1}>
|
|
@@ -119,7 +100,7 @@ function ProductCard({product}: {product: Product}) {
|
|
|
119
100
|
<Text type="body" color="secondary" maxLines={2}>
|
|
120
101
|
{product.description}
|
|
121
102
|
</Text>
|
|
122
|
-
<Text type="large"
|
|
103
|
+
<Text type="large" hasTabularNumbers>
|
|
123
104
|
{fmt(product.price)}
|
|
124
105
|
</Text>
|
|
125
106
|
</VStack>
|
|
@@ -136,17 +117,16 @@ export default function ProductGallery() {
|
|
|
136
117
|
contentWidth={1200}
|
|
137
118
|
content={
|
|
138
119
|
<LayoutContent padding={6}>
|
|
139
|
-
<VStack gap={
|
|
120
|
+
<VStack gap={8}>
|
|
140
121
|
{/* Header — Grid handles responsive stacking */}
|
|
141
122
|
<Grid columns={{minWidth: 280}} gap={4} align="start">
|
|
142
|
-
<Heading level={1}>
|
|
123
|
+
<Heading level={1} type="display-2">
|
|
143
124
|
Small comforts for the midnight hours.
|
|
144
125
|
</Heading>
|
|
145
126
|
<VStack gap={3} hAlign="start">
|
|
146
127
|
<Text type="body">
|
|
147
128
|
Provisions from the castle workshops: kiln-fired, hand-poured,
|
|
148
|
-
and stitched by the coven.
|
|
149
|
-
and restocked with every full moon.
|
|
129
|
+
and stitched by the coven.
|
|
150
130
|
</Text>
|
|
151
131
|
<Button
|
|
152
132
|
label="Browse the collection"
|
|
@@ -162,11 +142,20 @@ export default function ProductGallery() {
|
|
|
162
142
|
</Grid>
|
|
163
143
|
|
|
164
144
|
{/* Product Grid — reflows 3 → 2 → 1 columns as width narrows */}
|
|
165
|
-
<
|
|
166
|
-
{
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
145
|
+
<VStack gap={8}>
|
|
146
|
+
<VStack gap={2}>
|
|
147
|
+
<Heading level={2}>The collection</Heading>
|
|
148
|
+
<Text type="body" color="secondary">
|
|
149
|
+
Each piece is made in small batches and restocked with every
|
|
150
|
+
full moon.
|
|
151
|
+
</Text>
|
|
152
|
+
</VStack>
|
|
153
|
+
<Grid columns={{minWidth: 280}} gap={4} id="products">
|
|
154
|
+
{PRODUCTS.map(product => (
|
|
155
|
+
<ProductCard key={product.id} product={product} />
|
|
156
|
+
))}
|
|
157
|
+
</Grid>
|
|
158
|
+
</VStack>
|
|
170
159
|
</VStack>
|
|
171
160
|
</LayoutContent>
|
|
172
161
|
}
|