astryx-dracula 0.5.0 → 0.6.2
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 +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
type: "page",
|
|
3
|
+
name: "Product Detail",
|
|
4
|
+
description: "Product page with gallery and collapsible sections",
|
|
5
|
+
keywords: ["product", "detail", "buy", "pdp", "shop", "specifications"],
|
|
6
|
+
category: "commerce",
|
|
6
7
|
};
|
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
5
|
+
// L[h=fill] > LC[p=6] > G[c={min:280} g=8] > (V[g=3] > (AR[ratio=4/5] > Scene{SceneTile}) + (G[c=3 g=2] > (AR[ratio=1] > SC > Scene{SceneTile})*6)) + (V[g=0] > 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"Glaze" > SGI*3) + (V[g=3] > Tx"Finish"[t=label] + SG"Finish" > SGI*3) + (V[g=3] > Tx"Quantity"[t=label] + (H[g=1] > B.ghost"-" + (Ctr[w=100] > NI"1") + B.ghost"+")) + (V[g=3] > Bn + B.primary"Add to cart" + B.secondary"Buy it now") + (ColG > (Col > (Hd"Composition"[level=3] + Tx"High-fire stoneware clay…"[t=body])) + (Col > (Hd"Delivery & Returns"[level=3] + Tx"Free shipping on all ceramics orders over $75…"[t=body])) + (Col > (Hd"Dimensions"[level=3] + Tx"Mug height: 9.5 cm / 3.75 in…"[t=body]))))
|
|
4
6
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from
|
|
17
|
-
import {Token} from
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import type {CSSProperties} from
|
|
23
|
-
import {
|
|
24
|
-
import {SceneTile} from 'astryx-dracula/shared/scene-tile';
|
|
25
|
-
import {StarRating} from 'astryx-dracula/shared/stars';
|
|
26
|
-
import {formatMoney} from 'astryx-dracula/shared/format-money';
|
|
7
|
+
import { AspectRatio } from "@astryxdesign/core/AspectRatio";
|
|
8
|
+
import { Banner } from "@astryxdesign/core/Banner";
|
|
9
|
+
import { Button } from "@astryxdesign/core/Button";
|
|
10
|
+
import { Center } from "@astryxdesign/core/Center";
|
|
11
|
+
import { Collapsible, CollapsibleGroup } from "@astryxdesign/core/Collapsible";
|
|
12
|
+
import { Grid } from "@astryxdesign/core/Grid";
|
|
13
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
14
|
+
import { HStack, Layout, LayoutContent, VStack } from "@astryxdesign/core/Layout";
|
|
15
|
+
import { NumberInput } from "@astryxdesign/core/NumberInput";
|
|
16
|
+
import { SegmentedControl, SegmentedControlItem } from "@astryxdesign/core/SegmentedControl";
|
|
17
|
+
import { SelectableCard } from "@astryxdesign/core/SelectableCard";
|
|
18
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
19
|
+
import { Token } from "@astryxdesign/core/Token";
|
|
20
|
+
import { formatMoney } from "astryx-dracula/shared/format-money";
|
|
21
|
+
import { galleryImageClip } from "astryx-dracula/shared/gallery-image";
|
|
22
|
+
import { SceneTile } from "astryx-dracula/shared/scene-tile";
|
|
23
|
+
import { StarRating } from "astryx-dracula/shared/stars";
|
|
24
|
+
import type { CSSProperties } from "react";
|
|
25
|
+
import { useState } from "react";
|
|
27
26
|
|
|
28
27
|
// Custom CSS here is limited to what Astryx components can't express today:
|
|
29
28
|
// - image radius-clip (no Image primitive — #2582, see shared/gallery-image)
|
|
@@ -31,284 +30,274 @@ import {formatMoney} from 'astryx-dracula/shared/format-money';
|
|
|
31
30
|
// Keeps the info column in view while the gallery scrolls. No sticky prop on
|
|
32
31
|
// Astryx layout primitives.
|
|
33
32
|
const stickyInfo: CSSProperties = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
position: "sticky",
|
|
34
|
+
top: "var(--spacing-8)",
|
|
35
|
+
alignSelf: "start",
|
|
37
36
|
};
|
|
38
|
-
|
|
39
|
-
import type {SceneHue} from
|
|
37
|
+
|
|
38
|
+
import type { SceneHue } from "astryx-dracula/shared/scene-hues";
|
|
39
|
+
import { Minus, Plus } from "lucide-react";
|
|
40
40
|
|
|
41
41
|
// ─── Image hues ─────────────────────────────────────────────────────────────
|
|
42
42
|
// IMAGES[selected] is the hero; all six double as thumbnails so the 3-column
|
|
43
43
|
// grid closes into two full rows (a slice(1) subset left a ragged 3+2 row and
|
|
44
44
|
// a fallback entry that never rendered).
|
|
45
45
|
const IMAGES: readonly SceneHue[] = [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
"var(--dracula-cyan)",
|
|
47
|
+
"var(--dracula-pink)",
|
|
48
|
+
"var(--dracula-yellow)",
|
|
49
|
+
"var(--dracula-green)",
|
|
50
|
+
"var(--dracula-orange)",
|
|
51
|
+
// The sixth is `muted` (Comment): a legal / neutral / out-of-stock
|
|
52
|
+
// register, which is exactly what a product photo set needs an extra slot
|
|
53
|
+
// for. Cyan, pink, yellow, green and orange are all saturated identity
|
|
54
|
+
// colours; a sixth saturated one would read as a sixth product variant
|
|
55
|
+
// rather than as "this one is the plain shot". Keeps the 3-column grid
|
|
56
|
+
// closing on two full rows, which is the reason the set exists.
|
|
57
|
+
"var(--dracula-comment)",
|
|
58
58
|
];
|
|
59
59
|
|
|
60
60
|
// ─── Product Data ───────────────────────────────────────────────────────────
|
|
61
61
|
const PRODUCT = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
name: "Midnight Ceremony Mug & Plate Set",
|
|
63
|
+
price: 89.0,
|
|
64
|
+
originalPrice: 119.0,
|
|
65
|
+
description:
|
|
66
|
+
"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.",
|
|
67
|
+
composition:
|
|
68
|
+
"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.",
|
|
69
|
+
deliveryReturns:
|
|
70
|
+
"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.",
|
|
71
|
+
dimensions:
|
|
72
|
+
"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).",
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const COLORS = [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
{ value: "midnight", label: "Midnight" },
|
|
77
|
+
{ value: "crypt", label: "Crypt" },
|
|
78
|
+
{ value: "moonlight", label: "Moonlight" },
|
|
79
79
|
];
|
|
80
80
|
|
|
81
81
|
const FINISHES = [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
{ value: "matte", label: "Matte" },
|
|
83
|
+
{ value: "satin", label: "Satin" },
|
|
84
|
+
{ value: "speckled", label: "Speckled" },
|
|
85
85
|
];
|
|
86
86
|
|
|
87
87
|
// ─── Image Gallery ──────────────────────────────────────────────────────────
|
|
88
|
-
function ImageGallery({
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}: {
|
|
92
|
-
selected: number;
|
|
93
|
-
onSelect: (i: number) => void;
|
|
94
|
-
}) {
|
|
95
|
-
const heroHue: SceneHue = IMAGES[selected];
|
|
96
|
-
const thumbnails = IMAGES;
|
|
88
|
+
function ImageGallery({ selected, onSelect }: { selected: number; onSelect: (i: number) => void }) {
|
|
89
|
+
const heroHue: SceneHue = IMAGES[selected];
|
|
90
|
+
const thumbnails = IMAGES;
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
92
|
+
return (
|
|
93
|
+
<VStack gap={3}>
|
|
94
|
+
<AspectRatio ratio={4 / 5} style={galleryImageClip}>
|
|
95
|
+
<SceneTile label={PRODUCT.name} hue={heroHue} size="lg" />
|
|
96
|
+
</AspectRatio>
|
|
97
|
+
<Grid columns={3} gap={2}>
|
|
98
|
+
{thumbnails.map((hue, i) => (
|
|
99
|
+
<AspectRatio key={hue} ratio={1}>
|
|
100
|
+
<SelectableCard
|
|
101
|
+
label={`Product image ${i + 1}`}
|
|
102
|
+
isSelected={selected === i}
|
|
103
|
+
onChange={() => onSelect(i)}
|
|
104
|
+
variant="transparent"
|
|
105
|
+
padding={0}
|
|
106
|
+
width="100%"
|
|
107
|
+
height="100%"
|
|
108
|
+
>
|
|
109
|
+
<SceneTile label={`Product image ${i + 1}`} hue={hue} size="lg" />
|
|
110
|
+
</SelectableCard>
|
|
111
|
+
</AspectRatio>
|
|
112
|
+
))}
|
|
113
|
+
</Grid>
|
|
114
|
+
</VStack>
|
|
115
|
+
);
|
|
121
116
|
}
|
|
122
117
|
|
|
123
118
|
// ─── Product Info ───────────────────────────────────────────────────────────
|
|
124
119
|
function ProductInfo() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
const [color, setColor] = useState("midnight");
|
|
121
|
+
const [finish, setFinish] = useState("matte");
|
|
122
|
+
const [quantity, setQuantity] = useState<number | null>(1);
|
|
123
|
+
const [notice, setNotice] = useState<{
|
|
124
|
+
status: "success" | "info";
|
|
125
|
+
title: string;
|
|
126
|
+
} | null>(null);
|
|
132
127
|
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
const decrement = () => setQuantity((q) => Math.max(1, (q ?? 1) - 1));
|
|
129
|
+
const increment = () => setQuantity((q) => Math.min(10, (q ?? 1) + 1));
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
</CollapsibleGroup>
|
|
287
|
-
</VStack>
|
|
288
|
-
);
|
|
131
|
+
return (
|
|
132
|
+
<VStack gap={8}>
|
|
133
|
+
<VStack gap={3}>
|
|
134
|
+
<Heading level={1} type="display-2">
|
|
135
|
+
{PRODUCT.name}
|
|
136
|
+
</Heading>
|
|
137
|
+
<StarRating rating={4.3} count={128} />
|
|
138
|
+
<HStack gap={2} vAlign="center">
|
|
139
|
+
<Text type="large" hasTabularNumbers>
|
|
140
|
+
{formatMoney(PRODUCT.price)}
|
|
141
|
+
</Text>
|
|
142
|
+
<Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
|
|
143
|
+
{formatMoney(PRODUCT.originalPrice)}
|
|
144
|
+
</Text>
|
|
145
|
+
<Token label="Sale" color="yellow" />
|
|
146
|
+
</HStack>
|
|
147
|
+
</VStack>
|
|
148
|
+
<Text type="large" weight="normal">
|
|
149
|
+
{PRODUCT.description}
|
|
150
|
+
</Text>
|
|
151
|
+
<VStack gap={3}>
|
|
152
|
+
<Text type="label">Glaze</Text>
|
|
153
|
+
<VStack hAlign="start">
|
|
154
|
+
<SegmentedControl value={color} onChange={setColor} label="Glaze">
|
|
155
|
+
{COLORS.map((c) => (
|
|
156
|
+
<SegmentedControlItem key={c.value} value={c.value} label={c.label} />
|
|
157
|
+
))}
|
|
158
|
+
</SegmentedControl>
|
|
159
|
+
</VStack>
|
|
160
|
+
</VStack>
|
|
161
|
+
<VStack gap={3}>
|
|
162
|
+
<Text type="label">Finish</Text>
|
|
163
|
+
<VStack hAlign="start">
|
|
164
|
+
<SegmentedControl value={finish} onChange={setFinish} label="Finish">
|
|
165
|
+
{FINISHES.map((f) => (
|
|
166
|
+
<SegmentedControlItem key={f.value} value={f.value} label={f.label} />
|
|
167
|
+
))}
|
|
168
|
+
</SegmentedControl>
|
|
169
|
+
</VStack>
|
|
170
|
+
</VStack>
|
|
171
|
+
<VStack gap={3}>
|
|
172
|
+
<Text type="label">Quantity</Text>
|
|
173
|
+
<HStack gap={1} vAlign="center">
|
|
174
|
+
<Button
|
|
175
|
+
label="Decrease quantity"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
icon={<Icon icon={Minus} size="sm" />}
|
|
178
|
+
clickAction={decrement}
|
|
179
|
+
isDisabled={(quantity ?? 1) <= 1}
|
|
180
|
+
isIconOnly
|
|
181
|
+
tooltip="Decrease quantity"
|
|
182
|
+
/>
|
|
183
|
+
<Center width={100}>
|
|
184
|
+
<NumberInput
|
|
185
|
+
label="Quantity"
|
|
186
|
+
isLabelHidden
|
|
187
|
+
value={quantity}
|
|
188
|
+
onChange={setQuantity}
|
|
189
|
+
min={1}
|
|
190
|
+
max={10}
|
|
191
|
+
isIntegerOnly
|
|
192
|
+
/>
|
|
193
|
+
</Center>
|
|
194
|
+
<Button
|
|
195
|
+
label="Increase quantity"
|
|
196
|
+
variant="ghost"
|
|
197
|
+
icon={<Icon icon={Plus} size="sm" />}
|
|
198
|
+
clickAction={increment}
|
|
199
|
+
isDisabled={(quantity ?? 1) >= 10}
|
|
200
|
+
isIconOnly
|
|
201
|
+
tooltip="Increase quantity"
|
|
202
|
+
/>
|
|
203
|
+
</HStack>
|
|
204
|
+
</VStack>
|
|
205
|
+
<VStack gap={3}>
|
|
206
|
+
{notice && (
|
|
207
|
+
<Banner
|
|
208
|
+
status={notice.status}
|
|
209
|
+
// Core 0.7.0 stopped deriving the live-region role from
|
|
210
|
+
// `status`, so this banner states its own. It mirrors the
|
|
211
|
+
// result of Add to cart / Buy it now, and a screen reader
|
|
212
|
+
// must hear it.
|
|
213
|
+
role="status"
|
|
214
|
+
title={notice.title}
|
|
215
|
+
container="card"
|
|
216
|
+
isDismissable
|
|
217
|
+
onDismiss={() => setNotice(null)}
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
220
|
+
<Button
|
|
221
|
+
label="Add to cart"
|
|
222
|
+
variant="primary"
|
|
223
|
+
size="lg"
|
|
224
|
+
clickAction={() =>
|
|
225
|
+
setNotice({
|
|
226
|
+
status: "success",
|
|
227
|
+
title: `Added ${quantity ?? 1} × ${PRODUCT.name} to your cart.`,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
<Button
|
|
232
|
+
label="Buy it now"
|
|
233
|
+
variant="secondary"
|
|
234
|
+
size="lg"
|
|
235
|
+
clickAction={() =>
|
|
236
|
+
setNotice({
|
|
237
|
+
status: "info",
|
|
238
|
+
title: "Checkout is disabled in this preview.",
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
/>
|
|
242
|
+
</VStack>
|
|
243
|
+
<CollapsibleGroup type="multiple" defaultValue={["composition"]} hasDividers>
|
|
244
|
+
<Collapsible
|
|
245
|
+
value="composition"
|
|
246
|
+
trigger={
|
|
247
|
+
<Heading level={3} accessibilityLevel={2}>
|
|
248
|
+
Composition
|
|
249
|
+
</Heading>
|
|
250
|
+
}
|
|
251
|
+
>
|
|
252
|
+
<Text type="body">{PRODUCT.composition}</Text>
|
|
253
|
+
</Collapsible>
|
|
254
|
+
<Collapsible
|
|
255
|
+
value="delivery"
|
|
256
|
+
defaultIsOpen={false}
|
|
257
|
+
trigger={
|
|
258
|
+
<Heading level={3} accessibilityLevel={2}>
|
|
259
|
+
Delivery & Returns
|
|
260
|
+
</Heading>
|
|
261
|
+
}
|
|
262
|
+
>
|
|
263
|
+
<Text type="body">{PRODUCT.deliveryReturns}</Text>
|
|
264
|
+
</Collapsible>
|
|
265
|
+
<Collapsible
|
|
266
|
+
value="dimensions"
|
|
267
|
+
defaultIsOpen={false}
|
|
268
|
+
trigger={
|
|
269
|
+
<Heading level={3} accessibilityLevel={2}>
|
|
270
|
+
Dimensions
|
|
271
|
+
</Heading>
|
|
272
|
+
}
|
|
273
|
+
>
|
|
274
|
+
<Text type="body" hasTabularNumbers>
|
|
275
|
+
{PRODUCT.dimensions}
|
|
276
|
+
</Text>
|
|
277
|
+
</Collapsible>
|
|
278
|
+
</CollapsibleGroup>
|
|
279
|
+
</VStack>
|
|
280
|
+
);
|
|
289
281
|
}
|
|
290
282
|
|
|
291
283
|
// ─── Main Page ──────────────────────────────────────────────────────────────
|
|
292
284
|
export default function ProductDetail() {
|
|
293
|
-
|
|
285
|
+
const [selectedThumb, setSelectedThumb] = useState(0);
|
|
294
286
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
/>
|
|
313
|
-
);
|
|
287
|
+
return (
|
|
288
|
+
<Layout
|
|
289
|
+
height="fill"
|
|
290
|
+
contentWidth={1200}
|
|
291
|
+
content={
|
|
292
|
+
<LayoutContent padding={6}>
|
|
293
|
+
<Grid columns={{ minWidth: 280, repeat: "fit" }} gap={8}>
|
|
294
|
+
<ImageGallery selected={selectedThumb} onSelect={setSelectedThumb} />
|
|
295
|
+
<VStack gap={0} style={stickyInfo}>
|
|
296
|
+
<ProductInfo />
|
|
297
|
+
</VStack>
|
|
298
|
+
</Grid>
|
|
299
|
+
</LayoutContent>
|
|
300
|
+
}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
314
303
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
type: "page",
|
|
3
|
+
name: "Product Gallery",
|
|
4
|
+
description: "Card grid with images, prices, and descriptions",
|
|
5
|
+
keywords: ["product", "gallery", "images", "shop", "store", "pdp"],
|
|
6
|
+
category: "commerce",
|
|
6
7
|
};
|