astryx-dracula 0.5.0 → 0.6.2

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 (129) hide show
  1. package/BRAND.md +3 -3
  2. package/README.md +7 -4
  3. package/USAGE.md +49 -49
  4. package/astryx-dracula.d.ts +2 -2
  5. package/astryx-dracula.js +25 -9
  6. package/astryx-dracula.variants.d.ts +2 -2
  7. package/astryx.integration.mjs +1 -1
  8. package/icons.mjs +21 -17
  9. package/icons.tsx +69 -62
  10. package/package.json +125 -120
  11. package/shared/auth-chrome-config.ts +6 -6
  12. package/shared/auth-chrome.tsx +143 -144
  13. package/shared/auth-copy.ts +14 -14
  14. package/shared/chaptered-doc-config.ts +48 -51
  15. package/shared/chaptered-doc.tsx +267 -289
  16. package/shared/chart-hues.ts +25 -10
  17. package/shared/chart-labels.tsx +31 -30
  18. package/shared/chart-legend.tsx +55 -56
  19. package/shared/chart-panel-style.ts +3 -3
  20. package/shared/data-bar.tsx +97 -102
  21. package/shared/format-money.ts +1 -1
  22. package/shared/gallery-image.ts +6 -6
  23. package/shared/login-demo.ts +1 -1
  24. package/shared/metric-card.tsx +101 -105
  25. package/shared/metric-delta.tsx +28 -27
  26. package/shared/order-desk.tsx +205 -216
  27. package/shared/revenue-chart.tsx +145 -165
  28. package/shared/scene-castle.tsx +206 -291
  29. package/shared/scene-frame.tsx +33 -32
  30. package/shared/scene-hues.ts +17 -17
  31. package/shared/scene-tile.tsx +88 -92
  32. package/shared/settings-data.ts +119 -125
  33. package/shared/settings-rows.tsx +122 -159
  34. package/shared/sparkline.tsx +75 -88
  35. package/shared/sso-icons.tsx +10 -22
  36. package/shared/stars.tsx +16 -25
  37. package/templates/ai-chat-landing.template.mjs +5 -4
  38. package/templates/ai-chat-landing.tsx +473 -460
  39. package/templates/ai-chat.template.mjs +5 -4
  40. package/templates/ai-chat.tsx +543 -612
  41. package/templates/blank.template.mjs +5 -4
  42. package/templates/blank.tsx +18 -14
  43. package/templates/centered-hero.template.mjs +5 -4
  44. package/templates/centered-hero.tsx +55 -68
  45. package/templates/classic-gallery.template.mjs +5 -4
  46. package/templates/classic-gallery.tsx +97 -104
  47. package/templates/contact-form.template.mjs +5 -4
  48. package/templates/contact-form.tsx +247 -282
  49. package/templates/dashboard-portfolio.template.mjs +5 -4
  50. package/templates/dashboard-portfolio.tsx +717 -548
  51. package/templates/dashboard.template.mjs +5 -4
  52. package/templates/dashboard.tsx +596 -567
  53. package/templates/detail-page.template.mjs +5 -4
  54. package/templates/detail-page.tsx +546 -603
  55. package/templates/documentation-design.template.mjs +5 -4
  56. package/templates/documentation-design.tsx +231 -243
  57. package/templates/documentation-technical.template.mjs +5 -4
  58. package/templates/documentation-technical.tsx +236 -239
  59. package/templates/documentation.template.mjs +5 -4
  60. package/templates/documentation.tsx +267 -279
  61. package/templates/editor.template.mjs +5 -4
  62. package/templates/editor.tsx +844 -877
  63. package/templates/file-explorer.template.mjs +5 -4
  64. package/templates/file-explorer.tsx +566 -590
  65. package/templates/form-two-column.template.mjs +5 -4
  66. package/templates/form-two-column.tsx +195 -210
  67. package/templates/gallery-hero.template.mjs +5 -4
  68. package/templates/gallery-hero.tsx +93 -114
  69. package/templates/ide.template.mjs +5 -4
  70. package/templates/ide.tsx +407 -453
  71. package/templates/incident-console.template.mjs +5 -4
  72. package/templates/incident-console.tsx +593 -575
  73. package/templates/kanban-board.template.mjs +5 -4
  74. package/templates/kanban-board.tsx +665 -683
  75. package/templates/library.template.mjs +5 -4
  76. package/templates/library.tsx +409 -430
  77. package/templates/login-card.template.mjs +5 -4
  78. package/templates/login-card.tsx +63 -72
  79. package/templates/login-split.template.mjs +5 -4
  80. package/templates/login-split.tsx +192 -193
  81. package/templates/login-sso.template.mjs +5 -4
  82. package/templates/login-sso.tsx +257 -258
  83. package/templates/login.template.mjs +5 -4
  84. package/templates/login.tsx +53 -49
  85. package/templates/messaging-shell.template.mjs +5 -4
  86. package/templates/messaging-shell.tsx +556 -574
  87. package/templates/mixed-gallery.template.mjs +5 -4
  88. package/templates/mixed-gallery.tsx +60 -58
  89. package/templates/payment-form.template.mjs +5 -4
  90. package/templates/payment-form.tsx +882 -991
  91. package/templates/product-detail.template.mjs +5 -4
  92. package/templates/product-detail.tsx +261 -272
  93. package/templates/product-gallery.template.mjs +5 -4
  94. package/templates/product-gallery.tsx +126 -137
  95. package/templates/product-tour.template.mjs +5 -4
  96. package/templates/product-tour.tsx +741 -773
  97. package/templates/settings-dialog.template.mjs +5 -4
  98. package/templates/settings-dialog.tsx +688 -717
  99. package/templates/settings-sidebar.template.mjs +5 -4
  100. package/templates/settings-sidebar.tsx +762 -807
  101. package/templates/settings.template.mjs +5 -4
  102. package/templates/settings.tsx +289 -304
  103. package/templates/shell-nav.template.mjs +5 -4
  104. package/templates/shell-nav.tsx +212 -219
  105. package/templates/shell-side-nav.template.mjs +5 -4
  106. package/templates/shell-side-nav.tsx +135 -157
  107. package/templates/shell-top-nav.template.mjs +5 -4
  108. package/templates/shell-top-nav.tsx +182 -203
  109. package/templates/side-gallery.template.mjs +5 -4
  110. package/templates/side-gallery.tsx +85 -90
  111. package/templates/table-grouped.template.mjs +5 -4
  112. package/templates/table-grouped.tsx +1000 -1053
  113. package/templates/table-page-chart.template.mjs +5 -4
  114. package/templates/table-page-chart.tsx +400 -398
  115. package/templates/table-page-heatmap-status.template.mjs +5 -4
  116. package/templates/table-page-heatmap-status.tsx +483 -512
  117. package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
  118. package/templates/table-page-shoe-store-heatmap.tsx +748 -746
  119. package/templates/table-page.template.mjs +5 -4
  120. package/templates/table-page.tsx +396 -405
  121. package/templates/table.template.mjs +5 -4
  122. package/templates/table.tsx +92 -93
  123. package/templates/tech-report.template.mjs +5 -4
  124. package/templates/tech-report.tsx +1316 -1304
  125. package/templates/theme-showcase.template.mjs +5 -4
  126. package/templates/theme-showcase.tsx +871 -951
  127. package/theme.css +35 -77
  128. package/theme.css.d.ts +9 -0
  129. package/tokens.css +373 -380
@@ -1,11 +1,13 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
- // XLE (canonical structure, validated with `bunx astryx layout check`):
3
- // A[cp=0 @sideNav=(SN > (SNS"Getting started" > (SNI"Introduction"! + SNI"Install")) + (SNS"The brand" > (SNI"Palette" + SNI"Type and shape")) + (SNS"In practice" > (SNI"Components" + SNI"Templates")) + (SNS"Reference" > (SNI"Tokens" + SNI"Migrating")))] > L[h=auto] > (LC[p=8 !scroll] > V[g=8] > (V[g=2] > (H[g=2 a=center] > MNT + Tx[t=supporting]) + Hd"Introduction"[level=1 t=display-2] + Tx"v0.5.0"[t=supporting]) + Tx"Intro"[t=large] + AR + (V[g=8] > (V[g=3] > Hd"What it is"[level=2] + Tx"Body"[t=body] + UL + Cd)*2) + D + (H[j=between] > B.secondary"Previous" + B.secondary"Next")) + (LP[!scroll] > Outline)
2
+ // XLE (canonical structure of the tree below; the CLI had no XLE validator
3
+ // after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
4
+ // human drift check — update it with the file, never alone):
5
+ // A[cp=0 h=auto @sideNav=(SN[collapsible header=SideNavHeading] > (SNS"Getting started" > (SNI"Introduction"! + SNI"Install")) + (SNS"The brand" > (SNI"Palette" + SNI"Type and shape")) + (SNS"In practice" > (SNI"Components" + SNI"Templates")) + (SNS"Reference" > (SNI"Tokens" + SNI"Migrating"))))] > L[h=auto] > (LC[p=8 !scroll] > V[g=8] > (V[g=2] > (H[g=2 a=center] > MNT + Tx"Documentation"[t=supporting]) + Hd"Introduction"[level=1 t=display-2] + Tx"astryx-dracula v0.6.2"[t=supporting] + SE{Selector}"On this page") + Tx"astryx-dracula is a pure Dracula brand theme"[t=large] + AR[ratio=16/9] + (V[g=8] > (V[g=3] > Hd"What it is"[level=2] + Tx"Four decisions shape everything else"[t=body] + (UL > LI*4)))*2) + D + (H[g=3 j=between a=center] > SI[fill] + B.secondary"Install")) + (LP[!scroll] > Outline)
4
6
 
5
7
  /**
6
8
  * Product Tour — a chaptered walkthrough of this theme.
7
9
  *
8
- * Frame-first layout (see `bunx astryx docs layout`):
10
+ * Frame-first layout (see `bunx @astryxdesign/cli docs layout`):
9
11
  *
10
12
  * Frame: chapter rail 264 (SideNav) | chapter content (fill) | outline 240
11
13
  *
@@ -19,90 +21,87 @@
19
21
  * and status or metadata rides on Text.
20
22
  */
21
23
 
