rikiki-deck 0.5.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude/skills/rikiki-debug/SKILL.md +65 -0
  2. package/.claude/skills/rikiki-deck/SKILL.md +366 -0
  3. package/.claude/skills/rikiki-theme/SKILL.md +70 -0
  4. package/README.md +101 -38
  5. package/bin/lib/assemble.mjs +154 -0
  6. package/bin/lib/box-geometry.mjs +66 -0
  7. package/bin/lib/browser.mjs +279 -0
  8. package/bin/lib/check.mjs +992 -0
  9. package/bin/lib/cli-error.mjs +26 -0
  10. package/bin/lib/diff.mjs +275 -0
  11. package/bin/lib/export-pdf.mjs +42 -0
  12. package/bin/lib/graph-hit.mjs +86 -0
  13. package/bin/lib/inline.mjs +71 -8
  14. package/bin/lib/prune-icons.mjs +81 -0
  15. package/bin/lib/render.mjs +195 -0
  16. package/bin/lib/scan-external.mjs +126 -0
  17. package/bin/lib/starter.mjs +27 -14
  18. package/bin/lib/visual.mjs +120 -0
  19. package/bin/rikiki.mjs +410 -30
  20. package/dist/annotation-marks.js +1 -0
  21. package/dist/application/deep-link.d.ts +29 -0
  22. package/dist/application/keymap.d.ts +40 -0
  23. package/dist/application/mouse-nav.d.ts +12 -0
  24. package/dist/atoms/deck-code-highlighter.d.ts +6 -0
  25. package/dist/atoms/deck-code.d.ts +14 -0
  26. package/dist/atoms/deck-punch.d.ts +6 -0
  27. package/dist/atoms/deck-source.d.ts +12 -0
  28. package/dist/bar-segments.js +1 -0
  29. package/dist/browser-location.js +1 -0
  30. package/dist/cards-syntax.js +6 -0
  31. package/dist/click-stages.js +1 -1
  32. package/dist/contrast.js +1 -0
  33. package/dist/deck-agenda.js +6 -0
  34. package/dist/deck-annotate.js +18 -0
  35. package/dist/deck-bar.js +19 -0
  36. package/dist/deck-bento.js +4 -0
  37. package/dist/deck-callout.js +1 -1
  38. package/dist/deck-cell.js +1 -0
  39. package/dist/deck-checklist.js +1 -0
  40. package/dist/deck-code-highlighter.js +1 -0
  41. package/dist/deck-code.js +2 -2
  42. package/dist/deck-cover.js +6 -6
  43. package/dist/deck-csv.js +15 -0
  44. package/dist/deck-feature-cards.js +2 -2
  45. package/dist/deck-feature.js +2 -2
  46. package/dist/deck-figure.js +8 -0
  47. package/dist/deck-fit.js +1 -0
  48. package/dist/deck-flow.js +7 -0
  49. package/dist/deck-graph.js +25 -0
  50. package/dist/deck-grid.js +1 -1
  51. package/dist/deck-icon.js +1 -0
  52. package/dist/deck-kpi-grid.js +4 -0
  53. package/dist/deck-link.js +1 -0
  54. package/dist/deck-md.js +8 -3
  55. package/dist/deck-mermaid.js +15 -3
  56. package/dist/deck-outline.js +1 -0
  57. package/dist/deck-overview.js +15 -2
  58. package/dist/deck-persona.js +6 -0
  59. package/dist/deck-photo.js +1 -1
  60. package/dist/deck-point.js +1 -0
  61. package/dist/deck-presenter.js +85 -16
  62. package/dist/deck-pull.js +1 -0
  63. package/dist/deck-punch.js +1 -1
  64. package/dist/deck-quote.js +6 -0
  65. package/dist/deck-root.js +17 -13
  66. package/dist/deck-section.js +2 -2
  67. package/dist/deck-source.js +2 -0
  68. package/dist/deck-split.js +5 -3
  69. package/dist/deck-stat.js +2 -2
  70. package/dist/deck-step-list.js +4 -2
  71. package/dist/deck-table.js +1 -0
  72. package/dist/deck-takeaway.js +2 -2
  73. package/dist/deck-timeline.js +5 -0
  74. package/dist/deck-transition.js +3 -3
  75. package/dist/deck-versus.js +9 -0
  76. package/dist/deep-link.js +1 -0
  77. package/dist/domain/deck-link.d.ts +21 -0
  78. package/dist/domain/deck-outline.d.ts +50 -0
  79. package/dist/domain/navigation.d.ts +25 -0
  80. package/dist/domain/viewport.d.ts +42 -0
  81. package/dist/escape-html.js +1 -0
  82. package/dist/extras/deck-agenda.d.ts +25 -0
  83. package/dist/extras/deck-annotate.d.ts +108 -0
  84. package/dist/extras/deck-bar.d.ts +32 -0
  85. package/dist/extras/deck-checklist.d.ts +20 -0
  86. package/dist/extras/deck-figure.d.ts +26 -0
  87. package/dist/extras/deck-flow.d.ts +41 -0
  88. package/dist/extras/deck-graph.d.ts +92 -0
  89. package/dist/extras/deck-icon.d.ts +20 -0
  90. package/dist/extras/deck-kpi-grid.d.ts +26 -0
  91. package/dist/extras/deck-persona.d.ts +31 -0
  92. package/dist/extras/deck-pull.d.ts +13 -0
  93. package/dist/extras/deck-quote.d.ts +28 -0
  94. package/dist/extras/deck-table.d.ts +26 -0
  95. package/dist/extras/deck-timeline.d.ts +31 -0
  96. package/dist/extras/deck-versus.d.ts +18 -0
  97. package/dist/extras/signature.d.ts +2 -0
  98. package/dist/fit-controller.js +1 -0
  99. package/dist/graph-layout.js +1 -0
  100. package/dist/grid-tracks.js +1 -0
  101. package/dist/icon-set.js +1 -0
  102. package/dist/index.d.ts +9 -0
  103. package/dist/index.js +92 -49
  104. package/dist/infrastructure/browser-location.d.ts +3 -0
  105. package/dist/keymap.js +1 -0
  106. package/dist/layouts/deck-bento.d.ts +38 -0
  107. package/dist/layouts/deck-cover.d.ts +6 -0
  108. package/dist/layouts/deck-feature.d.ts +7 -0
  109. package/dist/layouts/deck-split.d.ts +12 -0
  110. package/dist/layouts/deck-takeaway.d.ts +7 -0
  111. package/dist/molecules/deck-callout.d.ts +2 -0
  112. package/dist/molecules/deck-cell.d.ts +19 -0
  113. package/dist/molecules/deck-csv.d.ts +38 -0
  114. package/dist/molecules/deck-fit.d.ts +14 -0
  115. package/dist/molecules/deck-md.d.ts +3 -0
  116. package/dist/molecules/deck-point.d.ts +22 -0
  117. package/dist/molecules/deck-stat.d.ts +2 -0
  118. package/dist/molecules/deck-step-list.d.ts +8 -0
  119. package/dist/mouse-nav.js +1 -0
  120. package/dist/navigation.js +1 -0
  121. package/dist/parse-csv.js +3 -0
  122. package/dist/plugins/click-stages.d.ts +5 -0
  123. package/dist/plugins/shiki.d.ts +2 -2
  124. package/dist/runtime/deck-root.d.ts +179 -2
  125. package/dist/shared/annotation-marks.d.ts +60 -0
  126. package/dist/shared/bar-segments.d.ts +28 -0
  127. package/dist/shared/cards-syntax.d.ts +31 -0
  128. package/dist/shared/contrast.d.ts +52 -0
  129. package/dist/shared/escape-html.d.ts +3 -0
  130. package/dist/shared/fit-controller.d.ts +27 -0
  131. package/dist/shared/graph-layout.d.ts +35 -0
  132. package/dist/shared/grid-tracks.d.ts +17 -0
  133. package/dist/shared/icon-set.d.ts +6 -0
  134. package/dist/shared/parse-csv.d.ts +9 -0
  135. package/dist/shared/slide-fill.d.ts +8 -0
  136. package/dist/shared-styles.js +1 -1
  137. package/dist/shiki.js +1 -1
  138. package/dist/signature.js +1 -0
  139. package/dist/slide-fill.js +1 -0
  140. package/dist/standalone.js +223 -98
  141. package/dist/vendor/THIRD-PARTY-NOTICES.txt +4347 -0
  142. package/dist/vendor/inventory.json +3029 -0
  143. package/dist/vendor/lit.js +62 -2
  144. package/dist/vendor/shiki.js +1 -57
  145. package/dist/viewport.js +1 -0
  146. package/docs/llms/rikiki-reference.md +1048 -72
  147. package/docs/llms/rikiki-workflow.md +536 -0
  148. package/llms.txt +42 -13
  149. package/package.json +25 -8
  150. package/themes/rikiki.css +176 -50
  151. package/themes/siliceum.css +176 -54
