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