22
- import type {ReactNode} from 'react';
23
- import {Icon} from '@astryxdesign/core/Icon';
24
- import ChapteredDoc, {ChapterArtFrame} from 'astryx-dracula/shared/chaptered-doc';
25
- import {type ChapterGroup} from 'astryx-dracula/shared/chaptered-doc-config';
26
- import {SceneFrame} from 'astryx-dracula/shared/scene-frame';
27
-
24
+ import { Icon } from "@astryxdesign/core/Icon";
25
+ import ChapteredDoc, { ChapterArtFrame } from "astryx-dracula/shared/chaptered-doc";
26
+ import type { ChapterGroup } from "astryx-dracula/shared/chaptered-doc-config";
27
+ import { SceneFrame } from "astryx-dracula/shared/scene-frame";
28
28
  import {
29
- Hash,
30
- LayoutGrid,
31
- Rocket,
32
- RotateCw,
33
- Rows3,
34
- Sparkles,
35
- SquareTerminal,
36
- Type,
37
- } from 'lucide-react';
38
-
39
- const SELF_HASH = '#/templates/product-tour';
40
-
41
-
29
+ Hash,
30
+ LayoutGrid,
31
+ Rocket,
32
+ RotateCw,
33
+ Rows3,
34
+ Sparkles,
35
+ SquareTerminal,
36
+ Type,
37
+ } from "lucide-react";
38
+ import type { ReactNode } from "react";
39
+
40
+ const SELF_HASH = "#/templates/product-tour";
42
41
 
43
42
  // ─── Content ─────────────────────────────────────────────────────────────────
44
43
 
