deckrun 1.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/dist/themes.js ADDED
@@ -0,0 +1,971 @@
1
+ /**
2
+ * The theme registry.
3
+ *
4
+ * ── Adding a theme ────────────────────────────────────────────────────────
5
+ * Append one entry to `SPECS` below and it appears everywhere: the CLI's
6
+ * `--theme` flag, the editor's theme menu, the live preview, the HTML export,
7
+ * and the PDF. Nothing else needs touching.
8
+ *
9
+ * A spec is four things:
10
+ *
11
+ * neutrals An 11-step ramp from the page's outermost background (`crust`)
12
+ * to its strongest foreground (`text`). Light themes run the same
13
+ * direction: crust is still the backdrop, text is still the ink.
14
+ * accents Eleven hues, named after the Catppuccin slots so palettes port
15
+ * across easily. They are the deck's paint box: `mark`, list
16
+ * markers, table headers, and the five pen colors all pull from
17
+ * here.
18
+ * roles Which three of those colors lead. `accent` carries h1, the
19
+ * caret, focus rings, and every piece of chrome; `accent2` carries
20
+ * h2 and links; `accent3` carries h3. Point them at any neutral or
21
+ * accent key — that is how Nord leads with frost blue while
22
+ * Gruvbox leads with amber.
23
+ * type A display face for headings, a body face for prose, a mono face
24
+ * for code, plus the weight and tracking that face wants. Faces
25
+ * come from `FONTS`; add an entry there to use a new one.
26
+ *
27
+ * `decor` picks the animated geometry that sits behind every slide. The
28
+ * available names live in `DECORS` at the bottom of this file — each is a set
29
+ * of CSS custom properties, so a new pattern is a new entry there and a
30
+ * matching name here.
31
+ *
32
+ * Everything else — tints, overlays, shadows, glows, the decor's own colors —
33
+ * is derived from the four inputs, so a new theme cannot fall out of step with
34
+ * itself.
35
+ *
36
+ * The type scale lives at the bottom of this file. It is deliberately separate
37
+ * from the themes: every theme can be set at any of the four sizes, so the two
38
+ * choices compose instead of multiplying into fifty-six presets.
39
+ */
40
+ // ── Color plumbing ────────────────────────────────────────────────────────
41
+ /** `#rrggbb` → `r, g, b`, ready to drop into an `rgba()`. */
42
+ function channels(hex) {
43
+ const h = hex.replace("#", "");
44
+ const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
45
+ return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
46
+ }
47
+ function alpha(hex, a) {
48
+ return `rgba(${channels(hex)}, ${a})`;
49
+ }
50
+ /** "Space+Grotesk:wght@400;700" → "Space Grotesk". */
51
+ export function fontName(key) {
52
+ return FONTS[key].param.split(":")[0].replace(/\+/g, " ");
53
+ }
54
+ const SANS = "system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif";
55
+ const SERIF = "Georgia, 'Times New Roman', serif";
56
+ const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
57
+ export const FONTS = {
58
+ inter: { param: "Inter:wght@300;400;500;600;700;800", stack: `'Inter', ${SANS}`, kind: "sans" },
59
+ spaceGrotesk: { param: "Space+Grotesk:wght@400;500;600;700", stack: `'Space Grotesk', ${SANS}`, kind: "sans" },
60
+ sora: { param: "Sora:wght@300;400;500;600;700;800", stack: `'Sora', ${SANS}`, kind: "sans" },
61
+ manrope: { param: "Manrope:wght@400;500;600;700;800", stack: `'Manrope', ${SANS}`, kind: "sans" },
62
+ figtree: { param: "Figtree:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400", stack: `'Figtree', ${SANS}`, kind: "sans" },
63
+ outfit: { param: "Outfit:wght@300;400;500;600;700;800", stack: `'Outfit', ${SANS}`, kind: "sans" },
64
+ archivo: { param: "Archivo:wght@400;500;600;700;800;900", stack: `'Archivo', ${SANS}`, kind: "sans" },
65
+ syne: { param: "Syne:wght@400;500;600;700;800", stack: `'Syne', ${SANS}`, kind: "sans" },
66
+ bricolage: { param: "Bricolage+Grotesque:wght@400;500;600;700;800", stack: `'Bricolage Grotesque', ${SANS}`, kind: "sans" },
67
+ workSans: { param: "Work+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Work Sans', ${SANS}`, kind: "sans" },
68
+ plexSans: { param: "IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400", stack: `'IBM Plex Sans', ${SANS}`, kind: "sans" },
69
+ fraunces: { param: "Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;0,9..144,700;0,9..144,900;1,9..144,400", stack: `'Fraunces', ${SERIF}`, kind: "serif" },
70
+ playfair: { param: "Playfair+Display:ital,wght@0,500;0,600;0,700;0,800;1,500", stack: `'Playfair Display', ${SERIF}`, kind: "serif" },
71
+ newsreader: { param: "Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400", stack: `'Newsreader', ${SERIF}`, kind: "serif" },
72
+ lora: { param: "Lora:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Lora', ${SERIF}`, kind: "serif" },
73
+ plexMono: { param: "IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400", stack: `'IBM Plex Mono', ${MONO}`, kind: "mono" },
74
+ jetbrains: { param: "JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'JetBrains Mono', ${MONO}`, kind: "mono" },
75
+ firaCode: { param: "Fira+Code:wght@400;500;600;700", stack: `'Fira Code', ${MONO}`, kind: "mono" },
76
+ spaceMono: { param: "Space+Mono:ital,wght@0,400;0,700;1,400", stack: `'Space Mono', ${MONO}`, kind: "mono" },
77
+ sourceCodePro: { param: "Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400", stack: `'Source Code Pro', ${MONO}`, kind: "mono" },
78
+ };
79
+ /**
80
+ * One stylesheet request covering every face the given themes need. Google
81
+ * serves the @font-face rules for all of them but the browser only downloads
82
+ * the files actually painted, so asking for the whole registry — which the
83
+ * editor and its preview must do, since the theme changes at runtime — costs
84
+ * one small CSS file.
85
+ */
86
+ export function googleFontsHref(themes = THEME_IDS.slice(), extra = []) {
87
+ const wanted = new Set();
88
+ for (const input of themes) {
89
+ const t = THEMES[resolveThemeName(input)];
90
+ wanted.add(t.type.display);
91
+ wanted.add(t.type.body);
92
+ wanted.add(t.type.mono);
93
+ }
94
+ // A deck set in a face its theme does not use still has to fetch it.
95
+ for (const key of extra)
96
+ if (key && FONTS[key])
97
+ wanted.add(key);
98
+ const families = [...wanted]
99
+ .map((key) => FONTS[key].param)
100
+ .sort()
101
+ .map((param) => `family=${param}`)
102
+ .join("&");
103
+ return `https://fonts.googleapis.com/css2?${families}&display=swap`;
104
+ }
105
+ // ── The themes ────────────────────────────────────────────────────────────
106
+ const HL = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/";
107
+ const SPECS = {
108
+ // ── Dark ───────────────────────────────────────────────────────────────
109
+ midnight: {
110
+ label: "midnight",
111
+ mood: "dark",
112
+ blurb: "Catppuccin Mocha. Violet on deep indigo, drifting orbs.",
113
+ neutrals: {
114
+ crust: "#11111b", mantle: "#181825", base: "#1e1e2e",
115
+ surface0: "#313244", surface1: "#45475a", surface2: "#585b70",
116
+ overlay0: "#6c7086", overlay1: "#7f849c",
117
+ subtext0: "#a6adc8", subtext1: "#bac2de", text: "#cdd6f4",
118
+ },
119
+ accents: {
120
+ lavender: "#b4befe", blue: "#89b4fa", sapphire: "#74c7ec", sky: "#89dceb",
121
+ teal: "#94e2d5", green: "#a6e3a1", yellow: "#f9e2af", peach: "#fab387",
122
+ red: "#f38ba8", mauve: "#cba6f7", pink: "#f5c2e7",
123
+ },
124
+ roles: { accent: "mauve", accent2: "blue", accent3: "sky" },
125
+ type: { display: "spaceGrotesk", body: "inter", mono: "plexMono", weight: 700, tracking: "-0.025em" },
126
+ decor: "orbs",
127
+ hljs: `${HL}tokyo-night-dark.min.css`,
128
+ },
129
+ tokyo: {
130
+ label: "tokyo night",
131
+ mood: "dark",
132
+ blurb: "Neon cyan over a wireframe grid, lifted from a rainy skyline.",
133
+ neutrals: {
134
+ crust: "#16161e", mantle: "#1a1b26", base: "#1f2335",
135
+ surface0: "#292e42", surface1: "#3b4261", surface2: "#545c7e",
136
+ overlay0: "#626c96", overlay1: "#7c86ab",
137
+ subtext0: "#a5b0d6", subtext1: "#b7c0e0", text: "#c8d3f5",
138
+ },
139
+ accents: {
140
+ lavender: "#a9b1ff", blue: "#7aa2f7", sapphire: "#2ac3de", sky: "#7dcfff",
141
+ teal: "#73daca", green: "#9ece6a", yellow: "#e0af68", peach: "#ff9e64",
142
+ red: "#f7768e", mauve: "#bb9af7", pink: "#ff7eb6",
143
+ },
144
+ roles: { accent: "sapphire", accent2: "mauve", accent3: "teal" },
145
+ type: { display: "sora", body: "inter", mono: "jetbrains", weight: 700, tracking: "-0.03em" },
146
+ decor: "grid",
147
+ hljs: `${HL}tokyo-night-dark.min.css`,
148
+ },
149
+ nord: {
150
+ label: "nord",
151
+ mood: "dark",
152
+ blurb: "Arctic frost blue on polar slate, with slow contour waves.",
153
+ neutrals: {
154
+ crust: "#232831", mantle: "#2e3440", base: "#353c4a",
155
+ surface0: "#3b4252", surface1: "#434c5e", surface2: "#4c566a",
156
+ overlay0: "#707d92", overlay1: "#8e9aae",
157
+ subtext0: "#c3ccda", subtext1: "#d8dee9", text: "#eceff4",
158
+ },
159
+ accents: {
160
+ lavender: "#c9a9c8", blue: "#81a1c1", sapphire: "#88c0d0", sky: "#a3d4e0",
161
+ teal: "#8fbcbb", green: "#a3be8c", yellow: "#ebcb8b", peach: "#d08770",
162
+ red: "#bf616a", mauve: "#b48ead", pink: "#d3a3c6",
163
+ },
164
+ roles: { accent: "sapphire", accent2: "blue", accent3: "teal" },
165
+ type: { display: "manrope", body: "manrope", mono: "jetbrains", weight: 800, tracking: "-0.035em" },
166
+ decor: "waves",
167
+ hljs: `${HL}nord.min.css`,
168
+ },
169
+ dracula: {
170
+ label: "dracula",
171
+ mood: "dark",
172
+ blurb: "Purple and hot pink over charcoal, lit by a gradient mesh.",
173
+ neutrals: {
174
+ crust: "#1a1b23", mantle: "#21222c", base: "#282a36",
175
+ surface0: "#343746", surface1: "#44475a", surface2: "#565a70",
176
+ overlay0: "#6272a4", overlay1: "#7b85b6",
177
+ subtext0: "#c8cbe0", subtext1: "#e2e4f0", text: "#f8f8f2",
178
+ },
179
+ accents: {
180
+ lavender: "#d6bcff", blue: "#79b8ff", sapphire: "#8be9fd", sky: "#a4f0fd",
181
+ teal: "#66e0c8", green: "#50fa7b", yellow: "#f1fa8c", peach: "#ffb86c",
182
+ red: "#ff5555", mauve: "#bd93f9", pink: "#ff79c6",
183
+ },
184
+ roles: { accent: "mauve", accent2: "pink", accent3: "sapphire" },
185
+ type: { display: "syne", body: "figtree", mono: "firaCode", weight: 800, tracking: "-0.02em" },
186
+ decor: "mesh",
187
+ hljs: `${HL}base16/dracula.min.css`,
188
+ },
189
+ gruvbox: {
190
+ label: "gruvbox",
191
+ mood: "dark",
192
+ blurb: "Warm amber and moss on retro brown, hatched like graph paper.",
193
+ neutrals: {
194
+ crust: "#1b1e1f", mantle: "#232728", base: "#282828",
195
+ surface0: "#32302f", surface1: "#3c3836", surface2: "#504945",
196
+ overlay0: "#7c6f64", overlay1: "#928374",
197
+ subtext0: "#bdae93", subtext1: "#d5c4a1", text: "#ebdbb2",
198
+ },
199
+ accents: {
200
+ lavender: "#d3b8c8", blue: "#83a598", sapphire: "#7daea3", sky: "#8ec07c",
201
+ teal: "#89b482", green: "#b8bb26", yellow: "#fabd2f", peach: "#fe8019",
202
+ red: "#fb4934", mauve: "#d3869b", pink: "#e2a3b4",
203
+ },
204
+ roles: { accent: "yellow", accent2: "peach", accent3: "green" },
205
+ type: { display: "bricolage", body: "workSans", mono: "plexMono", weight: 700, tracking: "-0.02em" },
206
+ decor: "topo",
207
+ hljs: `${HL}base16/gruvbox-dark-medium.min.css`,
208
+ },
209
+ rosepine: {
210
+ label: "rosé pine",
211
+ mood: "dark",
212
+ blurb: "Muted iris and gold on plum, under a slow aurora.",
213
+ neutrals: {
214
+ crust: "#14121f", mantle: "#191724", base: "#1f1d2e",
215
+ surface0: "#26233a", surface1: "#403d52", surface2: "#524f67",
216
+ overlay0: "#6e6a86", overlay1: "#817c9c",
217
+ subtext0: "#b6b2d0", subtext1: "#cdc9e6", text: "#e0def4",
218
+ },
219
+ accents: {
220
+ lavender: "#d5c4ee", blue: "#6a9fb5", sapphire: "#6ba8bf", sky: "#9ccfd8",
221
+ teal: "#86cfc4", green: "#9ccfa8", yellow: "#f6c177", peach: "#f0a882",
222
+ red: "#eb6f92", mauve: "#c4a7e7", pink: "#ebbcba",
223
+ },
224
+ roles: { accent: "mauve", accent2: "sky", accent3: "yellow" },
225
+ type: { display: "fraunces", body: "newsreader", mono: "plexMono", weight: 600, tracking: "-0.02em", bodyTracking: "0.004em" },
226
+ decor: "aurora",
227
+ hljs: `${HL}base16/ros-pine.min.css`,
228
+ },
229
+ neon: {
230
+ label: "neon",
231
+ mood: "dark",
232
+ blurb: "Electric cyan and magenta on true black, raked by light beams.",
233
+ neutrals: {
234
+ crust: "#050509", mantle: "#0a0a12", base: "#0e0e1a",
235
+ surface0: "#16162a", surface1: "#22223c", surface2: "#2e2e50",
236
+ overlay0: "#4f4f78", overlay1: "#6a6a96",
237
+ subtext0: "#a8a8cc", subtext1: "#c8c8e4", text: "#f0f0ff",
238
+ },
239
+ accents: {
240
+ lavender: "#b39dff", blue: "#4d9fff", sapphire: "#22d3ee", sky: "#38e8ff",
241
+ teal: "#2dd4bf", green: "#4ade80", yellow: "#fde047", peach: "#fb923c",
242
+ red: "#fb5c7d", mauve: "#d946ef", pink: "#ff5cc8",
243
+ },
244
+ roles: { accent: "sapphire", accent2: "pink", accent3: "sky" },
245
+ type: { display: "spaceGrotesk", body: "spaceGrotesk", mono: "spaceMono", weight: 700, tracking: "0.04em", case: "uppercase" },
246
+ decor: "beams",
247
+ hljs: `${HL}night-owl.min.css`,
248
+ },
249
+ forest: {
250
+ label: "forest",
251
+ mood: "dark",
252
+ blurb: "Everforest sage on deep pine, rippling in concentric rings.",
253
+ neutrals: {
254
+ crust: "#232a2e", mantle: "#2d353b", base: "#343f44",
255
+ surface0: "#3d484d", surface1: "#475258", surface2: "#4f585e",
256
+ overlay0: "#7a8478", overlay1: "#9da9a0",
257
+ subtext0: "#c2c9bd", subtext1: "#d3c6aa", text: "#e5dfd2",
258
+ },
259
+ accents: {
260
+ lavender: "#d8b9c8", blue: "#7fbbb3", sapphire: "#6fb5ac", sky: "#9ed3c8",
261
+ teal: "#83c092", green: "#a7c080", yellow: "#dbbc7f", peach: "#e69875",
262
+ red: "#e67e80", mauve: "#d699b6", pink: "#e3aec3",
263
+ },
264
+ roles: { accent: "green", accent2: "sapphire", accent3: "yellow" },
265
+ type: { display: "outfit", body: "figtree", mono: "jetbrains", weight: 600, tracking: "-0.025em" },
266
+ decor: "rings",
267
+ hljs: `${HL}atom-one-dark.min.css`,
268
+ },
269
+ // ── Light ──────────────────────────────────────────────────────────────
270
+ daylight: {
271
+ label: "daylight",
272
+ mood: "light",
273
+ blurb: "Catppuccin Latte, contrast-tuned for a projector. Dot matrix.",
274
+ neutrals: {
275
+ crust: "#ebebeb", mantle: "#f5f5f5", base: "#fafaf8",
276
+ surface0: "#ccd0da", surface1: "#9ca0b0", surface2: "#8c8fa1",
277
+ overlay0: "#6c6f85", overlay1: "#5c5f77",
278
+ subtext0: "#4c4f69", subtext1: "#3a3c52", text: "#1e2030",
279
+ },
280
+ accents: {
281
+ lavender: "#7287fd", blue: "#1e66f5", sapphire: "#209fb5", sky: "#04a5e5",
282
+ teal: "#179299", green: "#40a02b", yellow: "#df8e1d", peach: "#fe640b",
283
+ red: "#d20f39", mauve: "#8839ef", pink: "#ea76cb",
284
+ },
285
+ roles: { accent: "mauve", accent2: "blue", accent3: "teal" },
286
+ type: { display: "spaceGrotesk", body: "inter", mono: "plexMono", weight: 700, tracking: "-0.025em" },
287
+ decor: "dots",
288
+ hljs: `${HL}atom-one-light.min.css`,
289
+ },
290
+ paper: {
291
+ label: "paper",
292
+ mood: "light",
293
+ blurb: "Crimson serif on warm cream. Editorial, print-first, very legible.",
294
+ neutrals: {
295
+ crust: "#efe8dc", mantle: "#f7f2e9", base: "#fdfbf5",
296
+ surface0: "#e4dccc", surface1: "#cec4b0", surface2: "#b0a48d",
297
+ overlay0: "#8b8069", overlay1: "#6e6553",
298
+ subtext0: "#4d4535", subtext1: "#38311f", text: "#211c11",
299
+ },
300
+ accents: {
301
+ lavender: "#6f5ba8", blue: "#2d5fa8", sapphire: "#1f7f96", sky: "#10788f",
302
+ teal: "#1c7a6b", green: "#3f7a2e", yellow: "#9a6c12", peach: "#b35311",
303
+ red: "#a81f2c", mauve: "#7a3f9c", pink: "#a83f77",
304
+ },
305
+ roles: { accent: "red", accent2: "sapphire", accent3: "yellow" },
306
+ type: { display: "fraunces", body: "newsreader", mono: "plexMono", weight: 700, tracking: "-0.02em", bodyTracking: "0.003em" },
307
+ decor: "rings",
308
+ hljs: `${HL}atom-one-light.min.css`,
309
+ },
310
+ solarized: {
311
+ label: "solarized",
312
+ mood: "light",
313
+ blurb: "The classic low-glare cream, paired with Lora for long prose.",
314
+ neutrals: {
315
+ crust: "#eee8d5", mantle: "#f5efdc", base: "#fdf6e3",
316
+ surface0: "#e3ddc8", surface1: "#c9c3ad", surface2: "#a8a48e",
317
+ overlay0: "#839496", overlay1: "#657b83",
318
+ subtext0: "#586e75", subtext1: "#3f5b62", text: "#073642",
319
+ },
320
+ accents: {
321
+ lavender: "#8a8fd0", blue: "#268bd2", sapphire: "#1f8fa8", sky: "#2aa198",
322
+ teal: "#21958c", green: "#859900", yellow: "#b58900", peach: "#cb4b16",
323
+ red: "#dc322f", mauve: "#6c71c4", pink: "#d33682",
324
+ },
325
+ roles: { accent: "blue", accent2: "mauve", accent3: "teal" },
326
+ type: { display: "outfit", body: "lora", mono: "sourceCodePro", weight: 600, tracking: "-0.02em" },
327
+ decor: "topo",
328
+ hljs: `${HL}base16/solarized-light.min.css`,
329
+ },
330
+ rosequartz: {
331
+ label: "rose quartz",
332
+ mood: "light",
333
+ blurb: "Rosé Pine Dawn. Blush and iris on linen, with soft orbs.",
334
+ neutrals: {
335
+ crust: "#f2e9e1", mantle: "#faf4ed", base: "#fffaf3",
336
+ surface0: "#ece0d8", surface1: "#d8ccc6", surface2: "#bdb0ae",
337
+ overlay0: "#9893a5", overlay1: "#7d7791",
338
+ subtext0: "#625b7d", subtext1: "#55506e", text: "#423d5c",
339
+ },
340
+ accents: {
341
+ lavender: "#a68fc0", blue: "#286983", sapphire: "#2f7d92", sky: "#56949f",
342
+ teal: "#4a9188", green: "#5a8f52", yellow: "#ea9d34", peach: "#d7827e",
343
+ red: "#b4637a", mauve: "#907aa9", pink: "#c4759a",
344
+ },
345
+ roles: { accent: "red", accent2: "mauve", accent3: "sky" },
346
+ type: { display: "playfair", body: "figtree", mono: "plexMono", weight: 700, tracking: "-0.02em" },
347
+ decor: "orbs",
348
+ hljs: `${HL}base16/ros-pine-dawn.min.css`,
349
+ },
350
+ swiss: {
351
+ label: "swiss",
352
+ mood: "light",
353
+ blurb: "Black on white, one red. Heavy grotesk, tight tracking, hard grid.",
354
+ neutrals: {
355
+ crust: "#f0f0f0", mantle: "#f8f8f8", base: "#ffffff",
356
+ surface0: "#dcdcdc", surface1: "#bebebe", surface2: "#9a9a9a",
357
+ overlay0: "#767676", overlay1: "#5a5a5a",
358
+ subtext0: "#3a3a3a", subtext1: "#222222", text: "#0a0a0a",
359
+ },
360
+ accents: {
361
+ lavender: "#4b4b8f", blue: "#1a4fd6", sapphire: "#0a6e8a", sky: "#0e7490",
362
+ teal: "#0f766e", green: "#15803d", yellow: "#a16207", peach: "#c2410c",
363
+ red: "#e01b24", mauve: "#5b21b6", pink: "#be185d",
364
+ },
365
+ roles: { accent: "red", accent2: "text", accent3: "overlay1" },
366
+ type: { display: "archivo", body: "inter", mono: "plexMono", weight: 900, tracking: "-0.045em" },
367
+ decor: "grid",
368
+ hljs: `${HL}github.min.css`,
369
+ },
370
+ arctic: {
371
+ label: "arctic",
372
+ mood: "light",
373
+ blurb: "Nord inverted. Frost blue on cool paper, with contour waves.",
374
+ neutrals: {
375
+ crust: "#e3e8ef", mantle: "#eceff4", base: "#f7f9fc",
376
+ surface0: "#dbe1ea", surface1: "#c2cad6", surface2: "#a3adbd",
377
+ overlay0: "#7b8797", overlay1: "#5f6b7c",
378
+ subtext0: "#445060", subtext1: "#364150", text: "#2e3440",
379
+ },
380
+ accents: {
381
+ lavender: "#9c6f96", blue: "#4a7ba7", sapphire: "#2f7d94", sky: "#3d8fa8",
382
+ teal: "#3b8b8a", green: "#5d8a45", yellow: "#b08834", peach: "#c06a4a",
383
+ red: "#b0505a", mauve: "#96628f", pink: "#a86a94",
384
+ },
385
+ roles: { accent: "sapphire", accent2: "blue", accent3: "teal" },
386
+ type: { display: "manrope", body: "plexSans", mono: "jetbrains", weight: 800, tracking: "-0.035em" },
387
+ decor: "waves",
388
+ hljs: `${HL}atom-one-light.min.css`,
389
+ },
390
+ };
391
+ /**
392
+ * Ordered for the theme menu: dark first, then light, each roughly from
393
+ * calmest to loudest.
394
+ */
395
+ export const THEME_IDS = [
396
+ "midnight", "tokyo", "nord", "dracula", "gruvbox", "rosepine", "forest", "neon",
397
+ "daylight", "arctic", "solarized", "paper", "rosequartz", "swiss",
398
+ ];
399
+ export const THEMES = Object.fromEntries(THEME_IDS.map((id) => [id, { id, ...SPECS[id] }]));
400
+ /** `dark` and `light` predate the registry and still name the two originals. */
401
+ const ALIASES = {
402
+ dark: "midnight",
403
+ light: "daylight",
404
+ mocha: "midnight",
405
+ latte: "daylight",
406
+ "tokyo-night": "tokyo",
407
+ "rose-pine": "rosepine",
408
+ "rose-quartz": "rosequartz",
409
+ };
410
+ export const DEFAULT_THEME = "midnight";
411
+ /** A theme id from untrusted input, or `null` if it names nothing. */
412
+ export function findTheme(input) {
413
+ if (!input)
414
+ return null;
415
+ const key = String(input).trim().toLowerCase();
416
+ if (THEMES[key])
417
+ return key;
418
+ return ALIASES[key] ?? null;
419
+ }
420
+ /** A theme id from untrusted input, falling back to the default. */
421
+ export function resolveThemeName(input) {
422
+ return findTheme(input) ?? DEFAULT_THEME;
423
+ }
424
+ /** What the editor's theme picker and the CLI's `--theme` help text list. */
425
+ export function themeSummaries() {
426
+ return THEME_IDS.map((id) => {
427
+ const t = THEMES[id];
428
+ const n = t.neutrals;
429
+ return {
430
+ id,
431
+ label: t.label,
432
+ mood: t.mood,
433
+ blurb: t.blurb,
434
+ decor: t.decor,
435
+ colors: {
436
+ crust: n.crust, mantle: n.mantle, base: n.base, surface0: n.surface0,
437
+ overlay1: n.overlay1, subtext1: n.subtext1, text: n.text,
438
+ accent: role(t, "accent"),
439
+ accent2: role(t, "accent2"),
440
+ accent3: role(t, "accent3"),
441
+ },
442
+ fonts: {
443
+ display: FONTS[t.type.display].stack,
444
+ body: FONTS[t.type.body].stack,
445
+ mono: FONTS[t.type.mono].stack,
446
+ },
447
+ };
448
+ });
449
+ }
450
+ /** One line per theme, for `deckrun --list-themes`. */
451
+ export function themeListing() {
452
+ const pad = Math.max(...THEME_IDS.map((id) => id.length));
453
+ return THEME_IDS.map((id) => {
454
+ const t = THEMES[id];
455
+ return `${id.padEnd(pad)} ${t.mood === "dark" ? "dark " : "light"} ${t.blurb}`;
456
+ });
457
+ }
458
+ function palette(t) {
459
+ return { ...t.neutrals, ...t.accents };
460
+ }
461
+ function role(t, which) {
462
+ return palette(t)[t.roles[which]];
463
+ }
464
+ // ── CSS emission ──────────────────────────────────────────────────────────
465
+ /**
466
+ * Every custom property a theme owns. The palette slots come out verbatim;
467
+ * everything below them is derived, so the tints, shadows, and decor colors
468
+ * of a new theme land in the right register without being hand-picked.
469
+ */
470
+ function themeVars(t) {
471
+ const n = t.neutrals;
472
+ const a = t.accents;
473
+ const dark = t.mood === "dark";
474
+ const accent = role(t, "accent");
475
+ const accent2 = role(t, "accent2");
476
+ const accent3 = role(t, "accent3");
477
+ // Light themes need a tinted shadow rather than black, or every panel edge
478
+ // turns to soot; dark themes need real black to read as depth at all.
479
+ const sh = dark ? "0, 0, 0" : channels(n.text);
480
+ const s = (y, blur, op) => `0 ${y}px ${blur}px rgba(${sh}, ${dark ? op : op * 0.42})`;
481
+ const lines = [
482
+ ["crust", n.crust], ["mantle", n.mantle], ["base", n.base],
483
+ ["surface0", n.surface0], ["surface1", n.surface1], ["surface2", n.surface2],
484
+ ["overlay0", n.overlay0], ["overlay1", n.overlay1],
485
+ ["subtext0", n.subtext0], ["subtext1", n.subtext1], ["text", n.text],
486
+ // All eleven ship whether or not the stylesheet names each one: they are
487
+ // the paint box a deck reaches into, and the deck reads several of them
488
+ // back out at runtime for the pen's color swatches.
489
+ ["lavender", a.lavender], ["blue", a.blue], ["sapphire", a.sapphire],
490
+ ["sky", a.sky], ["teal", a.teal], ["green", a.green], ["yellow", a.yellow],
491
+ ["peach", a.peach], ["red", a.red], ["mauve", a.mauve], ["pink", a.pink],
492
+ // The three leading colors, and the tints every surface borrows from them.
493
+ ["accent", accent],
494
+ ["accent-2", accent2],
495
+ ["accent-3", accent3],
496
+ ["accent-soft", alpha(accent, dark ? 0.1 : 0.09)],
497
+ ["accent-line", alpha(accent, dark ? 0.42 : 0.34)],
498
+ ["glow", alpha(accent, dark ? 0.34 : 0.22)],
499
+ ["gradient", `linear-gradient(115deg, ${accent}, ${accent2} 58%, ${accent3})`],
500
+ ["accent-fade", `linear-gradient(90deg, ${accent}, ${alpha(accent, 0.45)} 62%, transparent)`],
501
+ ["surface-soft", alpha(n.surface0, dark ? 0.34 : 0.42)],
502
+ ["crust-overlay", alpha(n.crust, dark ? 0.84 : 0.88)],
503
+ ["scrim", dark ? "rgba(0, 0, 0, 0.5)" : alpha(n.text, 0.26)],
504
+ ["hairline", alpha(n.text, dark ? 0.09 : 0.11)],
505
+ ["shadow-md", s(8, 28, 0.42)],
506
+ ["shadow-lg", s(24, 68, 0.52)],
507
+ ["code-shadow", s(6, 26, 0.4)],
508
+ ["font-display", FONTS[t.type.display].stack],
509
+ ["font-body", FONTS[t.type.body].stack],
510
+ ["font-mono", FONTS[t.type.mono].stack],
511
+ ["display-weight", String(t.type.weight ?? 700)],
512
+ ["display-tracking", t.type.tracking ?? "-0.02em"],
513
+ ["display-case", t.type.case ?? "none"],
514
+ ["body-tracking", t.type.bodyTracking ?? "0"],
515
+ // Decor colors. Light themes take darker lines and gentler glows, since a
516
+ // pale wash on paper reads as a smudge rather than as light.
517
+ ["decor-line", alpha(n.text, dark ? 0.05 : 0.07)],
518
+ ["decor-dot", alpha(n.text, dark ? 0.085 : 0.1)],
519
+ ["decor-glow-1", alpha(accent, dark ? 0.2 : 0.15)],
520
+ ["decor-glow-2", alpha(accent2, dark ? 0.16 : 0.12)],
521
+ ["decor-glow-3", alpha(accent3, dark ? 0.13 : 0.1)],
522
+ ];
523
+ const pad = Math.max(...lines.map(([k]) => k.length));
524
+ return lines
525
+ .map(([k, v]) => ` --${k}:${" ".repeat(pad - k.length + 1)}${v};`)
526
+ .join("\n");
527
+ }
528
+ /** The palette for one baked-in theme, for a deck that never switches. */
529
+ export function themeRootCss(theme) {
530
+ return `:root {\n${themeVars(THEMES[resolveThemeName(theme)])}\n}`;
531
+ }
532
+ /** Every palette, switchable at runtime via `[data-theme]` on the root. */
533
+ export function themeSwitchableCss() {
534
+ const blocks = THEME_IDS.map((id) => {
535
+ const selector = id === DEFAULT_THEME
536
+ ? `:root, :root[data-theme="${id}"]`
537
+ : `:root[data-theme="${id}"]`;
538
+ return `${selector} {\n${themeVars(THEMES[id])}\n}`;
539
+ });
540
+ return blocks.join("\n\n");
541
+ }
542
+ /** Stylesheet URL for the Highlight.js theme that pairs with a palette. */
543
+ export function hljsHref(theme) {
544
+ return THEMES[resolveThemeName(theme)].hljs;
545
+ }
546
+ /** `{ midnight: "https://…", … }`, for the preview's runtime theme swap. */
547
+ export function hljsMapJson() {
548
+ return JSON.stringify(Object.fromEntries(THEME_IDS.map((id) => [id, THEMES[id].hljs])));
549
+ }
550
+ /** `{ midnight: "orbs", … }`, so a theme swap swaps its backdrop with it. */
551
+ export function decorMapJson() {
552
+ return JSON.stringify(Object.fromEntries(THEME_IDS.map((id) => [id, THEMES[id].decor])));
553
+ }
554
+ export function decorOf(theme) {
555
+ return THEMES[resolveThemeName(theme)].decor;
556
+ }
557
+ // ── Decor ─────────────────────────────────────────────────────────────────
558
+ /**
559
+ * Each pattern is nothing but custom properties, read by the two rules in
560
+ * `DECOR_CSS` that paint the backdrop. So a new pattern is a new entry here
561
+ * plus its name in `DecorName` — no new selectors, and it works on screen and
562
+ * in the PDF alike.
563
+ *
564
+ * --dc-layers / -size / -pos / -repeat the texture, as background layers
565
+ * --dc-mask vignette, applied to the container
566
+ * --dc-inset overscan, so motion never shows an edge
567
+ * --dc-anim + --dc-dx/-dy the drift; dx/dy must equal exactly
568
+ * one tile of the pattern, or the loop
569
+ * visibly jumps
570
+ * --dc-glow* a second layer of pure light
571
+ */
572
+ const DECORS = {
573
+ none: `
574
+ --dc-glow:
575
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
576
+ radial-gradient(closest-side, var(--decor-glow-2), transparent);
577
+ --dc-glow-size: 74vmax 74vmax, 58vmax 58vmax;
578
+ --dc-glow-pos: 92% -20%, -14% 112%;
579
+ --dc-glow-anim: dc-breathe 34s ease-in-out infinite;`,
580
+ grid: `
581
+ --dc-layers:
582
+ linear-gradient(to right, var(--decor-line) 1px, transparent 1px),
583
+ linear-gradient(to bottom, var(--decor-line) 1px, transparent 1px);
584
+ --dc-size: 72px 72px;
585
+ --dc-mask: radial-gradient(ellipse 88% 76% at 50% 40%, #000 24%, transparent 82%);
586
+ --dc-anim: dc-pan 150s linear infinite;
587
+ --dc-dx: 72px;
588
+ --dc-dy: 72px;
589
+ --dc-glow:
590
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
591
+ radial-gradient(closest-side, var(--decor-glow-2), transparent);
592
+ --dc-glow-size: 70vmax 70vmax, 56vmax 56vmax;
593
+ --dc-glow-pos: 90% -22%, -12% 110%;
594
+ --dc-glow-anim: dc-breathe 30s ease-in-out infinite;`,
595
+ dots: `
596
+ --dc-layers: radial-gradient(var(--decor-dot) 1.4px, transparent 1.6px);
597
+ --dc-size: 30px 30px;
598
+ --dc-mask: radial-gradient(ellipse 92% 80% at 50% 42%, #000 20%, transparent 84%);
599
+ --dc-anim: dc-pan 160s linear infinite;
600
+ --dc-dx: 30px;
601
+ --dc-dy: 30px;
602
+ --dc-glow:
603
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
604
+ radial-gradient(closest-side, var(--decor-glow-3), transparent);
605
+ --dc-glow-size: 64vmax 64vmax, 50vmax 50vmax;
606
+ --dc-glow-pos: -14% -18%, 106% 96%;
607
+ --dc-glow-anim: dc-breathe 36s ease-in-out infinite;`,
608
+ topo: `
609
+ --dc-layers: repeating-linear-gradient(52deg,
610
+ var(--decor-line) 0 1px,
611
+ transparent 1px 22px);
612
+ --dc-mask: radial-gradient(ellipse 92% 84% at 46% 44%, #000 18%, transparent 86%);
613
+ --dc-anim: dc-pan 90s linear infinite;
614
+ --dc-dx: 17.34px;
615
+ --dc-dy: -13.54px;
616
+ --dc-glow:
617
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
618
+ radial-gradient(closest-side, var(--decor-glow-2), transparent);
619
+ --dc-glow-size: 72vmax 72vmax, 52vmax 52vmax;
620
+ --dc-glow-pos: 96% 8%, -10% 104%;
621
+ --dc-glow-anim: dc-breathe 32s ease-in-out infinite;`,
622
+ beams: `
623
+ --dc-layers: repeating-linear-gradient(105deg,
624
+ transparent 0 44px,
625
+ var(--decor-glow-1) 44px 118px,
626
+ transparent 118px 166px,
627
+ var(--decor-glow-2) 166px 214px,
628
+ transparent 214px 300px);
629
+ --dc-mask: linear-gradient(to bottom, transparent, #000 22%, #000 70%, transparent);
630
+ --dc-inset: -30%;
631
+ --dc-anim: dc-pan 44s linear infinite;
632
+ --dc-dx: 289.8px;
633
+ --dc-dy: 77.6px;
634
+ --dc-glow: radial-gradient(closest-side, var(--decor-glow-3), transparent);
635
+ --dc-glow-size: 80vmax 80vmax;
636
+ --dc-glow-pos: 50% 118%;
637
+ --dc-glow-anim: dc-breathe 26s ease-in-out infinite;`,
638
+ rings: `
639
+ --dc-layers: repeating-radial-gradient(circle at 50% 50%,
640
+ transparent 0 78px,
641
+ var(--decor-line) 78px 79px,
642
+ transparent 79px 158px);
643
+ --dc-size: 100% 100%;
644
+ --dc-repeat: no-repeat;
645
+ --dc-mask: radial-gradient(circle at 50% 46%, #000 12%, transparent 74%);
646
+ --dc-inset: -45%;
647
+ --dc-anim: dc-zoom 52s ease-in-out infinite;
648
+ --dc-glow:
649
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
650
+ radial-gradient(closest-side, var(--decor-glow-2), transparent);
651
+ --dc-glow-size: 62vmax 62vmax, 48vmax 48vmax;
652
+ --dc-glow-pos: 8% 4%, 96% 92%;
653
+ --dc-glow-anim: dc-breathe 38s ease-in-out infinite;`,
654
+ waves: `
655
+ --dc-layers:
656
+ radial-gradient(130% 62% at 50% 128%, transparent 57.6%, var(--decor-line) 58%, transparent 58.4%),
657
+ radial-gradient(130% 66% at 46% 140%, transparent 57.6%, var(--decor-line) 58%, transparent 58.4%),
658
+ radial-gradient(140% 72% at 54% 154%, transparent 57.6%, var(--decor-line) 58%, transparent 58.4%),
659
+ radial-gradient(150% 80% at 50% 172%, transparent 57.6%, var(--decor-line) 58%, transparent 58.4%);
660
+ --dc-size: 100% 100%;
661
+ --dc-repeat: no-repeat;
662
+ --dc-inset: -14%;
663
+ --dc-anim: dc-sway 40s ease-in-out infinite;
664
+ --dc-glow:
665
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
666
+ radial-gradient(closest-side, var(--decor-glow-3), transparent);
667
+ --dc-glow-size: 76vmax 60vmax, 54vmax 54vmax;
668
+ --dc-glow-pos: 50% 116%, 88% -16%;
669
+ --dc-glow-anim: dc-breathe 30s ease-in-out infinite;`,
670
+ orbs: `
671
+ --dc-layers:
672
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
673
+ radial-gradient(closest-side, var(--decor-glow-2), transparent),
674
+ radial-gradient(closest-side, var(--decor-glow-3), transparent);
675
+ --dc-size: 64vmax 64vmax, 50vmax 50vmax, 42vmax 42vmax;
676
+ --dc-pos: 10% 6%, 86% 28%, 60% 98%;
677
+ --dc-repeat: no-repeat;
678
+ --dc-inset: -22%;
679
+ --dc-anim: dc-float 56s ease-in-out infinite;`,
680
+ mesh: `
681
+ --dc-layers:
682
+ radial-gradient(closest-side, var(--decor-glow-1), transparent),
683
+ radial-gradient(closest-side, var(--decor-glow-2), transparent),
684
+ radial-gradient(closest-side, var(--decor-glow-3), transparent),
685
+ radial-gradient(var(--decor-dot) 1px, transparent 1.4px);
686
+ --dc-size: 58vmax 58vmax, 54vmax 54vmax, 46vmax 46vmax, 26px 26px;
687
+ --dc-pos: -8% -14%, 98% 16%, 44% 106%, 0 0;
688
+ --dc-repeat: no-repeat, no-repeat, no-repeat, repeat;
689
+ --dc-inset: -20%;
690
+ --dc-anim: dc-float 62s ease-in-out infinite;`,
691
+ aurora: `
692
+ --dc-layers:
693
+ radial-gradient(ellipse 72% 34% at 20% 20%, var(--decor-glow-1), transparent 68%),
694
+ radial-gradient(ellipse 62% 30% at 78% 34%, var(--decor-glow-2), transparent 68%),
695
+ radial-gradient(ellipse 94% 28% at 50% 84%, var(--decor-glow-3), transparent 70%);
696
+ --dc-size: 100% 100%;
697
+ --dc-repeat: no-repeat;
698
+ --dc-inset: -18%;
699
+ --dc-anim: dc-aurora 58s ease-in-out infinite;`,
700
+ };
701
+ /**
702
+ * The backdrop: one fixed layer behind every slide on screen, and the same
703
+ * geometry re-attached to each page when the deck is printed, since a fixed
704
+ * element does not repeat across a paged medium.
705
+ */
706
+ export const DECOR_CSS = `/* ── Backdrop geometry ────────────────────────────────────────────────── */
707
+ ${Object.entries(DECORS)
708
+ .map(([name, vars]) => `:root[data-decor="${name}"] {${vars}\n}`)
709
+ .join("\n\n")}
710
+
711
+ #backdrop {
712
+ position: fixed;
713
+ inset: 0;
714
+ z-index: 0;
715
+ overflow: hidden;
716
+ pointer-events: none;
717
+ -webkit-mask-image: var(--dc-mask, none);
718
+ mask-image: var(--dc-mask, none);
719
+ }
720
+
721
+ #backdrop::before,
722
+ #backdrop::after {
723
+ content: '';
724
+ position: absolute;
725
+ inset: var(--dc-inset, -12%);
726
+ background-repeat: var(--dc-repeat, repeat);
727
+ will-change: transform;
728
+ }
729
+
730
+ #backdrop::before {
731
+ background-image: var(--dc-layers, none);
732
+ background-size: var(--dc-size, auto);
733
+ background-position: var(--dc-pos, 0 0);
734
+ animation: var(--dc-anim, none);
735
+ }
736
+
737
+ #backdrop::after {
738
+ background-image: var(--dc-glow, none);
739
+ background-size: var(--dc-glow-size, auto);
740
+ background-position: var(--dc-glow-pos, 0 0);
741
+ background-repeat: no-repeat;
742
+ animation: var(--dc-glow-anim, none);
743
+ }
744
+
745
+ /* One tile of travel per cycle, so the pattern lands back on itself. */
746
+ @keyframes dc-pan {
747
+ to { transform: translate3d(var(--dc-dx, 0px), var(--dc-dy, 0px), 0); }
748
+ }
749
+
750
+ @keyframes dc-float {
751
+ 0%, 100% { transform: translate3d(-1.5%, -1%, 0) scale(1); }
752
+ 34% { transform: translate3d(2%, 1.5%, 0) scale(1.06); }
753
+ 67% { transform: translate3d(-1%, 2%, 0) scale(0.97); }
754
+ }
755
+
756
+ @keyframes dc-sway {
757
+ 0%, 100% { transform: translate3d(-2.5%, 0, 0) scale(1); }
758
+ 50% { transform: translate3d(2.5%, 1.5%, 0) scale(1.04); }
759
+ }
760
+
761
+ @keyframes dc-zoom {
762
+ 0%, 100% { transform: scale(1); }
763
+ 50% { transform: scale(1.1); }
764
+ }
765
+
766
+ @keyframes dc-aurora {
767
+ 0%, 100% { transform: translate3d(0, 0, 0) skewY(0deg); }
768
+ 50% { transform: translate3d(3%, -2%, 0) skewY(-1.6deg); }
769
+ }
770
+
771
+ @keyframes dc-breathe {
772
+ 0%, 100% { opacity: 0.75; transform: scale(1); }
773
+ 50% { opacity: 1; transform: scale(1.07); }
774
+ }
775
+
776
+ /* A drifting backdrop is exactly the kind of motion this setting turns off. */
777
+ @media (prefers-reduced-motion: reduce) {
778
+ #backdrop::before,
779
+ #backdrop::after { animation: none !important; }
780
+ }
781
+
782
+ @media print {
783
+ /* Fixed elements do not repeat across pages, so the backdrop is rebuilt on
784
+ each slide instead — same custom properties, no second definition. */
785
+ #backdrop { display: none !important; }
786
+
787
+ .slide::before,
788
+ .slide::after {
789
+ content: '';
790
+ position: absolute;
791
+ inset: 0;
792
+ z-index: 0;
793
+ animation: none !important;
794
+ -webkit-mask-image: var(--dc-mask, none);
795
+ mask-image: var(--dc-mask, none);
796
+ }
797
+
798
+ .slide::before {
799
+ background-image: var(--dc-layers, none);
800
+ background-size: var(--dc-size, auto);
801
+ background-position: var(--dc-pos, 0 0);
802
+ background-repeat: var(--dc-repeat, repeat);
803
+ }
804
+
805
+ .slide::after {
806
+ background-image: var(--dc-glow, none);
807
+ background-size: var(--dc-glow-size, auto);
808
+ background-position: var(--dc-glow-pos, 0 0);
809
+ background-repeat: no-repeat;
810
+ }
811
+ }`;
812
+ const SIZE_SPECS = {
813
+ s: {
814
+ label: "small",
815
+ blurb: "More on a slide. For dense reference decks and close screens.",
816
+ display: 0.86, body: 0.88, code: 0.92, lead: 1.03,
817
+ padY: "4rem", padX: "6.4rem",
818
+ },
819
+ m: {
820
+ label: "medium",
821
+ blurb: "The default. Reads from the middle of a normal room.",
822
+ display: 1, body: 1, code: 1, lead: 1,
823
+ padY: "4.4rem", padX: "6rem",
824
+ },
825
+ l: {
826
+ label: "large",
827
+ blurb: "For a wide room, or a talk driven by a handful of lines a slide.",
828
+ display: 1.12, body: 1.18, code: 1.12, lead: 0.97,
829
+ padY: "4rem", padX: "5rem",
830
+ },
831
+ xl: {
832
+ label: "x-large",
833
+ blurb: "Readable from the back row. Expect three or four lines a slide.",
834
+ display: 1.24, body: 1.38, code: 1.24, lead: 0.94,
835
+ padY: "3.4rem", padX: "4.2rem",
836
+ },
837
+ };
838
+ /** Ordered smallest to largest, which is the order the picker shows them in. */
839
+ export const SIZE_IDS = ["s", "m", "l", "xl"];
840
+ export const DEFAULT_SIZE = "m";
841
+ /** Long spellings, so `--size large` works as well as `--size l`. */
842
+ const SIZE_ALIASES = {
843
+ small: "s",
844
+ medium: "m",
845
+ m: "m",
846
+ large: "l",
847
+ xlarge: "xl",
848
+ "x-large": "xl",
849
+ "extra-large": "xl",
850
+ huge: "xl",
851
+ };
852
+ /** A size id from untrusted input, or `null` if it names nothing. */
853
+ export function findSize(input) {
854
+ if (!input)
855
+ return null;
856
+ const key = String(input).trim().toLowerCase();
857
+ if (key in SIZE_SPECS)
858
+ return key;
859
+ return SIZE_ALIASES[key] ?? null;
860
+ }
861
+ /** A size id from untrusted input, falling back to the default. */
862
+ export function resolveSizeName(input) {
863
+ return findSize(input) ?? DEFAULT_SIZE;
864
+ }
865
+ /** What the editor's size control and the CLI's help text list. */
866
+ export function sizeSummaries() {
867
+ return SIZE_IDS.map((id) => ({
868
+ id,
869
+ label: SIZE_SPECS[id].label,
870
+ blurb: SIZE_SPECS[id].blurb,
871
+ // The picker sets each button at its own ratio, so the row is its own key.
872
+ display: SIZE_SPECS[id].display,
873
+ }));
874
+ }
875
+ /** One line per size, for `deckrun --list-sizes`. */
876
+ export function sizeListing() {
877
+ const pad = Math.max(...SIZE_IDS.map((id) => id.length));
878
+ return SIZE_IDS.map((id) => {
879
+ const z = SIZE_SPECS[id];
880
+ return `${id.padEnd(pad)} ${z.label.padEnd(7)} ${z.blurb}`;
881
+ });
882
+ }
883
+ function sizeVars(z) {
884
+ const lines = [
885
+ ["type-display", String(z.display)],
886
+ ["type-body", String(z.body)],
887
+ ["type-code", String(z.code)],
888
+ ["type-lead", String(z.lead)],
889
+ ["slide-pad-y", z.padY],
890
+ ["slide-pad-x", z.padX],
891
+ ];
892
+ const pad = Math.max(...lines.map(([k]) => k.length));
893
+ return lines
894
+ .map(([k, v]) => ` --${k}:${" ".repeat(pad - k.length + 1)}${v};`)
895
+ .join("\n");
896
+ }
897
+ /** The scale for one baked-in size, for a deck that never switches. */
898
+ export function sizeRootCss(size) {
899
+ return `:root {\n${sizeVars(SIZE_SPECS[resolveSizeName(size)])}\n}`;
900
+ }
901
+ /** Every scale, switchable at runtime via `[data-size]` on the root. */
902
+ export function sizeSwitchableCss() {
903
+ return SIZE_IDS.map((id) => {
904
+ const selector = id === DEFAULT_SIZE
905
+ ? `:root, :root[data-size="${id}"]`
906
+ : `:root[data-size="${id}"]`;
907
+ return `${selector} {\n${sizeVars(SIZE_SPECS[id])}\n}`;
908
+ }).join("\n\n");
909
+ }
910
+ // ── Font overrides ────────────────────────────────────────────────────────
911
+ /**
912
+ * A theme names a display face and a body face. Either can be replaced without
913
+ * leaving the theme, and the two are chosen separately, so a serif heading over
914
+ * a sans body — or the reverse — is a setting rather than a fork.
915
+ *
916
+ * The monospace face stays the theme's: code wants the face the palette's
917
+ * highlighting was picked against.
918
+ *
919
+ * Overrides are attributes rather than a second palette. `[data-head]` and
920
+ * `[data-body]` carry the same specificity as `[data-theme]`, and the blocks
921
+ * below are emitted after it, so source order decides and the override wins.
922
+ * No attribute means the theme's own face, which is why there is no "default"
923
+ * entry to keep in step with anything.
924
+ */
925
+ export const FONT_IDS = Object.keys(FONTS);
926
+ /** A font key from untrusted input, or `null` for "leave it to the theme". */
927
+ export function findFont(input) {
928
+ if (!input)
929
+ return null;
930
+ const key = String(input).trim();
931
+ if (FONTS[key])
932
+ return key;
933
+ // Also accept the human name: "space grotesk", "IBM Plex Mono".
934
+ const wanted = key.toLowerCase().replace(/[\s_-]+/g, "");
935
+ for (const id of FONT_IDS) {
936
+ if (id.toLowerCase() === wanted)
937
+ return id;
938
+ if (fontName(id).toLowerCase().replace(/\s+/g, "") === wanted)
939
+ return id;
940
+ }
941
+ return null;
942
+ }
943
+ /** What the editor's font menu lists, in catalog order. */
944
+ export function fontSummaries() {
945
+ return FONT_IDS.map((id) => ({
946
+ id,
947
+ name: fontName(id),
948
+ kind: FONTS[id].kind,
949
+ stack: FONTS[id].stack,
950
+ }));
951
+ }
952
+ /** One line per face, for `deckrun --list-fonts`. */
953
+ export function fontListing() {
954
+ const pad = Math.max(...FONT_IDS.map((id) => id.length));
955
+ return FONT_IDS.map((id) => `${id.padEnd(pad)} ${FONTS[id].kind.padEnd(5)} ${fontName(id)}`);
956
+ }
957
+ /**
958
+ * Every face, in both slots. Emitted once and shared by the deck, the editor,
959
+ * and the preview, so a runtime change is an attribute swap rather than a
960
+ * restyle.
961
+ */
962
+ export function fontOverrideCss() {
963
+ const blocks = [];
964
+ for (const id of FONT_IDS) {
965
+ blocks.push(`:root[data-head="${id}"] { --font-display: ${FONTS[id].stack}; }`);
966
+ }
967
+ for (const id of FONT_IDS) {
968
+ blocks.push(`:root[data-body="${id}"] { --font-body: ${FONTS[id].stack}; }`);
969
+ }
970
+ return `/* ── Font overrides ───────────────────────────────────────────────────── */\n${blocks.join("\n")}`;
971
+ }