astryx-dracula 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,238 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {useState} from 'react';
4
+ import {AppShell} from '@astryxdesign/core/AppShell';
5
+ import {Divider} from '@astryxdesign/core/Divider';
6
+ import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core/Layout';
7
+ import {
8
+ SideNav,
9
+ SideNavHeading,
10
+ SideNavItem,
11
+ SideNavSection,
12
+ } from '@astryxdesign/core/SideNav';
13
+ import {NavIcon} from '@astryxdesign/core/NavIcon';
14
+ import {Icon} from '@astryxdesign/core/Icon';
15
+ import type {IconType} from '@astryxdesign/core/Icon';
16
+ import {StatusDot} from '@astryxdesign/core/StatusDot';
17
+ import type {StatusDotVariant} from '@astryxdesign/core/StatusDot';
18
+ import {Card} from '@astryxdesign/core/Card';
19
+ import {TextInput} from '@astryxdesign/core/TextInput';
20
+ import {VStack, HStack} from '@astryxdesign/core/Stack';
21
+ import {
22
+ Sparkles,
23
+ Plus,
24
+ Search,
25
+ BookOpen,
26
+ Settings,
27
+ CircleUserRound,
28
+ User,
29
+ Building2,
30
+ CodeXml,
31
+ } from 'lucide-react';
32
+
33
+ type Conversation = {
34
+ label: string;
35
+ status: StatusDotVariant;
36
+ statusLabel: string;
37
+ };
38
+
39
+ type Workspace = {
40
+ name: string;
41
+ icon: IconType;
42
+ chats: Conversation[];
43
+ };
44
+
45
+ const WORKSPACES: Workspace[] = [
46
+ {
47
+ name: 'Personal',
48
+ icon: User,
49
+ chats: [
50
+ {
51
+ label: 'Weekend trip planning',
52
+ status: 'success',
53
+ statusLabel: 'Active',
54
+ },
55
+ {
56
+ label: 'Recipe ideas for the week',
57
+ status: 'neutral',
58
+ statusLabel: 'Idle',
59
+ },
60
+ {
61
+ label: 'Book recommendations',
62
+ status: 'warning',
63
+ statusLabel: 'Needs review',
64
+ },
65
+ {label: 'Home workout plan', status: 'neutral', statusLabel: 'Idle'},
66
+ ],
67
+ },
68
+ {
69
+ name: 'Acme Corp',
70
+ icon: Building2,
71
+ chats: [
72
+ {label: 'Q3 roadmap draft', status: 'accent', statusLabel: 'In progress'},
73
+ {
74
+ label: 'Customer onboarding flow',
75
+ status: 'success',
76
+ statusLabel: 'Active',
77
+ },
78
+ {
79
+ label: 'Pricing strategy review',
80
+ status: 'warning',
81
+ statusLabel: 'Needs review',
82
+ },
83
+ {label: 'Standup summary', status: 'neutral', statusLabel: 'Idle'},
84
+ ],
85
+ },
86
+ {
87
+ name: 'Open Source',
88
+ icon: CodeXml,
89
+ chats: [
90
+ {
91
+ label: 'StyleX migration notes',
92
+ status: 'accent',
93
+ statusLabel: 'In progress',
94
+ },
95
+ {
96
+ label: 'Skeleton loading states',
97
+ status: 'success',
98
+ statusLabel: 'Active',
99
+ },
100
+ {label: 'Accessibility audit', status: 'error', statusLabel: 'Blocked'},
101
+ {label: 'Release notes v4.0', status: 'neutral', statusLabel: 'Idle'},
102
+ ],
103
+ },
104
+ ];
105
+
106
+ const SELECTED_CHAT = 'StyleX migration notes';
107
+ // Same-route hash: demo links stay focusable anchors without escaping the
108
+ // template through the hash router (bare "#" would drop back to the home page).
109
+ const SELF_HASH = '#/templates/shell-side-nav';
110
+
111
+ const MESSAGES = [
112
+ {role: 'assistant', width: '78%', height: 104},
113
+ {role: 'user', width: '48%', height: 48},
114
+ {role: 'assistant', width: '64%', height: 132},
115
+ {role: 'user', width: '38%', height: 40},
116
+ ];
117
+
118
+ function ConversationItem({
119
+ label,
120
+ status,
121
+ statusLabel,
122
+ isSelected,
123
+ onSelect,
124
+ }: {
125
+ label: string;
126
+ status: StatusDotVariant;
127
+ statusLabel: string;
128
+ isSelected?: boolean;
129
+ onSelect: () => void;
130
+ }) {
131
+ // The hover-only MoreMenu was four dead actions unreachable by keyboard and
132
+ // touch, so it goes: the status dot is always visible (its label doubles as
133
+ // the tooltip), and selecting a conversation actually switches selection.
134
+ return (
135
+ <SideNavItem
136
+ label={label}
137
+ href={SELF_HASH}
138
+ isSelected={isSelected}
139
+ onClick={onSelect}
140
+ endContent={<StatusDot variant={status} label={statusLabel} />}
141
+ />
142
+ );
143
+ }
144
+
145
+ export default function ShellSideNav() {
146
+ const [selectedChat, setSelectedChat] = useState(SELECTED_CHAT);
147
+ const [draft, setDraft] = useState('');
148
+ return (
149
+ <AppShell
150
+ contentPadding={0}
151
+ sideNav={
152
+ <SideNav
153
+ collapsible
154
+ resizable={{defaultWidth: 300, minWidth: 220, maxWidth: 420}}
155
+ header={
156
+ <SideNavHeading
157
+ heading="Night Owl"
158
+ icon={<NavIcon icon={<Icon icon={Sparkles} size="sm" />} />}
159
+ headingHref={SELF_HASH}
160
+ />
161
+ }
162
+ footer={
163
+ <SideNavSection title="Account" isHeaderHidden>
164
+ <SideNavItem label="Settings" icon={Settings} href={SELF_HASH} />
165
+ <SideNavItem
166
+ label="Sarah Chen"
167
+ icon={CircleUserRound}
168
+ href={SELF_HASH}
169
+ />
170
+ </SideNavSection>
171
+ }>
172
+ <SideNavSection title="Menu" isHeaderHidden>
173
+ <SideNavItem label="New chat" icon={Plus} href={SELF_HASH} />
174
+ <SideNavItem label="Search" icon={Search} href={SELF_HASH} />
175
+ <SideNavItem label="Library" icon={BookOpen} href={SELF_HASH} />
176
+ </SideNavSection>
177
+ <Divider />
178
+ <SideNavSection title="Workspaces" isHeaderHidden>
179
+ {WORKSPACES.map(workspace => (
180
+ <SideNavItem
181
+ key={workspace.name}
182
+ label={workspace.name}
183
+ icon={workspace.icon}
184
+ collapsible={{defaultIsCollapsed: false}}>
185
+ <VStack gap={0.5}>
186
+ {workspace.chats.map(chat => (
187
+ <ConversationItem
188
+ key={chat.label}
189
+ label={chat.label}
190
+ status={chat.status}
191
+ statusLabel={chat.statusLabel}
192
+ isSelected={chat.label === selectedChat}
193
+ onSelect={() => setSelectedChat(chat.label)}
194
+ />
195
+ ))}
196
+ </VStack>
197
+ </SideNavItem>
198
+ ))}
199
+ </SideNavSection>
200
+ </SideNav>
201
+ }>
202
+ <Layout
203
+ height="fill"
204
+ contentWidth={768}
205
+ content={
206
+ <LayoutContent padding={6}>
207
+ <VStack gap={5}>
208
+ {MESSAGES.map((message, mi) => (
209
+ <HStack
210
+ key={mi}
211
+ hAlign={message.role === 'assistant' ? 'start' : 'end'}>
212
+ <Card
213
+ variant="muted"
214
+ padding={0}
215
+ width={message.width}
216
+ height={message.height}
217
+ />
218
+ </HStack>
219
+ ))}
220
+ </VStack>
221
+ </LayoutContent>
222
+ }
223
+ footer={
224
+ <LayoutFooter>
225
+ <TextInput
226
+ label="Message Night Owl"
227
+ isLabelHidden
228
+ placeholder="Message Night Owl…"
229
+ value={draft}
230
+ onChange={setDraft}
231
+ width="100%"
232
+ />
233
+ </LayoutFooter>
234
+ }
235
+ />
236
+ </AppShell>
237
+ );
238
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Storefront Shell',
4
+ description: 'Mega-menu top nav with product grid',
5
+ category: 'tooling',
6
+ };
@@ -0,0 +1,243 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import type {CSSProperties} from 'react';
4
+ import {AppShell} from '@astryxdesign/core/AppShell';
5
+ import {useMediaQuery} from '@astryxdesign/core/hooks';
6
+ import {
7
+ TopNav,
8
+ TopNavHeading,
9
+ TopNavItem,
10
+ TopNavMegaMenu,
11
+ TopNavMegaMenuItem,
12
+ TopNavMegaMenuFeaturedCard,
13
+ useTopNavRenderMode,
14
+ } from '@astryxdesign/core/TopNav';
15
+ import {NavIcon} from '@astryxdesign/core/NavIcon';
16
+ import {Icon} from '@astryxdesign/core/Icon';
17
+ import type {IconType} from '@astryxdesign/core/Icon';
18
+ import {IconButton} from '@astryxdesign/core/IconButton';
19
+ import {Button} from '@astryxdesign/core/Button';
20
+ import {Badge} from '@astryxdesign/core/Badge';
21
+ import {Card} from '@astryxdesign/core/Card';
22
+ import {Grid} from '@astryxdesign/core/Grid';
23
+ import {Stack, VStack} from '@astryxdesign/core/Stack';
24
+ import {
25
+ ShoppingBag,
26
+ ShoppingCart,
27
+ Search,
28
+ Sparkles,
29
+ SwatchBook,
30
+ Tag,
31
+ House,
32
+ Smile,
33
+ BadgePercent,
34
+ Gift,
35
+ Cloud,
36
+ Zap,
37
+ Sun,
38
+ Star,
39
+ Flame,
40
+ Globe,
41
+ Moon,
42
+ } from 'lucide-react';
43
+
44
+ // Cap + center the page body so wide screens show whitespace gutters.
45
+ const contentMax: CSSProperties = {maxWidth: 1100, marginInline: 'auto'};
46
+ // Same-route hash: demo links stay focusable anchors without escaping the
47
+ // template through the hash router (bare "#" would drop back to the home page).
48
+ const SELF_HASH = '#/templates/shell-top-nav';
49
+ // Lock both mega-menu panels to an identical size. Without this, Shop and
50
+ // Brands size to their own content (different widths); since both anchor to
51
+ // the centered nav, switching between them resizes the panel — which reads
52
+ // as flashing/jumping. Fixed item + featured widths make the panels
53
+ // pixel-identical so the transition is seamless.
54
+ const megaItems: CSSProperties = {gridColumn: '1 / -1', width: 520};
55
+ const megaFeatured: CSSProperties = {width: 240};
56
+
57
+ type MegaItem = {name: string; tagline: string; icon: IconType};
58
+
59
+ // Shop and Brands each render 8 items — the mega menu's built-in 2-column grid
60
+ // lays them out as 2 columns × 4 rows, alongside a featured card.
61
+ const SHOP_ITEMS: MegaItem[] = [
62
+ {name: 'New Arrivals', tagline: 'The latest drops', icon: Sparkles},
63
+ {name: 'Womenswear', tagline: 'Dresses, knitwear & more', icon: SwatchBook},
64
+ {name: 'Menswear', tagline: 'Shirts, tailoring & more', icon: Tag},
65
+ {name: 'Home', tagline: 'Bedding, lighting & décor', icon: House},
66
+ {
67
+ name: 'Beauty',
68
+ tagline: 'Skincare, fragrance & makeup',
69
+ icon: Smile,
70
+ },
71
+ {
72
+ name: 'Accessories',
73
+ tagline: 'Bags, hats & sunglasses',
74
+ icon: ShoppingBag,
75
+ },
76
+ {name: 'Sale', tagline: 'Up to 50% off', icon: BadgePercent},
77
+ {name: 'Gift Cards', tagline: 'The perfect present', icon: Gift},
78
+ ];
79
+
80
+ const BRAND_ITEMS: MegaItem[] = [
81
+ {name: 'Aether', tagline: 'Performance essentials', icon: Sparkles},
82
+ {name: 'Northwind', tagline: 'Outdoor & technical', icon: Cloud},
83
+ {name: 'Loomwell', tagline: 'Everyday knitwear', icon: Zap},
84
+ {name: 'Verdant', tagline: 'Sustainable basics', icon: Sun},
85
+ {name: 'Studio Mara', tagline: 'Modern tailoring', icon: Star},
86
+ {name: 'Atelier Kos', tagline: 'Limited ateliers', icon: Flame},
87
+ {name: 'Rue & Co', tagline: 'City streetwear', icon: Globe},
88
+ {name: 'Halden', tagline: 'Minimal staples', icon: Moon},
89
+ ];
90
+
91
+ const CATEGORY_TILES = [
92
+ 'New Arrivals',
93
+ 'Womenswear',
94
+ 'Menswear',
95
+ 'Home & Living',
96
+ 'Beauty',
97
+ 'Accessories',
98
+ ];
99
+
100
+ // Wraps the 8 items in a fixed-width 2-column grid so every mega menu's item
101
+ // area is exactly the same width regardless of its content.
102
+ function MegaItems({items}: {items: MegaItem[]}) {
103
+ // In the mobile drawer the fixed 520px panel would overflow the ~350px
104
+ // drawer, and the 2-column grid overlaps item text there — so the drawer
105
+ // gets a natural-width single column. Desktop keeps the lock.
106
+ const isDrawer = useTopNavRenderMode() === 'drawer';
107
+ if (isDrawer) {
108
+ return (
109
+ <VStack gap={1}>
110
+ {items.map(item => (
111
+ <TopNavMegaMenuItem
112
+ key={item.name}
113
+ title={item.name}
114
+ description={item.tagline}
115
+ icon={<Icon icon={item.icon} size="md" color="secondary" />}
116
+ href={SELF_HASH}
117
+ />
118
+ ))}
119
+ </VStack>
120
+ );
121
+ }
122
+ return (
123
+ <Stack style={megaItems}>
124
+ <Grid columns={2} gap={2}>
125
+ {items.map(item => (
126
+ <TopNavMegaMenuItem
127
+ key={item.name}
128
+ title={item.name}
129
+ description={item.tagline}
130
+ icon={<Icon icon={item.icon} size="md" color="secondary" />}
131
+ href={SELF_HASH}
132
+ />
133
+ ))}
134
+ </Grid>
135
+ </Stack>
136
+ );
137
+ }
138
+
139
+ // Pins the featured card to a fixed width so both panels match exactly.
140
+ function MegaFeatured(props: {
141
+ title: string;
142
+ description: string;
143
+ linkLabel: string;
144
+ linkHref: string;
145
+ }) {
146
+ const isDrawer = useTopNavRenderMode() === 'drawer';
147
+ return (
148
+ <Stack style={isDrawer ? undefined : megaFeatured}>
149
+ <TopNavMegaMenuFeaturedCard {...props} />
150
+ </Stack>
151
+ );
152
+ }
153
+
154
+ export default function ShellTopNav() {
155
+ // Below ~640px the mobile bar (heading + actions + toggle) overflows 390px
156
+ // viewports, pushing the nav toggle off-screen. Dropping the text Sign in
157
+ // button there restores room for search, checkout, and the toggle.
158
+ const isCompact = useMediaQuery('(max-width: 640px)');
159
+ return (
160
+ <AppShell
161
+ variant="surface"
162
+ contentPadding={6}
163
+ topNav={
164
+ <TopNav
165
+ label="Nocturne storefront navigation"
166
+ heading={
167
+ <TopNavHeading
168
+ heading="Nocturne"
169
+ logo={
170
+ <NavIcon icon={<Icon icon={ShoppingBag} size="sm" />} />
171
+ }
172
+ />
173
+ }
174
+ centerContent={
175
+ <>
176
+ <TopNavMegaMenu
177
+ label="Shop"
178
+ items={<MegaItems items={SHOP_ITEMS} />}
179
+ featured={
180
+ <MegaFeatured
181
+ title="The Midnight Edit"
182
+ description="Layered staples in moonlit purples."
183
+ linkLabel="Shop the edit"
184
+ linkHref={SELF_HASH}
185
+ />
186
+ }
187
+ />
188
+ <TopNavMegaMenu
189
+ label="Brands"
190
+ items={<MegaItems items={BRAND_ITEMS} />}
191
+ featured={
192
+ <MegaFeatured
193
+ title="Meet Studio Mara"
194
+ description="Modern tailoring, made to last."
195
+ linkLabel="Discover the label"
196
+ linkHref={SELF_HASH}
197
+ />
198
+ }
199
+ />
200
+ <TopNavItem label="Sale" href={SELF_HASH} />
201
+ <TopNavItem label="Service" href={SELF_HASH} />
202
+ </>
203
+ }
204
+ endContent={
205
+ <>
206
+ <IconButton
207
+ label="Search products"
208
+ tooltip="Search"
209
+ variant="ghost"
210
+ icon={<Icon icon={Search} size="sm" />}
211
+ />
212
+ {!isCompact && <Button label="Sign in" variant="ghost" />}
213
+ <Button
214
+ label="Checkout"
215
+ variant="primary"
216
+ tooltip="Cart, 3 items"
217
+ icon={<Icon icon={ShoppingCart} size="sm" />}
218
+ endContent={<Badge label={3} />}
219
+ />
220
+ </>
221
+ }
222
+ />
223
+ }>
224
+ <VStack gap={10} style={contentMax}>
225
+ <Card variant="muted" padding={0} width="100%" height={360} />
226
+
227
+ {[0, 1, 2].map(section => (
228
+ <VStack key={section} gap={4}>
229
+ <Card variant="muted" padding={0} width={200} height={24} />
230
+ <Grid columns={{minWidth: 160, repeat: 'fit'}} gap={4}>
231
+ {CATEGORY_TILES.map(tile => (
232
+ <VStack key={tile} gap={2}>
233
+ <Card variant="muted" padding={0} width="100%" height={120} />
234
+ <Card variant="muted" padding={0} width="60%" height={14} />
235
+ </VStack>
236
+ ))}
237
+ </Grid>
238
+ </VStack>
239
+ ))}
240
+ </VStack>
241
+ </AppShell>
242
+ );
243
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Side Gallery',
4
+ description: 'Headline, CTA, and image collage side by side',
5
+ category: 'marketing',
6
+ };
@@ -0,0 +1,179 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import type {CSSProperties} from 'react';
4
+ import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
5
+ import {Text, Heading} from '@astryxdesign/core/Text';
6
+ import {Button} from '@astryxdesign/core/Button';
7
+ import {AspectRatio} from '@astryxdesign/core/AspectRatio';
8
+ import {Grid} from '@astryxdesign/core/Grid';
9
+ import {Divider} from '@astryxdesign/core/Divider';
10
+
11
+ // NOTE: The only custom styling here is gallery fill + corner radius. It
12
+ // exists because Astryx has no image primitive — AspectRatio exposes no
13
+ // objectFit or radius props and there's no Image. Tracked in issue #2582;
14
+ // replace these with component props once it lands.
15
+ // Fills the AspectRatio box. No objectFit prop on AspectRatio (#2582).
16
+ const galleryImage: CSSProperties = {
17
+ width: '100%',
18
+ height: '100%',
19
+ display: 'block',
20
+ };
21
+ // Rounds the gallery corners. No radius prop on AspectRatio (#2582);
22
+ // overflow clip masks the SVG scene to the rounded corners.
23
+ const galleryImageClip: CSSProperties = {
24
+ borderRadius: 'var(--radius-container)',
25
+ overflow: 'clip',
26
+ };
27
+
28
+ // ─── Gallery Data ─────────────────────────────────────────────────────────────
29
+
30
+ // One Dracula accent per tile from the fixed categorical vocabulary.
31
+ const GALLERY_SCENES = [
32
+ {alt: 'Moonlit ridge trail under a harvest moon', hue: 'var(--dracula-cyan)'},
33
+ {alt: 'Late portrait in violet lamplight', hue: 'var(--dracula-pink)'},
34
+ {
35
+ alt: 'Lamplit reading nook after midnight',
36
+ hue: 'var(--dracula-yellow)',
37
+ },
38
+ {alt: 'Fog rolling over the night pines', hue: 'var(--dracula-purple)'},
39
+ {alt: 'Dancer caught mid-step in stage pink', hue: 'var(--dracula-pink)'},
40
+ {
41
+ alt: 'Kitchen table set for a midnight feast',
42
+ hue: 'var(--dracula-orange)',
43
+ },
44
+ {alt: 'Castle silhouette over sleeping hills', hue: 'var(--dracula-cyan)'},
45
+ {alt: 'Attic window glowing amber at 2am', hue: 'var(--dracula-yellow)'},
46
+ {alt: 'Cellar shelves lined with bottled dusk', hue: 'var(--dracula-green)'},
47
+ ];
48
+
49
+ // ─── Gallery Art ────────────────────────────────────────────────────────────
50
+
51
+ function NightScene({alt, hue, index}: {alt: string; hue: string; index: number}) {
52
+ const moonX = 90 + ((index * 53) % 220);
53
+ const moonY = 62 + ((index * 29) % 60);
54
+ const hillA = 190 + ((index * 13) % 40);
55
+ const hillB = 215 + ((index * 17) % 40);
56
+ return (
57
+ <svg
58
+ viewBox="0 0 400 300"
59
+ preserveAspectRatio="xMidYMid slice"
60
+ style={galleryImage}
61
+ role="img"
62
+ aria-label={alt}>
63
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
64
+ <g fill="var(--dracula-purple)" opacity={0.55}>
65
+ <circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
66
+ <circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
67
+ <circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
68
+ <circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
69
+ </g>
70
+ <circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
71
+ <circle
72
+ cx={moonX - 12}
73
+ cy={moonY - 8}
74
+ r={28}
75
+ fill="var(--dracula-bg-light)"
76
+ opacity={0.55}
77
+ />
78
+ <path
79
+ d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
80
+ fill="var(--dracula-current-line)"
81
+ />
82
+ <path
83
+ d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
84
+ fill="var(--dracula-bg)"
85
+ />
86
+ <g
87
+ transform={`translate(${60 + ((index * 41) % 280)} ${hillB - 34})`}
88
+ fill="none"
89
+ stroke={hue}
90
+ strokeWidth="4"
91
+ strokeLinecap="round"
92
+ strokeLinejoin="round">
93
+ <rect x="-22" y="-22" width="44" height="44" rx={4} />
94
+ <circle cx="9" cy="-9" r="2" fill={hue} stroke="none" />
95
+ <path d="M-17 15 L-4 0 L5 9 L10 4 L17 12" />
96
+ </g>
97
+ </svg>
98
+ );
99
+ }
100
+
101
+ // ─── Stat Block ─────────────────────────────────────────────────────────────
102
+
103
+ function StatBlock({value, label}: {value: string; label: string}) {
104
+ return (
105
+ <VStack gap={0}>
106
+ <Text type="large" weight="bold" hasTabularNumbers>
107
+ {value}
108
+ </Text>
109
+ <Text type="supporting" color="secondary">
110
+ {label}
111
+ </Text>
112
+ </VStack>
113
+ );
114
+ }
115
+
116
+ // ─── Image Grid ─────────────────────────────────────────────────────────────
117
+
118
+ function ImageGrid() {
119
+ return (
120
+ <Grid columns={3} gap={3}>
121
+ {GALLERY_SCENES.map((scene, index) => (
122
+ <AspectRatio key={scene.alt} ratio={1} style={galleryImageClip}>
123
+ <NightScene alt={scene.alt} hue={scene.hue} index={index} />
124
+ </AspectRatio>
125
+ ))}
126
+ </Grid>
127
+ );
128
+ }
129
+
130
+ // ─── Main Page ──────────────────────────────────────────────────────────────
131
+
132
+ export default function SideGallery() {
133
+ return (
134
+ <Layout
135
+ height="fill"
136
+ contentWidth={1400}
137
+ content={
138
+ <LayoutContent padding={6}>
139
+ <Grid columns={{minWidth: 360, repeat: 'fit'}} gap={8} align="center">
140
+ {/* Left side: Text + CTA */}
141
+ <VStack gap={6} vAlign="center">
142
+ <VStack gap={3}>
143
+ <Text type="supporting" color="secondary" weight="semibold">
144
+ AFTER DARK
145
+ </Text>
146
+ <Heading level={1}>
147
+ Make every night a little more spellbinding, one small ritual
148
+ at a time.
149
+ </Heading>
150
+ <Text type="body" color="secondary">
151
+ The smallest rituals are the ones that matter most. A little
152
+ lamplight that catches your eye and makes you pause;
153
+ that&apos;s what turns an ordinary evening into something
154
+ worth remembering.
155
+ </Text>
156
+ </VStack>
157
+
158
+ <HStack gap={3} vAlign="center">
159
+ <Button label="Explore the night" variant="primary" />
160
+ </HStack>
161
+
162
+ <VStack gap={4}>
163
+ <Divider />
164
+ <HStack gap={6}>
165
+ <StatBlock value="12k+" label="Night shots" />
166
+ <StatBlock value="350+" label="Dark builds" />
167
+ <StatBlock value="8yrs" label="Years prowling" />
168
+ </HStack>
169
+ </VStack>
170
+ </VStack>
171
+
172
+ {/* Right side: Image Grid */}
173
+ <ImageGrid />
174
+ </Grid>
175
+ </LayoutContent>
176
+ }
177
+ />
178
+ );
179
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Grouped Table',
4
+ description: 'Collapsible status sections with detail panel',
5
+ category: 'data',
6
+ };