45
44
  const CHAPTER_GROUPS: ChapterGroup[] = [
46
- {
47
- title: 'Getting started',
48
- chapters: [
49
- {
50
- id: 'introduction',
51
- title: 'Introduction',
52
- icon: Rocket,
53
- hasArt: true,
54
- intro:
55
- 'astryx-dracula is a pure Dracula brand theme for Astryx React sites. It pins the official palette, compiles to prebuilt CSS, and costs nothing at runtime. Dark is not a mode here, it is the brand.',
56
- sections: [
57
- {
58
- key: 'what-it-is',
59
- heading: 'What it is',
60
- body: 'Four decisions shape everything else in this theme.',
61
- bullets: [
62
- 'Dark only. The Dracula spec is a dark scheme, so every token pins the same value in both mode slots. No light theme exists or is planned.',
63
- 'Spec exact. Every colour is a hex from the official specification. Nothing is invented, and a new colour need is a brand question, not a one-off value.',
64
- 'Zero runtime cost. The theme compiles to prebuilt CSS, with a runtime injection path for prototyping.',
65
- 'Zero theme dependencies. Fonts and icons are vendored, so adopting the theme installs nothing else.',
66
- ],
67
- },
68
- {
69
- key: 'how-it-fits',
70
- heading: 'How it fits together',
71
- body: 'One source of truth, three outputs, and a gate that refuses to let them drift.',
72
- bullets: [
73
- 'astryx-theme.ts is the only place a colour is decided.',
74
- 'theme.css is the compiled result that apps import.',
75
- 'tokens.css is a plain :root fallback for stacks that are not Astryx.',
76
- 'scripts/check.ts enforces the palette and the contrast floors, so drift fails the build instead of shipping.',
77
- ],
78
- },
79
- ],
80
- },
81
- {
82
- id: 'install',
83
- title: 'Install',
84
- icon: SquareTerminal,
85
- intro:
86
- 'Three import paths, same hexes. Pick one per site and stay on it. The prebuilt path is the recommended one for an Astryx app.',
87
- sections: [
88
- {
89
- key: 'install-package',
90
- heading: 'Install the package',
91
- body: 'The package carries the compiled theme, the plain-CSS fallback, the fonts and the icon registry.',
92
- code: {
93
- language: 'bash',
94
- title: 'shell',
95
- source: `bun add astryx-dracula`,
96
- },
97
- },
98
- {
99
- key: 'wire-the-entry',
100
- heading: 'Wire the entry',
101
- body: 'Import order matters. Core reset and component CSS load first, then the theme tokens, then the compiled theme so its overrides win.',
102
- code: {
103
- language: 'tsx',
104
- title: 'main.tsx',
105
- source: `import '@astryxdesign/core/reset.css';
45
+ {
46
+ title: "Getting started",
47
+ chapters: [
48
+ {
49
+ id: "introduction",
50
+ title: "Introduction",
51
+ icon: Rocket,
52
+ hasArt: true,
53
+ intro:
54
+ "astryx-dracula is a pure Dracula brand theme for Astryx React sites. It pins the official palette, compiles to prebuilt CSS, and costs nothing at runtime. Dark is not a mode here, it is the brand.",
55
+ sections: [
56
+ {
57
+ key: "what-it-is",
58
+ heading: "What it is",
59
+ body: "Four decisions shape everything else in this theme.",
60
+ bullets: [
61
+ "Dark only. The Dracula spec is a dark scheme, so every token pins the same value in both mode slots. No light theme exists or is planned.",
62
+ "Spec exact. Every colour is a hex from the official specification. Nothing is invented, and a new colour need is a brand question, not a one-off value.",
63
+ "Zero runtime cost. The theme compiles to prebuilt CSS, with a runtime injection path for prototyping.",
64
+ "Zero theme dependencies. Fonts and icons are vendored, so adopting the theme installs nothing else.",
65
+ ],
66
+ },
67
+ {
68
+ key: "how-it-fits",
69
+ heading: "How it fits together",
70
+ body: "One source of truth, three outputs, and a gate that refuses to let them drift.",
71
+ bullets: [
72
+ "astryx-theme.ts is the only place a colour is decided.",
73
+ "theme.css is the compiled result that apps import.",
74
+ "tokens.css is a plain :root fallback for stacks that are not Astryx.",
75
+ "scripts/check.ts enforces the palette and the contrast floors, so drift fails the build instead of shipping.",
76
+ ],
77
+ },
78
+ ],
79
+ },
80
+ {
81
+ id: "install",
82
+ title: "Install",
83
+ icon: SquareTerminal,
84
+ intro:
85
+ "Three import paths, same hexes. Pick one per site and stay on it. The prebuilt path is the recommended one for an Astryx app.",
86
+ sections: [
87
+ {
88
+ key: "install-package",
89
+ heading: "Install the package",
90
+ body: "The package carries the compiled theme, the plain-CSS fallback, the fonts and the icon registry.",
91
+ code: {
92
+ language: "bash",
93
+ title: "shell",
94
+ source: `bun add astryx-dracula`,
95
+ },
96
+ },
97
+ {
98
+ key: "wire-the-entry",
99
+ heading: "Wire the entry",
100
+ body: "Import order matters. Core reset and component CSS load first, then the theme tokens, then the compiled theme so its overrides win.",
101
+ code: {
102
+ language: "tsx",
103
+ title: "main.tsx",
104
+ source: `import '@astryxdesign/core/reset.css';
106
105
  import '@astryxdesign/core/astryx.css';
107
106
  import 'astryx-dracula/tokens.css';
108
107
  import 'astryx-dracula/theme.css';
@@ -112,191 +111,192 @@ import {astryxDraculaTheme} from 'astryx-dracula';
112
111
  <Theme theme={astryxDraculaTheme} mode="dark">
113
112
  <App />
114
113
  </Theme>;`,
115
- },
116
- },
117
- {
118
- key: 'ship-the-fonts',
119
- heading: 'Ship the fonts',
120
- body: 'Copy the package fonts directory into your served static folder. tokens.css declares the font faces, so skipping the copy falls back to a system monospace rather than failing loudly.',
121
- bullets: [
122
- 'Vite and similar bundlers serve from public/, so copy to public/fonts/.',
123
- 'The theme sets one family, JetBrains Mono, for body, heading and code alike.',
124
- 'If text renders in a system monospace, the copy step is the first thing to check.',
125
- ],
126
- },
127
- ],
128
- },
129
- ],
130
- },
131
- {
132
- title: 'The brand',
133
- chapters: [
134
- {
135
- id: 'palette',
136
- title: 'Palette',
137
- icon: Sparkles,
138
- hasArt: true,
139
- intro:
140
- 'Fifteen colours carry the entire brand: twelve from the specification and three derived surface steps. Every one is a variable, and none of them is ever written by hand in a component.',
141
- sections: [
142
- {
143
- key: 'the-spec',
144
- heading: 'The specification twelve',
145
- body: 'Background, Current Line, Selection and Foreground set the stage. Comment, Cyan, Green, Orange, Pink, Purple, Red and Yellow do the semantic work.',
146
- bullets: [
147
- 'Current Line doubles as the colour for subtle borders and separators.',
148
- 'Comment and Current Line intentionally share one hex in the spec, which is why they look identical in the strip above.',
149
- 'Purple is reserved for anything tappable, so it never appears as decoration.',
150
- ],
151
- },
152
- {
153
- key: 'surfaces',
154
- heading: 'Surface steps',
155
- body: 'Cards, popovers and shadows come from the spec UI palette rather than from opacity tricks.',
156
- bullets: [
157
- 'Background Light is the card and surface colour.',
158
- 'Background Lighter is for popovers and floating elements.',
159
- 'Background Dark carries shadows and recessed areas.',
160
- ],
161
- },
162
- {
163
- key: 'derived-lifts',
164
- heading: 'Derived lifts',
165
- body: 'Three text greys are deliberately not spec hexes. The spec Comment colour fails the WCAG AA floor for body text, so these are same-hue lifts that clear it. They are documented as deviations rather than silently invented.',
166
- },
167
- ],
168
- },
169
- {
170
- id: 'type-shape',
171
- title: 'Type and shape',
172
- icon: Type,
173
- hasArt: true,
174
- intro:
175
- 'One typeface, a quiet weight range, and borders instead of shadows. The shape language stays flat on purpose.',
176
- sections: [
177
- {
178
- key: 'one-family',
179
- heading: 'One family, on purpose',
180
- body: 'JetBrains Mono runs body, heading and code alike. A single family is what makes the brand legible at a glance next to any sans-serif product.',
181
- bullets: [
182
- 'Roles beat raw sizes: body, supporting, code, large and label.',
183
- 'Nothing meaningful renders below twelve pixels.',
184
- 'Quantities always carry tabular numerals so columns line up.',
185
- ],
186
- },
187
- {
188
- key: 'the-scale',
189
- heading: 'The scale',
190
- body: 'A base of fourteen with a 1.2 ladder generates the steps. Headings take their size from the scale roles, so a level three heading renders the large step rather than a pinned pixel value.',
191
- },
192
- {
193
- key: 'flat-and-crisp',
194
- heading: 'Flat and crisp',
195
- body: 'Five pixel radii on elements and four on inner surfaces. Depth comes from separator borders, never from soft grey shadows, and there are no pills or decorative circles.',
196
- },
197
- ],
198
- },
199
- ],
200
- },
201
- {
202
- title: 'In practice',
203
- chapters: [
204
- {
205
- id: 'components',
206
- title: 'Components',
207
- icon: Rows3,
208
- intro:
209
- 'Styling follows one order: reach for a component prop first, then a theme override, and only then a token in a style object. Per-app CSS is not part of the plan.',
210
- sections: [
211
- {
212
- key: 'props-first',
213
- heading: 'Props before styles',
214
- body: 'Every Astryx component already reads the theme through its props. Passing a prop keeps the component in charge of its own layout and states; an inline style takes that control away.',
215
- bullets: [
216
- 'Layout and spacing come from props, never from a wrapper element.',
217
- 'Dense data renders as rows. Wrapping each row in a card is the fastest way to make a product look templated.',
218
- 'Components do the layout. A raw div for structure is a defect, not a shortcut.',
219
- ],
220
- },
221
- {
222
- key: 'status-vocabulary',
223
- heading: 'Status has a fixed vocabulary',
224
- body: 'Green positive, red negative, yellow tags, cyan information, pink flair, orange warning. Status surfaces use a ten percent categorical wash with a semantic border rather than a direct fill.',
225
- code: {
226
- language: 'tsx',
227
- title: 'Status.tsx',
228
- source: `// Status rides on the component built for it.
114
+ },
115
+ },
116
+ {
117
+ key: "ship-the-fonts",
118
+ heading: "Ship the fonts",
119
+ body: "Copy the package fonts directory into your served static folder. tokens.css declares the font faces, so skipping the copy falls back to a system monospace rather than failing loudly.",
120
+ bullets: [
121
+ "Vite and similar bundlers serve from public/, so copy to public/fonts/.",
122
+ "The theme sets one family, JetBrains Mono, for body, heading and code alike.",
123
+ "If text renders in a system monospace, the copy step is the first thing to check.",
124
+ ],
125
+ },
126
+ ],
127
+ },
128
+ ],
129
+ },
130
+ {
131
+ title: "The brand",
132
+ chapters: [
133
+ {
134
+ id: "palette",
135
+ title: "Palette",
136
+ icon: Sparkles,
137
+ hasArt: true,
138
+ intro:
139
+ "Fifteen colours carry the entire brand: twelve from the specification and three derived surface steps. Every one is a variable, and none of them is ever written by hand in a component.",
140
+ sections: [
141
+ {
142
+ key: "the-spec",
143
+ heading: "The specification twelve",
144
+ body: "Background, Current Line, Selection and Foreground set the stage. Comment, Cyan, Green, Orange, Pink, Purple, Red and Yellow do the semantic work.",
145
+ bullets: [
146
+ "Current Line doubles as the colour for subtle borders and separators.",
147
+ "Comment and Current Line intentionally share one hex in the spec, which is why they look identical in the strip above.",
148
+ "Purple is reserved for anything tappable, so it never appears as decoration.",
149
+ ],
150
+ },
151
+ {
152
+ key: "surfaces",
153
+ heading: "Surface steps",
154
+ body: "Cards, popovers and shadows come from the spec UI palette rather than from opacity tricks.",
155
+ bullets: [
156
+ "Background Light is the card and surface colour.",
157
+ "Background Lighter is for popovers and floating elements.",
158
+ "Background Dark carries shadows and recessed areas.",
159
+ ],
160
+ },
161
+ {
162
+ key: "derived-lifts",
163
+ heading: "Derived lifts",
164
+ body: "Three text greys are deliberately not spec hexes. The spec Comment colour fails the WCAG AA floor for body text, so these are same-hue lifts that clear it. They are documented as deviations rather than silently invented.",
165
+ },
166
+ ],
167
+ },
168
+ {
169
+ id: "type-shape",
170
+ title: "Type and shape",
171
+ icon: Type,
172
+ hasArt: true,
173
+ intro:
174
+ "One typeface, a quiet weight range, and borders instead of shadows. The shape language stays flat on purpose.",
175
+ sections: [
176
+ {
177
+ key: "one-family",
178
+ heading: "One family, on purpose",
179
+ body: "JetBrains Mono runs body, heading and code alike. A single family is what makes the brand legible at a glance next to any sans-serif product.",
180
+ bullets: [
181
+ "Roles beat raw sizes: body, supporting, code, large and label.",
182
+ "Nothing meaningful renders below twelve pixels.",
183
+ "Quantities always carry tabular numerals so columns line up.",
184
+ ],
185
+ },
186
+ {
187
+ key: "the-scale",
188
+ heading: "The scale",
189
+ body: "A base of fourteen with a 1.2 ladder generates the steps. Headings take their size from the scale roles, so a level three heading renders the large step rather than a pinned pixel value.",
190
+ },
191
+ {
192
+ key: "flat-and-crisp",
193
+ heading: "Flat and crisp",
194
+ body: "Five pixel radii on elements and four on inner surfaces. Depth comes from separator borders, never from soft grey shadows, and there are no pills or decorative circles.",
195
+ },
196
+ ],
197
+ },
198
+ ],
199
+ },
200
+ {
201
+ title: "In practice",
202
+ chapters: [
203
+ {
204
+ id: "components",
205
+ title: "Components",
206
+ icon: Rows3,
207
+ intro:
208
+ "Styling follows one order: reach for a component prop first, then a theme override, and only then a token in a style object. Per-app CSS is not part of the plan.",
209
+ sections: [
210
+ {
211
+ key: "props-first",
212
+ heading: "Props before styles",
213
+ body: "Every Astryx component already reads the theme through its props. Passing a prop keeps the component in charge of its own layout and states; an inline style takes that control away.",
214
+ bullets: [
215
+ "Layout and spacing come from props, never from a wrapper element.",
216
+ "Dense data renders as rows. Wrapping each row in a card is the fastest way to make a product look templated.",
217
+ "Components do the layout. A raw div for structure is a defect, not a shortcut.",
218
+ ],
219
+ },
220
+ {
221
+ key: "status-vocabulary",
222
+ heading: "Status has a fixed vocabulary",
223
+ body: "Green positive, red negative, yellow tags, cyan information, pink flair, orange warning. Status surfaces use a ten percent categorical wash with a semantic border rather than a direct fill.",
224
+ code: {
225
+ language: "tsx",
226
+ title: "Status.tsx",
227
+ source: `// Status rides on the component built for it.
229
228
  <StatusDot variant="success" label="Live" />
230
229
 
231
230
  // Counts are what Badge is for. Status is not.
232
231
  <Badge label={unread} variant="neutral" />`,
233
- },
234
- },
235
- {
236
- key: 'purple-means-tappable',
237
- heading: 'Purple means tappable',
238
- body: 'Links, titles and primary actions are purple. Hover resolves to the foreground with an underline, and visited falls back to body text. Users learn the signal in seconds, which is exactly why nothing decorative is allowed to wear it.',
239
- },
240
- ],
241
- },
242
- {
243
- id: 'templates',
244
- title: 'Templates',
245
- icon: LayoutGrid,
246
- hasArt: true,
247
- intro:
248
- 'Forty-five themed pages ship with the package as an Astryx integration pack. Each one is a real page, already retokened and structurally validated, and each one scaffolds in a single command.',
249
- sections: [
250
- {
251
- key: 'scaffold-a-page',
252
- heading: 'Scaffold a page',
253
- body: 'List the package in your Astryx config, then pull any page into your project.',
254
- code: {
255
- language: 'bash',
256
- title: 'shell',
257
- source: `# any of the 45 page ids
258
- bunx astryx template dashboard --package astryx-dracula
259
- bunx astryx template product-tour --package astryx-dracula`,
260
- },
261
- },
262
- {
263
- key: 'pack-rules',
264
- heading: 'What the pages may import',
265
- body: 'Each template is self-contained: React, the Astryx core, Lucide icons, and the kit shared modules (astryx-dracula/shared/*). No chart library, no CSS framework, no other internal module. Every chart is a hand-drawn inline SVG, which is why a scaffolded page has no dependency surprises.',
266
- },
267
- {
268
- key: 'xle-headers',
269
- heading: 'Read the header first',
270
- body: 'Every template leads with a one-line structural expression describing the frame it renders, validated by the CLI. Reading that line costs a fraction of reading the file, which makes adapting a page much cheaper than rebuilding it.',
271
- code: {
272
- language: 'bash',
273
- title: 'shell',
274
- source: `# validate the structure comment against the render
275
- bunx astryx layout check "L > (LC > V[g=4] > Hd\\"Title\\"[level=1])"`,
276
- },
277
- },
278
- ],
279
- },
280
- ],
281
- },
282
- {
283
- title: 'Reference',
284
- chapters: [
285
- {
286
- id: 'tokens',
287
- title: 'Tokens',
288
- icon: Hash,
289
- intro:
290
- 'The theme is a set of custom properties and nothing else. Knowing the four namespaces is enough to build any surface without inventing a value.',
291
- sections: [
292
- {
293
- key: 'namespaces',
294
- heading: 'The four namespaces',
295
- body: 'Each namespace answers a different question, and the answer is always a variable.',
296
- code: {
297
- language: 'css',
298
- title: 'tokens.css',
299
- source: `/* semantic role, set by the theme */
232
+ },
233
+ },
234
+ {
235
+ key: "purple-means-tappable",
236
+ heading: "Purple means tappable",
237
+ body: "Links, titles and primary actions are purple. Hover resolves to the foreground with an underline, and visited falls back to body text. Users learn the signal in seconds, which is exactly why nothing decorative is allowed to wear it.",
238
+ },
239
+ ],
240
+ },
241
+ {
242
+ id: "templates",
243
+ title: "Templates",
244
+ icon: LayoutGrid,
245
+ hasArt: true,
246
+ intro:
247
+ "Forty-five themed pages ship with the package as an Astryx integration pack. Each one is a real page, already retokened and structurally validated, and each one scaffolds in a single command.",
248
+ sections: [
249
+ {
250
+ key: "scaffold-a-page",
251
+ heading: "Scaffold a page",
252
+ body: "List the package in your Astryx config, then pull any page into your project.",
253
+ code: {
254
+ language: "bash",
255
+ title: "shell",
256
+ source: `# any of the 45 page ids — the path argument is required; without it the
257
+ # CLI prints the template source and writes nothing
258
+ bunx @astryxdesign/cli template dashboard --package astryx-dracula ./src/pages/dashboard.tsx
259
+ bunx @astryxdesign/cli template product-tour --package astryx-dracula ./src/pages/product-tour.tsx`,
260
+ },
261
+ },
262
+ {
263
+ key: "pack-rules",
264
+ heading: "What the pages may import",
265
+ body: "Each template is self-contained: React, the Astryx core, Lucide icons, and the kit shared modules (astryx-dracula/shared/*). No chart library, no CSS framework, no other internal module. Every chart is a hand-drawn inline SVG, which is why a scaffolded page has no dependency surprises.",
266
+ },
267
+ {
268
+ key: "xle-headers",
269
+ heading: "Read the header first",
270
+ body: "Every template leads with a one-line structural expression describing the frame it renders. Reading that line costs a fraction of reading the file, which makes adapting a page much cheaper than rebuilding it. The CLI no longer ships a validator for it — `astryx layout` was removed in 0.7.0 — so it is a reading aid and a human drift check: update it with the file, never alone.",
271
+ code: {
272
+ language: "bash",
273
+ title: "shell",
274
+ source: `# read the structure comment at the top of the template
275
+ head -4 src/pages/dashboard.tsx`,
276
+ },
277
+ },
278
+ ],
279
+ },
280
+ ],
281
+ },
282
+ {
283
+ title: "Reference",
284
+ chapters: [
285
+ {
286
+ id: "tokens",
287
+ title: "Tokens",
288
+ icon: Hash,
289
+ intro:
290
+ "The theme is a set of custom properties and nothing else. Knowing the four namespaces is enough to build any surface without inventing a value.",
291
+ sections: [
292
+ {
293
+ key: "namespaces",
294
+ heading: "The four namespaces",
295
+ body: "Each namespace answers a different question, and the answer is always a variable.",
296
+ code: {
297
+ language: "css",
298
+ title: "tokens.css",
299
+ source: `/* semantic role, set by the theme */
300
300
  color: var(--color-text-primary);
301
301
 
302
302
  /* raw Dracula hex, for art and one-off data colours */
@@ -307,61 +307,59 @@ padding: var(--spacing-6);
307
307
 
308
308
  /* layout and shape */
309
309
  border-radius: var(--radius-container);`,
310
- },
311
- },
312
- {
313
- key: 'never-invent-a-hex',
314
- heading: 'Never invent a hex',
315
- body: 'A new colour need goes through the theme source and the audit, not into a component. Overriding colour variables in an app root is the other half of the same rule: brand changes belong in the theme, so every site that uses it moves together.',
316
- },
317
- {
318
- key: 'audit-gates',
319
- heading: 'The audit gates',
320
- body: 'Two commands stand between a colour decision and a published package.',
321
- code: {
322
- language: 'bash',
323
- title: 'shell',
324
- source: `bun run audit # palette purity plus contrast floors
310
+ },
311
+ },
312
+ {
313
+ key: "never-invent-a-hex",
314
+ heading: "Never invent a hex",
315
+ body: "A new colour need goes through the theme source and the audit, not into a component. Overriding colour variables in an app root is the other half of the same rule: brand changes belong in the theme, so every site that uses it moves together.",
316
+ },
317
+ {
318
+ key: "audit-gates",
319
+ heading: "The audit gates",
320
+ body: "Two commands stand between a colour decision and a published package.",
321
+ code: {
322
+ language: "bash",
323
+ title: "shell",
324
+ source: `bun run audit # palette purity plus contrast floors
325
325
  bun run theme:check # fails when compiled CSS is stale`,
326
- },
327
- },
328
- ],
329
- },
330
- {
331
- id: 'migrating',
332
- title: 'Migrating',
333
- icon: RotateCw,
334
- intro:
335
- 'Bringing an existing site onto the theme is an inventory problem before it is a styling problem. Find the colours first, then map them.',
336
- sections: [
337
- {
338
- key: 'inventory',
339
- heading: 'Inventory what is there',
340
- body: 'Search for raw hex values, existing root override blocks, utility frameworks and any theme provider already in the tree. The goal is a complete list before a single file is touched.',
341
- },
342
- {
343
- key: 'map-and-replace',
344
- heading: 'Map, then replace',
345
- body: 'Every found colour maps to a row in the brand guide. A colour that will not map is a brand question rather than a new hex, which is the point of doing the inventory first.',
346
- bullets: [
347
- 'Delete the old provider and the root overrides in the same change that adds the new ones.',
348
- 'Swap raw elements for the components that already carry the layout.',
349
- 'Keep the diff boring: one mechanism per site, not a migration and a redesign at once.',
350
- ],
351
- },
352
- {
353
- key: 'verify',
354
- heading: 'Verify',
355
- body: 'Build, screenshot the key pages, and grep the source for stray hexes. A surviving hex in application code means the mapping was incomplete, not that the theme needed a new colour.',
356
- },
357
- ],
358
- },
359
- ],
360
- },
326
+ },
327
+ },
328
+ ],
329
+ },
330
+ {
331
+ id: "migrating",
332
+ title: "Migrating",
333
+ icon: RotateCw,
334
+ intro:
335
+ "Bringing an existing site onto the theme is an inventory problem before it is a styling problem. Find the colours first, then map them.",
336
+ sections: [
337
+ {
338
+ key: "inventory",
339
+ heading: "Inventory what is there",
340
+ body: "Search for raw hex values, existing root override blocks, utility frameworks and any theme provider already in the tree. The goal is a complete list before a single file is touched.",
341
+ },
342
+ {
343
+ key: "map-and-replace",
344
+ heading: "Map, then replace",
345
+ body: "Every found colour maps to a row in the brand guide. A colour that will not map is a brand question rather than a new hex, which is the point of doing the inventory first.",
346
+ bullets: [
347
+ "Delete the old provider and the root overrides in the same change that adds the new ones.",
348
+ "Swap raw elements for the components that already carry the layout.",
349
+ "Keep the diff boring: one mechanism per site, not a migration and a redesign at once.",
350
+ ],
351
+ },
352
+ {
353
+ key: "verify",
354
+ heading: "Verify",
355
+ body: "Build, screenshot the key pages, and grep the source for stray hexes. A surviving hex in application code means the mapping was incomplete, not that the theme needed a new colour.",
356
+ },
357
+ ],
358
+ },
359
+ ],
360
+ },
361
361
  ];
