astryx-dracula 0.2.0 → 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 +22 -11
- 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 +67 -32
- package/templates/ai-chat.tsx +41 -20
- package/templates/blank.tsx +5 -2
- package/templates/centered-hero.tsx +15 -124
- package/templates/classic-gallery.tsx +21 -65
- package/templates/contact-form.tsx +62 -40
- package/templates/dashboard-portfolio.tsx +146 -227
- package/templates/dashboard.tsx +123 -121
- package/templates/detail-page.tsx +101 -95
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +85 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +33 -21
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +54 -74
- package/templates/editor.tsx +119 -89
- package/templates/file-explorer.tsx +133 -84
- package/templates/form-two-column.tsx +62 -124
- package/templates/gallery-hero.tsx +17 -171
- package/templates/ide.tsx +27 -21
- package/templates/incident-console.tsx +105 -69
- package/templates/kanban-board.tsx +100 -41
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +38 -41
- package/templates/login-card.tsx +59 -86
- package/templates/login-split.tsx +88 -128
- package/templates/login-sso.tsx +107 -68
- package/templates/login.tsx +108 -52
- package/templates/messaging-shell.tsx +172 -143
- package/templates/mixed-gallery.tsx +31 -130
- package/templates/payment-form.tsx +369 -289
- package/templates/product-detail.tsx +40 -57
- package/templates/product-gallery.tsx +31 -40
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +145 -286
- package/templates/settings-sidebar.tsx +179 -267
- package/templates/settings.tsx +42 -13
- package/templates/shell-nav.tsx +32 -23
- package/templates/shell-side-nav.tsx +30 -19
- package/templates/shell-top-nav.tsx +81 -43
- package/templates/side-gallery.tsx +39 -93
- package/templates/table-grouped.tsx +63 -33
- package/templates/table-page-chart.tsx +24 -297
- package/templates/table-page-heatmap-status.tsx +157 -91
- package/templates/table-page-shoe-store-heatmap.tsx +19 -288
- package/templates/table-page.tsx +24 -6
- package/templates/table.tsx +24 -7
- 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 +313 -249
- package/theme.css +76 -14
- package/tokens.css +425 -30
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
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"))
|
|
2
4
|
|
|
3
5
|
import {useState} from 'react';
|
|
4
6
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
@@ -12,16 +14,18 @@ import {
|
|
|
12
14
|
SegmentedControl,
|
|
13
15
|
SegmentedControlItem,
|
|
14
16
|
} from '@astryxdesign/core/SegmentedControl';
|
|
15
|
-
import {
|
|
17
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
16
18
|
import {Banner} from '@astryxdesign/core/Banner';
|
|
17
19
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
18
20
|
import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
|
|
19
21
|
import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
20
22
|
import {SelectableCard} from '@astryxdesign/core/SelectableCard';
|
|
21
23
|
import type {CSSProperties} from 'react';
|
|
24
|
+
import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
|
|
25
|
+
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
22
26
|
|
|
23
27
|
// Custom CSS here is limited to what Astryx components can't express today:
|
|
24
|
-
// - image
|
|
28
|
+
// - image radius-clip (no Image primitive — #2582, see shared/gallery-image)
|
|
25
29
|
// - the sticky info column (no sticky prop on Astryx layout primitives — #2613)
|
|
26
30
|
// Keeps the info column in view while the gallery scrolls. No sticky prop on
|
|
27
31
|
// Astryx layout primitives.
|
|
@@ -30,49 +34,16 @@ const stickyInfo: CSSProperties = {
|
|
|
30
34
|
top: 'var(--spacing-8)',
|
|
31
35
|
alignSelf: 'start',
|
|
32
36
|
};
|
|
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
37
|
|
|
48
38
|
// One Dracula placeholder scene per view, drawn from the fixed badge
|
|
49
|
-
// vocabulary. Resolves to theme tokens so the
|
|
50
|
-
// dark-only theme.
|
|
51
|
-
function ProductScene({hue, label}: {hue:
|
|
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
|
-
);
|
|
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" />;
|
|
73
43
|
}
|
|
74
44
|
|
|
75
45
|
import {Minus, Plus, Star} from 'lucide-react';
|
|
46
|
+
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
76
47
|
|
|
77
48
|
// ─── Star Rating ─────────────────────────────────────────────────────────────
|
|
78
49
|
function StarRating({rating, count}: {rating: number; count: number}) {
|
|
@@ -82,10 +53,15 @@ function StarRating({rating, count}: {rating: number; count: number}) {
|
|
|
82
53
|
return (
|
|
83
54
|
<HStack gap={1} vAlign="center">
|
|
84
55
|
{Array.from({length: filled}, (_, i) => (
|
|
85
|
-
<Icon key={`full-${i}`} icon={Star} size="sm" color="
|
|
56
|
+
<Icon key={`full-${i}`} icon={Star} size="sm" color="yellow" />
|
|
86
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. */}
|
|
87
63
|
{Array.from({length: empty}, (_, i) => (
|
|
88
|
-
<Icon key={`empty-${i}`} icon={Star} size="sm" />
|
|
64
|
+
<Icon key={`empty-${i}`} icon={Star} size="sm" color="secondary" />
|
|
89
65
|
))}
|
|
90
66
|
<Text type="body" color="secondary" hasTabularNumbers>
|
|
91
67
|
{rating} ({count})
|
|
@@ -98,13 +74,19 @@ function StarRating({rating, count}: {rating: number; count: number}) {
|
|
|
98
74
|
// IMAGES[selected] is the hero; all six double as thumbnails so the 3-column
|
|
99
75
|
// grid closes into two full rows (a slice(1) subset left a ragged 3+2 row and
|
|
100
76
|
// a fallback entry that never rendered).
|
|
101
|
-
const IMAGES = [
|
|
102
|
-
'var(--dracula-purple)',
|
|
77
|
+
const IMAGES: readonly SceneHue[] = [
|
|
103
78
|
'var(--dracula-cyan)',
|
|
104
79
|
'var(--dracula-pink)',
|
|
105
80
|
'var(--dracula-yellow)',
|
|
106
81
|
'var(--dracula-green)',
|
|
107
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)',
|
|
108
90
|
];
|
|
109
91
|
|
|
110
92
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
@@ -144,17 +126,17 @@ function ImageGallery({
|
|
|
144
126
|
selected: number;
|
|
145
127
|
onSelect: (i: number) => void;
|
|
146
128
|
}) {
|
|
147
|
-
const heroHue = IMAGES[selected];
|
|
129
|
+
const heroHue: SceneHue = IMAGES[selected];
|
|
148
130
|
const thumbnails = IMAGES;
|
|
149
131
|
|
|
150
132
|
return (
|
|
151
133
|
<VStack gap={3}>
|
|
152
|
-
<AspectRatio ratio={4 / 5} style={
|
|
134
|
+
<AspectRatio ratio={4 / 5} style={galleryImageClip}>
|
|
153
135
|
<ProductScene hue={heroHue} label={PRODUCT.name} />
|
|
154
136
|
</AspectRatio>
|
|
155
137
|
<Grid columns={3} gap={2}>
|
|
156
138
|
{thumbnails.map((hue, i) => (
|
|
157
|
-
<AspectRatio key={
|
|
139
|
+
<AspectRatio key={hue} ratio={1}>
|
|
158
140
|
<SelectableCard
|
|
159
141
|
label={`Product image ${i + 1}`}
|
|
160
142
|
isSelected={selected === i}
|
|
@@ -186,26 +168,26 @@ function ProductInfo() {
|
|
|
186
168
|
const increment = () => setQuantity(q => Math.min(10, (q ?? 1) + 1));
|
|
187
169
|
|
|
188
170
|
return (
|
|
189
|
-
<VStack gap={
|
|
190
|
-
<VStack gap={
|
|
171
|
+
<VStack gap={8}>
|
|
172
|
+
<VStack gap={3}>
|
|
191
173
|
<Heading level={1} type="display-2">
|
|
192
174
|
{PRODUCT.name}
|
|
193
175
|
</Heading>
|
|
194
176
|
<StarRating rating={4.3} count={128} />
|
|
195
177
|
<HStack gap={2} vAlign="center">
|
|
196
|
-
<Text type="large"
|
|
178
|
+
<Text type="large" hasTabularNumbers>
|
|
197
179
|
{fmt(PRODUCT.price)}
|
|
198
180
|
</Text>
|
|
199
181
|
<Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
|
|
200
182
|
{fmt(PRODUCT.originalPrice)}
|
|
201
183
|
</Text>
|
|
202
|
-
<
|
|
184
|
+
<Token label="Sale" color="yellow" />
|
|
203
185
|
</HStack>
|
|
204
186
|
</VStack>
|
|
205
187
|
<Text type="large" weight="normal">
|
|
206
188
|
{PRODUCT.description}
|
|
207
189
|
</Text>
|
|
208
|
-
<VStack gap={
|
|
190
|
+
<VStack gap={3}>
|
|
209
191
|
<Text type="label">Glaze</Text>
|
|
210
192
|
<VStack hAlign="start">
|
|
211
193
|
<SegmentedControl value={color} onChange={setColor} label="Glaze">
|
|
@@ -219,7 +201,7 @@ function ProductInfo() {
|
|
|
219
201
|
</SegmentedControl>
|
|
220
202
|
</VStack>
|
|
221
203
|
</VStack>
|
|
222
|
-
<VStack gap={
|
|
204
|
+
<VStack gap={3}>
|
|
223
205
|
<Text type="label">Finish</Text>
|
|
224
206
|
<VStack hAlign="start">
|
|
225
207
|
<SegmentedControl value={finish} onChange={setFinish} label="Finish">
|
|
@@ -233,7 +215,7 @@ function ProductInfo() {
|
|
|
233
215
|
</SegmentedControl>
|
|
234
216
|
</VStack>
|
|
235
217
|
</VStack>
|
|
236
|
-
<VStack gap={
|
|
218
|
+
<VStack gap={3}>
|
|
237
219
|
<Text type="label">Quantity</Text>
|
|
238
220
|
<HStack gap={1} vAlign="center">
|
|
239
221
|
<Button
|
|
@@ -267,7 +249,7 @@ function ProductInfo() {
|
|
|
267
249
|
/>
|
|
268
250
|
</HStack>
|
|
269
251
|
</VStack>
|
|
270
|
-
<VStack gap={
|
|
252
|
+
<VStack gap={3}>
|
|
271
253
|
{notice && (
|
|
272
254
|
<Banner
|
|
273
255
|
status={notice.status}
|
|
@@ -278,7 +260,7 @@ function ProductInfo() {
|
|
|
278
260
|
/>
|
|
279
261
|
)}
|
|
280
262
|
<Button
|
|
281
|
-
label="Add to
|
|
263
|
+
label="Add to cart"
|
|
282
264
|
variant="primary"
|
|
283
265
|
size="lg"
|
|
284
266
|
clickAction={() =>
|
|
@@ -290,6 +272,7 @@ function ProductInfo() {
|
|
|
290
272
|
/>
|
|
291
273
|
<Button
|
|
292
274
|
label="Buy it now"
|
|
275
|
+
variant="secondary"
|
|
293
276
|
size="lg"
|
|
294
277
|
clickAction={() =>
|
|
295
278
|
setNotice({
|
|
@@ -350,7 +333,7 @@ export default function ProductDetail() {
|
|
|
350
333
|
contentWidth={1200}
|
|
351
334
|
content={
|
|
352
335
|
<LayoutContent padding={6}>
|
|
353
|
-
<Grid columns={{minWidth:
|
|
336
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={8}>
|
|
354
337
|
<ImageGallery
|
|
355
338
|
selected={selectedThumb}
|
|
356
339
|
onSelect={setSelectedThumb}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
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))
|
|
2
4
|
|
|
3
5
|
import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
4
6
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
@@ -8,17 +10,12 @@ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
|
|
|
8
10
|
import {Card} from '@astryxdesign/core/Card';
|
|
9
11
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
10
12
|
import {ArrowRight} from 'lucide-react';
|
|
11
|
-
import
|
|
13
|
+
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
14
|
+
import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
|
|
12
15
|
|
|
13
16
|
// ─── Styles ─────────────────────────────────────────────────────────────────
|
|
14
|
-
// Product
|
|
15
|
-
// the AspectRatio box with `object-fit` (#2582).
|
|
16
|
-
|
|
17
|
-
const svgImage: CSSProperties = {
|
|
18
|
-
width: '100%',
|
|
19
|
-
height: '100%',
|
|
20
|
-
display: 'block',
|
|
21
|
-
};
|
|
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).
|
|
22
19
|
|
|
23
20
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
24
21
|
|
|
@@ -27,7 +24,7 @@ interface Product {
|
|
|
27
24
|
name: string;
|
|
28
25
|
description: string;
|
|
29
26
|
price: number;
|
|
30
|
-
hue:
|
|
27
|
+
hue: SceneHue;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
// One accent per product from the fixed badge vocabulary.
|
|
@@ -38,7 +35,7 @@ const PRODUCTS: Product[] = [
|
|
|
38
35
|
description:
|
|
39
36
|
'A hand-thrown mug that sits easy in the hand through the longest night watch.',
|
|
40
37
|
price: 75.0,
|
|
41
|
-
hue: 'var(--dracula-
|
|
38
|
+
hue: 'var(--dracula-orange)',
|
|
42
39
|
},
|
|
43
40
|
{
|
|
44
41
|
id: 2,
|
|
@@ -91,25 +88,11 @@ function ProductCard({product}: {product: Product}) {
|
|
|
91
88
|
<VStack gap={3}>
|
|
92
89
|
<Card padding={0}>
|
|
93
90
|
<AspectRatio ratio={1}>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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>
|
|
91
|
+
<SceneTile
|
|
92
|
+
label={`${product.name} thumbnail`}
|
|
93
|
+
hue={product.hue}
|
|
94
|
+
size="lg"
|
|
95
|
+
/>
|
|
113
96
|
</AspectRatio>
|
|
114
97
|
</Card>
|
|
115
98
|
<VStack gap={1}>
|
|
@@ -117,7 +100,7 @@ function ProductCard({product}: {product: Product}) {
|
|
|
117
100
|
<Text type="body" color="secondary" maxLines={2}>
|
|
118
101
|
{product.description}
|
|
119
102
|
</Text>
|
|
120
|
-
<Text type="large"
|
|
103
|
+
<Text type="large" hasTabularNumbers>
|
|
121
104
|
{fmt(product.price)}
|
|
122
105
|
</Text>
|
|
123
106
|
</VStack>
|
|
@@ -134,17 +117,16 @@ export default function ProductGallery() {
|
|
|
134
117
|
contentWidth={1200}
|
|
135
118
|
content={
|
|
136
119
|
<LayoutContent padding={6}>
|
|
137
|
-
<VStack gap={
|
|
120
|
+
<VStack gap={8}>
|
|
138
121
|
{/* Header — Grid handles responsive stacking */}
|
|
139
122
|
<Grid columns={{minWidth: 280}} gap={4} align="start">
|
|
140
|
-
<Heading level={1}>
|
|
123
|
+
<Heading level={1} type="display-2">
|
|
141
124
|
Small comforts for the midnight hours.
|
|
142
125
|
</Heading>
|
|
143
126
|
<VStack gap={3} hAlign="start">
|
|
144
127
|
<Text type="body">
|
|
145
128
|
Provisions from the castle workshops: kiln-fired, hand-poured,
|
|
146
|
-
and stitched by the coven.
|
|
147
|
-
and restocked with every full moon.
|
|
129
|
+
and stitched by the coven.
|
|
148
130
|
</Text>
|
|
149
131
|
<Button
|
|
150
132
|
label="Browse the collection"
|
|
@@ -160,11 +142,20 @@ export default function ProductGallery() {
|
|
|
160
142
|
</Grid>
|
|
161
143
|
|
|
162
144
|
{/* Product Grid — reflows 3 → 2 → 1 columns as width narrows */}
|
|
163
|
-
<
|
|
164
|
-
{
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
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>
|
|
168
159
|
</VStack>
|
|
169
160
|
</LayoutContent>
|
|
170
161
|
}
|