astryx-dracula 0.2.0 → 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 +22 -11
  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 +67 -32
  31. package/templates/ai-chat.tsx +41 -20
  32. package/templates/blank.tsx +5 -2
  33. package/templates/centered-hero.tsx +15 -124
  34. package/templates/classic-gallery.tsx +21 -65
  35. package/templates/contact-form.tsx +62 -40
  36. package/templates/dashboard-portfolio.tsx +146 -227
  37. package/templates/dashboard.tsx +123 -121
  38. package/templates/detail-page.tsx +101 -95
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +85 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +33 -21
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +54 -74
  45. package/templates/editor.tsx +119 -89
  46. package/templates/file-explorer.tsx +133 -84
  47. package/templates/form-two-column.tsx +62 -124
  48. package/templates/gallery-hero.tsx +17 -171
  49. package/templates/ide.tsx +27 -21
  50. package/templates/incident-console.tsx +105 -69
  51. package/templates/kanban-board.tsx +100 -41
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +38 -41
  54. package/templates/login-card.tsx +59 -86
  55. package/templates/login-split.tsx +88 -128
  56. package/templates/login-sso.tsx +107 -68
  57. package/templates/login.tsx +108 -52
  58. package/templates/messaging-shell.tsx +172 -143
  59. package/templates/mixed-gallery.tsx +31 -130
  60. package/templates/payment-form.tsx +369 -289
  61. package/templates/product-detail.tsx +40 -57
  62. package/templates/product-gallery.tsx +31 -40
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +145 -286
  66. package/templates/settings-sidebar.tsx +179 -267
  67. package/templates/settings.tsx +42 -13
  68. package/templates/shell-nav.tsx +32 -23
  69. package/templates/shell-side-nav.tsx +30 -19
  70. package/templates/shell-top-nav.tsx +81 -43
  71. package/templates/side-gallery.tsx +39 -93
  72. package/templates/table-grouped.tsx +63 -33
  73. package/templates/table-page-chart.tsx +24 -297
  74. package/templates/table-page-heatmap-status.tsx +157 -91
  75. package/templates/table-page-shoe-store-heatmap.tsx +19 -288
  76. package/templates/table-page.tsx +24 -6
  77. package/templates/table.tsx +24 -7
  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 +313 -249
  82. package/theme.css +76 -14
  83. package/tokens.css +425 -30
@@ -1,4 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L > LC[p=0] > H > (SI[fill] > ChL > ChML > (ChM > ChB)*4 + ChC"Ask a follow up...") + (C.transparent > Tbar + S.section > MD) ;; Dlg#artifact[variant=fullscreen] > L > (DH"JWT Token Refresh" + (LC[p=0] > S.section > MD))
2
4
 
3
5
  import {useRef, useState, type CSSProperties} from 'react';
4
6
 
@@ -33,6 +35,7 @@ import {Token} from '@astryxdesign/core/Token';
33
35
  import {Button} from '@astryxdesign/core/Button';
34
36
  import {Icon} from '@astryxdesign/core/Icon';
35
37
  import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
38
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
36
39
  import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
37
40
  import {MoreMenu} from '@astryxdesign/core/MoreMenu';
38
41
  import {Toolbar} from '@astryxdesign/core/Toolbar';
