astryx-dracula 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +17 -12
  3. package/USAGE.md +29 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +174 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +299 -237
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,6 +1,22 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // V[g6] > (V[g1] > Hd"Settings"[level=2] + Tx"Profile"[t=body]) + (G[c2 g4] > C[p4] + C[p4])
3
+ // L > (LH[divider] > H[a=center wrap] > (SI[fill] > Hd"Settings"[level=1 t=display-2]) + TY"Search") + (LP[w=260 p=2] > UL > LI*6) + (LC[p=6] > V[g=4] > (V[a=center] > TabList) + ((G[c={min:280} g=8] > (V[g=1] > Hd"Basic information"[level=2] + Tx"View and update your details"[t=body]) + (V[g=4] > TI"Username" + TI"Email address" + (H > B.primary"Save"))) + D)*3)
4
+
5
+ /**
6
+ * Settings — one scrolling page of account sections.
7
+ *
8
+ * Frame: Layout header (title + settings search) | optional 260px section nav |
9
+ * content column of three section grids, divided.
10
+ *
11
+ * Responsive contract:
12
+ * > 768px the section nav is a 260px LayoutPanel beside the content
13
+ * <= 768px the panel is dropped, the nav collapses to a centered TabList
14
+ * above the content, the header search wraps under the title, and
15
+ * every section grid falls to one column (280px floor)
16
+ *
17
+ * Seam rule: nav/content whitespace pairs use hasDivider=false (LayoutPanel
18
+ * beside LayoutContent); dialog chrome and inset panels keep their divider.
19
+ */
4
20
 
5
21
  import {useState} from 'react';
6
22
  import {useMediaQuery} from '@astryxdesign/core/hooks';
@@ -34,6 +50,12 @@ const NAV_ITEMS = [
34
50
  'API',
35
51
  ];
36
52
 
