rikiki-deck 0.6.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 (144) hide show
  1. package/.claude/skills/rikiki-debug/SKILL.md +17 -7
  2. package/.claude/skills/rikiki-deck/SKILL.md +362 -77
  3. package/.claude/skills/rikiki-theme/SKILL.md +1 -1
  4. package/README.md +69 -50
  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 +375 -28
  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-punch.d.ts +6 -0
  25. package/dist/atoms/deck-source.d.ts +12 -0
  26. package/dist/bar-segments.js +1 -0
  27. package/dist/browser-location.js +1 -0
  28. package/dist/cards-syntax.js +6 -0
  29. package/dist/contrast.js +1 -0
  30. package/dist/deck-agenda.js +6 -0
  31. package/dist/deck-annotate.js +18 -0
  32. package/dist/deck-bar.js +19 -0
  33. package/dist/deck-bento.js +4 -0
  34. package/dist/deck-callout.js +1 -1
  35. package/dist/deck-cell.js +1 -0
  36. package/dist/deck-checklist.js +1 -0
  37. package/dist/deck-cover.js +6 -6
  38. package/dist/deck-csv.js +15 -0
  39. package/dist/deck-feature-cards.js +2 -2
  40. package/dist/deck-feature.js +2 -2
  41. package/dist/deck-figure.js +8 -0
  42. package/dist/deck-fit.js +1 -0
  43. package/dist/deck-flow.js +7 -0
  44. package/dist/deck-graph.js +25 -0
  45. package/dist/deck-grid.js +1 -1
  46. package/dist/deck-icon.js +1 -0
  47. package/dist/deck-kpi-grid.js +4 -0
  48. package/dist/deck-link.js +1 -0
  49. package/dist/deck-md.js +8 -3
  50. package/dist/deck-mermaid.js +15 -3
  51. package/dist/deck-outline.js +1 -0
  52. package/dist/deck-overview.js +15 -2
  53. package/dist/deck-persona.js +6 -0
  54. package/dist/deck-photo.js +1 -1
  55. package/dist/deck-point.js +1 -0
  56. package/dist/deck-presenter.js +43 -14
  57. package/dist/deck-pull.js +1 -0
  58. package/dist/deck-punch.js +1 -1
  59. package/dist/deck-quote.js +6 -0
  60. package/dist/deck-root.js +17 -13
  61. package/dist/deck-section.js +2 -2
  62. package/dist/deck-source.js +2 -0
  63. package/dist/deck-split.js +5 -3
  64. package/dist/deck-stat.js +2 -2
  65. package/dist/deck-step-list.js +4 -2
  66. package/dist/deck-table.js +1 -0
  67. package/dist/deck-takeaway.js +2 -2
  68. package/dist/deck-timeline.js +5 -0
  69. package/dist/deck-transition.js +3 -3
  70. package/dist/deck-versus.js +9 -0
  71. package/dist/deep-link.js +1 -0
  72. package/dist/domain/deck-link.d.ts +21 -0
  73. package/dist/domain/deck-outline.d.ts +50 -0
  74. package/dist/domain/navigation.d.ts +25 -0
  75. package/dist/domain/viewport.d.ts +42 -0
  76. package/dist/escape-html.js +1 -0
  77. package/dist/extras/deck-agenda.d.ts +25 -0
  78. package/dist/extras/deck-annotate.d.ts +108 -0
  79. package/dist/extras/deck-bar.d.ts +32 -0
  80. package/dist/extras/deck-checklist.d.ts +20 -0
  81. package/dist/extras/deck-figure.d.ts +26 -0
  82. package/dist/extras/deck-flow.d.ts +41 -0
  83. package/dist/extras/deck-graph.d.ts +92 -0
  84. package/dist/extras/deck-icon.d.ts +20 -0
  85. package/dist/extras/deck-kpi-grid.d.ts +26 -0
  86. package/dist/extras/deck-persona.d.ts +31 -0
  87. package/dist/extras/deck-pull.d.ts +13 -0
  88. package/dist/extras/deck-quote.d.ts +28 -0
  89. package/dist/extras/deck-table.d.ts +26 -0
  90. package/dist/extras/deck-timeline.d.ts +31 -0
  91. package/dist/extras/deck-versus.d.ts +18 -0
  92. package/dist/extras/signature.d.ts +2 -0
  93. package/dist/fit-controller.js +1 -0
  94. package/dist/graph-layout.js +1 -0
  95. package/dist/grid-tracks.js +1 -0
  96. package/dist/icon-set.js +1 -0
  97. package/dist/index.d.ts +6 -0
  98. package/dist/index.js +92 -49
  99. package/dist/infrastructure/browser-location.d.ts +3 -0
  100. package/dist/keymap.js +1 -0
  101. package/dist/layouts/deck-bento.d.ts +38 -0
  102. package/dist/layouts/deck-cover.d.ts +6 -0
  103. package/dist/layouts/deck-feature.d.ts +7 -0
  104. package/dist/layouts/deck-split.d.ts +12 -0
  105. package/dist/layouts/deck-takeaway.d.ts +7 -0
  106. package/dist/molecules/deck-callout.d.ts +2 -0
  107. package/dist/molecules/deck-cell.d.ts +19 -0
  108. package/dist/molecules/deck-csv.d.ts +38 -0
  109. package/dist/molecules/deck-fit.d.ts +14 -0
  110. package/dist/molecules/deck-md.d.ts +3 -0
  111. package/dist/molecules/deck-point.d.ts +22 -0
  112. package/dist/molecules/deck-stat.d.ts +2 -0
  113. package/dist/molecules/deck-step-list.d.ts +8 -0
  114. package/dist/mouse-nav.js +1 -0
  115. package/dist/navigation.js +1 -0
  116. package/dist/parse-csv.js +3 -0
  117. package/dist/plugins/shiki.d.ts +2 -2
  118. package/dist/runtime/deck-root.d.ts +74 -5
  119. package/dist/shared/annotation-marks.d.ts +60 -0
  120. package/dist/shared/bar-segments.d.ts +28 -0
  121. package/dist/shared/cards-syntax.d.ts +31 -0
  122. package/dist/shared/contrast.d.ts +52 -0
  123. package/dist/shared/escape-html.d.ts +3 -0
  124. package/dist/shared/fit-controller.d.ts +27 -0
  125. package/dist/shared/graph-layout.d.ts +35 -0
  126. package/dist/shared/grid-tracks.d.ts +17 -0
  127. package/dist/shared/icon-set.d.ts +6 -0
  128. package/dist/shared/parse-csv.d.ts +9 -0
  129. package/dist/shared/slide-fill.d.ts +8 -0
  130. package/dist/shared-styles.js +1 -1
  131. package/dist/signature.js +1 -0
  132. package/dist/slide-fill.js +1 -0
  133. package/dist/standalone.js +180 -95
  134. package/dist/vendor/THIRD-PARTY-NOTICES.txt +4347 -0
  135. package/dist/vendor/inventory.json +3029 -0
  136. package/dist/vendor/lit.js +62 -2
  137. package/dist/vendor/shiki.js +1 -57
  138. package/dist/viewport.js +1 -0
  139. package/docs/llms/rikiki-reference.md +927 -63
  140. package/docs/llms/rikiki-workflow.md +536 -0
  141. package/llms.txt +39 -12
  142. package/package.json +21 -7
  143. package/themes/rikiki.css +173 -47
  144. package/themes/siliceum.css +171 -51