@@ -56,10 +59,9 @@ import {
56
59
  const MOBILE_MAX_WIDTH = 1023;
57
60
 
58
61
  const root: CSSProperties = {
59
- // The showcase renders a 44px breadcrumb row above the template, so a full
60
- // 100dvh root always spills exactly that far into page-level scroll. Subtract
61
- // it so scrolling stays inside the message list and artifact body.
62
- 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',
63
65
  width: '100%',
64
66
  containerType: 'inline-size',
65
67
  containerName: 'artifact',
@@ -74,8 +76,11 @@ const chatLayout: CSSProperties = {
74
76
  flex: 1,
75
77
  minHeight: 0,
76
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.
77
82
  const artifactCard: CSSProperties = {
78
- marginBlockStart: 'var(--space-gap)',
83
+ marginBlockStart: 'var(--spacing-2)',
79
84
  };
80
85
  const artifactScroll: CSSProperties = {
81
86
  flex: 1,
@@ -121,7 +126,7 @@ const AI_CHAT_CSS = `
121
126
  // Artifact content
122
127
 
123
128
  const MENTION_TOKENS = [
124
- {value: '@agent', label: '@Agent', variant: 'blue' as const},
129
+ {value: '@agent', label: '@Agent', variant: 'cyan' as const},
125
130
  ];
126
131
 
127
132
  const ARTIFACT_TITLE = 'JWT Token Refresh: Design & Rollout';
@@ -130,7 +135,7 @@ const ARTIFACT_SUBTITLE = 'Grimoire · Sealed just now';
130
135
 
131
136
  const ARTIFACT_CONTENT = `## Overview
132
137
 
133
- 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.
134
139
 
135
140
  ## The Problem
136
141
 
@@ -138,7 +143,7 @@ Token validation ran **before** any refresh logic, so the middleware rejected ex
138
143
 
139
144
  1. A request arrives with an expired access token
140
145
  2. \`validateToken()\` throws \`TokenExpiredError\`
141
- 3. The catch block returns \`401\` — \`refreshToken()\` is never reached
146
+ 3. The catch block returns \`401\`: \`refreshToken()\` is never reached
142
147
 
143
148
  The result was users getting logged out whenever an access token lapsed mid-session.
144
149
 
@@ -146,9 +151,9 @@ The result was users getting logged out whenever an access token lapsed mid-sess
146
151
 
147
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\`.
148
153
 
149
- - **Transparent** — valid sessions never see an interruption
150
- - **Safe** — a missing or invalid refresh token still returns \`401\`
151
- - **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
152
157
 
153
158
  ## Testing
154
159
 
@@ -234,12 +239,24 @@ function MobileArtifactActions() {
234
239
  );
235
240
  }
236
241
 
237
- // Scrollable artifact body — the formatted document.
238
- 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}) {
239
246
  return (
240
- <Section variant="transparent" style={artifactScroll}>
247
+ <Section
248
+ variant="transparent"
249
+ style={artifactScroll}
250
+ role="region"
251
+ aria-label={ARTIFACT_TITLE}
252
+ tabIndex={0}>
241
253
  <VStack gap={2} style={articleBody}>
242
- <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
+ )}
243
260
  <Markdown>{ARTIFACT_CONTENT}</Markdown>
244
261
  </VStack>
245
262
  </Section>
@@ -360,6 +377,7 @@ export default function AiChat() {
360
377
  }
361
378
  />
362
379
  }>
380
+ <VisuallyHidden as="h1">Night thread</VisuallyHidden>
363
381
  <ChatMessageList>
364
382
  {/* Date divider */}
365
383
  <ChatSystemMessage variant="divider">
@@ -385,7 +403,7 @@ export default function AiChat() {
385
403
  }>
386
404
  <ChatTokenizedText tokens={MENTION_TOKENS}>
387
405
  @agent Can you review these auth files? The JWT
388
- refresh logic seems broken — tokens expire but the
406
+ refresh logic seems broken, tokens expire but the
389
407
  middleware doesn't catch it.
390
408
  </ChatTokenizedText>
391
409
  </ChatMessageBubble>
@@ -431,7 +449,7 @@ Here's the problematic sequence:
431
449
 
432
450
  1. Request arrives with an expired access token
433
451
  2. \`validateToken()\` throws \`TokenExpiredError\`
434
- 3. The catch block returns \`401\` — never reaching \`refreshToken()\`
452
+ 3. The catch block returns \`401\`, never reaching \`refreshToken()\`
435
453
 
436
454
  The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh before rejecting:`}</Markdown>
437
455
  </ChatMessageBubble>
@@ -440,6 +458,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
440
458
  <CodeBlock
441
459
  title="middleware.ts"
442
460
  language="typescript"
461
+ width="100%"
443
462
  code={`async function authMiddleware(req: Request) {
444
463
  try {
445
464
  const decoded = validateToken(req.headers.authorization);
@@ -556,6 +575,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
556
575
  <CodeBlock
557
576
  title="middleware.test.ts"
558
577
  language="typescript"
578
+ width="100%"
559
579
  code={`describe('authMiddleware', () => {
560
580
  it('refreshes an expired token silently', async () => {
561
581
  const expiredToken = createExpiredJWT(mockUser);
@@ -615,7 +635,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
615
635
  avatar={<Avatar name="Agent" size="md" />}>
616
636
  <ChatMessageBubble variant="ghost">
617
637
  <Markdown density="compact">
618
- {`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?`}
619
639
  </Markdown>
620
640
  </ChatMessageBubble>
621
641
  <ArtifactCard onOpen={openArtifact} />
@@ -654,7 +674,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
654
674
  avatar={<Avatar name="Agent" size="md" />}>
655
675
  <ChatMessageBubble variant="ghost">
656
676
  <Markdown density="compact">
657
- {`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.`}
658
678
  </Markdown>
659
679
  </ChatMessageBubble>
660
680
  <ChatToolCalls
@@ -741,6 +761,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
741
761
  purpose="info"
742
762
  variant="fullscreen">
743
763
  <Layout
764
+ height="auto"
744
765
  header={
745
766
  <DialogHeader
746
767
  title={ARTIFACT_TITLE}
@@ -752,7 +773,7 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
752
773
  }
753
774
  content={
754
775
  <LayoutContent padding={0}>
755
- <ArtifactBody />
776
+ <ArtifactBody hasOwnTitle={false} />
756
777
  </LayoutContent>
757
778
  }
758
779
  />
@@ -1,14 +1,17 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
3
+ // L[h=auto] > LC > Hd"A blank crypt awaits its story"[level=1]
2
4
 
3
5
  import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
4
- import {Text} from '@astryxdesign/core/Text';
6
+ import {Heading} from '@astryxdesign/core/Text';
5
7
 
6
8
  export default function BlankPage() {
7
9
  return (
8
10
  <Layout
11
+ height="auto"
9
12
  content={
10
13
  <LayoutContent>
11
- <Text type="large">A blank crypt awaits its story</Text>
14
+ <Heading level={1} type="display-2">A blank crypt awaits its story</Heading>
12
15
  </LayoutContent>
13
16
  }
14
17
  />
@@ -1,4 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
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]))
2
4
 
3
5
  import type {CSSProperties} from 'react';
4
6
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -8,12 +10,11 @@ import {Icon} from '@astryxdesign/core/Icon';
8
10
  import {AspectRatio} from '@astryxdesign/core/AspectRatio';
9
11
  import {Section} from '@astryxdesign/core/Section';
10
12
  import {ArrowRight} from 'lucide-react';
13
+ import {SceneCastle} from 'astryx-dracula/shared/scene-castle';
11
14
 
12
- const heroImage: CSSProperties = {
13
- width: '100%',
14
- height: '100%',
15
- display: 'block',
16
- };
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.
17
18
  const heroFrame: CSSProperties = {
18
19
  maxWidth: 1200,
19
20
  marginInline: 'auto',
@@ -22,127 +23,10 @@ const heroFrame: CSSProperties = {
22
23
  overflow: 'clip',
23
24
  };
24
25
 
25
- function NightCastleScene() {
26
- return (
27
- <svg
28
- style={heroImage}
29
- viewBox="0 0 800 450"
30
- role="img"
31
- aria-label="Moonlit castle on a hill in Dracula theme colors">
32
- <title>Castle under a harvest moon</title>
33
- <rect width="800" height="450" fill="var(--dracula-bg-dark)" />
34
- {/* Stars */}
35
- <g fill="var(--dracula-purple)">
36
- <circle cx="60" cy="60" r="2.5" />
37
- <circle cx="180" cy="120" r="2" />
38
- <circle cx="330" cy="50" r="2.5" />
39
- <circle cx="470" cy="90" r="2" />
40
- <circle cx="700" cy="200" r="2.5" />
41
- <circle cx="750" cy="80" r="2" />
42
- </g>
43
- <g fill="var(--dracula-cyan)">
44
- <circle cx="120" cy="90" r="2" />
45
- <circle cx="270" cy="70" r="2.5" />
46
- <circle cx="540" cy="50" r="2" />
47
- <circle cx="660" cy="160" r="2" />
48
- </g>
49
- <g fill="var(--dracula-pink)">
50
- <circle cx="90" cy="170" r="2" />
51
- <circle cx="400" cy="130" r="2.5" />
52
- <circle cx="590" cy="200" r="2" />
53
- <circle cx="730" cy="260" r="2" />
54
- </g>
55
- {/* Harvest moon */}
56
- <circle cx="620" cy="110" r="56" fill="var(--dracula-yellow)" />
57
- <circle
58
- cx="600"
59
- cy="95"
60
- r="10"
61
- fill="var(--dracula-orange)"
62
- opacity={0.5}
63
- />
64
- <circle
65
- cx="638"
66
- cy="125"
67
- r="7"
68
- fill="var(--dracula-orange)"
69
- opacity={0.5}
70
- />
71
- {/* Bats */}
72
- <g
73
- fill="none"
74
- stroke="var(--dracula-purple)"
75
- strokeWidth={4}
76
- strokeLinecap="round">
77
- <path d="M120 130 q12 -12 24 0 q12 -12 24 0" />
78
- <path d="M210 90 q9 -9 18 0 q9 -9 18 0" />
79
- <path d="M470 160 q9 -9 18 0 q9 -9 18 0" />
80
- </g>
81
- {/* Hills */}
82
- <path
83
- d="M0 330 Q200 260 400 320 T800 300 V450 H0 Z"
84
- fill="var(--dracula-selection)"
85
- />
86
- <path
87
- d="M0 385 Q240 320 480 375 T800 360 V450 H0 Z"
88
- fill="var(--dracula-bg-light)"
89
- />
90
- {/* Castle */}
91
- <g>
92
- <rect
93
- x="150"
94
- y="225"
95
- width="120"
96
- height="120"
97
- rx={4}
98
- fill="var(--dracula-bg-lighter)"
99
- />
100
- <rect
101
- x="130"
102
- y="185"
103
- width="44"
104
- height="160"
105
- rx={4}
106
- fill="var(--dracula-bg-lighter)"
107
- />
108
- <rect
109
- x="246"
110
- y="185"
111
- width="44"
112
- height="160"
113
- rx={4}
114
- fill="var(--dracula-bg-lighter)"
115
- />
116
- <g fill="var(--dracula-bg-lighter)">
117
- <rect x="130" y="175" width="12" height="14" rx={2} />
118
- <rect x="149" y="175" width="12" height="14" rx={2} />
119
- <rect x="246" y="175" width="12" height="14" rx={2} />
120
- <rect x="265" y="175" width="12" height="14" rx={2} />
121
- <rect x="150" y="215" width="14" height="16" rx={2} />
122
- <rect x="173" y="215" width="14" height="16" rx={2} />
123
- <rect x="196" y="215" width="14" height="16" rx={2} />
124
- <rect x="219" y="215" width="14" height="16" rx={2} />
125
- <rect x="242" y="215" width="14" height="16" rx={2} />
126
- </g>
127
- <g fill="var(--dracula-yellow)">
128
- <rect x="168" y="255" width="14" height="20" rx={2} />
129
- <rect x="203" y="255" width="14" height="20" rx={2} />
130
- <rect x="238" y="255" width="14" height="20" rx={2} />
131
- <rect x="143" y="215" width="10" height="14" rx={2} />
132
- <rect x="259" y="215" width="10" height="14" rx={2} />
133
- </g>
134
- <path
135
- d="M195 345 v-30 a15 15 0 0 1 30 0 v30 Z"
136
- fill="var(--dracula-bg-dark)"
137
- />
138
- </g>
139
- </svg>
140
- );
141
- }
142
-
143
26
  export default function CenteredHero() {
144
27
  return (
145
28
  <Layout
29
+ height="auto"
146
30
  content={
147
31
  <LayoutContent padding={6}>
148
32
  <VStack gap={10}>
@@ -163,6 +47,13 @@ export default function CenteredHero() {
163
47
  Sometimes all it takes is one small ritual to turn the whole
164
48
  night around.
165
49
  </Text>
50
+ <Text
51
+ type="supporting"
52
+ color="secondary"
53
+ justify="center"
54
+ hasTabularNumbers>
55
+ astryx-dracula v0.3.0
56
+ </Text>
166
57
  </VStack>
167
58
  <HStack gap={3} wrap="wrap" justify="center">
168
59
  <Button
@@ -177,7 +68,7 @@ export default function CenteredHero() {
177
68
  </VStack>
178
69
  <Section variant="transparent" padding={0}>
179
70
  <AspectRatio ratio={16 / 9} style={heroFrame}>
180
- <NightCastleScene />
71
+ <SceneCastle variant="wide" />
181
72
  </AspectRatio>
182
73
  </Section>
183
74
  </VStack>
@@ -1,4 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // XLE (canonical structure, validated with `bunx astryx layout check`):
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))
2
4
 
3
5
  import {useState, type CSSProperties} from 'react';
4
6
  import {VStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
@@ -9,6 +11,9 @@ import {Text, Heading} from '@astryxdesign/core/Text';
9
11
  import {Grid} from '@astryxdesign/core/Grid';
10
12
  import {Section} from '@astryxdesign/core/Section';
11
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';
12
17
 
13
18
  // ─── Styles ─────────────────────────────────────────────────────────────────
14
19
 
@@ -21,11 +26,6 @@ const outer: CSSProperties = {
21
26
  const frameClip: CSSProperties = {
22
27
  overflow: 'clip',
23
28
  };
24
- const artImage: CSSProperties = {
25
- width: '100%',
26
- height: '100%',
27
- display: 'block',
28
- };
29
29
  const textCenter: CSSProperties = {
30
30
  textAlign: 'center',
31
31
  };
@@ -40,76 +40,32 @@ interface GalleryImage {
40
40
  }
41
41
 
42
42
  // Dracula accent per gallery shelf.
43
- const CATEGORY_HUES: Record<Exclude<Category, 'all'>, string> = {
43
+ const CATEGORY_HUES: Record<Exclude<Category, 'all'>, SceneHue> = {
44
44
  scene: 'var(--dracula-cyan)',
45
45
  lifestyle: 'var(--dracula-pink)',
46
46
  home: 'var(--dracula-yellow)',
47
47
  };
48
48
 
49
49
  const GALLERY_IMAGES: GalleryImage[] = [
50
- {alt: 'Moonlit ridge trail under a harvest moon', category: 'scene'},
51
- {alt: 'Late portrait in violet lamplight', category: 'lifestyle'},
52
- {alt: 'Lamplit reading nook after midnight', category: 'home'},
53
- {alt: 'Fog rolling over the night pines', category: 'scene'},
54
- {alt: 'Dancer caught mid-step in stage pink', category: 'lifestyle'},
55
- {alt: 'Kitchen table set for a midnight feast', category: 'lifestyle'},
56
- {alt: 'Castle silhouette over sleeping hills', category: 'scene'},
57
- {alt: 'Attic window glowing amber at 2am', category: 'home'},
58
- {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'},
59
59
  {alt: 'Bats crossing a violet harvest moon', category: 'scene'},
60
60
  ];
61
61
 
62
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).
63
65
 
64
66
  function GalleryArt({image, index}: {image: GalleryImage; index: number}) {
65
67
  const hue = CATEGORY_HUES[image.category];
66
- const moonX = 90 + ((index * 53) % 220);
67
- const moonY = 62 + ((index * 29) % 60);
68
- const hillA = 190 + ((index * 13) % 40);
69
- const hillB = 215 + ((index * 17) % 40);
70
- return (
71
- <svg
72
- viewBox="0 0 400 300"
73
- preserveAspectRatio="xMidYMid slice"
74
- style={artImage}
75
- role="img"
76
- aria-label={image.alt}>
77
- <rect width="400" height="300" fill="var(--dracula-bg-light)" />
78
- <g fill="var(--dracula-purple)" opacity={0.55}>
79
- <circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
80
- <circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
81
- <circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
82
- <circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
83
- </g>
84
- <circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
85
- <circle
86
- cx={moonX - 12}
87
- cy={moonY - 8}
88
- r={28}
89
- fill="var(--dracula-bg-light)"
90
- opacity={0.55}
91
- />
92
- <path
93
- d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
94
- fill="var(--dracula-current-line)"
95
- />
96
- <path
97
- d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
98
- fill="var(--dracula-bg)"
99
- />
100
- <g
101
- transform={`translate(${60 + ((index * 41) % 280)} ${hillB - 34})`}
102
- fill="none"
103
- stroke={hue}
104
- strokeWidth="4"
105
- strokeLinecap="round"
106
- strokeLinejoin="round">
107
- <rect x="-22" y="-22" width="44" height="44" rx="5" />
108
- <circle cx="9" cy="-9" r="2" fill={hue} stroke="none" />
109
- <path d="M-17 15 L-4 0 L5 9 L10 4 L17 12" />
110
- </g>
111
- </svg>
112
- );
68
+ return <SceneTile label={image.alt} hue={hue} size="sm" index={index} />;
113
69
  }
114
70
 
115
71
  // ─── Main Page ──────────────────────────────────────────────────────────────
@@ -137,8 +93,8 @@ export default function ClassicGallery() {
137
93
  <Heading level={1}>Hung after dark</Heading>
138
94
  <Text type="body" color="secondary">
139
95
  A coven-curated wall of moonlit ridges, late portraits,
140
- and lamplit rooms. Filter by haunt and stay a while —
141
- the gallery never sleeps.
96
+ and lamplit rooms. Filter by haunt and stay a while.
97
+ The gallery never sleeps.
142
98
  </Text>
143
99
  </VStack>
144
100
 
@@ -151,7 +107,7 @@ export default function ClassicGallery() {
151
107
  <Tab value="home" label="Home" />
152
108
  </TabList>
153
109
  <Text
154
- type="supporting"
110
+ type="body"
155
111
  color="secondary"
156
112
  hasTabularNumbers>
157
113
  {filteredImages.length}{' '}