362
362
 
363
-
364
-
365
363
  // ─── Scene: the theme at night ───────────────────────────────────────────────
366
364
  // Drawn, not loaded. Inline SVG keeps a template dependency-free and paints in
367
365
  // theme tokens instead of a fixed palette, so every scene follows the brand.
@@ -371,191 +369,179 @@ bun run theme:check # fails when compiled CSS is stale`,
371
369
  // leading edge out to the tip and a scalloped trailing edge back; the second
372
370
  // subpath is the same wing mirrored.
373
371
  const BAT_WING =
374
- 'M 2 -4.6 C 5.6 -8.6 12 -11 19.5 -9.6 C 18 -5 16.5 -1.6 14.2 1.2 ' +
375
- 'C 13.6 -1.2 12.6 -2.2 11.2 -2.1 C 10.8 0.4 10 2.2 8.4 3.2 ' +
376
- 'C 7.9 0.6 7.2 -0.4 6.1 -0.3 C 5.8 1.6 5.2 2.8 4.2 3.6 ' +
377
- 'C 3.4 1.4 2.6 -1.6 2 -4.6 Z ' +
378
- 'M -2 -4.6 C -5.6 -8.6 -12 -11 -19.5 -9.6 C -18 -5 -16.5 -1.6 -14.2 1.2 ' +
379
- 'C -13.6 -1.2 -12.6 -2.2 -11.2 -2.1 C -10.8 0.4 -10 2.2 -8.4 3.2 ' +
380
- 'C -7.9 0.6 -7.2 -0.4 -6.1 -0.3 C -5.8 1.6 -5.2 2.8 -4.2 3.6 ' +
381
- 'C -3.4 1.4 -2.6 -1.6 -2 -4.6 Z';
372
+ "M 2 -4.6 C 5.6 -8.6 12 -11 19.5 -9.6 C 18 -5 16.5 -1.6 14.2 1.2 " +
373
+ "C 13.6 -1.2 12.6 -2.2 11.2 -2.1 C 10.8 0.4 10 2.2 8.4 3.2 " +
374
+ "C 7.9 0.6 7.2 -0.4 6.1 -0.3 C 5.8 1.6 5.2 2.8 4.2 3.6 " +
375
+ "C 3.4 1.4 2.6 -1.6 2 -4.6 Z " +
376
+ "M -2 -4.6 C -5.6 -8.6 -12 -11 -19.5 -9.6 C -18 -5 -16.5 -1.6 -14.2 1.2 " +
377
+ "C -13.6 -1.2 -12.6 -2.2 -11.2 -2.1 C -10.8 0.4 -10 2.2 -8.4 3.2 " +
378
+ "C -7.9 0.6 -7.2 -0.4 -6.1 -0.3 C -5.8 1.6 -5.2 2.8 -4.2 3.6 " +
379
+ "C -3.4 1.4 -2.6 -1.6 -2 -4.6 Z";
382
380
 
383
381
  // Body between the wings: two ears, a shoulder, a taper to the tail.
384
382
  const BAT_BODY =