package/README.md CHANGED
@@ -6,15 +6,48 @@
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.6.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.
21
+
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`).
18
51
 
19
52
  ## For LLMs / coding assistants
20
53
 
@@ -24,6 +57,10 @@ npm package:
24
57
 
25
58
  - **[`llms.txt`](./llms.txt)** — concise capability map and entry points (the
26
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.
27
64
  - **[`docs/llms/rikiki-reference.md`](./docs/llms/rikiki-reference.md)** — every
28
65
  tag, attribute, slot, design token, plugin, and recipe in one file. Have the
29
66
  assistant read this first; tell it not to invent tags or tokens outside it.
@@ -31,42 +68,30 @@ npm package:
31
68
  `.claude/skills/` (see [Claude Code skills](#claude-code-skills) to install
32
69
  them); they teach an assistant the authoring/theming/debugging workflows.
33
70
 
34
- ## Layout
71
+ ## What the install gives you
35
72
 
36
73
  ```
37
- rikiki/
74
+ node_modules/rikiki-deck/
38
75
  ├── tokens.css ← entry point · re-exports the default theme
39
76
  ├── themes/
40
77
  │ ├── rikiki.css ← default theme (acid greens + mango, on dark)
41
78
  │ ├── siliceum.css ← alternative theme (warm paper + yellow)
42
79
  │ └── siliceum-fonts.css ← self-hosted Source Sans Pro + JetBrains Mono
43
80
  ├── fonts/ ← woff2 files used by the Siliceum theme
