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.
Files changed (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,6 +1,7 @@
1
1
  export default {
2
- type: 'page',
3
- name: 'Product Detail',
4
- description: 'Product page with gallery and collapsible sections',
5
- category: 'commerce',
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, 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
+ // 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 {useState} from 'react';
6
- import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
7
- import {Center} from '@astryxdesign/core/Center';
8
- import {Grid} from '@astryxdesign/core/Grid';
9
- import {Text, Heading} from '@astryxdesign/core/Text';
10
- import {Button} from '@astryxdesign/core/Button';
11
- import {NumberInput} from '@astryxdesign/core/NumberInput';
12
- import {Icon} from '@astryxdesign/core/Icon';
13
- import {
14
- SegmentedControl,
15
- SegmentedControlItem,
16
- } from '@astryxdesign/core/SegmentedControl';
17
- import {Token} from '@astryxdesign/core/Token';
18
- import {Banner} from '@astryxdesign/core/Banner';
19
- import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
20
- import {AspectRatio} from '@astryxdesign/core/AspectRatio';
21
- import {SelectableCard} from '@astryxdesign/core/SelectableCard';
22
- import type {CSSProperties} from 'react';
23
- import {galleryImageClip} from 'astryx-dracula/shared/gallery-image';
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
- position: 'sticky',
35
- top: 'var(--spacing-8)',
36
- alignSelf: 'start',
33
+ position: "sticky",
34
+ top: "var(--spacing-8)",
35
+ alignSelf: "start",
37
36
  };
38
- import {Minus, Plus} from 'lucide-react';
39
- import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
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
- '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)',
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
- 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).',
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
- {value: 'midnight', label: 'Midnight'},
77
- {value: 'crypt', label: 'Crypt'},
78
- {value: 'moonlight', label: 'Moonlight'},
76
+ { value: "midnight", label: "Midnight" },
77
+ { value: "crypt", label: "Crypt" },
78
+ { value: "moonlight", label: "Moonlight" },
79
79
  ];
80
80
 
