astryx-dracula 0.2.1 → 0.3.1

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 +30 -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 +181 -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 +102 -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 +433 -30
@@ -0,0 +1,322 @@
1
+ // Shared castle scenes for owned hero/gallery/form/auth surfaces.
2
+ // `wide` is the centered-hero banner (800x450); `card` and `tall` are the
3
+ // gallery-hero castle and pines tiles (400x500); `rooftops` is the
4
+ // login-split cover (400x300, crescent-overlay moon, rooftop skyline).
5
+ // Chevron bats stay foreground stroke (w4 wide, w3 card/rooftops); stars
6
+ // carry a `comment` label so the decorative-purple rule survives refactors.
7
+ // Moons are harvest yellow in every scene. Hills live in per-scene
8
+ // constants below.
9
+
10
+ import {galleryImage} from 'astryx-dracula/shared/gallery-image';
11
+
12
+ export type SceneCastleVariant = 'wide' | 'card' | 'tall' | 'rooftops';
13
+
14
+ const WIDE_HILLS = {
15
+ back: 'M0 330 Q200 260 400 320 T800 300 V450 H0 Z',
16
+ front: 'M0 385 Q240 320 480 375 T800 360 V450 H0 Z',
17
+ } as const;
18
+
19
+ const CARD_HILLS = {
20
+ back: 'M0 360 Q140 300 260 350 T400 335 V500 H0 Z',
21
+ front: 'M0 420 Q160 365 300 410 T400 400 V500 H0 Z',
22
+ } as const;
23
+
24
+ const TALL_HILL = 'M0 380 Q200 320 400 370 V500 H0 Z' as const;
25
+
26
+ function WideCastle() {
27
+ return (
28
+ <svg
29
+ style={galleryImage}
30
+ viewBox="0 0 800 450"
31
+ role="img"
32
+ aria-label="Moonlit castle on a hill in Dracula theme colors">
33
+ <rect width="800" height="450" fill="var(--dracula-bg-dark)" />
34
+ {/* Stars */}
35
+ <g fill="var(--dracula-comment)">
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-fg)"
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 d={WIDE_HILLS.back} fill="var(--dracula-selection)" />
83
+ <path d={WIDE_HILLS.front} fill="var(--dracula-bg-light)" />
84
+ {/* Castle */}
85
+ <g>
86
+ <rect
87
+ x="150"
88
+ y="225"
89
+ width="120"
90
+ height="120"
91
+ rx={4}
92
+ fill="var(--dracula-bg-lighter)"
93
+ />
94
+ <rect
95
+ x="130"
96
+ y="185"
97
+ width="44"
98
+ height="160"
99
+ rx={4}
100
+ fill="var(--dracula-bg-lighter)"
101
+ />
102
+ <rect
103
+ x="246"
104
+ y="185"
105
+ width="44"
106
+ height="160"
107
+ rx={4}
108
+ fill="var(--dracula-bg-lighter)"
109
+ />
110
+ <g fill="var(--dracula-bg-lighter)">
111
+ <rect x="130" y="175" width="12" height="14" rx={2} />
112
+ <rect x="149" y="175" width="12" height="14" rx={2} />
113
+ <rect x="246" y="175" width="12" height="14" rx={2} />
114
+ <rect x="265" y="175" width="12" height="14" rx={2} />
115
+ <rect x="150" y="215" width="14" height="16" rx={2} />
116
+ <rect x="173" y="215" width="14" height="16" rx={2} />
117
+ <rect x="196" y="215" width="14" height="16" rx={2} />
118
+ <rect x="219" y="215" width="14" height="16" rx={2} />
119
+ <rect x="242" y="215" width="14" height="16" rx={2} />
120
+ </g>
121
+ <g fill="var(--dracula-yellow)">
122
+ <rect x="168" y="255" width="14" height="20" rx={2} />
123
+ <rect x="203" y="255" width="14" height="20" rx={2} />
124
+ <rect x="238" y="255" width="14" height="20" rx={2} />
125
+ <rect x="143" y="215" width="10" height="14" rx={2} />
126
+ <rect x="259" y="215" width="10" height="14" rx={2} />
127
+ </g>
128
+ <path
129
+ d="M195 345 v-30 a15 15 0 0 1 30 0 v30 Z"
130
+ fill="var(--dracula-bg-dark)"
131
+ />
132
+ </g>
133
+ </svg>
134
+ );
135
+ }
136
+
137
+ function CardCastle() {
138
+ return (
139
+ <svg
140
+ style={galleryImage}
141
+ viewBox="0 0 400 500"
142
+ role="img"
143
+ aria-label="Moonlit castle on midnight hills">
144
+ <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
145
+ {/* Stars */}
146
+ <g fill="var(--dracula-comment)">
147
+ <circle cx="50" cy="60" r="2.5" />
148
+ <circle cx="140" cy="130" r="2" />
149
+ <circle cx="240" cy="50" r="2.5" />
150
+ <circle cx="330" cy="110" r="2" />
151
+ <circle cx="300" cy="200" r="2.5" />
152
+ </g>
153
+ <g fill="var(--dracula-cyan)">
154
+ <circle cx="95" cy="90" r="2" />
155
+ <circle cx="200" cy="100" r="2.5" />
156
+ <circle cx="360" cy="60" r="2" />
157
+ </g>
158
+ <circle cx="290" cy="120" r="52" fill="var(--dracula-yellow)" />
159
+ <circle
160
+ cx="272"
161
+ cy="106"
162
+ r="9"
163
+ fill="var(--dracula-orange)"
164
+ opacity={0.5}
165
+ />
166
+ <g
167
+ fill="none"
168
+ stroke="var(--dracula-fg)"
169
+ strokeWidth={3}
170
+ strokeLinecap="round">
171
+ <path d="M70 170 q12 -12 24 0 q12 -12 24 0" />
172
+ <path d="M150 130 q9 -9 18 0 q9 -9 18 0" />
173
+ </g>
174
+ <path d={CARD_HILLS.back} fill="var(--dracula-selection)" />
175
+ <path d={CARD_HILLS.front} fill="var(--dracula-bg-light)" />
176
+ <g>
177
+ <rect
178
+ x="140"
179
+ y="270"
180
+ width="120"
181
+ height="110"
182
+ rx={4}
183
+ fill="var(--dracula-bg-lighter)"
184
+ />
185
+ <rect
186
+ x="118"
187
+ y="235"
188
+ width="44"
189
+ height="145"
190
+ rx={4}
191
+ fill="var(--dracula-bg-lighter)"
192
+ />
193
+ <rect
194
+ x="238"
195
+ y="235"
196
+ width="44"
197
+ height="145"
198
+ rx={4}
199
+ fill="var(--dracula-bg-lighter)"
200
+ />
201
+ <g fill="var(--dracula-yellow)">
202
+ <rect x="160" y="300" width="14" height="20" rx={2} />
203
+ <rect x="196" y="300" width="14" height="20" rx={2} />
204
+ <rect x="232" y="300" width="14" height="20" rx={2} />
205
+ <rect x="131" y="262" width="10" height="14" rx={2} />
206
+ <rect x="253" y="262" width="10" height="14" rx={2} />
207
+ </g>
208
+ <path
209
+ d="M185 380 v-28 a15 15 0 0 1 30 0 v28 Z"
210
+ fill="var(--dracula-bg-dark)"
211
+ />
212
+ </g>
213
+ </svg>
214
+ );
215
+ }
216
+
217
+ function TallPines() {
218
+ return (
219
+ <svg
220
+ style={galleryImage}
221
+ viewBox="0 0 400 500"
222
+ role="img"
223
+ aria-label="Harvest moon over midnight pines">
224
+ <rect width="400" height="500" fill="var(--dracula-bg-dark)" />
225
+ {/* Stars */}
226
+ <g fill="var(--dracula-pink)">
227
+ <circle cx="60" cy="80" r="2" />
228
+ <circle cx="160" cy="50" r="2.5" />
229
+ <circle cx="320" cy="70" r="2" />
230
+ <circle cx="90" cy="190" r="2.5" />
231
+ <circle cx="250" cy="160" r="2" />
232
+ </g>
233
+ <g fill="var(--dracula-cyan)">
234
+ <circle cx="110" cy="120" r="2" />
235
+ <circle cx="220" cy="90" r="2" />
236
+ <circle cx="350" cy="150" r="2.5" />
237
+ </g>
238
+ <circle cx="200" cy="190" r="72" fill="var(--dracula-yellow)" />
239
+ <circle
240
+ cx="176"
241
+ cy="170"
242
+ r="12"
243
+ fill="var(--dracula-orange)"
244
+ opacity={0.5}
245
+ />
246
+ <circle
247
+ cx="222"
248
+ cy="210"
249
+ r="8"
250
+ fill="var(--dracula-orange)"
251
+ opacity={0.5}
252
+ />
253
+ <g
254
+ fill="none"
255
+ stroke="var(--dracula-fg)"
256
+ strokeWidth={3}
257
+ strokeLinecap="round">
258
+ <path d="M90 260 q12 -12 24 0 q12 -12 24 0" />
259
+ <path d="M260 250 q9 -9 18 0 q9 -9 18 0" />
260
+ </g>
261
+ <path d={TALL_HILL} fill="var(--dracula-selection)" />
262
+ <g fill="var(--dracula-bg-light)">
263
+ <path d="M70 500 V400 l28 -44 28 44 v100 Z" />
264
+ <path d="M170 500 V420 l28 -44 28 44 v80 Z" />
265
+ <path d="M270 500 V410 l28 -44 28 44 v90 Z" />
266
+ </g>
267
+ <g fill="var(--dracula-yellow)">
268
+ <rect x="81" y="442" width="8" height="12" rx={2} />
269
+ <rect x="181" y="458" width="8" height="12" rx={2} />
270
+ <rect x="281" y="450" width="8" height="12" rx={2} />
271
+ </g>
272
+ </svg>
273
+ );
274
+ }
275
+
276
+ function Rooftops() {
277
+ return (
278
+ <svg
279
+ style={galleryImage}
280
+ viewBox="0 0 400 300"
281
+ preserveAspectRatio="xMidYMid slice"
282
+ role="img"
283
+ aria-label="Harvest moon over the castle rooftops">
284
+ <rect width="400" height="300" fill="var(--dracula-bg)" />
285
+ <g fill="var(--dracula-fg)">
286
+ <circle cx="68" cy="52" r="2" />
287
+ <circle cx="330" cy="70" r="2" />
288
+ </g>
289
+ <g fill="var(--dracula-comment)">
290
+ <circle cx="140" cy="36" r="1.5" />
291
+ <circle cx="292" cy="34" r="1.5" />
292
+ <circle cx="360" cy="140" r="1.5" />
293
+ </g>
294
+ <circle cx="200" cy="118" r="52" fill="var(--dracula-yellow)" />
295
+ <circle cx="182" cy="104" r="44" fill="var(--dracula-bg)" />
296
+ <g
297
+ fill="var(--dracula-current-line)"
298
+ stroke="var(--dracula-comment)"
299
+ strokeWidth="3"
300
+ strokeLinejoin="round">
301
+ <rect x="20" y="220" width="110" height="80" />
302
+ <path d="M20 220 L75 178 L130 220 Z" />
303
+ <rect x="150" y="200" width="100" height="100" />
304
+ <path d="M150 200 L200 162 L250 200 Z" />
305
+ <rect x="270" y="228" width="110" height="72" />
306
+ <path d="M270 228 L325 190 L380 228 Z" />
307
+ </g>
308
+ <g fill="var(--dracula-yellow)">
309
+ <rect x="58" y="244" width="14" height="18" />
310
+ <rect x="188" y="224" width="14" height="18" />
311
+ <rect x="312" y="250" width="14" height="18" />
312
+ </g>
313
+ </svg>
314
+ );
315
+ }
316
+
317
+ export function SceneCastle({variant}: {variant: SceneCastleVariant}) {
318
+ if (variant === 'wide') return <WideCastle />;
319
+ if (variant === 'card') return <CardCastle />;
320
+ if (variant === 'rooftops') return <Rooftops />;
321
+ return <TallPines />;
322
+ }
@@ -0,0 +1,88 @@
1
+ // Shared scene frame: the accessible, colour-correct wrapper every hand-drawn
2
+ // scene SVG in the kit sits inside.
3
+ //
4
+ // WHY THIS EXISTS: twelve scene components across tech-report (795 lines) and
5
+ // product-tour (504 lines) each open with the same seven lines — viewBox,
6
+ // preserveAspectRatio, a fill style, role="img", aria-label, and a backdrop
7
+ // rect — and shared/scene-tile.tsx and shared/scene-castle.tsx carry the same
8
+ // frame a third and fourth time. Beyond the duplication, those scenes painted
9
+ // 45 values with --dracula-comment, which is a BORDER colour, not a text or
10
+ // data colour. This module exists so that mistake has exactly one place to
11
+ // happen.
12
+ //
13
+ // COLOUR CONTRACT (UIAuditColorMotion, measured against --dracula-bg-dark
14
+ // #21222C, the tier every scene paints on — a tier darker than the page #282A36,
15
+ // which is why the old ratios were worse than they looked):
16
+ // value #F8F8F2 14.81:1 --color-text-highlight
17
+ // para #B0B3C4 7.59:1 --color-text-paragraph (axes, ticks, captions)
18
+ // sec #9AA1BC 6.16:1 --color-text-secondary
19
+ // cyan #8BE9FD 11.41:1 --color-data-categorical-cyan (accent marks)
20
+ // COMMENT #6272A4 3.36:1 clears the 3:1 GRAPHICAL floor, FAILS the 4.5:1
21
+ // TEXT floor, and any fillOpacity below 0.9 makes
22
+ // it worse: 0.85 -> 2.81, 0.60 -> 2.06. The two
23
+ // floors are different rules and Comment sits
24
+ // between them, so its correctness depends entirely
25
+ // on what it paints:
26
+ // TEXT -> never Comment. It is disabled text.
27
+ // Use paragraph (7.59:1) or highlight
28
+ // (14.81:1) for values. This is the
29
+ // 45-instance defect.
30
+ // EMPTY CELL / subtle grid line -> Comment is
31
+ // CORRECT at full opacity (3.36:1).
32
+ // An empty cell is a graphical object
33
+ // carrying data — a reader must see
34
+ // that a position exists and was not
35
+ // kept — so the 3:1 floor applies, and
36
+ // 3.36:1 clears it.
37
+ // Do NOT "improve" an empty cell to --color-separator #44475A. Measured on
38
+ // #21222C: separator's CEILING is 1.73:1 at full opacity, and at 0.55 it is
39
+ // 1.32:1. No alpha fixes it, and a grid whose empty cells have vanished is not
40
+ // a grid. Subtle is not the same as invisible.
41
+ //
42
+ // CORRECTNESS CONSTRAINT, not a style note: this module MUST NOT own a <defs>
43
+ // id namespace. product-tour hardcodes gradient ids pt-night-glow and
44
+ // pt-night-pool, and twelve scenes can render on one page. If this file ever
45
+ // generates ids, those scenes collide and the second scene's gradients resolve
46
+ // to the first's. Callers pass their own gradient ids in via children.
47
+ //
48
+ // Not interactive. A scene is artwork, not a control: no hover, no focus ring.
49
+
50
+ import type {CSSProperties, ReactNode} from 'react';
51
+
52
+ export interface SceneFrameProps {
53
+ /** Accessible name for the art. Required and unique per instance: four or
54
+ * more scenes sharing one name is the a11y defect the Sparkline `label` prop
55
+ * was extracted to fix. */
56
+ label: string;
57
+ /** Scene contents. Any <defs> and gradient ids belong to the caller. */
58
+ children: ReactNode;
59
+ /** @default '0 0 400 225' */
60
+ viewBox?: string;
61
+ /** Backdrop fill painted behind the scene. @default 'var(--dracula-bg-dark)' */
62
+ fill?: string;
63
+ /** Paints the backdrop rect. Set false when the scene draws its own. @default true */
64
+ hasBackdrop?: boolean;
65
+ /** Forwarded to the root svg. */
66
+ style?: CSSProperties;
67
+ }
68
+
69
+ export function SceneFrame({
70
+ label,
71
+ children,
72
+ viewBox = '0 0 400 225',
73
+ fill = 'var(--dracula-bg-dark)',
74
+ hasBackdrop = true,
75
+ style,
76
+ }: SceneFrameProps) {
77
+ return (
78
+ <svg
79
+ viewBox={viewBox}
80
+ preserveAspectRatio="xMidYMid slice"
81
+ style={{width: '100%', height: '100%', display: 'block', ...style}}
82
+ role="img"
83
+ aria-label={label}>
84
+ {hasBackdrop ? <rect width="100%" height="100%" fill={fill} /> : null}
85
+ {children}
86
+ </svg>
87
+ );
88
+ }
@@ -0,0 +1,49 @@
1
+ // Scene art-treatment vocabulary, split from the component that renders it.
2
+ //
3
+ // WHY A SEPARATE MODULE: a .tsx that exports both components and
4
+ // non-components breaks React Fast Refresh -- editing a constant forces a
5
+ // full remount instead of a hot update. `react-doctor/only-export-components`
6
+ // catches it; this is the fix it asks for. It also matches shared/chart-hues.ts,
7
+ // which the chart vocabulary already uses, so the kit has one shape rather than
8
+ // one precedent and one exception.
9
+
10
+ export type SceneTileSize = 'lg' | 'sm';
11
+
12
+ /**
13
+ * Art-treatment hues for scene tiles. Purple-free by this module's remit.
14
+ * Membership is not a contrast decision — every candidate clears WCAG 1.4.11
15
+ * on both dark surfaces at the disc's 0.9 opacity, including purple.
16
+ */
17
+ export const SCENE_HUES = {
18
+ muted: 'var(--dracula-comment)',
19
+ cyan: 'var(--dracula-cyan)',
20
+ green: 'var(--dracula-green)',
21
+ yellow: 'var(--dracula-yellow)',
22
+ orange: 'var(--dracula-orange)',
23
+ pink: 'var(--dracula-pink)',
24
+ red: 'var(--dracula-red)',
25
+ } as const;
26
+
27
+ export type SceneHue = (typeof SCENE_HUES)[keyof typeof SCENE_HUES];
28
+
29
+ /**
30
+ * The union above constrains the KEY, not the value it resolves to. An entry
31
+ * rewritten to `var(--dracula-purple)` still typechecks. `scripts/check.ts`
32
+ * gates the values of this object; add the gate there if it is ever removed.
33
+ * A prose rule does not stop a caller — see the remit note above.
34
+ */
35
+ export const SCENE_HUE_VALUES: readonly SceneHue[] = Object.values(SCENE_HUES);
36
+
37
+ // Shared alt list for the nine-tile side/classic wall (folded into one
38
+ // module so the two surfaces cannot drift apart again).
39
+ export const SCENE_TILE_ALTS = [
40
+ 'Moonlit ridge trail under a harvest moon',
41
+ 'Late portrait in violet lamplight',
42
+ 'Lamplit reading nook after midnight',
43
+ 'Fog rolling over the night pines',
44
+ 'Dancer caught mid-step in stage pink',
45
+ 'Kitchen table set for a midnight feast',
46
+ 'Castle silhouette over sleeping hills',
47
+ 'Attic window glowing amber at 2am',
48
+ 'Cellar shelves lined with bottled dusk',
49
+ ] as const;
@@ -0,0 +1,128 @@
1
+ // Shared mountain-glyph scene tile for owned gallery surfaces.
2
+ // Large fork: documentation/library/mixed/payment/product-detail/
3
+ // product-gallery — centered glyph on a flat field. Small fork:
4
+ // classic/side — full night landscape (stars, moon, hills) with a tighter
5
+ // glyph frame. The glyph stroke stays comment so tiles stay on-brand in the
6
+ // dark-only theme.
7
+ // Wave 2 owns the documentation + payment-form migration — those templates
8
+ // keep their local glyphs until then.
9
+ //
10
+ // REMIT (art treatment, not data): this module's scenes take a `SceneHue` from
11
+ // `SCENE_HUES` below. Purple is excluded because this module reserves it for
12
+ // tappables — NOT because of contrast, which it passes (4.27:1 on the surface
13
+ // tier at the disc's 0.9 opacity). This is a separate vocabulary from
14
+ // `CHART_HUES` on purpose: that set is purple-free because purple must never
15
+ // encode DATA, this one is purple-free because this module says so. Two sets,
16
+ // two stated reasons. Reusing `CHART_HUES` here would make it purple-free for
17
+ // a reason that has nothing to do with this rule.
18
+ //
19
+ // The type is the enforcement, not this comment. `hue: string` accepted any
20
+ // `var(--dracula-*)` and every caller decided alone, which is how purple
21
+ // reached a 34px disc. The union turns that into a compile error at the call
22
+ // site with the exact offending value named.
23
+
24
+ import {galleryImage} from 'astryx-dracula/shared/gallery-image';
25
+ import {
26
+ SCENE_HUES,
27
+ type SceneHue,
28
+ type SceneTileSize,
29
+ } from 'astryx-dracula/shared/scene-hues';
30
+
31
+
32
+ interface SceneTileProps {
33
+ /**
34
+ * Per-tile accent: the sm fork paints it as the moon disc (`:70`, a 34px
35
+ * radius at 0.9 opacity) and as glyph dots; the lg fork paints the glyph.
36
+ * One prop, three areas of very different size — the split by role is a
37
+ * deliberate follow-up, not an oversight. Both areas clear 1.4.11 at the
38
+ * same `SceneHue`, so the split is about which role may take which subset,
39
+ * not about a second contrast axis.
40
+ */
41
+ hue: SceneHue;
42
+ /** Accessible name for the tile art. */
43
+ label: string;
44
+ /** `lg` for grid thumbs, `sm` for the classic/side landscape tiles. */
45
+ size?: SceneTileSize;
46
+ /** Star wash for the sm landscape. Decorative, and purple-free by the same
47
+ * remit as `hue`, so it takes the same union. */
48
+ stars?: SceneHue;
49
+ /** Positional seed so repeated sm tiles vary moon, hills, and star field.
50
+ * Ignored by the lg fork (one centered glyph, no variance). */
51
+ index?: number;
52
+ }
53
+
54
+
55
+ export function SceneTile({
56
+ hue,
57
+ label,
58
+ size = 'lg',
59
+ stars = 'var(--dracula-comment)',
60
+ index = 0,
61
+ }: SceneTileProps) {
62
+ const moonX = 90 + ((index * 53) % 220);
63
+ const moonY = 62 + ((index * 29) % 60);
64
+ const hillA = 190 + ((index * 13) % 40);
65
+ const hillB = 215 + ((index * 17) % 40);
66
+ return (
67
+ <svg
68
+ viewBox="0 0 400 300"
69
+ preserveAspectRatio="xMidYMid slice"
70
+ style={galleryImage}
71
+ role="img"
72
+ aria-label={label}>
73
+ <rect width="400" height="300" fill="var(--dracula-bg-light)" />
74
+ {size === 'sm' && (
75
+ <>
76
+ {/* Stars */}
77
+ <g fill={stars} opacity={0.55}>
78
+ <circle cx={40 + ((index * 37) % 320)} cy={30} r={2} />
79
+ <circle cx={120 + ((index * 23) % 200)} cy={52} r={1.6} />
80
+ <circle cx={260 + ((index * 11) % 110)} cy={26} r={2.2} />
81
+ <circle cx={330} cy={70 + ((index * 7) % 30)} r={1.6} />
82
+ </g>
83
+ <circle cx={moonX} cy={moonY} r={34} fill={hue} opacity={0.9} />
84
+ <circle
85
+ cx={moonX - 12}
86
+ cy={moonY - 8}
87
+ r={28}
88
+ fill="var(--dracula-bg-light)"
89
+ opacity={0.55}
90
+ />
91
+ <path
92
+ d={`M0 ${hillA} Q100 ${hillA - 50} 200 ${hillA - 10} T400 ${hillA - 30} V300 H0 Z`}
93
+ fill="var(--dracula-current-line)"
94
+ />
95
+ <path
96
+ d={`M0 ${hillB} Q120 ${hillB - 40} 240 ${hillB} T400 ${hillB - 20} V300 H0 Z`}
97
+ fill="var(--dracula-bg)"
98
+ />
99
+ </>
100
+ )}
101
+ {size === 'sm' ? (
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={4} />
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
+ ) : (
114
+ <g
115
+ transform="translate(200 150)"
116
+ fill="none"
117
+ stroke="var(--dracula-comment)"
118
+ strokeWidth="5"
119
+ strokeLinecap="round"
120
+ strokeLinejoin="round">
121
+ <rect x="-44" y="-44" width="88" height="88" rx="5" />
122
+ <circle cx="18" cy="-18" r="2.5" fill={hue} stroke="none" />
123
+ <path d="M-34 30 L-8 0 L10 18 L20 8 L34 24" />
124
+ </g>
125
+ )}
126
+ </svg>
127
+ );
128
+ }