44
- ├── src/ ← TypeScript sources · organised by DS bucket
45
- │ ├── index.ts ← registers every component
46
- │ ├── shared-styles.ts
47
- │ ├── livereload.ts
48
- │ ├── runtime/ ← deck-root, deck-help, deck-overview,
49
- │ │ deck-presenter, deck-transition, deck-notes
50
- │ ├── layouts/ ← deck-cover, deck-section, deck-feature,
51
- │ │ deck-split, deck-feature-cards, deck-takeaway,
52
- │ │ deck-photo
53
- │ ├── molecules/ ← deck-callout, deck-card, deck-md, deck-mermaid,
54
- │ │ deck-stat, deck-metric, deck-tier-list,
55
- │ │ deck-step-list, deck-shortcut, deck-stack,
56
- │ │ deck-grid
57
- │ ├── atoms/ ← deck-badge, deck-kicker, deck-punch, deck-code
58
- │ └── plugins/ ← opt-in (shiki for advanced syntax highlighting)
59
- ├── dist/ ← built output · FLAT regardless of src bucket
60
- │ (deck-root's dynamic imports rely on it)
61
- ├── build.mjs ← esbuild script
62
- ├── tsconfig.json
63
- └── 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`
64
86
  ```
65
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
+
66
91
  ## Three-layer styling
67
92
 
68
93
  1. **Theme tokens** (`themes/<name>.css`) at `:root` · custom properties cross the Shadow DOM, so they reach every component.
69
- 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.
70
95
  3. **Layout-specific CSS** · each component's own Shadow DOM.
71
96
 
72
97
  To re-theme: copy a theme file, change the values, that's it. All components follow.
@@ -92,10 +117,14 @@ To re-theme: copy a theme file, change the values, that's it. All components fol
92
117
  | `<deck-code lang="js" hero?>` | Code block with light syntax highlighting |
93
118
  | `<deck-callout type="info\|warn\|danger\|ok">` | Information callout |
94
119
  | `<deck-card color="yellow\|orange\|green\|red?">` | Tinted card |
95
- | `<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 |
96
123
 
97
124
  Plus `<deck-badge>`, `<deck-metric>`, `<deck-tier-list>`, `<deck-step-list>`, `<deck-kicker>`, `<deck-stack>`, `<deck-grid>`, `<deck-punch>`.
98
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
+
99
128
  ## Common patterns
100
129
 
101
130
  ### Slide with markdown + code
@@ -139,7 +168,7 @@ Plus `<deck-badge>`, `<deck-metric>`, `<deck-tier-list>`, `<deck-step-list>`, `<
139
168
 
140
169
  ## Markdown support (`<deck-md>`)
141
170
 
142
- 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, `---`.
143
172
 
144
173
  ## Navigation
145
174
 
@@ -233,37 +262,28 @@ deck-cover::part(brand) { font-family: 'Comic Sans'; }
233
262
 
234
263
  ### Add a layout
235
264
 
236
- 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).
237
268
 
238
- ## Build
269
+ ## No build step
239
270
 
240
- ```bash
241
- npm install
242
- npm run build # node build.mjs (esbuild) + tsc --emitDeclarationOnly
243
- npm run watch # esbuild watch mode
244
- npm run typecheck # tsc --noEmit
245
- ```
246
-
247
- `dist/` is versioned · consumers don't run a build.
271
+ The published runtime is plain ES modules. Nothing here needs compiling,
272
+ bundling or transpiling to author, serve or present a deck.
248
273
 
249
274
  ## Claude Code skills
250
275
 
251
276
  The package ships three Claude Code skills so an assistant authoring your deck
252
277
  knows the framework: `rikiki-deck` (build a deck), `rikiki-theme` (theming), and
253
- `rikiki-debug` (diagnose a deck). After `npm install rikiki-deck`, copy them into
254
- your project (or `~/.claude/skills` for all projects):
278
+ `rikiki-debug` (diagnose a deck).
255
279
 
256
280
  ```sh
257
- # project-local · available in this repo only
258
- mkdir -p .claude/skills
259
- cp -r node_modules/rikiki-deck/.claude/skills/* .claude/skills/
260
-
261
- # or global · available in every project
262
- cp -r node_modules/rikiki-deck/.claude/skills/* ~/.claude/skills/
281
+ npx rikiki skills # into ./.claude/skills/
282
+ npx rikiki skills --dir ~/.claude/skills # or once, for every project
263
283
  ```
264
284
 
265
- Claude Code discovers them automatically on the next session. Re-run the copy
266
- after `npm update rikiki-deck` to pick up skill changes.
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.
267
287
 
268
288
  ## Reveals & animations
269
289
 
@@ -271,8 +291,7 @@ Per-element click-through builds are an opt-in plugin (`installClickStages()` fr
271
291
  `dist/click-stages.js`): annotate elements with `data-click`, `data-click-hide`,
272
292
  `data-click-auto`, `data-click-stagger`, and `data-morph` (Keynote-style Magic
273
293
  Move via View Transitions). Slide transitions are driven by `transition="…"` on
274
- `<deck-root>`. See `docs/llms/rikiki-reference.md` §7 for the full attribute set,
275
- 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.
276
295
 
277
296
  ## Known limits
278
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
+ })`;