81
81
  const FINISHES = [
82
- {value: 'matte', label: 'Matte'},
83
- {value: 'satin', label: 'Satin'},
84
- {value: 'speckled', label: 'Speckled'},
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
- selected,
90
- onSelect,
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
- return (
99
- <VStack gap={3}>
100
- <AspectRatio ratio={4 / 5} style={galleryImageClip}>
101
- <SceneTile label={PRODUCT.name} hue={heroHue} size="lg" />
102
- </AspectRatio>
103
- <Grid columns={3} gap={2}>
104
- {thumbnails.map((hue, i) => (
105
- <AspectRatio key={hue} ratio={1}>
106
- <SelectableCard
107
- label={`Product image ${i + 1}`}
108
- isSelected={selected === i}
109
- onChange={() => onSelect(i)}
110
- variant="transparent"
111
- padding={0}
112
- width="100%"
113
- height="100%">
114
- <SceneTile label={`Product image ${i + 1}`} hue={hue} size="lg" />
115
- </SelectableCard>
116
- </AspectRatio>
117
- ))}
118
- </Grid>
119
- </VStack>
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
- const [color, setColor] = useState('midnight');
126
- const [finish, setFinish] = useState('matte');
127
- const [quantity, setQuantity] = useState<number | null>(1);
128
- const [notice, setNotice] = useState<{
129
- status: 'success' | 'info';
130
- title: string;
131
- } | null>(null);
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
- const decrement = () => setQuantity(q => Math.max(1, (q ?? 1) - 1));
134
- const increment = () => setQuantity(q => Math.min(10, (q ?? 1) + 1));
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
- return (
137
- <VStack gap={8}>
138
- <VStack gap={3}>
139
- <Heading level={1} type="display-2">
140
- {PRODUCT.name}
141
- </Heading>
142
- <StarRating rating={4.3} count={128} />
143
- <HStack gap={2} vAlign="center">
144
- <Text type="large" hasTabularNumbers>
145
- {formatMoney(PRODUCT.price)}
146
- </Text>
147
- <Text type="body" color="secondary" hasStrikethrough hasTabularNumbers>
148
- {formatMoney(PRODUCT.originalPrice)}
149
- </Text>
150
- <Token label="Sale" color="yellow" />
151
- </HStack>
152
- </VStack>
153
- <Text type="large" weight="normal">
154
- {PRODUCT.description}
155
- </Text>
156
- <VStack gap={3}>
157
- <Text type="label">Glaze</Text>
158
- <VStack hAlign="start">
159
- <SegmentedControl value={color} onChange={setColor} label="Glaze">
160
- {COLORS.map(c => (
161
- <SegmentedControlItem
162
- key={c.value}
163
- value={c.value}
164
- label={c.label}
165
- />
166
- ))}
167
- </SegmentedControl>
168
- </VStack>
169
- </VStack>
170
- <VStack gap={3}>
171
- <Text type="label">Finish</Text>
172
- <VStack hAlign="start">
173
- <SegmentedControl value={finish} onChange={setFinish} label="Finish">
174
- {FINISHES.map(f => (
175
- <SegmentedControlItem
176
- key={f.value}
177
- value={f.value}
178
- label={f.label}
179
- />
180
- ))}
181
- </SegmentedControl>
182
- </VStack>
183
- </VStack>
184
- <VStack gap={3}>
185
- <Text type="label">Quantity</Text>
186
- <HStack gap={1} vAlign="center">
187
- <Button
188
- label="Decrease quantity"
189
- variant="ghost"
190
- icon={<Icon icon={Minus} size="sm" />}
191
- clickAction={decrement}
192
- isDisabled={(quantity ?? 1) <= 1}
193
- isIconOnly
194
- tooltip="Decrease quantity"
195
- />
196
- <Center width={100}>
197
- <NumberInput
198
- label="Quantity"
199
- isLabelHidden
200
- value={quantity}
201
- onChange={setQuantity}
202
- min={1}
203
- max={10}
204
- isIntegerOnly
205
- />
206
- </Center>
207
- <Button
208
- label="Increase quantity"
209
- variant="ghost"
210
- icon={<Icon icon={Plus} size="sm" />}
211
- clickAction={increment}
212
- isDisabled={(quantity ?? 1) >= 10}
213
- isIconOnly
214
- tooltip="Increase quantity"
215
- />
216
- </HStack>
217
- </VStack>
218
- <VStack gap={3}>
219
- {notice && (
220
- <Banner
221
- status={notice.status}
222
- title={notice.title}
223
- container="card"
224
- isDismissable
225
- onDismiss={() => setNotice(null)}
226
- />
227
- )}
228
- <Button
229
- label="Add to cart"
230
- variant="primary"
231
- size="lg"
232
- clickAction={() =>
233
- setNotice({
234
- status: 'success',
235
- title: `Added ${quantity ?? 1} × ${PRODUCT.name} to your cart.`,
236
- })
237
- }
238
- />
239
- <Button
240
- label="Buy it now"
241
- variant="secondary"
242
- size="lg"
243
- clickAction={() =>
244
- setNotice({
245
- status: 'info',
246
- title: 'Checkout is disabled in this preview.',
247
- })
248
- }
249
- />
250
- </VStack>
251
- <CollapsibleGroup
252
- type="multiple"
253
- defaultValue={['composition']}
254
- hasDividers>
255
- <Collapsible
256
- value="composition"
257
- trigger={
258
- <Heading level={3} accessibilityLevel={2}>
259
- Composition
260
- </Heading>
261
- }>
262
- <Text type="body">{PRODUCT.composition}</Text>
263
- </Collapsible>
264
- <Collapsible
265
- value="delivery"
266
- defaultIsOpen={false}
267
- trigger={
268
- <Heading level={3} accessibilityLevel={2}>
269
- Delivery &amp; Returns
270
- </Heading>
271
- }>
272
- <Text type="body">{PRODUCT.deliveryReturns}</Text>
273
- </Collapsible>
274
- <Collapsible
275
- value="dimensions"
276
- defaultIsOpen={false}
277
- trigger={
278
- <Heading level={3} accessibilityLevel={2}>
279
- Dimensions
280
- </Heading>
281
- }>
282
- <Text type="body" hasTabularNumbers>
283
- {PRODUCT.dimensions}
284
- </Text>
285
- </Collapsible>
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 &amp; 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
- const [selectedThumb, setSelectedThumb] = useState(0);
285
+ const [selectedThumb, setSelectedThumb] = useState(0);
294
286
 
295
- return (
296
- <Layout
297
- height="fill"
298
- contentWidth={1200}
299
- content={
300
- <LayoutContent padding={6}>
301
- <Grid columns={{minWidth: 280, repeat: 'fit'}} gap={8}>
302
- <ImageGallery
303
- selected={selectedThumb}
304
- onSelect={setSelectedThumb}
305
- />
306
- <VStack gap={0} style={stickyInfo}>
307
- <ProductInfo />
308
- </VStack>
309
- </Grid>
310
- </LayoutContent>
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
- type: 'page',
3
- name: 'Product Gallery',
4
- description: 'Card grid with images, prices, and descriptions',
5
- category: 'commerce',
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
  };