53
+ // WCAG 1.3.5 wants autocomplete on identity fields. TextInput forwards unknown
54
+ // props to the <input>, but its prop type omits input-only attributes, so the
55
+ // attribute is spread in through a widened record.
56
+ const inputAutoComplete = (value: string) =>
57
+ ({autoComplete: value}) as Record<string, string>;
58
+
37
59
  const SETTINGS_ITEMS: SearchableItem[] = [
38
60
  {id: '1', label: 'Username'},
39
61
  {id: '2', label: 'First name'},
@@ -71,17 +93,17 @@ export default function SettingsTemplate() {
71
93
  return (
72
94
  <Layout
73
95
  height="fill"
74
- contentWidth={1440}
96
+ contentWidth={1200}
75
97
  header={
76
- <LayoutHeader hasDivider>
77
- <HStack vAlign="center">
98
+ <LayoutHeader hasDivider padding={6}>
99
+ <HStack vAlign="center" wrap="wrap">
78
100
  <StackItem size="fill">
79
- <Heading level={1}>Settings</Heading>
101
+ <Heading level={1} type="display-2">Settings</Heading>
80
102
  </StackItem>
81
103
  <Typeahead
82
104
  label="Search"
83
105
  isLabelHidden
84
- placeholder="Search coven settings..."
106
+ placeholder="Search coven settings…"
85
107
  searchSource={settingsSearchSource}
86
108
  value={searchValue}
87
109
  onChange={setSearchValue}
@@ -108,7 +130,7 @@ export default function SettingsTemplate() {
108
130
  )
109
131
  }
110
132
  content={
111
- <LayoutContent padding={4}>
133
+ <LayoutContent padding={6}>
112
134
  <VStack gap={4}>
113
135
  {/* Mobile: the sidebar nav collapses to a horizontal, centered
114
136
  tab bar above the content. */}
@@ -128,9 +150,9 @@ export default function SettingsTemplate() {
128
150
  </TabList>
129
151
  </VStack>
130
152
  )}
131
- <Grid columns={{minWidth: 320}} gap={10}>
153
+ <Grid columns={{minWidth: 280}} gap={8}>
132
154
  <VStack gap={1}>
133
- <Heading level={3}>Basic information</Heading>
155
+ <Heading level={2}>Basic information</Heading>
134
156
  <Text type="body" color="secondary">
135
157
  View and update your crypt details and coven account information.
136
158
  </Text>
@@ -153,6 +175,8 @@ export default function SettingsTemplate() {
153
175
  />
154
176
  <TextInput
155
177
  label="Email address"
178
+ type="email"
179
+ {...inputAutoComplete('email')}
156
180
  value={email}
157
181
  onChange={setEmail}
158
182
  />
@@ -164,9 +188,9 @@ export default function SettingsTemplate() {
164
188
 
165
189
  <Divider />
166
190
 
167
- <Grid columns={{minWidth: 320}} gap={10}>
191
+ <Grid columns={{minWidth: 280}} gap={8}>
168
192
  <VStack gap={1}>
169
- <Heading level={3}>Change password</Heading>
193
+ <Heading level={2}>Change password</Heading>
170
194
  <Text type="body" color="secondary">
171
195
  Update your password to keep your coffin sealed.
172
196
  </Text>
@@ -175,18 +199,21 @@ export default function SettingsTemplate() {
175
199
  <TextInput
176
200
  label="Verify current password"
177
201
  type="password"
202
+ {...inputAutoComplete('current-password')}
178
203
  value={currentPw}
179
204
  onChange={setCurrentPw}
180
205
  />
181
206
  <TextInput
182
207
  label="New password"
183
208
  type="password"
209
+ {...inputAutoComplete('new-password')}
184
210
  value={newPw}
185
211
  onChange={setNewPw}
186
212
  />
187
213
  <TextInput
188
214
  label="Confirm password"
189
215
  type="password"
216
+ {...inputAutoComplete('new-password')}
190
217
  value={confirmPw}
191
218
  onChange={setConfirmPw}
192
219
  />
@@ -198,14 +225,14 @@ export default function SettingsTemplate() {
198
225
 
199
226
  <Divider />
200
227
 
201
- <Grid columns={{minWidth: 320}} gap={10}>
228
+ <Grid columns={{minWidth: 280}} gap={8}>
202
229
  <VStack gap={1}>
203
- <Heading level={3}>Advanced settings</Heading>
230
+ <Heading level={2}>Advanced settings</Heading>
204
231
  <Text type="body" color="secondary">
205
232
  Configure detailed coven preferences and warding options.
206
233
  </Text>
207
234
  </VStack>
208
- <VStack gap={5}>
235
+ <VStack gap={4}>
209
236
  <CheckboxInput
210
237
  label="Data Export Access"
211
238
  description="Allow export of personal data and backups."
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // A[cp=0 @topNav=(TN) @sideNav=(SN > TL)] > L > (LH[divider] > H[g=2] > C.muted[p=0]*3) + (LC[p=6] > V[g=2] > (H[g=3 a=center] > C.muted[p=0] + C.muted[p=0])*8)
3
+ // A[cp=0 @topNav=(TN) @sideNav=(SN > TL)] > L > (LH[divider] > H[g=2 wrap] > C.muted[p=0]*3) + (LC[p=6] > V[g=2] > (H[g=3 a=center] > C.muted[p=0] + C.muted[p=0])*3 + C.muted[p=0] + (H[g=3 a=center] > C.muted[p=0] + C.muted[p=0])*4 + C.muted[p=0] + (H[g=3 a=center] > C.muted[p=0] + C.muted[p=0])*5)
4
+ // Note: skeleton placeholders carry no Hd by design (skill skeleton-shell
5
+ // exemption) — routed pages supply the h1.
4
6
 
5
7
  import {Fragment, useState, useMemo, useEffect} from 'react';
6
8
  import {AppShell} from '@astryxdesign/core/AppShell';
@@ -77,9 +79,9 @@ const MENUS: {label: string; groups: MenuEntry[][]}[] = [
77
79
  ['New Window', '⇧⌘N'],
78
80
  ],
79
81
  [
80
- ['Open...', '⌘O'],
82
+ ['Open…', '⌘O'],
81
83
  ['Save', '⌘S'],
82
- ['Save As...', '⇧⌘S'],
84
+ ['Save As…', '⇧⌘S'],
83
85
  ],
84
86
  [['Close Editor', '⌘W']],
85
87
  ],
@@ -141,20 +143,20 @@ const MENUS: {label: string; groups: MenuEntry[][]}[] = [
141
143
  ];
142
144
 
143
145
  const CODE_LINES = [
144
- '38%',
145
- '62%',
146
- '54%',
147
- '0%',
148
- '46%',
149
- '70%',
150
- '58%',
151
- '34%',
152
- '0%',
153
- '50%',
154
- '66%',
155
- '42%',
156
- '60%',
157
- '28%',
146
+ {id: 'line-1', width: '38%'},
147
+ {id: 'line-2', width: '62%'},
148
+ {id: 'line-3', width: '54%'},
149
+ {id: 'line-4', width: '0%'},
150
+ {id: 'line-5', width: '46%'},
151
+ {id: 'line-6', width: '70%'},
152
+ {id: 'line-7', width: '58%'},
153
+ {id: 'line-8', width: '34%'},
154
+ {id: 'line-9', width: '0%'},
155
+ {id: 'line-10', width: '50%'},
156
+ {id: 'line-11', width: '66%'},
157
+ {id: 'line-12', width: '42%'},
158
+ {id: 'line-13', width: '60%'},
159
+ {id: 'line-14', width: '28%'},
158
160
  ];
159
161
 
160
162
  const EDITOR_TABS = ['AppShell.tsx', 'TopNav.tsx', 'theme.ts'];
@@ -203,8 +205,11 @@ export default function ShellNav() {
203
205
  button={{label: menu.label, variant: 'ghost', size: 'sm'}}
204
206
  hasChevron={false}
205
207
  menuWidth={MENU_WIDTH}>
208
+ {/* Keyed by the group's first label, not its position:
209
+ an index key remounts every group when the menu
210
+ data shifts. `gi` only decides the divider. */}
206
211
  {menu.groups.map((group, gi) => (
207
- <Fragment key={gi}>
212
+ <Fragment key={group[0][0]}>
208
213
  {gi > 0 && <Divider />}
209
214
  {group.map(([label, shortcut]) => (
210
215
  <DropdownMenuItem
@@ -249,7 +254,8 @@ export default function ShellNav() {
249
254
  height="fill"
250
255
  header={
251
256
  <LayoutHeader hasDivider padding={6}>
252
- <HStack gap={2}>
257
+ {/* Wraps rather than clipping: three 132px tabs need ~410px. */}
258
+ <HStack gap={2} wrap="wrap">
253
259
  {EDITOR_TABS.map(tab => (
254
260
  <Card
255
261
  key={tab}
@@ -264,18 +270,19 @@ export default function ShellNav() {
264
270
  }
265
271
  content={
266
272
  <LayoutContent padding={6}>
273
+ {/* Skeleton chrome only — the open file supplies the h1. */}
267
274
  <VStack gap={2}>
268
- {CODE_LINES.map((width, i) =>
269
- width === '0%' ? (
275
+ {CODE_LINES.map(line =>
276
+ line.width === '0%' ? (
270
277
  <Card
271
- key={i}
278
+ key={line.id}
272
279
  variant="muted"
273
280
  padding={0}
274
281
  width={1}
275
282
  height={14}
276
283
  />
277
284
  ) : (
278
- <HStack key={i} gap={3} vAlign="center">
285
+ <HStack key={line.id} gap={3} vAlign="center">
279
286
  <Card
280
287
  variant="muted"
281
288
  padding={0}
@@ -285,7 +292,7 @@ export default function ShellNav() {
285
292
  <Card
286
293
  variant="muted"
287
294
  padding={0}
288
- width={width}
295
+ width={line.width}
289
296
  height={14}
290
297
  />
291
298
  </HStack>
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // A[cp=0 @sideNav=(SN > SNI"New chat" + SNI"Search" + SNI"Library" + D + (SNI"Personal" > SNI"Weekend trip planning")*3)] > L > (LC[p=6] > V[g=5] > (H > C.muted[p=0])*4) + (LF > TI"Message Night Owl")
3
+ // A[cp=0 @sideNav=(SN > SNI"New chat" + SNI"Search" + SNI"Library" + D + (SNI"Personal" > SNI"Full-moon rite planning")*3)] > L > (LC[p=6] > V[g=5] > (H > C.muted[p=0])*4) + (LF > TI"Message Night Owl")
4
+ // Note: skeleton placeholders carry no Hd by design (skill skeleton-shell
5
+ // exemption) — routed pages supply the h1.
4
6
 
5
7
  import {useState} from 'react';
6
8
  import {AppShell} from '@astryxdesign/core/AppShell';
@@ -50,39 +52,43 @@ const WORKSPACES: Workspace[] = [
50
52
  icon: User,
51
53
  chats: [
52
54
  {
53
- label: 'Weekend trip planning',
55
+ label: 'Full-moon rite planning',
54
56
  status: 'success',
55
57
  statusLabel: 'Active',
56
58
  },
57
59
  {
58
- label: 'Recipe ideas for the week',
60
+ label: 'Potion recipes for the week',
59
61
  status: 'neutral',
60
62
  statusLabel: 'Idle',
61
63
  },
62
64
  {
63
- label: 'Book recommendations',
65
+ label: 'Grimoire recommendations',
64
66
  status: 'warning',
65
67
  statusLabel: 'Needs review',
66
68
  },
67
- {label: 'Home workout plan', status: 'neutral', statusLabel: 'Idle'},
69
+ {label: 'Crypt workout plan', status: 'neutral', statusLabel: 'Idle'},
68
70
  ],
69
71
  },
70
72
  {
71
- name: 'Acme Corp',
73
+ name: 'Night Watch',
72
74
  icon: Building2,
73
75
  chats: [
74
- {label: 'Q3 roadmap draft', status: 'accent', statusLabel: 'In progress'},
75
76
  {
76
- label: 'Customer onboarding flow',
77
+ label: 'Crypt expansion draft',
78
+ status: 'info',
79
+ statusLabel: 'In progress',
80
+ },
81
+ {
82
+ label: 'Neonate welcoming rite',
77
83
  status: 'success',
78
84
  statusLabel: 'Active',
79
85
  },
80
86
  {
81
- label: 'Pricing strategy review',
87
+ label: 'Tribute ledger review',
82
88
  status: 'warning',
83
89
  statusLabel: 'Needs review',
84
90
  },
85
- {label: 'Standup summary', status: 'neutral', statusLabel: 'Idle'},
91
+ {label: 'Night-watch summary', status: 'neutral', statusLabel: 'Idle'},
86
92
  ],
87
93
  },
88
94
  {
@@ -90,12 +96,12 @@ const WORKSPACES: Workspace[] = [
90
96
  icon: CodeXml,
91
97
  chats: [
92
98
  {
93
- label: 'StyleX migration notes',
94
- status: 'accent',
99
+ label: 'Coven theme migration notes',
100
+ status: 'info',
95
101
  statusLabel: 'In progress',
96
102
  },
97
103
  {
98
- label: 'Skeleton loading states',
104
+ label: 'Crypt loading states',
99
105
  status: 'success',
100
106
  statusLabel: 'Active',
101
107
  },
@@ -105,16 +111,18 @@ const WORKSPACES: Workspace[] = [
105
111
  },
106
112
  ];
107
113
 
108
- const SELECTED_CHAT = 'StyleX migration notes';
114
+ const SELECTED_CHAT = 'Coven theme migration notes';
109
115
  // Same-route hash: demo links stay focusable anchors without escaping the
110
116
  // template through the hash router (bare "#" would drop back to the home page).
111
117
  const SELF_HASH = '#/templates/shell-side-nav';
112
118
 
119
+ // `id` carries the React key: the bubbles are positional placeholders, and an
120
+ // index key would remount every row below an inserted one.
113
121
  const MESSAGES = [
114
- {role: 'assistant', width: '78%', height: 104},
115
- {role: 'user', width: '48%', height: 48},
116
- {role: 'assistant', width: '64%', height: 132},
117
- {role: 'user', width: '38%', height: 40},
122
+ {id: 'context', role: 'assistant', width: '78%', height: 104},
123
+ {id: 'follow-up', role: 'user', width: '48%', height: 48},
124
+ {id: 'answer', role: 'assistant', width: '64%', height: 132},
125
+ {id: 'reply', role: 'user', width: '38%', height: 40},
118
126
  ];
119
127
 
120
128
  function ConversationItem({
@@ -206,10 +214,11 @@ export default function ShellSideNav() {
206
214
  contentWidth={768}
207
215
  content={
208
216
  <LayoutContent padding={6}>
217
+ {/* Skeleton chrome only — the routed conversation supplies the h1. */}
209
218
  <VStack gap={5}>
210
- {MESSAGES.map((message, mi) => (
219
+ {MESSAGES.map(message => (
211
220
  <HStack
212
- key={mi}
221
+ key={message.id}
213
222
  hAlign={message.role === 'assistant' ? 'start' : 'end'}>
214
223
  <Card
215
224
  variant="muted"
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // A[cp=6 @topNav=(TN > TNI"Shop" + TNI"Brands" + TNI"Sale" + TNI"Service")] > V[g=10] > C.muted[p=0] + (V[g=4] > C.muted[p=0] + (G[c={min:160} g=4] > (V[g=2] > C.muted[p=0] + C.muted[p=0])*6))*3
3
+ // A[cp=6 @topNav=(TN > TNI"Shop" + TNI"Brands" + TNI"Sale" + TNI"Service")] > Ctr[axis=horizontal] > S[mw=1100 w=100% p=0] > V[g=10] > C.muted[p=0 w=100% h=360] + (V[g=4] > C.muted[p=0 w=200 h=24] + (G[c={min:160} g=4] > (V[g=2] > C.muted[p=0 w=100% h=120] + C.muted[p=0 w=60% h=14])*6))*3
4
+ // Note: skeleton placeholders carry no Hd by design (skill skeleton-shell
5
+ // exemption) — routed pages supply the h1.
4
6
 
5
7
  import type {CSSProperties} from 'react';
6
8
  import {AppShell} from '@astryxdesign/core/AppShell';
@@ -21,7 +23,9 @@ import {IconButton} from '@astryxdesign/core/IconButton';
21
23
  import {Button} from '@astryxdesign/core/Button';
22
24
  import {Badge} from '@astryxdesign/core/Badge';
23
25
  import {Card} from '@astryxdesign/core/Card';
26
+ import {Center} from '@astryxdesign/core/Center';
24
27
  import {Grid} from '@astryxdesign/core/Grid';
28
+ import {Section} from '@astryxdesign/core/Section';
25
29
  import {Stack, VStack} from '@astryxdesign/core/Stack';
26
30
  import {
27
31
  ShoppingBag,
@@ -44,7 +48,7 @@ import {
44
48
  } from 'lucide-react';
45
49
 
46
50
  // Cap + center the page body so wide screens show whitespace gutters.
47
- const contentMax: CSSProperties = {maxWidth: 1100, marginInline: 'auto'};
51
+ const CONTENT_MAX_WIDTH = 1100;
48
52
  // Same-route hash: demo links stay focusable anchors without escaping the
49
53
  // template through the hash router (bare "#" would drop back to the home page).
50
54
  const SELF_HASH = '#/templates/shell-top-nav';
@@ -52,51 +56,60 @@ const SELF_HASH = '#/templates/shell-top-nav';
52
56
  // Brands size to their own content (different widths); since both anchor to
53
57
  // the centered nav, switching between them resizes the panel — which reads
54
58
  // as flashing/jumping. Fixed item + featured widths make the panels
55
- // pixel-identical so the transition is seamless.
59
+ // pixel-identical so the transition is seamless. The pair needs roughly
60
+ // 800px of clear space, so narrower viewports fall back to natural-width
61
+ // single columns (see MegaItems) instead of painting past the edge.
56
62
  const megaItems: CSSProperties = {gridColumn: '1 / -1', width: 520};
57
63
  const megaFeatured: CSSProperties = {width: 240};
58
64
 
65
+ // Three identical shelves; the ids keep React keys stable if they reorder.
66
+ const SHELVES = ['new-in', 'featured', 'sale'];
67
+
59
68
  type MegaItem = {name: string; tagline: string; icon: IconType};
60
69
 
61
70
  // Shop and Brands each render 8 items — the mega menu's built-in 2-column grid
62
71
  // lays them out as 2 columns × 4 rows, alongside a featured card.
63
72
  const SHOP_ITEMS: MegaItem[] = [
64
- {name: 'New Arrivals', tagline: 'The latest drops', icon: Sparkles},
65
- {name: 'Womenswear', tagline: 'Dresses, knitwear & more', icon: SwatchBook},
66
- {name: 'Menswear', tagline: 'Shirts, tailoring & more', icon: Tag},
67
- {name: 'Home', tagline: 'Bedding, lighting & décor', icon: House},
73
+ {name: 'New Moon Arrivals', tagline: 'Fresh from the kiln', icon: Sparkles},
74
+ {
75
+ name: 'Gowns & Cloaks',
76
+ tagline: 'Dresses, knitwear & moonlit layers',
77
+ icon: SwatchBook,
78
+ },
79
+ {name: 'Doublets & Tailoring', tagline: 'Shirts, cloaks & more', icon: Tag},
80
+ {name: 'Keep & Crypt', tagline: 'Bedding, candlelight & décor', icon: House},
68
81
  {
69
- name: 'Beauty',
70
- tagline: 'Skincare, fragrance & makeup',
82
+ name: 'Potions',
83
+ tagline: 'Salves, scents & powders',
71
84
  icon: Smile,
72
85
  },
73
86
  {
74
- name: 'Accessories',
75
- tagline: 'Bags, hats & sunglasses',
87
+ name: 'Relics',
88
+ tagline: 'Satchels, hats & moonshades',
76
89
  icon: ShoppingBag,
77
90
  },
78
91
  {name: 'Sale', tagline: 'Up to 50% off', icon: BadgePercent},
79
- {name: 'Gift Cards', tagline: 'The perfect present', icon: Gift},
92
+ {name: 'Gift Tokens', tagline: 'The perfect tribute', icon: Gift},
80
93
  ];
81
94
 
82
95
  const BRAND_ITEMS: MegaItem[] = [
83
- {name: 'Aether', tagline: 'Performance essentials', icon: Sparkles},
84
- {name: 'Northwind', tagline: 'Outdoor & technical', icon: Cloud},
85
- {name: 'Loomwell', tagline: 'Everyday knitwear', icon: Zap},
86
- {name: 'Verdant', tagline: 'Sustainable basics', icon: Sun},
87
- {name: 'Studio Mara', tagline: 'Modern tailoring', icon: Star},
88
- {name: 'Atelier Kos', tagline: 'Limited ateliers', icon: Flame},
89
- {name: 'Rue & Co', tagline: 'City streetwear', icon: Globe},
90
- {name: 'Halden', tagline: 'Minimal staples', icon: Moon},
96
+ {name: 'Aether', tagline: 'Moonlit essentials', icon: Sparkles},
97
+ {name: 'Northcrypt', tagline: 'Cold-weather & travel', icon: Cloud},
98
+ {name: 'Loomwell', tagline: 'Everyday knitwear, coven-stitched', icon: Zap},
99
+ {name: 'Verdant', tagline: 'Earth-kept basics', icon: Sun},
100
+ {name: 'Studio Mara', tagline: 'Modern tailoring for the night', icon: Star},
101
+ {name: 'Atelier Kos', tagline: 'Small-batch ateliers', icon: Flame},
102
+ {name: 'Rue & Co', tagline: 'Old-town streetwear', icon: Globe},
103
+ {name: 'Halden', tagline: 'Spare, lasting staples', icon: Moon},
91
104
  ];
92
105
 
93
106
  const CATEGORY_TILES = [
94
- 'New Arrivals',
95
- 'Womenswear',
96
- 'Menswear',
97
- 'Home & Living',
98
- 'Beauty',
99
- 'Accessories',
107
+ 'New Moon Arrivals',
108
+ 'Gowns & Cloaks',
109
+ 'Doublets & Tailoring',
110
+ 'Keep & Crypt',
111
+ 'Potions',
112
+ 'Relics',
100
113
  ];
101
114
 
102
115
  // Wraps the 8 items in a fixed-width 2-column grid so every mega menu's item
@@ -104,9 +117,12 @@ const CATEGORY_TILES = [
104
117
  function MegaItems({items}: {items: MegaItem[]}) {
105
118
  // In the mobile drawer the fixed 520px panel would overflow the ~350px
106
119
  // drawer, and the 2-column grid overlaps item text there — so the drawer
107
- // gets a natural-width single column. Desktop keeps the lock.
120
+ // gets a natural-width single column. The same goes for viewports too
121
+ // narrow to seat the locked desktop panel, which would otherwise paint
122
+ // past the edge: below 1024px the popover keeps the natural-width column.
108
123
  const isDrawer = useTopNavRenderMode() === 'drawer';
109
- if (isDrawer) {
124
+ const isCompact = useMediaQuery('(max-width: 1024px)');
125
+ if (isDrawer || isCompact) {
110
126
  return (
111
127
  <VStack gap={1}>
112
128
  {items.map(item => (
@@ -146,8 +162,9 @@ function MegaFeatured(props: {
146
162
  linkHref: string;
147
163
  }) {
148
164
  const isDrawer = useTopNavRenderMode() === 'drawer';
165
+ const isCompact = useMediaQuery('(max-width: 1024px)');
149
166
  return (
150
- <Stack style={isDrawer ? undefined : megaFeatured}>
167
+ <Stack style={isDrawer || isCompact ? undefined : megaFeatured}>
151
168
  <TopNavMegaMenuFeaturedCard {...props} />
152
169
  </Stack>
153
170
  );
@@ -223,23 +240,42 @@ export default function ShellTopNav() {
223
240
  }
224
241
  />
225
242
  }>
226
- <VStack gap={10} style={contentMax}>
227
- <Card variant="muted" padding={0} width="100%" height={360} />
243
+ <Center axis="horizontal">
244
+ <Section
245
+ variant="transparent"
246
+ maxWidth={CONTENT_MAX_WIDTH}
247
+ width="100%"
248
+ padding={0}>
249
+ <VStack gap={10}>
250
+ {/* Skeleton shelves only — the routed collection supplies the h1. */}
251
+ <Card variant="muted" padding={0} width="100%" height={360} />
228
252
 
229
- {[0, 1, 2].map(section => (
230
- <VStack key={section} gap={4}>
231
- <Card variant="muted" padding={0} width={200} height={24} />
232
- <Grid columns={{minWidth: 160, repeat: 'fit'}} gap={4}>
233
- {CATEGORY_TILES.map(tile => (
234
- <VStack key={tile} gap={2}>
235
- <Card variant="muted" padding={0} width="100%" height={120} />
236
- <Card variant="muted" padding={0} width="60%" height={14} />
237
- </VStack>
238
- ))}
239
- </Grid>
253
+ {SHELVES.map(shelf => (
254
+ <VStack key={shelf} gap={4}>
255
+ <Card variant="muted" padding={0} width={200} height={24} />
256
+ <Grid columns={{minWidth: 160, repeat: 'fit'}} gap={4}>
257
+ {CATEGORY_TILES.map(tile => (
258
+ <VStack key={tile} gap={2}>
259
+ <Card
260
+ variant="muted"
261
+ padding={0}
262
+ width="100%"
263
+ height={120}
264
+ />
265
+ <Card
266
+ variant="muted"
267
+ padding={0}
268
+ width="60%"
269
+ height={14}
270
+ />
271
+ </VStack>
272
+ ))}
273
+ </Grid>
274
+ </VStack>
275
+ ))}
240
276
  </VStack>
241
- ))}
242
- </VStack>
277
+ </Section>
278
+ </Center>
243
279
  </AppShell>
244
280
  );
245
281
  }