385
- 'M -2.2 -5 C -2.5 -6.6 -3.2 -8.2 -4 -9.2 L -2.3 -7.6 ' +
386
- 'C -1.5 -8.2 -0.8 -8.4 0 -8.4 C 0.8 -8.4 1.5 -8.2 2.3 -7.6 ' +
387
- 'L 4 -9.2 C 3.2 -8.2 2.5 -6.6 2.2 -5 C 2.4 -0.8 1.9 2.6 0 6 ' +
388
- 'C -1.9 2.6 -2.4 -0.8 -2.2 -5 Z';
383
+ "M -2.2 -5 C -2.5 -6.6 -3.2 -8.2 -4 -9.2 L -2.3 -7.6 " +
384
+ "C -1.5 -8.2 -0.8 -8.4 0 -8.4 C 0.8 -8.4 1.5 -8.2 2.3 -7.6 " +
385
+ "L 4 -9.2 C 3.2 -8.2 2.5 -6.6 2.2 -5 C 2.4 -0.8 1.9 2.6 0 6 " +
386
+ "C -1.9 2.6 -2.4 -0.8 -2.2 -5 Z";
389
387
 
390
388
  // A stepped conifer, 20 tall on its base at the origin. Three of these on the
391
389
  // near crest give the castle a scale to be measured against.
392
390
  const FIR =
393
- 'M 0 -20 L 2.6 -13.5 L 1.4 -13.5 L 4.2 -7.5 L 2.8 -7.5 L 5.6 -1.5 ' +
394
- 'L -5.6 -1.5 L -2.8 -7.5 L -4.2 -7.5 L -1.4 -13.5 L -2.6 -13.5 Z';
395
-
396
- function Bat({x, y, scale, opacity}: {x: number; y: number; scale: number; opacity: number}) {
397
- return (
398
- <g
399
- fill="var(--dracula-comment)"
400
- opacity={opacity}
401
- transform={`translate(${x} ${y}) scale(${scale})`}>
402
- <path d={BAT_WING} />
403
- <path d={BAT_BODY} />
404
- </g>
405
- );
391
+ "M 0 -20 L 2.6 -13.5 L 1.4 -13.5 L 4.2 -7.5 L 2.8 -7.5 L 5.6 -1.5 " +
392
+ "L -5.6 -1.5 L -2.8 -7.5 L -4.2 -7.5 L -1.4 -13.5 L -2.6 -13.5 Z";
393
+
394
+ function Bat({ x, y, scale, opacity }: { x: number; y: number; scale: number; opacity: number }) {
395
+ return (
396
+ <g
397
+ fill="var(--dracula-comment)"
398
+ opacity={opacity}
399
+ transform={`translate(${x} ${y}) scale(${scale})`}
400
+ >
401
+ <path d={BAT_WING} />
402
+ <path d={BAT_BODY} />
403
+ </g>
404
+ );
406
405
  }
407
406
 
408
- function Fir({x, y, scale}: {x: number; y: number; scale: number}) {
409
- return <path d={FIR} transform={`translate(${x} ${y}) scale(${scale})`} />;
407
+ function Fir({ x, y, scale }: { x: number; y: number; scale: number }) {
408
+ return <path d={FIR} transform={`translate(${x} ${y}) scale(${scale})`} />;
410
409
  }
411
410
 
412
- function NightScene({alt}: {alt: string}) {
413
- return (
414
- <SceneFrame label={alt} hasBackdrop={false}>
415
- <defs>
416
- {/* The moon's halo and the light it drops on the ridge below. Drawn as
411
+ function NightScene({ alt }: { alt: string }) {
412
+ return (
413
+ <SceneFrame label={alt} hasBackdrop={false}>
414
+ <defs>
415
+ {/* The moon's halo and the light it drops on the ridge below. Drawn as
417
416
  gradients: stacked flat circles band badly at this contrast. */}
418
- <radialGradient id="pt-night-glow">
419
- <stop
420
- offset="30%"
421
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0.16}}
422
- />
423
- <stop
424
- offset="65%"
425
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0.05}}
426
- />
427
- <stop
428
- offset="100%"
429
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0}}
430
- />
431
- </radialGradient>
432
- <radialGradient id="pt-night-pool">
433
- <stop
434
- offset="0%"
435
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0.13}}
436
- />
437
- <stop
438
- offset="55%"
439
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0.04}}
440
- />
441
- <stop
442
- offset="100%"
443
- style={{stopColor: 'var(--dracula-yellow)', stopOpacity: 0}}
444
- />
445
- </radialGradient>
446
- {/* The far ridge doubles as the clip for its own pool of moonlight. */}
447
- <clipPath id="pt-night-lit">
448
- <path d="M0 118 Q34 88 78 82 Q124 74 150 78 Q188 84 226 104 Q268 126 318 128 Q360 130 400 122 V225 H0 Z" />
449
- </clipPath>
450
- </defs>
451
- <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
452
-
453
- {/* Star field. Comment grey, never purple: purple is reserved. */}
454
- <g fill="var(--dracula-comment)">
455
- <circle cx="38" cy="34" r="1.5" opacity="0.85" />
456
- <circle cx="72" cy="58" r="1.1" opacity="0.6" />
457
- <circle cx="108" cy="26" r="1.3" opacity="0.75" />
458
- <circle cx="146" cy="72" r="1" opacity="0.5" />
459
- <circle cx="176" cy="34" r="1.6" opacity="0.9" />
460
- <circle cx="206" cy="84" r="1.1" opacity="0.55" />
461
- <circle cx="232" cy="20" r="1.2" opacity="0.7" />
462
- <circle cx="258" cy="62" r="1" opacity="0.5" />
463
- <circle cx="120" cy="74" r="1" opacity="0.45" />
464
- <circle cx="356" cy="30" r="1.3" opacity="0.75" />
465
- <circle cx="380" cy="86" r="1.1" opacity="0.55" />
466
- <circle cx="28" cy="72" r="1.2" opacity="0.6" />
467
- <circle cx="188" cy="60" r="1" opacity="0.45" />
468
- </g>
469
- <g fill="var(--dracula-cyan)" opacity="0.85">
470
- <circle cx="166" cy="46" r="1.4" />
471
- <circle cx="372" cy="22" r="1.2" />
472
- </g>
473
- <g fill="var(--dracula-pink)" opacity="0.8">
474
- <circle cx="94" cy="44" r="1.3" />
475
- <circle cx="272" cy="94" r="1.1" />
476
- </g>
477
-
478
- {/* Harvest moon: halo, disc, then the maria. */}
479
- <circle cx="306" cy="62" r="74" fill="url(#pt-night-glow)" />
480
- <circle cx="306" cy="62" r="25" fill="var(--dracula-yellow)" />
481
- <circle cx="296" cy="53" r="7" fill="var(--dracula-orange)" opacity="0.5" />
482
- <circle cx="314" cy="71" r="4.5" fill="var(--dracula-orange)" opacity="0.5" />
483
- <circle cx="310" cy="50" r="3" fill="var(--dracula-orange)" opacity="0.35" />
484
-
485
- {/* Far ridge, hazed toward the moon. It carries the castle, so it is the
417
+ <radialGradient id="pt-night-glow">
418
+ <stop offset="30%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0.16 }} />
419
+ <stop offset="65%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0.05 }} />
420
+ <stop offset="100%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0 }} />
421
+ </radialGradient>
422
+ <radialGradient id="pt-night-pool">
423
+ <stop offset="0%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0.13 }} />
424
+ <stop offset="55%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0.04 }} />
425
+ <stop offset="100%" style={{ stopColor: "var(--dracula-yellow)", stopOpacity: 0 }} />
426
+ </radialGradient>
427
+ {/* The far ridge doubles as the clip for its own pool of moonlight. */}
428
+ <clipPath id="pt-night-lit">
429
+ <path d="M0 118 Q34 88 78 82 Q124 74 150 78 Q188 84 226 104 Q268 126 318 128 Q360 130 400 122 V225 H0 Z" />
430
+ </clipPath>
431
+ </defs>
432
+ <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
433
+
434
+ {/* Star field. Comment grey, never purple: purple is reserved. */}
435
+ <g fill="var(--dracula-comment)">
436
+ <circle cx="38" cy="34" r="1.5" opacity="0.85" />
437
+ <circle cx="72" cy="58" r="1.1" opacity="0.6" />
438
+ <circle cx="108" cy="26" r="1.3" opacity="0.75" />
439
+ <circle cx="146" cy="72" r="1" opacity="0.5" />
440
+ <circle cx="176" cy="34" r="1.6" opacity="0.9" />
441
+ <circle cx="206" cy="84" r="1.1" opacity="0.55" />
442
+ <circle cx="232" cy="20" r="1.2" opacity="0.7" />
443
+ <circle cx="258" cy="62" r="1" opacity="0.5" />
444
+ <circle cx="120" cy="74" r="1" opacity="0.45" />
445
+ <circle cx="356" cy="30" r="1.3" opacity="0.75" />
446
+ <circle cx="380" cy="86" r="1.1" opacity="0.55" />
447
+ <circle cx="28" cy="72" r="1.2" opacity="0.6" />
448
+ <circle cx="188" cy="60" r="1" opacity="0.45" />
449
+ </g>
450
+ <g fill="var(--dracula-cyan)" opacity="0.85">
451
+ <circle cx="166" cy="46" r="1.4" />
452
+ <circle cx="372" cy="22" r="1.2" />
453
+ </g>
454
+ <g fill="var(--dracula-pink)" opacity="0.8">
455
+ <circle cx="94" cy="44" r="1.3" />
456
+ <circle cx="272" cy="94" r="1.1" />
457
+ </g>
458
+
459
+ {/* Harvest moon: halo, disc, then the maria. */}
460
+ <circle cx="306" cy="62" r="74" fill="url(#pt-night-glow)" />
461
+ <circle cx="306" cy="62" r="25" fill="var(--dracula-yellow)" />
462
+ <circle cx="296" cy="53" r="7" fill="var(--dracula-orange)" opacity="0.5" />
463
+ <circle cx="314" cy="71" r="4.5" fill="var(--dracula-orange)" opacity="0.5" />
464
+ <circle cx="310" cy="50" r="3" fill="var(--dracula-orange)" opacity="0.35" />
465
+
466
+ {/* Far ridge, hazed toward the moon. It carries the castle, so it is the
486
467
  lightest plane in the scene. */}
