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
@@ -35,6 +35,7 @@ import {Token} from '@astryxdesign/core/Token';
35
35
  import {Button} from '@astryxdesign/core/Button';
36
36
  import {Icon} from '@astryxdesign/core/Icon';
37
37
  import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
38
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
38
39
  import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
39
40
  import {MoreMenu} from '@astryxdesign/core/MoreMenu';
40
41
  import {Toolbar} from '@astryxdesign/core/Toolbar';
@@ -58,10 +59,9 @@ import {
58
59
  const MOBILE_MAX_WIDTH = 1023;
59
60
 
60
61
  const root: CSSProperties = {
61
- // The showcase renders a 44px breadcrumb row above the template, so a full
62
- // 100dvh root always spills exactly that far into page-level scroll. Subtract
63
- // it so scrolling stays inside the message list and artifact body.
64
- height: 'calc(100dvh - 44px)',
62
+ // The viewer owns its chrome: the root fills the viewport and the message
63
+ // list plus artifact body scroll inside it (editor pattern).
64
+ height: '100dvh',
65
65
  width: '100%',
66
66
  containerType: 'inline-size',
67
67
  containerName: 'artifact',
@@ -76,8 +76,11 @@ const chatLayout: CSSProperties = {
76
76
  flex: 1,
77
77
  minHeight: 0,
78
78
  };
79
+ // ChatMessage already separates its children by --spacing-2, so this doubles
80
+ // that step to set the attached document apart from its bubble. --space-gap
81
+ // (24px) is a layout gutter, not an in-message rhythm.
79
82
  const artifactCard: CSSProperties = {
80
- marginBlockStart: 'var(--space-gap)',
83
+ marginBlockStart: 'var(--spacing-2)',
81
84
  };
82
85
  const artifactScroll: CSSProperties = {
83
86
  flex: 1,
@@ -132,7 +135,7 @@ const ARTIFACT_SUBTITLE = 'Grimoire · Sealed just now';
132
135
 
133
136
  const ARTIFACT_CONTENT = `## Overview
134
137
 
135
- Our API gateway authenticates every request with a short-lived JWT access token. Until now, an expired token meant an immediate \`401\` — even when the user still held a valid refresh token. This document describes the silent-refresh flow we just shipped and how we're rolling it out.
138
+ Our API gateway authenticates every request with a short-lived JWT access token. Until now, an expired token meant an immediate \`401\`, even when the user still held a valid refresh token. This document describes the silent-refresh flow we just shipped and how we're rolling it out.
136
139
 
137
140
  ## The Problem
138
141
 
@@ -140,7 +143,7 @@ Token validation ran **before** any refresh logic, so the middleware rejected ex
140
143
 
141
144
  1. A request arrives with an expired access token
142
145
  2. \`validateToken()\` throws \`TokenExpiredError\`
143
- 3. The catch block returns \`401\` — \`refreshToken()\` is never reached
146
+ 3. The catch block returns \`401\`: \`refreshToken()\` is never reached
144
147
 
145
148
  The result was users getting logged out whenever an access token lapsed mid-session.
146
149
 
@@ -148,9 +151,9 @@ The result was users getting logged out whenever an access token lapsed mid-sess
148
151
 
149
152
  The middleware now catches \`TokenExpiredError\` specifically and attempts a silent refresh before rejecting. On success it reissues an access token and continues the request; on failure it falls back to \`401\`.
150
153
 
151
- - **Transparent** — valid sessions never see an interruption
152
- - **Safe** — a missing or invalid refresh token still returns \`401\`
153
- - **Cheap** — refresh only runs on the expiry path, not on every request
154
+ - **Transparent**: valid sessions never see an interruption
155
+ - **Safe**: a missing or invalid refresh token still returns \`401\`
156
+ - **Cheap**: refresh only runs on the expiry path, not on every request
154
157
 
155
158
  ## Testing
156
159
 
@@ -236,12 +239,24 @@ function MobileArtifactActions() {
236
239
  );
237
240
  }
238
241
 
239
- // Scrollable artifact body — the formatted document.
240
- function ArtifactBody() {
242
+ // Scrollable artifact body — the formatted document. The mobile dialog renders
243
+ // the title in its DialogHeader, so it drops the in-body heading rather than
244
+ // announcing the same h1 twice.
245
+ function ArtifactBody({hasOwnTitle = true}: {hasOwnTitle?: boolean}) {
241
246
  return (
242
- <Section variant="transparent" style={artifactScroll}>
247
+ <Section
248
+ variant="transparent"
249
+ style={artifactScroll}
250
+ role="region"
251
+ aria-label={ARTIFACT_TITLE}
252
+ tabIndex={0}>
243
253
  <VStack gap={2} style={articleBody}>
244
- <Heading level={1}>{ARTIFACT_TITLE}</Heading>
254
+ {/* Page owns the h1 (VisuallyHidden "Night thread"); the panel title is h2. */}
255
+ {hasOwnTitle && (
256
+ <Heading level={2} type="display-2">
257
+ {ARTIFACT_TITLE}
258
+ </Heading>
259
+ )}
245
260
  <Markdown>{ARTIFACT_CONTENT}</Markdown>
246
261
  </VStack>
247
262
  </Section>
@@ -362,6 +377,7 @@ export default function AiChat() {
362
377
  }
363
378
  />
364
379
  }>
380
+ <VisuallyHidden as="h1">Night thread</VisuallyHidden>
365
381
  <ChatMessageList>
366
382
  {/* Date divider */}
367
383
  <ChatSystemMessage variant="divider">
@@ -387,7 +403,7 @@ export default function AiChat() {
387
403
  }>
388
404
  <ChatTokenizedText tokens={MENTION_TOKENS}>
389
405
  @agent Can you review these auth files? The JWT
390
- refresh logic seems broken — tokens expire but the
406
+ refresh logic seems broken, tokens expire but the
391
407
  middleware doesn't catch it.
392
408
  </ChatTokenizedText>
393
409
  </ChatMessageBubble>
@@ -433,7 +449,7 @@ Here's the problematic sequence:
433
449
 
434
450
  1. Request arrives with an expired access token
435
451
  2. \`validateToken()\` throws \`TokenExpiredError\`
436
- 3. The catch block returns \`401\` — never reaching \`refreshToken()\`
452
+ 3. The catch block returns \`401\`, never reaching \`refreshToken()\`
437
453
 
438
454
  The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh before rejecting:`}</Markdown>
439
455
  </ChatMessageBubble>
@@ -619,7 +635,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
619
635
  avatar={<Avatar name="Agent" size="md" />}>
620
636
  <ChatMessageBubble variant="ghost">
621
637
  <Markdown density="compact">
622
- {`I've drafted a design doc covering the problem, the fix, and the test matrix — pulling straight from the changes we just made.\n\nOpen the document below to review it. Want me to expand any section?`}
638
+ {`I've drafted a design doc covering the problem, the fix, and the test matrix, pulling straight from the changes we just made.\n\nOpen the document below to review it. Want me to expand any section?`}
623
639
  </Markdown>
624
640
  </ChatMessageBubble>
625
641
  <ArtifactCard onOpen={openArtifact} />
@@ -658,7 +674,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
658
674
  avatar={<Avatar name="Agent" size="md" />}>
659
675
  <ChatMessageBubble variant="ghost">
660
676
  <Markdown density="compact">
661
- {`On it — adding a **Rollout & Monitoring** section with a staged flag ramp and the alert thresholds. Updating the document now.`}
677
+ {`On it: adding a **Rollout & Monitoring** section with a staged flag ramp and the alert thresholds. Updating the document now.`}
662
678
  </Markdown>
663
679
  </ChatMessageBubble>
664
680
  <ChatToolCalls
@@ -745,6 +761,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
745
761
  purpose="info"
746
762
  variant="fullscreen">
747
763
  <Layout
764
+ height="auto"
748
765
  header={
749
766
  <DialogHeader
750
767
  title={ARTIFACT_TITLE}
@@ -756,7 +773,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
756
773
  }
757
774
  content={
758
775
  <LayoutContent padding={0}>
759
- <ArtifactBody />
776
+ <ArtifactBody hasOwnTitle={false} />
760
777
  </LayoutContent>
761
778
  }
762
779
  />
@@ -1,16 +1,17 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > Tx.lg"A blank crypt awaits its story"
3
+ // L[h=auto] > LC > Hd"A blank crypt awaits its story"[level=1]
4
4
 
5
5
  import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
6
- import {Text} from '@astryxdesign/core/Text';
6
+ import {Heading} from '@astryxdesign/core/Text';
7
7
 
8
8
  export default function BlankPage() {
9
9
  return (
10
10
  <Layout
11
+ height="auto"
11
12
  content={
12
13
  <LayoutContent>
13
- <Text type="large">A blank crypt awaits its story</Text>
14
+ <Heading level={1} type="display-2">A blank crypt awaits its story</Heading>
14
15
  </LayoutContent>
15
16
  }
16
17
  />
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC > (V[g6 a=center] > Hd"Hero"[level=1] + Tx"Remit"[t=body] + (H[j=center g3] > B.primary"CTA" + B"Secondary"))
3
+ // L > LC[p=6] > V[g=10] > ((V[g=6 a=center] > (V[g=3 a=center] > Hd"Small joys for creatures of the night"[level=1 t=display-2] + Tx"Sometimes all it takes is one small ritual to turn the whole night around."[t=body] + Tx"astryx-dracula v0.3.0"[t=supporting]) + (H[j=center g=3] > B.primary"Embrace the night" + B.secondary"Read the lore")) + (S.transparent[p=0] > AR[ratio=16/9]))
4
4
 
5
5
  import type {CSSProperties} from 'react';
6
6
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -10,12 +10,11 @@ import {Icon} from '@astryxdesign/core/Icon';
10
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
11
11
  import {Section} from '@astryxdesign/core/Section';
12
12
  import {ArrowRight} from 'lucide-react';
13
+ import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
13
14
 
14
- const heroImage: CSSProperties = {
15
- width: '100%',
16
- height: '100%',
17
- display: 'block',
18
- };
15
+ // Wide castle banner lives in shared/scene-castle (`wide` variant: harvest
16
+ // moon, fg-stroke bats, comment first star group). Frame keeps the page radius
17
+ // + separator border; the scene fills the AspectRatio box via gallery-image.
19
18
  const heroFrame: CSSProperties = {
20
19
  maxWidth: 1200,
21
20
  marginInline: 'auto',
@@ -24,127 +23,10 @@ const heroFrame: CSSProperties = {
24
23
  overflow: 'clip',
25
24
  };
26
25
 
27
- function NightCastleScene() {
28
- return (
29
- <svg
30
- style={heroImage}
31
- viewBox="0 0 800 450"
32
- role="img"
33
- aria-label="Moonlit castle on a hill in Dracula theme colors">
34
- <title>Castle under a harvest moon</title>
35
- <rect width="800" height="450" fill="var(--dracula-bg-dark)" />
36
- {/* Stars */}
37
- <g fill="var(--dracula-purple)">
38
- <circle cx="60" cy="60" r="2.5" />
39
- <circle cx="180" cy="120" r="2" />
40
- <circle cx="330" cy="50" r="2.5" />
41
- <circle cx="470" cy="90" r="2" />
42
- <circle cx="700" cy="200" r="2.5" />
43
- <circle cx="750" cy="80" r="2" />
44
- </g>
45
- <g fill="var(--dracula-cyan)">
46
- <circle cx="120" cy="90" r="2" />
47
- <circle cx="270" cy="70" r="2.5" />
48
- <circle cx="540" cy="50" r="2" />
49
- <circle cx="660" cy="160" r="2" />
50
- </g>
51
- <g fill="var(--dracula-pink)">
52
- <circle cx="90" cy="170" r="2" />
53
- <circle cx="400" cy="130" r="2.5" />
54
- <circle cx="590" cy="200" r="2" />
55
- <circle cx="730" cy="260" r="2" />
56
- </g>
57
- {/* Harvest moon */}
58
- <circle cx="620" cy="110" r="56" fill="var(--dracula-yellow)" />
59
- <circle
60
- cx="600"
61
- cy="95"
62
- r="10"
63
- fill="var(--dracula-orange)"
64
- opacity={0.5}
65
- />
66
- <circle
67
- cx="638"
68
- cy="125"
69
- r="7"
70
- fill="var(--dracula-orange)"
71
- opacity={0.5}
72
- />
73
- {/* Bats */}
74
- <g
75
- fill="none"
76
- stroke="var(--dracula-purple)"
77
- strokeWidth={4}
78
- strokeLinecap="round">
79
- <path d="M120 130 q12 -12 24 0 q12 -12 24 0" />
80
- <path d="M210 90 q9 -9 18 0 q9 -9 18 0" />
81
- <path d="M470 160 q9 -9 18 0 q9 -9 18 0" />
82
- </g>
83
- {/* Hills */}
84
- <path
85
- d="M0 330 Q200 260 400 320 T800 300 V450 H0 Z"
86
- fill="var(--dracula-selection)"
87
- />
88
- <path
89
- d="M0 385 Q240 320 480 375 T800 360 V450 H0 Z"
90
- fill="var(--dracula-bg-light)"
91
- />
92
- {/* Castle */}
93
- <g>
94
- <rect
95
- x="150"
96
- y="225"
97
- width="120"
98
- height="120"
99
- rx={4}
100
- fill="var(--dracula-bg-lighter)"
101
- />
102
- <rect
103
- x="130"
104
- y="185"
105
- width="44"
106
- height="160"
107
- rx={4}
108
- fill="var(--dracula-bg-lighter)"
109
- />
110
- <rect
111
- x="246"
112
- y="185"
113
- width="44"
114
- height="160"
115
- rx={4}
116
- fill="var(--dracula-bg-lighter)"
117
- />
118
- <g fill="var(--dracula-bg-lighter)">
119
- <rect x="130" y="175" width="12" height="14" rx={2} />
120
- <rect x="149" y="175" width="12" height="14" rx={2} />
121
- <rect x="246" y="175" width="12" height="14" rx={2} />
122
- <rect x="265" y="175" width="12" height="14" rx={2} />
123
- <rect x="150" y="215" width="14" height="16" rx={2} />
124
- <rect x="173" y="215" width="14" height="16" rx={2} />
125
- <rect x="196" y="215" width="14" height="16" rx={2} />
126
- <rect x="219" y="215" width="14" height="16" rx={2} />
127
- <rect x="242" y="215" width="14" height="16" rx={2} />
128
- </g>
129
- <g fill="var(--dracula-yellow)">
130
- <rect x="168" y="255" width="14" height="20" rx={2} />
131
- <rect x="203" y="255" width="14" height="20" rx={2} />
132
- <rect x="238" y="255" width="14" height="20" rx={2} />
133
- <rect x="143" y="215" width="10" height="14" rx={2} />
134
- <rect x="259" y="215" width="10" height="14" rx={2} />
135
- </g>
136
- <path
137
- d="M195 345 v-30 a15 15 0 0 1 30 0 v30 Z"
138
- fill="var(--dracula-bg-dark)"
139
- />
140
- </g>
141
- </svg>
142
- );
143
- }
144
-
145
26
  export default function CenteredHero() {
146
27
  return (
147
28
  <Layout
29
+ height="auto"
148
30
  content={
149
31
  <LayoutContent padding={6}>
150
32
  <VStack gap={10}>
@@ -165,6 +47,13 @@ export default function CenteredHero() {
165
47
  Sometimes all it takes is one small ritual to turn the whole
166
48
  night around.
167
49
  </Text>
50
+ <Text
51
+ type="supporting"
52
+ color="secondary"
53
+ justify="center"
54
+ hasTabularNumbers>
55
+ astryx-dracula v0.3.0
56
+ </Text>
168
57
  </VStack>
169
58
  <HStack gap={3} wrap="wrap" justify="center">
170
59
  <Button
@@ -179,7 +68,7 @@ export default function CenteredHero() {
179
68
  </VStack>
180
69
  <Section variant="transparent" padding={0}>
181
70
  <AspectRatio ratio={16 / 9} style={heroFrame}>
182
- <NightCastleScene />
71
+ <SceneCastle variant="wide" />
183
72
  </AspectRatio>
184
73
  </Section>
185
74
  </VStack>
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
  // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // L > LC[p=0] > Ctr > V[g=8] > (V[g=4 a=center] > (V[g=2 a=center] > Hd"Hung after dark"[level=1] + Tx"A coven-curated wall"[t=body]) + (TL > Tab"All"! + Tab"Lifestyle" + Tab"Scenery" + Tab"Home") + Tx"10 pieces on the wall"[t=supporting]) + (G[g=4] > (C[p=0] > AR)*6)
3
+ // L > LC[p=0] > Ctr > V[g=8] > ((Ctr > S.transparent > V[g=4 a=center] > (V[g=2 a=center] > Hd"Hung after dark"[level=1] + Tx"A coven-curated wall"[t=body]) + (TL > Tab"All"! + Tab"Lifestyle" + Tab"Scenery" + Tab"Home") + Tx"10 pieces on the wall"[t=body]) + (G[c={min:260} g=4] > (C[p=0] > AR[ratio=3/2])*10))
4
4
 
5
5
  import {useState, type CSSProperties} from 'react';
6
6
  import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -11,6 +11,9 @@ import {Text, Heading} from '@astryxdesign/core/Text';
11
11
  import {Grid} from '@astryxdesign/core/Grid';
12
12
  import {Section} from '@astryxdesign/core/Section';
13
13
  import {TabList, Tab} from '@astryxdesign/core/TabList';
14
+ import {SceneTile} from 'astryx-dracula/shared/scene-tile';
15
+ import {SCENE_TILE_ALTS} from 'astryx-dracula/shared/scene-hues';
16
+ import type {SceneHue} from 'astryx-dracula/shared/scene-hues';
14
17
 
15
18
  // ─── Styles ─────────────────────────────────────────────────────────────────
16
19
 
@@ -23,11 +26,6 @@ const outer: CSSProperties = {
23
26
  const frameClip: CSSProperties = {
24
27
  overflow: 'clip',
25
28
  };
26
- const artImage: CSSProperties = {
27
- width: '100%',
28
- height: '100%',
29
- display: 'block',
30
- };
31
29
  const textCenter: CSSProperties = {
32
30
  textAlign: 'center',
33
31
  };
@@ -42,76 +40,32 @@ interface GalleryImage {
42
40
  }
43
41
 
44
42
  // Dracula accent per gallery shelf.
45
- const CATEGORY_HUES: Record<Exclude<Category, 'all'>, string> = {
43
+ const CATEGORY_HUES: Record<Exclude<Category, 'all'>, SceneHue> = {
46
44
  scene: 'var(--dracula-cyan)',
47
45
  lifestyle: 'var(--dracula-pink)',
48
46
  home: 'var(--dracula-yellow)',
49
47
  };
50
48
 
51
49
  const GALLERY_IMAGES: GalleryImage[] = [
52
- {alt: 'Moonlit ridge trail under a harvest moon', category: 'scene'},
53
- {alt: 'Late portrait in violet lamplight', category: 'lifestyle'},
54
- {alt: 'Lamplit reading nook after midnight', category: 'home'},
55
- {alt: 'Fog rolling over the night pines', category: 'scene'},
56
- {alt: 'Dancer caught mid-step in stage pink', category: 'lifestyle'},
57
- {alt: 'Kitchen table set for a midnight feast', category: 'lifestyle'},
58
- {alt: 'Castle silhouette over sleeping hills', category: 'scene'},
59
- {alt: 'Attic window glowing amber at 2am', category: 'home'},
60
- {alt: 'Cellar shelves lined with bottled dusk', category: 'home'},
50
+ {alt: SCENE_TILE_ALTS[0], category: 'scene'},
51
+ {alt: SCENE_TILE_ALTS[1], category: 'lifestyle'},
52
+ {alt: SCENE_TILE_ALTS[2], category: 'home'},
53
+ {alt: SCENE_TILE_ALTS[3], category: 'scene'},
54
+ {alt: SCENE_TILE_ALTS[4], category: 'lifestyle'},
55
+ {alt: SCENE_TILE_ALTS[5], category: 'lifestyle'},
56
+ {alt: SCENE_TILE_ALTS[6], category: 'scene'},
57
+ {alt: SCENE_TILE_ALTS[7], category: 'home'},
58
+ {alt: SCENE_TILE_ALTS[8], category: 'home'},
61
59
  {alt: 'Bats crossing a violet harvest moon', category: 'scene'},
62
60
  ];
63
61
 
64
62
  // ─── Gallery Art ────────────────────────────────────────────────────────────
63
+ // Small mountain-glyph fork lives in shared/scene-tile; decorative stars
64
+ // resolve to comment here (never purple — purple means tappable).
65
65
 
66
66
  function GalleryArt({image, index}: {image: GalleryImage; index: number}) {
67
67
  const hue = CATEGORY_HUES[image.category];
68
- const moonX = 90 + ((index * 53) % 220);
69
- const moonY = 62 + ((index * 29) % 60);
70
- const hillA = 190 + ((index * 13) % 40);
71
- const hillB = 215 + ((index * 17) % 40);
72
- return (
73
- <svg
74
- viewBox="0 0 400 300"
75
- preserveAspectRatio="xMidYMid slice"
76
- style={artImage}
77
- role="img"
78
- aria-label={image.alt}>
79
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
80
- <g fill="var(--dracula-purple)" opacity={0.55}>
81
- <circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
82
- <circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
83
- <circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
84
- <circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
85
- </g>
86
- <circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
87
- <circle
88
- cx={moonX - 12}
89
- cy={moonY - 8}
90
- r={28}
91
- fill="var(--dracula-bg-light)"
92
- opacity={0.55}
93
- />
94
- <path
95
- d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
96
- fill="var(--dracula-current-line)"
97
- />
98
- <path
99
- d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
100
- fill="var(--dracula-bg)"
101
- />
102
- <g
103
- transform={`translate(${60 + ((index * 41) % 280)} ${hillB - 34})`}
104
- fill="none"
105
- stroke={hue}
106
- strokeWidth="4"
107
- strokeLinecap="round"
108
- strokeLinejoin="round">
109
- <rect x="-22" y="-22" width="44" height="44" rx="5" />
110
- <circle cx="9" cy="-9" r="2" fill={hue} stroke="none" />
111
- <path d="M-17 15 L-4 0 L5 9 L10 4 L17 12" />
112
- </g>
113
- </svg>
114
- );
68
+ return <SceneTile label={image.alt} hue={hue} size="sm" index={index} />;
115
69
  }
116
70
 
117
71
  // ─── Main Page ──────────────────────────────────────────────────────────────
@@ -139,8 +93,8 @@ export default function ClassicGallery() {
139
93
  <Heading level={1}>Hung after dark</Heading>
140
94
  <Text type="body" color="secondary">
141
95
  A coven-curated wall of moonlit ridges, late portraits,
142
- and lamplit rooms. Filter by haunt and stay a while —
143
- the gallery never sleeps.
96
+ and lamplit rooms. Filter by haunt and stay a while.
97
+ The gallery never sleeps.
144
98
  </Text>
145
99
  </VStack>
146
100
 
@@ -153,7 +107,7 @@ export default function ClassicGallery() {
153
107
  <Tab value="home" label="Home" />
154
108
  </TabList>
155
109
  <Text
156
- type="supporting"
110
+ type="body"
157
111
  color="secondary"
158
112
  hasTabularNumbers>
159
113
  {filteredImages.length}{' '}