package/README.md CHANGED
@@ -6,52 +6,92 @@
6
6
 
7
7
  A tiny **Lit Web Components** framework for technical presentations. Drop a folder anywhere, open `index.html`, give the talk. No build step on the consumer side · the framework itself is built from TypeScript, but the output is plain ES modules you import directly.
8
8
 
9
- This documentation tracks rikiki v0.5.0.
9
+ This documentation tracks rikiki v0.7.1.
10
10
 
11
11
  ## TL;DR
12
12
 
13
13
  ```sh
14
- cp starter.html my-deck.html
14
+ npm install rikiki-deck
15
+ npx rikiki init my-deck.html
16
+ python3 -m http.server # ES modules need http://, not file://
15
17
  ```
16
18
 
17
- Edit `my-deck.html`. Each slide is a custom element. Markdown is available anywhere via `<deck-md>`. Navigate with `←` / `→` (or click, or the scroll wheel), `O` for the overview grid. Decks are linear by default; add `nav="2d"` on `<deck-root>` for chapter/slide grid navigation.
19
+ `init` writes the deck and copies the runtime it loads into `./rikiki/` beside
20
+ it. Edit `my-deck.html`. Each slide is a custom element. Markdown is available anywhere via `<deck-md>`. Navigate with `←` / `→` (or click, or the scroll wheel), `O` for the overview grid. Decks are linear by default; add `nav="2d"` on `<deck-root>` for chapter/slide grid navigation.
18
21
 