487
- <path
488
- d="M0 118 Q34 88 78 82 Q124 74 150 78 Q188 84 226 104 Q268 126 318 128 Q360 130 400 122 V225 H0 Z"
489
- fill="var(--dracula-selection)"
490
- />
491
- <g clipPath="url(#pt-night-lit)">
492
- <ellipse
493
- cx="304"
494
- cy="182"
495
- rx="128"
496
- ry="68"
497
- fill="url(#pt-night-pool)"
498
- transform="rotate(-6 304 182)"
499
- />
500
- </g>
501
-
502
- {/* Castle, a silhouette against the lit ridge: battlements, two towers,
468
+ <path
469
+ d="M0 118 Q34 88 78 82 Q124 74 150 78 Q188 84 226 104 Q268 126 318 128 Q360 130 400 122 V225 H0 Z"
470
+ fill="var(--dracula-selection)"
471
+ />
472
+ <g clipPath="url(#pt-night-lit)">
473
+ <ellipse
474
+ cx="304"
475
+ cy="182"
476
+ rx="128"
477
+ ry="68"
478
+ fill="url(#pt-night-pool)"
479
+ transform="rotate(-6 304 182)"
480
+ />
481
+ </g>
482
+
483
+ {/* Castle, a silhouette against the lit ridge: battlements, two towers,
503
484
  the keep breaking the skyline, and four windows lit from inside. */}
504
- <g fill="var(--dracula-bg-dark)">
505
- <rect x="104" y="124" width="82" height="16" />
506
- <rect x="127" y="119" width="7" height="5" />
507
- <rect x="137" y="119" width="7" height="5" />
508
- <rect x="147" y="119" width="7" height="5" />
509
- <rect x="157" y="119" width="7" height="5" />
510
- <rect x="104" y="100" width="22" height="40" />
511
- <rect x="104.5" y="95" width="7" height="5" />
512
- <rect x="114.5" y="95" width="7" height="5" />
513
- <rect x="164" y="96" width="22" height="44" />
514
- <rect x="164.5" y="91" width="7" height="5" />
515
- <rect x="174.5" y="91" width="7" height="5" />
516
- <rect x="132" y="68" width="28" height="72" />
517
- <rect x="132.5" y="63" width="7" height="5" />
518
- <rect x="142" y="63" width="7" height="5" />
519
- <rect x="151.5" y="63" width="7" height="5" />
520
- </g>
521
- <g fill="var(--dracula-yellow)">
522
- <path d="M139 124 v-10 a6 6 0 0 1 12 0 v10 Z" />
523
- <rect x="142" y="84" width="6" height="8" rx="1" />
524
- <rect x="110" y="112" width="5" height="8" rx="1" />
525
- <rect x="173" y="108" width="5" height="8" rx="1" />
526
- </g>
527
- <rect x="173" y="124" width="5" height="8" rx="1" fill="var(--dracula-orange)" opacity="0.55" />
528
-
529
- {/* Three planes of ground, each darker than the one behind it. The near
485
+ <g fill="var(--dracula-bg-dark)">
486
+ <rect x="104" y="124" width="82" height="16" />
487
+ <rect x="127" y="119" width="7" height="5" />
488
+ <rect x="137" y="119" width="7" height="5" />
489
+ <rect x="147" y="119" width="7" height="5" />
490
+ <rect x="157" y="119" width="7" height="5" />
491
+ <rect x="104" y="100" width="22" height="40" />
492
+ <rect x="104.5" y="95" width="7" height="5" />
493
+ <rect x="114.5" y="95" width="7" height="5" />
494
+ <rect x="164" y="96" width="22" height="44" />
495
+ <rect x="164.5" y="91" width="7" height="5" />
496
+ <rect x="174.5" y="91" width="7" height="5" />
497
+ <rect x="132" y="68" width="28" height="72" />
498
+ <rect x="132.5" y="63" width="7" height="5" />
499
+ <rect x="142" y="63" width="7" height="5" />
500
+ <rect x="151.5" y="63" width="7" height="5" />
501
+ </g>
502
+ <g fill="var(--dracula-yellow)">
503
+ <path d="M139 124 v-10 a6 6 0 0 1 12 0 v10 Z" />
504
+ <rect x="142" y="84" width="6" height="8" rx="1" />
505
+ <rect x="110" y="112" width="5" height="8" rx="1" />
506
+ <rect x="173" y="108" width="5" height="8" rx="1" />
507
+ </g>
508
+ <rect
509
+ x="173"
510
+ y="124"
511
+ width="5"
512
+ height="8"
513
+ rx="1"
514
+ fill="var(--dracula-orange)"
515
+ opacity="0.55"
516
+ />
517
+
518
+ {/* Three planes of ground, each darker than the one behind it. The near
530
519
  rise crops the castle's base, which is what puts it at a distance. */}
531
- <path
532
- d="M0 162 Q58 134 120 131 Q182 128 240 150 Q312 176 400 152 V225 H0 Z"
533
- fill="var(--dracula-bg-light)"
534
- />
535
- <path
536
- d="M0 196 Q86 176 168 186 Q250 196 322 182 Q366 174 400 184 V225 H0 Z"
537
- fill="var(--dracula-bg)"
538
- />
539
- <path
540
- d="M0 212 Q118 200 236 210 Q324 217 400 208 V225 H0 Z"
541
- fill="var(--dracula-bg-dark)"
542
- />
543
-
544
- <g fill="var(--dracula-bg-dark)">
545
- <Fir x={30} y={149} scale={0.7} />
546
- <Fir x={48} y={144} scale={0.5} />
547
- <Fir x={250} y={152} scale={0.7} />
548
- <Fir x={266} y={156} scale={0.55} />
549
- <Fir x={286} y={159} scale={0.8} />
550
- <Fir x={100} y={185} scale={0.95} />
551
- </g>
552
-
553
- {/* Bats: nearest is largest and most solid. */}
554
- <Bat x={252} y={92} scale={1.1} opacity={0.92} />
555
- <Bat x={178} y={42} scale={0.72} opacity={0.78} />
556
- <Bat x={352} y={96} scale={0.5} opacity={0.55} />
557
- </SceneFrame>
558
- );
520
+ <path
521
+ d="M0 162 Q58 134 120 131 Q182 128 240 150 Q312 176 400 152 V225 H0 Z"
522
+ fill="var(--dracula-bg-light)"
523
+ />
524
+ <path
525
+ d="M0 196 Q86 176 168 186 Q250 196 322 182 Q366 174 400 184 V225 H0 Z"
526
+ fill="var(--dracula-bg)"
527
+ />
528
+ <path d="M0 212 Q118 200 236 210 Q324 217 400 208 V225 H0 Z" fill="var(--dracula-bg-dark)" />
529
+
530
+ <g fill="var(--dracula-bg-dark)">
531
+ <Fir x={30} y={149} scale={0.7} />
532
+ <Fir x={48} y={144} scale={0.5} />
533
+ <Fir x={250} y={152} scale={0.7} />
534
+ <Fir x={266} y={156} scale={0.55} />
535
+ <Fir x={286} y={159} scale={0.8} />
536
+ <Fir x={100} y={185} scale={0.95} />
537
+ </g>
538
+
539
+ {/* Bats: nearest is largest and most solid. */}
540
+ <Bat x={252} y={92} scale={1.1} opacity={0.92} />
541
+ <Bat x={178} y={42} scale={0.72} opacity={0.78} />
542
+ <Bat x={352} y={96} scale={0.5} opacity={0.55} />
543
+ </SceneFrame>
544
+ );
559
545
  }
560
546
 
561
547
  // ─── Scene: the palette ──────────────────────────────────────────────────────
@@ -563,63 +549,63 @@ function NightScene({alt}: {alt: string}) {
563
549
  // Three rows of five: the six surfaces and neutrals, then the nine accents,
564
550
  // in the same order as the palette table in the brand guide.
565
551
  const PALETTE_ROWS: string[][] = [
566
- [
567
- 'var(--dracula-bg)',
568
- 'var(--dracula-current-line)',
569
- 'var(--dracula-selection)',
570
- 'var(--dracula-bg-light)',
571
- 'var(--dracula-bg-lighter)',
572
- ],
573
- [
574
- 'var(--dracula-bg-dark)',
575
- 'var(--dracula-fg)',
576
- 'var(--dracula-comment)',
577
- 'var(--dracula-cyan)',
578
- 'var(--dracula-green)',
579
- ],
580
- [
581
- 'var(--dracula-orange)',
582
- 'var(--dracula-pink)',
583
- 'var(--dracula-purple)',
584
- 'var(--dracula-red)',
585
- 'var(--dracula-yellow)',
586
- ],
552
+ [
553
+ "var(--dracula-bg)",
554
+ "var(--dracula-current-line)",
555
+ "var(--dracula-selection)",
556
+ "var(--dracula-bg-light)",
557
+ "var(--dracula-bg-lighter)",
558
+ ],
559
+ [
560
+ "var(--dracula-bg-dark)",
561
+ "var(--dracula-fg)",
562
+ "var(--dracula-comment)",
563
+ "var(--dracula-cyan)",
564
+ "var(--dracula-green)",
565
+ ],
566
+ [
567
+ "var(--dracula-orange)",
568
+ "var(--dracula-pink)",
569
+ "var(--dracula-purple)",
570
+ "var(--dracula-red)",
571
+ "var(--dracula-yellow)",
572
+ ],
587
573
  ];
588
574
 
589
- function PaletteScene({alt}: {alt: string}) {
590
- return (
591
- <SceneFrame label={alt} hasBackdrop={false}>
592
- <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
593
- {/* Swatches run edge to edge with one hairline each. Without the stroke
575
+ function PaletteScene({ alt }: { alt: string }) {
576
+ return (
577
+ <SceneFrame label={alt} hasBackdrop={false}>
578
+ <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
579
+ {/* Swatches run edge to edge with one hairline each. Without the stroke
594
580
  the three darkest surfaces vanish into a backdrop of the same tone. */}
595
- {PALETTE_ROWS.map((row, rowIndex) => (
596
- <g key={row.join('-')}>
597
- {row.map((fill, columnIndex) => (
598
- <rect
599
- key={fill}
600
- x={13 + columnIndex * 76}
601
- y={27 + rowIndex * 62}
602
- width={70}
603
- height={50}
604
- rx={6}
605
- fill={fill}
606
- stroke="var(--dracula-comment)"
607
- strokeOpacity={0.55}
608
- />
609
- ))}
610
- </g>
611
- ))}
612
- </SceneFrame>
613
- );
581
+ {PALETTE_ROWS.map((row, rowIndex) => (
582
+ <g key={row.join("-")}>
583
+ {row.map((fill, columnIndex) => (
584
+ <rect
585
+ key={fill}
586
+ x={13 + columnIndex * 76}
587
+ y={27 + rowIndex * 62}
588
+ width={70}
589
+ height={50}
590
+ rx={6}
591
+ fill={fill}
592
+ stroke="var(--dracula-comment)"
593
+ strokeOpacity={0.55}
594
+ />
595
+ ))}
596
+ </g>
597
+ ))}
598
+ </SceneFrame>
599
+ );
614
600
  }
615
601
 
616
602
  // ─── Scene: the type scale ───────────────────────────────────────────────────
617
603
 
618
604
  const TYPE_LADDER = [
619
- {label: 'h1', size: 30},
620
- {label: 'h2', size: 24},
621
- {label: 'body', size: 18},
622
- {label: 'supporting', size: 14},
605
+ { label: "h1", size: 30 },
606
+ { label: "h2", size: 24 },
607
+ { label: "body", size: 18 },
608
+ { label: "supporting", size: 14 },
623
609
  ];
624
610
 
625
611
  // The specimen is set on a 34px baseline grid and the ladder lands on the same
@@ -627,99 +613,83 @@ const TYPE_LADDER = [
627
613
  const TYPE_LEADING = 34;
628
614
  const TYPE_BASELINE = 74;
629
615
 
630
- function TypeScene({alt}: {alt: string}) {
631
- return (
632
- <SceneFrame label={alt} hasBackdrop={false}>
633
- <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
634
-
635
- {/* One rule per ladder row: the grid is the scale's own rhythm. */}
636
- <g stroke="var(--dracula-comment)" strokeOpacity={0.3}>
637
- {TYPE_LADDER.map((step, index) => (
638
- <path
639
- key={step.label}
640
- d={`M0 ${TYPE_BASELINE + index * TYPE_LEADING} H400`}
641
- />
642
- ))}
643
- </g>
644
- {/* Specimen on the left of the rule, the ladder on the right. */}
645
- <path d="M206 52 V192" stroke="var(--dracula-comment)" strokeOpacity={0.22} />
646
-
647
- <g fontFamily="var(--font-family-mono)">
648
- <text x="20" y={TYPE_BASELINE} fontSize={52} fill="var(--dracula-fg)">
649
- Aa
650
- </text>
651
- <text
652
- x="98"
653
- y={TYPE_BASELINE}
654
- fontSize={30}
655
- fill="var(--dracula-cyan)">
656
- {'{}'}
657
- </text>
658
- <text
659
- x="152"
660
- y={TYPE_BASELINE}
661
- fontSize={20}
662
- fill="var(--dracula-pink)">
663
- =&gt;
664
- </text>
665
- <text
666
- x="20"
667
- y={TYPE_BASELINE + TYPE_LEADING}
668
- fontSize={15}
669
- fill="var(--dracula-comment)">
670
- 0O1lI| 5S 2Z
671
- </text>
672
- <text
673
- x="20"
674
- y={TYPE_BASELINE + TYPE_LEADING * 2}
675
- fontSize={15}
676
- fill="var(--dracula-green)">
677
- 0123456789
678
- </text>
679
- <text
680
- x="20"
681
- y={TYPE_BASELINE + TYPE_LEADING * 3}
682
- fontSize={15}
683
- fill="var(--dracula-orange)">
684
- $ % @ # &lt;-&gt; []
685
- </text>
686
-
687
- {TYPE_LADDER.map((step, index) => (
688
- <g key={step.label}>
689
- <text
690
- x="224"
691
- y={TYPE_BASELINE + index * TYPE_LEADING}
692
- fontSize={step.size}
693
- fill={index === 0 ? 'var(--dracula-fg)' : 'var(--dracula-comment)'}>
694
- {step.label}
695
- </text>
696
- <text
697
- x="384"
698
- y={TYPE_BASELINE + index * TYPE_LEADING}
699
- fontSize={9}
700
- textAnchor="end"
701
- fill="var(--dracula-comment)"
702
- fillOpacity={0.65}>
703
- {step.size}
704
- </text>
705
- </g>
706
- ))}
707
- </g>
708
- </SceneFrame>
709
- );
616
+ function TypeScene({ alt }: { alt: string }) {
617
+ return (
618
+ <SceneFrame label={alt} hasBackdrop={false}>
619
+ <rect width="400" height="225" fill="var(--dracula-bg-dark)" />
620
+
621
+ {/* One rule per ladder row: the grid is the scale's own rhythm. */}
622
+ <g stroke="var(--dracula-comment)" strokeOpacity={0.3}>
623
+ {TYPE_LADDER.map((step, index) => (
624
+ <path key={step.label} d={`M0 ${TYPE_BASELINE + index * TYPE_LEADING} H400`} />
625
+ ))}
626
+ </g>
627
+ {/* Specimen on the left of the rule, the ladder on the right. */}
628
+ <path d="M206 52 V192" stroke="var(--dracula-comment)" strokeOpacity={0.22} />
629
+
630
+ <g fontFamily="var(--font-family-mono)">
631
+ <text x="20" y={TYPE_BASELINE} fontSize={52} fill="var(--dracula-fg)">
632
+ Aa
633
+ </text>
634
+ <text x="98" y={TYPE_BASELINE} fontSize={30} fill="var(--dracula-cyan)">
635
+ {"{}"}
636
+ </text>
637
+ <text x="152" y={TYPE_BASELINE} fontSize={20} fill="var(--dracula-pink)">
638
+ =&gt;
639
+ </text>
640
+ <text x="20" y={TYPE_BASELINE + TYPE_LEADING} fontSize={15} fill="var(--dracula-comment)">
641
+ 0O1lI| 5S 2Z
642
+ </text>
643
+ <text x="20" y={TYPE_BASELINE + TYPE_LEADING * 2} fontSize={15} fill="var(--dracula-green)">
644
+ 0123456789
645
+ </text>
646
+ <text
647
+ x="20"
648
+ y={TYPE_BASELINE + TYPE_LEADING * 3}
649
+ fontSize={15}
650
+ fill="var(--dracula-orange)"
651
+ >
652
+ $ % @ # &lt;-&gt; []
653
+ </text>
654
+
655
+ {TYPE_LADDER.map((step, index) => (
656
+ <g key={step.label}>
657
+ <text
658
+ x="224"
659
+ y={TYPE_BASELINE + index * TYPE_LEADING}
660
+ fontSize={step.size}
661
+ fill={index === 0 ? "var(--dracula-fg)" : "var(--dracula-comment)"}
662
+ >
663
+ {step.label}
664
+ </text>
665
+ <text
666
+ x="384"
667
+ y={TYPE_BASELINE + index * TYPE_LEADING}
668
+ fontSize={9}
669
+ textAnchor="end"
670
+ fill="var(--dracula-comment)"
671
+ fillOpacity={0.65}
672
+ >
673
+ {step.size}
674
+ </text>
675
+ </g>
676
+ ))}
677
+ </g>
678
+ </SceneFrame>
679
+ );
710
680
  }
711
681
 
712
682
  // ─── Scene: the page grid ────────────────────────────────────────────────────
713
683
 
714
684
  const PAGE_CELLS = [
715
- {x: 24, y: 28, isAccent: true},
716
- {x: 116, y: 28, isAccent: false},
717
- {x: 208, y: 28, isAccent: false},
718
- {x: 300, y: 28, isAccent: false},
719
- {x: 24, y: 118, isAccent: false},
720
- {x: 116, y: 118, isAccent: false},
721
- {x: 208, y: 118, isAccent: false},
722
- {x: 300, y: 118, isAccent: false},
685
+ { x: 24, y: 28, isAccent: true },
686
+ { x: 116, y: 28, isAccent: false },
687
+ { x: 208, y: 28, isAccent: false },
688
+ { x: 300, y: 28, isAccent: false },
689
+ { x: 24, y: 118, isAccent: false },
690
+ { x: 116, y: 118, isAccent: false },
691
+ { x: 208, y: 118, isAccent: false },
692
+ { x: 300, y: 118, isAccent: false },
723
693
  ];
724
694
 
725
695
  const PAGE_LINES = [0, 1, 2];
@@ -728,167 +698,165 @@ const PAGE_LINES = [0, 1, 2];
728
698
  // body blocks that give the layout its shape, and copy rows on the PAGE_LINES
729
699
  // rhythm. Eight different pages, so the grid reads as a set of layouts.
730
700
  const PAGE_LAYOUTS: ReadonlyArray<{
731
- blocks: ReadonlyArray<readonly [number, number, number, number]>;
732
- rows: {x: number; y: number; width: number; count: number};
701
+ blocks: ReadonlyArray<readonly [number, number, number, number]>;
702
+ rows: { x: number; y: number; width: number; count: number };
733
703
  }> = [
734
- {blocks: [[10, 22, 56, 16]], rows: {x: 10, y: 44, width: 56, count: 3}},
735
- {
736
- blocks: [
737
- [40, 22, 26, 30],
738
- [10, 58, 56, 8],
739
- ],
740
- rows: {x: 10, y: 24, width: 26, count: 3},
741
- },
742
- {
743
- blocks: [
744
- [10, 22, 26, 15],
745
- [40, 22, 26, 15],
746
- [10, 41, 26, 15],
747
- [40, 41, 26, 15],
748
- ],
749
- rows: {x: 10, y: 62, width: 56, count: 1},
750
- },
751
- {blocks: [[10, 22, 20, 6]], rows: {x: 10, y: 34, width: 56, count: 3}},
752
- {
753
- blocks: [
754
- [10, 24, 5, 5],
755
- [10, 36, 5, 5],
756
- [10, 48, 5, 5],
757
- ],
758
- rows: {x: 19, y: 24, width: 47, count: 3},
759
- },
760
- {
761
- blocks: [
762
- [10, 22, 56, 9],
763
- [10, 35, 56, 9],
764
- [10, 50, 26, 9],
765
- ],
766
- rows: {x: 42, y: 50, width: 24, count: 1},
767
- },
768
- {
769
- blocks: [
770
- [10, 22, 16, 48],
771
- [32, 58, 34, 8],
772
- ],
773
- rows: {x: 32, y: 24, width: 34, count: 3},
774
- },
775
- {
776
- blocks: [
777
- [10, 22, 56, 9],
778
- [10, 35, 30, 6],
779
- [44, 35, 22, 6],
780
- [10, 45, 30, 6],
781
- [44, 45, 22, 6],
782
- [10, 55, 30, 6],
783
- [44, 55, 22, 6],
784
- ],
785
- rows: {x: 10, y: 65, width: 26, count: 1},
786
- },
704
+ { blocks: [[10, 22, 56, 16]], rows: { x: 10, y: 44, width: 56, count: 3 } },
705
+ {
706
+ blocks: [
707
+ [40, 22, 26, 30],
708
+ [10, 58, 56, 8],
709
+ ],
710
+ rows: { x: 10, y: 24, width: 26, count: 3 },
711
+ },
712
+ {
713
+ blocks: [
714
+ [10, 22, 26, 15],
715
+ [40, 22, 26, 15],
716
+ [10, 41, 26, 15],
717
+ [40, 41, 26, 15],
718
+ ],
719
+ rows: { x: 10, y: 62, width: 56, count: 1 },
720
+ },
721
+ { blocks: [[10, 22, 20, 6]], rows: { x: 10, y: 34, width: 56, count: 3 } },
722
+ {
723
+ blocks: [
724
+ [10, 24, 5, 5],
725
+ [10, 36, 5, 5],
726
+ [10, 48, 5, 5],
727
+ ],
728
+ rows: { x: 19, y: 24, width: 47, count: 3 },
729
+ },
730
+ {
731
+ blocks: [
732
+ [10, 22, 56, 9],
733
+ [10, 35, 56, 9],
734
+ [10, 50, 26, 9],
735
+ ],
736
+ rows: { x: 42, y: 50, width: 24, count: 1 },
737
+ },
738
+ {
739
+ blocks: [
740
+ [10, 22, 16, 48],
741
+ [32, 58, 34, 8],
742
+ ],
743
+ rows: { x: 32, y: 24, width: 34, count: 3 },
744
+ },
745
+ {
746
+ blocks: [
747
+ [10, 22, 56, 9],
748
+ [10, 35, 30, 6],
749
+ [44, 35, 22, 6],
750
+ [10, 45, 30, 6],
751
+ [44, 45, 22, 6],
752
+ [10, 55, 30, 6],
753
+ [44, 55, 22, 6],
754
+ ],
755
+ rows: { x: 10, y: 65, width: 26, count: 1 },
756
+ },
787
757
  ];
788
758
 
789
- function PagesScene({alt}: {alt: string}) {
790
- return (
791
- <SceneFrame label={alt} fill="var(--dracula-bg)">
792
- {PAGE_CELLS.map((cell, index) => {
793
- const layout = PAGE_LAYOUTS[index];
794
- // Cyan marks the page being viewed; purple stays reserved for
795
- // controls, so the art never borrows it.
796
- const accent = cell.isAccent
797
- ? 'var(--dracula-cyan)'
798
- : 'var(--dracula-comment)';
799
- return (
800
- <g key={`${cell.x}-${cell.y}`} transform={`translate(${cell.x} ${cell.y})`}>
801
- <rect
802
- x="0.5"
803
- y="0.5"
804
- width="75"
805
- height="75"
806
- rx="5"
807
- fill="var(--dracula-bg-dark)"
808
- stroke={accent}
809
- strokeOpacity={cell.isAccent ? 1 : 0.55}
810
- />
811
- <rect
812
- x="10"
813
- y="11"
814
- width="40"
815
- height="6"
816
- rx="2"
817
- fill={accent}
818
- fillOpacity={cell.isAccent ? 1 : 0.5}
819
- />
820
- {layout.blocks.map(([x, y, width, height]) => (
821
- <rect
822
- key={`${x}-${y}`}
823
- x={x}
824
- y={y}
825
- width={width}
826
- height={height}
827
- rx="2"
828
- fill={accent}
829
- fillOpacity={cell.isAccent ? 0.35 : 0.5}
830
- />
831
- ))}
832
- {PAGE_LINES.slice(0, layout.rows.count).map(line => (
833
- <rect
834
- key={line}
835
- x={layout.rows.x}
836
- y={layout.rows.y + line * 12}
837
- width={layout.rows.width}
838
- height="5"
839
- rx="2"
840
- fill="var(--dracula-comment)"
841
- fillOpacity={0.5}
842
- />
843
- ))}
844
- </g>
845
- );
846
- })}
847
- </SceneFrame>
848
- );
759
+ function PagesScene({ alt }: { alt: string }) {
760
+ return (
761
+ <SceneFrame label={alt} fill="var(--dracula-bg)">
762
+ {PAGE_CELLS.map((cell, index) => {
763
+ const layout = PAGE_LAYOUTS[index];
764
+ // Cyan marks the page being viewed; purple stays reserved for
765
+ // controls, so the art never borrows it.
766
+ const accent = cell.isAccent ? "var(--dracula-cyan)" : "var(--dracula-comment)";
767
+ return (
768
+ <g key={`${cell.x}-${cell.y}`} transform={`translate(${cell.x} ${cell.y})`}>
769
+ <rect
770
+ x="0.5"
771
+ y="0.5"
772
+ width="75"
773
+ height="75"
774
+ rx="5"
775
+ fill="var(--dracula-bg-dark)"
776
+ stroke={accent}
777
+ strokeOpacity={cell.isAccent ? 1 : 0.55}
778
+ />
779
+ <rect
780
+ x="10"
781
+ y="11"
782
+ width="40"
783
+ height="6"
784
+ rx="2"
785
+ fill={accent}
786
+ fillOpacity={cell.isAccent ? 1 : 0.5}
787
+ />
788
+ {layout.blocks.map(([x, y, width, height]) => (
789
+ <rect
790
+ key={`${x}-${y}`}
791
+ x={x}
792
+ y={y}
793
+ width={width}
794
+ height={height}
795
+ rx="2"
796
+ fill={accent}
797
+ fillOpacity={cell.isAccent ? 0.35 : 0.5}
798
+ />
799
+ ))}
800
+ {PAGE_LINES.slice(0, layout.rows.count).map((line) => (
801
+ <rect
802
+ key={line}
803
+ x={layout.rows.x}
804
+ y={layout.rows.y + line * 12}
805
+ width={layout.rows.width}
806
+ height="5"
807
+ rx="2"
808
+ fill="var(--dracula-comment)"
809
+ fillOpacity={0.5}
810
+ />
811
+ ))}
812
+ </g>
813
+ );
814
+ })}
815
+ </SceneFrame>
816
+ );
849
817
  }
850
818
 
851
- type ProductTourScene = {node: (alt: string) => ReactNode; alt: string};
819
+ type ProductTourScene = { node: (alt: string) => ReactNode; alt: string };
852
820
 
853
821
  const PRODUCT_TOUR_SCENES: Record<string, ProductTourScene> = {
854
- introduction: {
855
- node: alt => <NightScene alt={alt} />,
856
- alt: 'A full moon over a castle on the ridge, with bats crossing a lit sky',
857
- },
858
- palette: {
859
- node: alt => <PaletteScene alt={alt} />,
860
- alt: 'Fifteen palette swatches: six surfaces and neutrals, then the accent colours',
861
- },
862
- 'type-shape': {
863
- node: alt => <TypeScene alt={alt} />,
864
- alt: 'A type specimen: monospace glyphs on a baseline grid beside the type scale ladder',
865
- },
866
- templates: {
867
- node: alt => <PagesScene alt={alt} />,
868
- alt: 'Eight page thumbnails in a grid, the first one highlighted',
869
- },
822
+ introduction: {
823
+ node: (alt) => <NightScene alt={alt} />,
824
+ alt: "A full moon over a castle on the ridge, with bats crossing a lit sky",
825
+ },
826
+ palette: {
827
+ node: (alt) => <PaletteScene alt={alt} />,
828
+ alt: "Fifteen palette swatches: six surfaces and neutrals, then the accent colours",
829
+ },
830
+ "type-shape": {
831
+ node: (alt) => <TypeScene alt={alt} />,
832
+ alt: "A type specimen: monospace glyphs on a baseline grid beside the type scale ladder",
833
+ },
834
+ templates: {
835
+ node: (alt) => <PagesScene alt={alt} />,
836
+ alt: "Eight page thumbnails in a grid, the first one highlighted",
837
+ },
870
838
  };
871
839
 
872
- function ChapterArt({chapterId}: {chapterId: string}) {
873
- const scene = PRODUCT_TOUR_SCENES[chapterId];
874
- if (scene == null) {
875
- return null;
876
- }
877
- return <ChapterArtFrame>{scene.node(scene.alt)}</ChapterArtFrame>;
840
+ function ChapterArt({ chapterId }: { chapterId: string }) {
841
+ const scene = PRODUCT_TOUR_SCENES[chapterId];
842
+ if (scene == null) {
843
+ return null;
844
+ }
845
+ return <ChapterArtFrame>{scene.node(scene.alt)}</ChapterArtFrame>;
878
846
  }
879
847
 
880
848
  export default function ProductTour() {
881
- return (
882
- <ChapteredDoc
883
- groups={CHAPTER_GROUPS}
884
- defaultChapter="introduction"
885
- railHeading="astryx-dracula"
886
- railSubheading="Dracula theme for Astryx"
887
- railHref={SELF_HASH}
888
- railIcon={<Icon icon={Sparkles} size="sm" />}
889
- eyebrow="Documentation"
890
- badge={() => 'astryx-dracula v0.5.0'}
891
- art={chapterId => <ChapterArt chapterId={chapterId} />}
892
- />
893
- );
849
+ return (
850
+ <ChapteredDoc
851
+ groups={CHAPTER_GROUPS}
852
+ defaultChapter="introduction"
853
+ railHeading="astryx-dracula"
854
+ railSubheading="Dracula theme for Astryx"
855
+ railHref={SELF_HASH}
856
+ railIcon={<Icon icon={Sparkles} size="sm" />}
857
+ eyebrow="Documentation"
858
+ badge={() => "astryx-dracula v0.6.2"}
859
+ art={(chapterId) => <ChapterArt chapterId={chapterId} />}
860
+ />
861
+ );
894
862
  }