19
- ## Layout
22
+ ## Look at it, and measure it
23
+
24
+ You cannot review a deck you cannot see. Two commands stand in for eyes, both
25
+ needing the optional peer `playwright`:
26
+
27
+ ```sh
28
+ npx rikiki render talk.html # one PNG per slide + a gallery + a manifest
29
+ npx rikiki render talk.html --steps # every revealed state, not just the first
30
+ npx rikiki render talk.html --out after/ --baseline before/ # what moved since last time
31
+ npx rikiki check talk.html # what is wrong, where, and what to try
32
+ npx rikiki check talk.html --json # the same, as a versioned report
33
+ npx rikiki check talk.html --steps # measure every revealed state, not just the first
34
+ ```
35
+
36
+ `render --baseline <dir>` compares the fresh pictures to an earlier set, slide
37
+ by slide, and ranks them by how much changed, with the bounding box of what
38
+ moved and a `diff.json` beside the manifest. It exits 1 on a slide that
39
+ changed, disappeared or changed size. The default `--threshold 0.5` (percent of
40
+ pixels) keeps anti-aliasing noise out of the report; `--threshold 0` lists
41
+ every pixel change.
42
+
43
+ `check` reports a runtime that never loaded, a file that did not arrive, a
44
+ misspelled `deck-*` element that renders as nothing, content the slide clips
45
+ away, text too small for a room, and duplicate slide ids. It exits 0 when
46
+ nothing blocks, 1 on defects, 2 when it could not look at the deck at all. It
47
+ also lists what it did **not** check, because silence would read as approval.
48
+ Under `--steps`, each slide is measured in its opening state and in every
49
+ state its own reveals step through, and each diagnostic says which one
50
+ (`state: 2`).
51
+
52
+ ## For LLMs / coding assistants
53
+
54
+ If you point a coding assistant (Claude Code, Copilot, …) at this package, give it
55
+ the machine-oriented docs — they are the single source of truth and ship with the
56
+ npm package:
57
+
58
+ - **[`llms.txt`](./llms.txt)** — concise capability map and entry points (the
59
+ [llms.txt convention](https://llmstxt.org)).
60
+ - **[`docs/llms/rikiki-workflow.md`](./docs/llms/rikiki-workflow.md)** — the
61
+ working guide: brief to plan to slides to checks to delivery, with nine
62
+ compositions by intent whose HTML is verified by the test suite. The one to
63
+ read first.
64
+ - **[`docs/llms/rikiki-reference.md`](./docs/llms/rikiki-reference.md)** — every
65
+ tag, attribute, slot, design token, plugin, and recipe in one file. Have the
66
+ assistant read this first; tell it not to invent tags or tokens outside it.
67
+ - **Claude Code skills** — `rikiki-deck`, `rikiki-theme`, `rikiki-debug` ship in
68
+ `.claude/skills/` (see [Claude Code skills](#claude-code-skills) to install
69
+ them); they teach an assistant the authoring/theming/debugging workflows.
70
+
71
+ ## What the install gives you
20
72
 
21
73
  ```
22
- rikiki/
74
+ node_modules/rikiki-deck/
23
75
  ├── tokens.css ← entry point · re-exports the default theme
24
76
  ├── themes/
25
77
  │ ├── rikiki.css ← default theme (acid greens + mango, on dark)
26
78
  │ ├── siliceum.css ← alternative theme (warm paper + yellow)
27
79
  │ └── siliceum-fonts.css ← self-hosted Source Sans Pro + JetBrains Mono
28
80
  ├── fonts/ ← woff2 files used by the Siliceum theme
29
- ├── src/ ← TypeScript sources · organised by DS bucket
30
- │ ├── index.ts ← registers every component
31
- │ ├── shared-styles.ts
32
- │ ├── livereload.ts
33
- │ ├── runtime/ ← deck-root, deck-help, deck-overview,
34
- │ │ deck-presenter, deck-transition, deck-notes
35
- │ ├── layouts/ ← deck-cover, deck-section, deck-feature,
36
- │ │ deck-split, deck-feature-cards, deck-takeaway,
37
- │ │ deck-photo
38
- │ ├── molecules/ ← deck-callout, deck-card, deck-md, deck-mermaid,
39
- │ │ deck-stat, deck-metric, deck-tier-list,
40
- │ │ deck-step-list, deck-shortcut, deck-stack,
41
- │ │ deck-grid
42
- │ ├── atoms/ ← deck-badge, deck-kicker, deck-punch, deck-code
43
- │ └── plugins/ ← opt-in (shiki for advanced syntax highlighting)
44
- ├── dist/ ← built output · FLAT regardless of src bucket
45
- │ (deck-root's dynamic imports rely on it)
46
- ├── build.mjs ← esbuild script
47
- ├── tsconfig.json
48
- └── starter.html ← blank template, one slide per layout type
81
+ ├── dist/ ← the runtime · plain ES modules, FLAT layout
82
+ │ └── vendor/ ← lit and marked · mermaid and Shiki when asked
83
+ ├── bin/ ← the rikiki CLI
84
+ ├── docs/llms/ ← the full agent reference
85
+ └── .claude/skills/ ← three agent skills · install with `rikiki skills`
49
86
  ```
50
87
 
88
+ `init` copies the first five of those next to your deck, minus the heavy
89
+ plugin payloads unless you ask for them.
90
+
51
91
  ## Three-layer styling
52
92
 
53
93
  1. **Theme tokens** (`themes/<name>.css`) at `:root` · custom properties cross the Shadow DOM, so they reach every component.
54
- 2. **Shared styles** (`src/shared-styles.ts`) · base typography, helpers, imported by every layout via `static styles`.
94
+ 2. **Shared styles** · base typography and helpers, compiled into every layout's own Shadow DOM.
55
95
  3. **Layout-specific CSS** · each component's own Shadow DOM.
56
96
 
57
97
  To re-theme: copy a theme file, change the values, that's it. All components follow.
@@ -77,10 +117,14 @@ To re-theme: copy a theme file, change the values, that's it. All components fol
77
117
  | `<deck-code lang="js" hero?>` | Code block with light syntax highlighting |
78
118
  | `<deck-callout type="info\|warn\|danger\|ok">` | Information callout |
79
119
  | `<deck-card color="yellow\|orange\|green\|red?">` | Tinted card |
80
- | `<deck-mermaid>` | Diagram via Mermaid (loaded on demand from CDN) |
120
+ | `<deck-mermaid>` | Diagram via the vendored Mermaid runtime (opt-in bundle) |
121
+ | `<deck-figure>` | An image with its own caption and credit (opt-in bundle) |
122
+ | `<deck-source>` | The credit line under a table, chart, screenshot or quote |
81
123
 
82
124
  Plus `<deck-badge>`, `<deck-metric>`, `<deck-tier-list>`, `<deck-step-list>`, `<deck-kicker>`, `<deck-stack>`, `<deck-grid>`, `<deck-punch>`.
83
125
 
126
+ Opt-in components add `<deck-annotate>` (numbered badges on a screenshot, placed by `above` / `below` / `left` / `right` anchors as well as pixel offsets) and `<deck-versus slide>` (a before/after slide with its own `title`, `lead` and `footer` slots).
127
+
84
128
  ## Common patterns
85
129
 
86
130
  ### Slide with markdown + code
@@ -124,7 +168,7 @@ Plus `<deck-badge>`, `<deck-metric>`, `<deck-tier-list>`, `<deck-step-list>`, `<
124
168
 
125
169
  ## Markdown support (`<deck-md>`)
126
170
 
127
- Parser: `marked` 12 from CDN. Supports GFM (tables, task lists), code blocks, inline code, **bold**, *italic*, lists, blockquotes, links, `---`.
171
+ Parser: vendored `marked` 12. Supports GFM (tables, task lists), code blocks, inline code, **bold**, *italic*, lists, blockquotes, links, `---`.
128
172
 
129
173
  ## Navigation
130
174
 
@@ -172,6 +216,16 @@ reflows like a web page:
172
216
  <deck-root fluid> <!-- fills its box, reflows, no letterbox -->
173
217
  ```
174
218
 
219
+ A single slide can also escape the canvas by carrying its own `fluid` attribute
220
+ · that slide gets the real viewport (handy for an embedded live demo) while the
221
+ rest of the deck stays on the fixed canvas:
222
+
223
+ ```html
224
+ <deck-feature fluid>
225
+ <iframe src="playground.html" style="position:fixed; inset:0; border:0;"></iframe>
226
+ </deck-feature>
227
+ ```
228
+
175
229
  Both modes are embed-safe · a `<deck-root>` placed inside a larger page scales
176
230
  to (or fills) its own container and never touches the host page's scroll or
177
231
  typography.
@@ -208,18 +262,28 @@ deck-cover::part(brand) { font-family: 'Comic Sans'; }
208
262
 
209
263
  ### Add a layout
210
264
 
211
- Create `src/layouts/deck-my-layout.ts`, `import { slideBase } from '../shared-styles.js'`, extend `LitElement`, register in `src/index.ts`. Rebuild.
265
+ Adding a `deck-*` element means changing the framework itself, which lives in
266
+ the repository rather than in this package. See
267
+ [CONTRIBUTING](https://gitlab.com/tordu-jardin/rikiki/-/blob/main/CONTRIBUTING.md).
212
268
 
213
- ## Build
269
+ ## No build step
214
270
 
215
- ```bash
216
- npm install
217
- npm run build # node build.mjs (esbuild) + tsc --emitDeclarationOnly
218
- npm run watch # esbuild watch mode
219
- npm run typecheck # tsc --noEmit
271
+ The published runtime is plain ES modules. Nothing here needs compiling,
272
+ bundling or transpiling to author, serve or present a deck.
273
+
274
+ ## Claude Code skills
275
+
276
+ The package ships three Claude Code skills so an assistant authoring your deck
277
+ knows the framework: `rikiki-deck` (build a deck), `rikiki-theme` (theming), and
278
+ `rikiki-debug` (diagnose a deck).
279
+
280
+ ```sh
281
+ npx rikiki skills # into ./.claude/skills/
282
+ npx rikiki skills --dir ~/.claude/skills # or once, for every project
220
283
  ```
221
284
 
222
- `dist/` is versioned · consumers don't run a build.
285
+ Claude Code discovers them on the next session. Re-run the command with
286
+ `--force` after upgrading the package to pick up skill changes.
223
287
 
224
288
  ## Reveals & animations
225
289
 
@@ -227,8 +291,7 @@ Per-element click-through builds are an opt-in plugin (`installClickStages()` fr
227
291
  `dist/click-stages.js`): annotate elements with `data-click`, `data-click-hide`,
228
292
  `data-click-auto`, `data-click-stagger`, and `data-morph` (Keynote-style Magic
229
293
  Move via View Transitions). Slide transitions are driven by `transition="…"` on
230
- `<deck-root>`. See `docs/llms/rikiki-reference.md` §7 for the full attribute set,
231
- and `decks/tests/demo.html` for a runnable feature tour.
294
+ `<deck-root>`. See `docs/llms/rikiki-reference.md` §7 for the full attribute set.
232
295
 
233
296
  ## Known limits
234
297
 
@@ -0,0 +1,154 @@
1
+ // ════════════════════════════════════════════════════════════════
2
+ // rikiki assemble · build one deck HTML from ordered partials.
3
+ //
4
+ // A long talk is easier to write, review and diff in pieces. This joins them
5
+ // back into the single HTML file everything else in rikiki expects: `bundle`
6
+ // folds it, `export` prints it, a browser serves it.
7
+ //
8
+ // deck.config.{js,json} shape:
9
+ // export default {
10
+ // title: 'My talk',
11
+ // theme: 'rikiki/tokens.css', // href, relative to the OUTPUT file
12
+ // bundle: 'rikiki/dist/index.js', // runtime href, relative to OUTPUT
13
+ // transition: 'slide', // optional <deck-root transition="…">
14
+ // lang: 'fr', // optional <html lang="…">
15
+ // slides: ['parts/cover.html', 'parts/intro.md', 'parts/closing.html'],
16
+ // };
17
+ //
18
+ // .html partials are inlined verbatim (one or more <deck-*> elements).
19
+ // .md partials become slides · a line that is exactly `---` starts a new one.
20
+ // ════════════════════════════════════════════════════════════════
21
+
22
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
23
+ import { createRequire } from 'node:module';
24
+ import { dirname, extname, resolve } from 'node:path';
25
+ import { pathToFileURL } from 'node:url';
26
+ import { ExpectedError } from './cli-error.mjs';
27
+
28
+ // What `rikiki init` writes, so an assembled deck bundles like any other.
29
+ const DEFAULT_THEME = 'rikiki/tokens.css';
30
+ const DEFAULT_RUNTIME = 'rikiki/dist/index.js';
31
+
32
+ const escapeHtml = (s) =>
33
+ String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
34
+
35
+ /** Split a markdown file into slides on lines that are exactly `---`
36
+ * (reveal.js convention). One file, many slides. A blank chunk collapses. */
37
+ function splitMarkdownSlides(body) {
38
+ const chunks = [];
39
+ let current = [];
40
+ for (const line of body.split(/\r?\n/)) {
41
+ if (/^[ \t]*---[ \t]*$/.test(line)) {
42
+ chunks.push(current.join('\n'));
43
+ current = [];
44
+ } else current.push(line);
45
+ }
46
+ chunks.push(current.join('\n'));
47
+ return chunks.map((c) => c.trim()).filter((c) => c.length > 0);
48
+ }
49
+
50
+ function renderPartial(absPath) {
51
+ const body = readFileSync(absPath, 'utf8');
52
+ if (extname(absPath) !== '.md') return body.trimEnd();
53
+ // <deck-md> deindents and parses the raw markdown at runtime, so the body is
54
+ // inlined verbatim. `***` stays available as an in-slide rule.
55
+ return splitMarkdownSlides(body)
56
+ .map((slide) => `<deck-feature>\n<deck-md>\n${slide}\n</deck-md>\n</deck-feature>`)
57
+ .join('\n\n');
58
+ }
59
+
60
+ async function loadConfig(absPath) {
61
+ if (!existsSync(absPath) || !statSync(absPath).isFile()) {
62
+ throw new ExpectedError(`assemble · config not found: ${absPath}`);
63
+ }
64
+ if (extname(absPath) === '.json') {
65
+ try {
66
+ return JSON.parse(readFileSync(absPath, 'utf8'));
67
+ } catch (cause) {
68
+ throw new ExpectedError(`assemble · ${absPath} is not valid JSON · ${cause.message}`);
69
+ }
70
+ }
71
+ try {
72
+ const mod = await import(pathToFileURL(absPath).href);
73
+ return mod.default ?? mod;
74
+ } catch (cause) {
75
+ // A `.js` file is read as CommonJS or as an ES module depending on the
76
+ // nearest package.json, and `npm init -y` writes "type": "commonjs". Both
77
+ // dialects are legitimate here; only the mismatch is worth a message.
78
+ if (cause?.code === 'ERR_REQUIRE_ESM' || cause instanceof SyntaxError) {
79
+ throw new ExpectedError(
80
+ `assemble · ${absPath} looks like an ES module, but the nearest package.json\n` +
81
+ ' does not declare "type": "module". Use `module.exports = {…}`, rename the\n' +
82
+ ' file to .mjs, or use a .json config.',
83
+ );
84
+ }
85
+ throw cause;
86
+ }
87
+ }
88
+
89
+ /** The default output path: the title, slugged, next to the config. */
90
+ export function defaultOutputPath(configPath, title) {
91
+ const slug = String(title || 'deck')
92
+ .toLowerCase()
93
+ .replace(/[^a-z0-9]+/g, '-')
94
+ .replace(/^-|-$/g, '');
95
+ return resolve(dirname(configPath), (slug || 'deck') + '.html');
96
+ }
97
+
98
+ /**
99
+ * Assemble a deck from its config.
100
+ * @returns {Promise<{ html: string, outputPath: string, slides: number, unbundleable: string[] }>}
101
+ * `outputPath` is null when the caller asked for stdout · `unbundleable`
102
+ * lists the hrefs `rikiki bundle` will not be able to inline.
103
+ */
104
+ export async function assembleDeck(configPath, outputPath) {
105
+ const absConfig = resolve(process.cwd(), configPath);
106
+ const config = await loadConfig(absConfig);
107
+ const configDir = dirname(absConfig);
108
+
109
+ if (!Array.isArray(config.slides) || config.slides.length === 0) {
110
+ throw new ExpectedError(`assemble · ${configPath} · \`slides\` must be a non-empty array`);
111
+ }
112
+
113
+ const slidesHtml = config.slides
114
+ .map((rel) => {
115
+ const abs = resolve(configDir, rel);
116
+ if (!existsSync(abs)) throw new ExpectedError(`assemble · partial not found: ${rel}`);
117
+ return renderPartial(abs);
118
+ })
119
+ .join('\n\n');
120
+
121
+ const transitionAttr = config.transition ? ` transition="${escapeHtml(config.transition)}"` : '';
122
+ const html = `<!doctype html>
123
+ <html lang="${escapeHtml(config.lang ?? 'en')}">
124
+ <head>
125
+ <meta charset="UTF-8">
126
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
127
+ <title>${escapeHtml(config.title ?? 'Rikiki deck')}</title>
128
+ <link rel="stylesheet" href="${escapeHtml(config.theme ?? DEFAULT_THEME)}">
129
+ <script type="module" src="${escapeHtml(config.bundle ?? DEFAULT_RUNTIME)}"></script>
130
+ </head>
131
+ <body>
132
+ <deck-root${transitionAttr}>
133
+ ${slidesHtml}
134
+ </deck-root>
135
+ </body>
136
+ </html>
137
+ `;
138
+
139
+ // A href that does not follow the `rikiki/…` spelling serves fine but will
140
+ // not inline · saying so here beats a silent surprise at bundle time.
141
+ const hrefs = [config.theme ?? DEFAULT_THEME, config.bundle ?? DEFAULT_RUNTIME];
142
+ const unbundleable = hrefs.filter((href) => !/(^|\/)rikiki\//.test(href));
143
+
144
+ if (outputPath === '-') {
145
+ return { html, outputPath: null, slides: config.slides.length, unbundleable };
146
+ }
147
+
148
+ const absOutput = outputPath
149
+ ? resolve(process.cwd(), outputPath)
150
+ : defaultOutputPath(absConfig, config.title);
151
+ mkdirSync(dirname(absOutput), { recursive: true });
152
+ writeFileSync(absOutput, html);
153
+ return { html, outputPath: absOutput, slides: config.slides.length, unbundleable };
154
+ }
@@ -0,0 +1,66 @@
1
+ // ════════════════════════════════════════════════════════════════
2
+ // Does a painted box leave its container, or land on top of a sibling?
3
+ //
4
+ // Pure rect arithmetic, kept out of `check.mjs` for the same reason as
5
+ // `graph-hit.mjs`: the answer is the whole diagnostic, so the thresholds
6
+ // belong beside tests that pin them, not scattered through the DOM walk that
7
+ // calls them.
8
+ //
9
+ // The functions are also serialized into the browser by `check.mjs` (see
10
+ // BOX_GEOMETRY_READER), so they must stay self-contained: no imports, no
11
+ // closure over anything in this module.
12
+ // ════════════════════════════════════════════════════════════════
13
+
14
+ /**
15
+ * How far `rect` sits outside `container`, per side and at its worst side.
16
+ *
17
+ * Only the sides where `rect` actually spills are counted · a box that sits
18
+ * fully inside its container reports zero on every side, whatever slack it
19
+ * leaves.
20
+ */
21
+ export function escapeOf(rect, container) {
22
+ const sides = {
23
+ left: Math.max(0, container.left - rect.left),
24
+ right: Math.max(0, rect.right - container.right),
25
+ top: Math.max(0, container.top - rect.top),
26
+ bottom: Math.max(0, rect.bottom - container.bottom),
27
+ };
28
+ return { pixels: Math.max(sides.left, sides.right, sides.top, sides.bottom), sides };
29
+ }
30
+
31
+ /**
32
+ * How much two rects intersect, on each axis.
33
+ *
34
+ * A negative value means the rects do not touch on that axis at all · the
35
+ * caller compares both axes against its own tolerance rather than this
36
+ * function deciding what counts as an overlap.
37
+ */
38
+ export function overlapOf(a, b) {
39
+ return {
40
+ x: Math.min(a.right, b.right) - Math.max(a.left, b.left),
41
+ y: Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Does `outer` fully contain `inner`, within `tolerance` px on each side?
47
+ *
48
+ * The tolerance absorbs sub-pixel layout rounding · two rects that agree to
49
+ * the pixel should not read as "neither encloses the other".
50
+ */
51
+ export function encloses(outer, inner, tolerance = 1) {
52
+ return (
53
+ outer.left <= inner.left + tolerance &&
54
+ outer.right >= inner.right - tolerance &&
55
+ outer.top <= inner.top + tolerance &&
56
+ outer.bottom >= inner.bottom - tolerance
57
+ );
58
+ }
59
+
60
+ /** The same three functions, as source, for `page.evaluate` · the inspector
61
+ * runs in the browser and cannot import from here. */
62
+ export const BOX_GEOMETRY_READER = `({
63
+ escapeOf: ${escapeOf},
64
+ overlapOf: ${overlapOf},
65
+ encloses: ${encloses},
66
+ })`;