rikiki-deck 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/rikiki-debug/SKILL.md +17 -7
- package/.claude/skills/rikiki-deck/SKILL.md +362 -77
- package/.claude/skills/rikiki-theme/SKILL.md +1 -1
- package/README.md +69 -50
- package/bin/lib/assemble.mjs +154 -0
- package/bin/lib/box-geometry.mjs +66 -0
- package/bin/lib/browser.mjs +302 -0
- package/bin/lib/check-api.d.ts +28 -0
- package/bin/lib/check-api.mjs +6 -0
- package/bin/lib/check-plugins.mjs +228 -0
- package/bin/lib/check.mjs +1347 -0
- package/bin/lib/cli-error.mjs +26 -0
- package/bin/lib/component-deps.mjs +69 -0
- package/bin/lib/diff.mjs +275 -0
- package/bin/lib/export-pdf.mjs +65 -0
- package/bin/lib/graph-hit.mjs +86 -0
- package/bin/lib/inline.mjs +137 -39
- package/bin/lib/narrative.mjs +77 -0
- package/bin/lib/prune-icons.mjs +104 -0
- package/bin/lib/render.mjs +195 -0
- package/bin/lib/scan-external.mjs +126 -0
- package/bin/lib/starter.mjs +27 -14
- package/bin/lib/visual.mjs +120 -0
- package/bin/rikiki.mjs +420 -35
- package/dist/annotation-marks.d.ts +60 -0
- package/dist/annotation-marks.js +1 -0
- package/dist/bar-segments.d.ts +28 -0
- package/dist/bar-segments.js +1 -0
- package/dist/browser-location.d.ts +3 -0
- package/dist/browser-location.js +1 -0
- package/dist/cards-syntax.d.ts +31 -0
- package/dist/cards-syntax.js +6 -0
- package/dist/{plugins/click-stages.d.ts → click-stages.d.ts} +1 -1
- package/dist/deck-agenda.d.ts +25 -0
- package/dist/deck-agenda.js +6 -0
- package/dist/deck-annotate.d.ts +108 -0
- package/dist/deck-annotate.js +18 -0
- package/dist/deck-bar.d.ts +32 -0
- package/dist/deck-bar.js +19 -0
- package/dist/deck-bento.d.ts +38 -0
- package/dist/deck-bento.js +4 -0
- package/dist/{molecules/deck-callout.d.ts → deck-callout.d.ts} +2 -0
- package/dist/deck-callout.js +1 -1
- package/dist/deck-cell.d.ts +19 -0
- package/dist/deck-cell.js +1 -0
- package/dist/deck-checklist.d.ts +20 -0
- package/dist/deck-checklist.js +1 -0
- package/dist/{layouts/deck-cover.d.ts → deck-cover.d.ts} +8 -0
- package/dist/deck-cover.js +9 -6
- package/dist/deck-csv.d.ts +38 -0
- package/dist/deck-csv.js +15 -0
- package/dist/deck-feature-cards.js +2 -2
- package/dist/deck-feature.d.ts +18 -0
- package/dist/deck-feature.js +2 -2
- package/dist/deck-figure.d.ts +26 -0
- package/dist/deck-figure.js +8 -0
- package/dist/deck-fit.d.ts +14 -0
- package/dist/deck-fit.js +1 -0
- package/dist/deck-flow.d.ts +41 -0
- package/dist/deck-flow.js +7 -0
- package/dist/deck-graph.d.ts +92 -0
- package/dist/deck-graph.js +25 -0
- package/dist/deck-grid.js +1 -1
- package/dist/deck-icon.d.ts +20 -0
- package/dist/deck-icon.js +1 -0
- package/dist/{atoms/deck-kicker.d.ts → deck-kicker.d.ts} +5 -0
- package/dist/deck-kicker.js +1 -1
- package/dist/deck-kpi-grid.d.ts +26 -0
- package/dist/deck-kpi-grid.js +4 -0
- package/dist/deck-link.d.ts +21 -0
- package/dist/deck-link.js +1 -0
- package/dist/{molecules/deck-md.d.ts → deck-md.d.ts} +3 -0
- package/dist/deck-md.js +8 -3
- package/dist/deck-mermaid.js +15 -3
- package/dist/deck-outline.d.ts +50 -0
- package/dist/deck-outline.js +1 -0
- package/dist/deck-overview.js +53 -39
- package/dist/deck-persona.d.ts +31 -0
- package/dist/deck-persona.js +6 -0
- package/dist/deck-photo.js +1 -1
- package/dist/deck-point.d.ts +22 -0
- package/dist/deck-point.js +1 -0
- package/dist/deck-presenter.js +120 -48
- package/dist/deck-pull.d.ts +13 -0
- package/dist/deck-pull.js +1 -0
- package/dist/{atoms/deck-punch.d.ts → deck-punch.d.ts} +6 -0
- package/dist/deck-punch.js +1 -1
- package/dist/deck-quote.d.ts +28 -0
- package/dist/deck-quote.js +6 -0
- package/dist/{runtime/deck-root.d.ts → deck-root.d.ts} +88 -8
- package/dist/deck-root.js +17 -13
- package/dist/deck-section.js +2 -2
- package/dist/deck-source.d.ts +12 -0
- package/dist/deck-source.js +2 -0
- package/dist/deck-split.d.ts +30 -0
- package/dist/deck-split.js +5 -3
- package/dist/{molecules/deck-stat.d.ts → deck-stat.d.ts} +2 -0
- package/dist/deck-stat.js +2 -2
- package/dist/{molecules/deck-step-list.d.ts → deck-step-list.d.ts} +8 -0
- package/dist/deck-step-list.js +4 -2
- package/dist/deck-table.d.ts +26 -0
- package/dist/deck-table.js +1 -0
- package/dist/deck-takeaway.d.ts +18 -0
- package/dist/deck-takeaway.js +2 -2
- package/dist/deck-timeline.d.ts +37 -0
- package/dist/deck-timeline.js +5 -0
- package/dist/deck-transition.js +3 -3
- package/dist/deck-versus.d.ts +18 -0
- package/dist/deck-versus.js +9 -0
- package/dist/deep-link.d.ts +29 -0
- package/dist/deep-link.js +1 -0
- package/dist/escape-html.d.ts +3 -0
- package/dist/escape-html.js +1 -0
- package/dist/fit-controller.d.ts +27 -0
- package/dist/fit-controller.js +1 -0
- package/dist/graph-layout.d.ts +35 -0
- package/dist/graph-layout.js +1 -0
- package/dist/grid-tracks.d.ts +17 -0
- package/dist/grid-tracks.js +1 -0
- package/dist/icon-set.d.ts +6 -0
- package/dist/icon-set.js +1 -0
- package/dist/index.d.ts +37 -31
- package/dist/index.js +95 -49
- package/dist/keymap.d.ts +40 -0
- package/dist/keymap.js +1 -0
- package/dist/mouse-nav.d.ts +12 -0
- package/dist/mouse-nav.js +1 -0
- package/dist/navigation.d.ts +25 -0
- package/dist/navigation.js +1 -0
- package/dist/parse-csv.d.ts +9 -0
- package/dist/parse-csv.js +3 -0
- package/dist/shared-styles.js +1 -1
- package/dist/shiki.d.ts +8 -0
- package/dist/signature.d.ts +2 -0
- package/dist/signature.js +1 -0
- package/dist/slide-fill.d.ts +8 -0
- package/dist/slide-fill.js +1 -0
- package/dist/standalone.js +301 -169
- package/dist/vendor/THIRD-PARTY-NOTICES.txt +4347 -0
- package/dist/vendor/inventory.json +3029 -0
- package/dist/vendor/lit.js +62 -2
- package/dist/vendor/mermaid.min.js +95 -95
- package/dist/vendor/shiki.js +1 -57
- package/dist/viewport.d.ts +42 -0
- package/dist/viewport.js +1 -0
- package/docs/llms/rikiki-reference.md +955 -64
- package/docs/llms/rikiki-workflow.md +536 -0
- package/llms.txt +39 -12
- package/package.json +33 -12
- package/themes/rikiki.css +173 -47
- package/themes/siliceum.css +171 -51
- package/dist/layouts/deck-feature.d.ts +0 -11
- package/dist/layouts/deck-split.d.ts +0 -18
- package/dist/layouts/deck-takeaway.d.ts +0 -11
- package/dist/plugins/shiki.d.ts +0 -8
- /package/dist/{runtime/color.d.ts → color.d.ts} +0 -0
- /package/dist/{atoms/deck-badge.d.ts → deck-badge.d.ts} +0 -0
- /package/dist/{molecules/deck-card.d.ts → deck-card.d.ts} +0 -0
- /package/dist/{atoms/deck-code-highlighter.d.ts → deck-code-highlighter.d.ts} +0 -0
- /package/dist/{atoms/deck-code.d.ts → deck-code.d.ts} +0 -0
- /package/dist/{layouts/deck-feature-cards.d.ts → deck-feature-cards.d.ts} +0 -0
- /package/dist/{molecules/deck-grid.d.ts → deck-grid.d.ts} +0 -0
- /package/dist/{runtime/deck-help.d.ts → deck-help.d.ts} +0 -0
- /package/dist/{molecules/deck-mermaid.d.ts → deck-mermaid.d.ts} +0 -0
- /package/dist/{molecules/deck-metric.d.ts → deck-metric.d.ts} +0 -0
- /package/dist/{runtime/deck-notes.d.ts → deck-notes.d.ts} +0 -0
- /package/dist/{runtime/deck-overview.d.ts → deck-overview.d.ts} +0 -0
- /package/dist/{layouts/deck-photo.d.ts → deck-photo.d.ts} +0 -0
- /package/dist/{runtime/deck-presenter.d.ts → deck-presenter.d.ts} +0 -0
- /package/dist/{layouts/deck-section.d.ts → deck-section.d.ts} +0 -0
- /package/dist/{molecules/deck-shortcut.d.ts → deck-shortcut.d.ts} +0 -0
- /package/dist/{molecules/deck-stack.d.ts → deck-stack.d.ts} +0 -0
- /package/dist/{molecules/deck-tier-list.d.ts → deck-tier-list.d.ts} +0 -0
- /package/dist/{runtime/deck-transition.d.ts → deck-transition.d.ts} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════════════
|
|
2
|
+
// scanExternal · everything a supposedly self-contained deck could still
|
|
3
|
+
// fetch at runtime.
|
|
4
|
+
//
|
|
5
|
+
// The previous check only matched absolute URLs from a hardcoded domain list,
|
|
6
|
+
// so a bundle that reached for `./vendor/mermaid.min.js` reported success and
|
|
7
|
+
// rendered an empty diagram offline. This one works the other way round: it
|
|
8
|
+
// finds every reference the page can load by itself, then subtracts the ones
|
|
9
|
+
// that are genuinely inert (data:, blob:, in-page fragments, user-facing links).
|
|
10
|
+
//
|
|
11
|
+
// Scripts are scanned separately and narrowly · minified JS is full of strings
|
|
12
|
+
// that look like markup (`href:n.href`, `"http://"`), and a whole-file regex
|
|
13
|
+
// reports those on every clean bundle.
|
|
14
|
+
// ════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
/** Attributes through which an element loads something on its own. */
|
|
17
|
+
const LOADING_ATTRS = ['src', 'srcset', 'poster', 'href'];
|
|
18
|
+
|
|
19
|
+
/** `href` only loads for these elements · on `<a>` and `<use>` it is navigation. */
|
|
20
|
+
const HREF_LOADS_ON = /^(?:link|image|script|iframe|embed|track|source)$/i;
|
|
21
|
+
|
|
22
|
+
const isInert = (ref) =>
|
|
23
|
+
!ref ||
|
|
24
|
+
ref.startsWith('data:') ||
|
|
25
|
+
ref.startsWith('blob:') ||
|
|
26
|
+
ref.startsWith('#') ||
|
|
27
|
+
ref.startsWith('mailto:') ||
|
|
28
|
+
ref.startsWith('tel:') ||
|
|
29
|
+
ref.startsWith('javascript:');
|
|
30
|
+
|
|
31
|
+
/** Keep every opening tag, drop the text inside <script> and <style>.
|
|
32
|
+
* Their bodies are code, not markup · a `<link …>` quoted in a CSS comment or
|
|
33
|
+
* a JS string is documentation, not a resource the page loads. */
|
|
34
|
+
function stripCodeBodies(html) {
|
|
35
|
+
return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, (m, tag) => {
|
|
36
|
+
const open = m.match(new RegExp(`<${tag}\\b[^>]*>`, 'i'));
|
|
37
|
+
return open ? `${open[0]}</${tag}>` : '';
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scriptBodies(html) {
|
|
42
|
+
return [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function styleBodies(html) {
|
|
46
|
+
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scanTags(html, out) {
|
|
50
|
+
for (const tag of html.matchAll(/<([a-zA-Z][\w-]*)\b([^>]*)>/g)) {
|
|
51
|
+
const name = tag[1];
|
|
52
|
+
const attrs = tag[2] ?? '';
|
|
53
|
+
for (const attr of LOADING_ATTRS) {
|
|
54
|
+
// Quoted first, then unquoted · the inliner only ever handled quoted
|
|
55
|
+
// values, which is how an unquoted src survived it.
|
|
56
|
+
const re = new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, 'gi');
|
|
57
|
+
for (const m of attrs.matchAll(re)) {
|
|
58
|
+
const ref = (m[1] ?? m[2] ?? m[3] ?? '').trim();
|
|
59
|
+
if (isInert(ref)) continue;
|
|
60
|
+
if (attr === 'href' && !HREF_LOADS_ON.test(name)) continue;
|
|
61
|
+
out.push({ kind: 'attribute', element: name.toLowerCase(), attr, ref });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scanCss(rawCss, out) {
|
|
68
|
+
// A commented-out rule is not a dependency · tokens.css documents an
|
|
69
|
+
// alternative <link> inside its header comment.
|
|
70
|
+
const css = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
71
|
+
for (const m of css.matchAll(/@import\s+(?:url\(\s*)?["']?([^;"')]+)/g)) {
|
|
72
|
+
const ref = m[1].trim();
|
|
73
|
+
if (!isInert(ref)) out.push({ kind: 'css-import', ref });
|
|
74
|
+
}
|
|
75
|
+
// url() outside an @import · @import matches are removed first so a single
|
|
76
|
+
// reference is not reported twice.
|
|
77
|
+
const withoutImports = css.replace(/@import[^;]*;?/g, '');
|
|
78
|
+
for (const m of withoutImports.matchAll(/url\(\s*["']?([^)"']+)["']?\s*\)/g)) {
|
|
79
|
+
const ref = m[1].trim();
|
|
80
|
+
if (!isInert(ref)) out.push({ kind: 'css-url', ref });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scanScript(js, out) {
|
|
85
|
+
// Only constructs that actually fetch · a bare string in minified code does not.
|
|
86
|
+
for (const m of js.matchAll(/new URL\(\s*["'`]([^"'`]+)["'`]\s*,\s*import\.meta\.url/g)) {
|
|
87
|
+
if (!isInert(m[1])) out.push({ kind: 'module-url', ref: m[1] });
|
|
88
|
+
}
|
|
89
|
+
for (const m of js.matchAll(/\bimport\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
|
|
90
|
+
if (!isInert(m[1])) out.push({ kind: 'dynamic-import', ref: m[1] });
|
|
91
|
+
}
|
|
92
|
+
// A static import specifier is never a template literal, and the statement
|
|
93
|
+
// starts a line or follows a `;`/`}` · without both constraints, prose inside
|
|
94
|
+
// a library's own error message ("import `x` from `y`") reads as an import.
|
|
95
|
+
for (const m of js.matchAll(/(?:^|[;}])\s*import\s+[^;'"`]*?from\s*["']([^"']+)["']/gm)) {
|
|
96
|
+
if (!isInert(m[1])) out.push({ kind: 'static-import', ref: m[1] });
|
|
97
|
+
}
|
|
98
|
+
for (const m of js.matchAll(/importScripts\(\s*["'`]([^"'`]+)["'`]/g)) {
|
|
99
|
+
if (!isInert(m[1])) out.push({ kind: 'worker-import', ref: m[1] });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every runtime reference that would leave the file, deduplicated by ref.
|
|
104
|
+
* An empty array is the only proof that a bundle is self-contained. */
|
|
105
|
+
export function scanExternal(html) {
|
|
106
|
+
const out = [];
|
|
107
|
+
const markup = stripCodeBodies(html);
|
|
108
|
+
scanTags(markup, out);
|
|
109
|
+
// Style bodies come from the ORIGINAL html · markup has had them emptied.
|
|
110
|
+
for (const css of styleBodies(html)) scanCss(css, out);
|
|
111
|
+
for (const js of scriptBodies(html)) scanScript(js, out);
|
|
112
|
+
// Inline style="" attributes can carry url() too.
|
|
113
|
+
for (const m of markup.matchAll(/\bstyle\s*=\s*"([^"]*)"/gi)) scanCss(m[1], out);
|
|
114
|
+
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
return out.filter((hit) => {
|
|
117
|
+
if (seen.has(hit.ref)) return false;
|
|
118
|
+
seen.add(hit.ref);
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One line per hit, ready to print. */
|
|
124
|
+
export function formatExternal(hits) {
|
|
125
|
+
return hits.map((h) => ` · [${h.kind}] ${h.ref}`).join('\n');
|
|
126
|
+
}
|
package/bin/lib/starter.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// ════════════════════════════════════════════════════════════════
|
|
2
|
-
// Starter deck template for `rikiki init`. Returns plain deck HTML
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Starter deck template for `rikiki init`. Returns plain deck HTML.
|
|
3
|
+
//
|
|
4
|
+
// `assetBase` decides which of the two shapes it takes:
|
|
5
|
+
// '' · refs resolve against the rikiki package root · the inliner
|
|
6
|
+
// then folds everything into one self-contained file.
|
|
7
|
+
// 'rikiki/' · refs point at the runtime copied next to the deck · the file
|
|
8
|
+
// stays a readable source you serve over HTTP and keep editing.
|
|
9
|
+
// Both are bundle-able: the inliner maps a `rikiki/…` ref back to the package.
|
|
5
10
|
// ════════════════════════════════════════════════════════════════
|
|
6
11
|
|
|
7
12
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
@@ -13,21 +18,23 @@ const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
|
13
18
|
* @param {'rikiki'|'siliceum'} o.theme
|
|
14
19
|
* @param {boolean} o.withMermaid include + preload the mermaid runtime
|
|
15
20
|
* @param {boolean} o.withShiki include + activate the Shiki highlighter
|
|
21
|
+
* @param {string} o.assetBase prefix every asset ref ('' or 'rikiki/')
|
|
16
22
|
*/
|
|
17
|
-
export function starterHtml({ title = 'My deck', theme = 'rikiki', withMermaid = false, withShiki = false } = {}) {
|
|
18
|
-
const
|
|
23
|
+
export function starterHtml({ title = 'My deck', theme = 'rikiki', withMermaid = false, withShiki = false, assetBase = '' } = {}) {
|
|
24
|
+
const asset = (path) => assetBase + path;
|
|
25
|
+
const themeHref = asset(theme === 'siliceum' ? 'themes/siliceum.css' : 'tokens.css');
|
|
19
26
|
|
|
20
27
|
// Heavy plugins · injected as refs the inliner folds in. mermaid's UMD sets
|
|
21
28
|
// window.mermaid (deck-mermaid then skips its network load); the shiki module
|
|
22
29
|
// exposes the vendored highlighter as a global and activates it.
|
|
23
30
|
const mermaidTag = withMermaid
|
|
24
|
-
?
|
|
31
|
+
? `<script src="${asset('dist/vendor/mermaid.min.js')}"></script>\n`
|
|
25
32
|
: '';
|
|
26
33
|
const shikiTag = withShiki
|
|
27
34
|
? `<script type="module">
|
|
28
|
-
import { createHighlighter } from '
|
|
35
|
+
import { createHighlighter } from './${asset('dist/vendor/shiki.js')}';
|
|
29
36
|
globalThis.__rikikiShiki = createHighlighter;
|
|
30
|
-
import { installShiki } from '
|
|
37
|
+
import { installShiki } from './${asset('dist/shiki.js')}';
|
|
31
38
|
await installShiki();
|
|
32
39
|
</script>\n`
|
|
33
40
|
: '';
|
|
@@ -49,7 +56,7 @@ await installShiki();
|
|
|
49
56
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
50
57
|
<title>${esc(title)}</title>
|
|
51
58
|
<link rel="stylesheet" href="${themeHref}">
|
|
52
|
-
${mermaidTag}${shikiTag}<script type="module" src="dist/index.js"></script>
|
|
59
|
+
${mermaidTag}${shikiTag}<script type="module" src="${asset('dist/index.js')}"></script>
|
|
53
60
|
</head>
|
|
54
61
|
<body>
|
|
55
62
|
|
|
@@ -57,23 +64,29 @@ ${mermaidTag}${shikiTag}<script type="module" src="dist/index.js"></script>
|
|
|
57
64
|
|
|
58
65
|
<deck-cover brand="rikiki" speaker="Your name" duration="~10 min" audience="Your audience">
|
|
59
66
|
<h1>${esc(title)} <span class="accent">deck</span></h1>
|
|
60
|
-
<p class="sub"
|
|
67
|
+
<p class="sub">${assetBase ? 'An editable deck · one HTML file you own.' : 'A self-contained, shareable slide deck.'}</p>
|
|
61
68
|
</deck-cover>
|
|
62
69
|
|
|
63
70
|
<deck-feature eyebrow="Start here">
|
|
64
71
|
<h1 slot="title">Edit <span class="accent">this file</span></h1>
|
|
65
72
|
<deck-md>
|
|
66
|
-
|
|
73
|
+
${assetBase
|
|
74
|
+
? `This deck is **plain HTML** you edit by hand.
|
|
75
|
+
|
|
76
|
+
- each \`<deck-*>\` element is a slide
|
|
77
|
+
- the runtime sits in \`${assetBase.replace(/\/$/, '')}/\` next to this file
|
|
78
|
+
- serve the folder over HTTP, then \`rikiki bundle\` it to share one file`
|
|
79
|
+
: `This whole deck is **one HTML file** with zero external links.
|
|
67
80
|
|
|
68
81
|
- open it anywhere, offline
|
|
69
|
-
- each
|
|
70
|
-
- press **?** for keyboard shortcuts
|
|
82
|
+
- each \`<deck-*>\` element is a slide
|
|
83
|
+
- press **?** for keyboard shortcuts`}
|
|
71
84
|
</deck-md>
|
|
72
85
|
</deck-feature>
|
|
73
86
|
${mermaidSlide}
|
|
74
87
|
<deck-takeaway>
|
|
75
88
|
<h1>Ship it</h1>
|
|
76
|
-
<p
|
|
89
|
+
<p>${assetBase ? 'Write, check, bundle, share.' : 'One file. No network. Share it.'}</p>
|
|
77
90
|
</deck-takeaway>
|
|
78
91
|
|
|
79
92
|
</deck-root>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════════════
|
|
2
|
+
// What a slide looks like, measured on the pixels rather than on the DOM.
|
|
3
|
+
//
|
|
4
|
+
// The DOM says a box is 400px tall. It does not say the slide reads as
|
|
5
|
+
// top-heavy, or that the ink sits in one corner. Those are pixel facts, and
|
|
6
|
+
// they are the ones a room reacts to first.
|
|
7
|
+
//
|
|
8
|
+
// The signals here follow the low-level design cues that correlate with human
|
|
9
|
+
// judgement of slide quality (Inui et al., 2025): ink density and spatial
|
|
10
|
+
// balance carry most of it. We measure only what is objective and act only on
|
|
11
|
+
// the imbalance · empty space on its own is a choice, never a defect.
|
|
12
|
+
// ════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
/** Runs in the page: decode a screenshot and measure where the ink sits. */
|
|
15
|
+
const MEASURE_INK = `async (dataUrl) => {
|
|
16
|
+
const img = new Image();
|
|
17
|
+
img.src = dataUrl;
|
|
18
|
+
await img.decode();
|
|
19
|
+
|
|
20
|
+
// Downscale · the layout facts survive, and a 240-wide grid is 100x cheaper
|
|
21
|
+
// to walk than the full canvas.
|
|
22
|
+
const w = 240;
|
|
23
|
+
const h = Math.max(1, Math.round((img.height / img.width) * w));
|
|
24
|
+
const canvas = document.createElement('canvas');
|
|
25
|
+
canvas.width = w;
|
|
26
|
+
canvas.height = h;
|
|
27
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
28
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
29
|
+
const { data } = ctx.getImageData(0, 0, w, h);
|
|
30
|
+
|
|
31
|
+
const at = (x, y) => {
|
|
32
|
+
const i = (y * w + x) * 4;
|
|
33
|
+
return [data[i], data[i + 1], data[i + 2]];
|
|
34
|
+
};
|
|
35
|
+
// The background is the most common colour · a deck paints its own ground.
|
|
36
|
+
const tally = new Map();
|
|
37
|
+
for (let y = 0; y < h; y++) {
|
|
38
|
+
for (let x = 0; x < w; x++) {
|
|
39
|
+
const key = at(x, y).map((c) => c >> 4).join(',');
|
|
40
|
+
tally.set(key, (tally.get(key) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const ground = [...tally.entries()].sort((a, b) => b[1] - a[1])[0][0]
|
|
44
|
+
.split(',').map((c) => (Number(c) << 4) + 8);
|
|
45
|
+
|
|
46
|
+
const far = (p) =>
|
|
47
|
+
Math.abs(p[0] - ground[0]) + Math.abs(p[1] - ground[1]) + Math.abs(p[2] - ground[2]) > 40;
|
|
48
|
+
|
|
49
|
+
let inked = 0;
|
|
50
|
+
let sumY = 0;
|
|
51
|
+
let sumX = 0;
|
|
52
|
+
let top = h;
|
|
53
|
+
let bottom = -1;
|
|
54
|
+
let left = w;
|
|
55
|
+
let right = -1;
|
|
56
|
+
const rowInk = new Array(h).fill(0);
|
|
57
|
+
for (let y = 0; y < h; y++) {
|
|
58
|
+
for (let x = 0; x < w; x++) {
|
|
59
|
+
if (!far(at(x, y))) continue;
|
|
60
|
+
inked++;
|
|
61
|
+
rowInk[y]++;
|
|
62
|
+
sumY += y;
|
|
63
|
+
sumX += x;
|
|
64
|
+
if (y < top) top = y;
|
|
65
|
+
if (y > bottom) bottom = y;
|
|
66
|
+
if (x < left) left = x;
|
|
67
|
+
if (x > right) right = x;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (inked === 0) return { inkRatio: 0, empty: true };
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
empty: false,
|
|
74
|
+
inkRatio: inked / (w * h),
|
|
75
|
+
// 0 = ink centred on the slide, negative = pulled up, positive = pulled down.
|
|
76
|
+
verticalBias: (sumY / inked) / h - 0.5,
|
|
77
|
+
horizontalBias: (sumX / inked) / w - 0.5,
|
|
78
|
+
// The band under the last content row, as a share of the slide height.
|
|
79
|
+
tailBand: 1 - (bottom + 1) / h,
|
|
80
|
+
headBand: top / h,
|
|
81
|
+
box: { top: top / h, bottom: bottom / h, left: left / w, right: right / w },
|
|
82
|
+
};
|
|
83
|
+
}`;
|
|
84
|
+
|
|
85
|
+
/** The engine's own furniture · counted as ink, it makes every slide look
|
|
86
|
+
* bottom-anchored. Hidden for the measurement, restored after. */
|
|
87
|
+
const HIDE_CHROME = `() => {
|
|
88
|
+
const root = document.querySelector('deck-root');
|
|
89
|
+
if (!root?.shadowRoot) return;
|
|
90
|
+
const style = document.createElement('style');
|
|
91
|
+
style.id = 'rik-measure-chrome';
|
|
92
|
+
style.textContent =
|
|
93
|
+
'#progress,#counter,#step-dots,#nav-arrows,#kb-hint,#live{display:none !important}';
|
|
94
|
+
root.shadowRoot.appendChild(style);
|
|
95
|
+
}`;
|
|
96
|
+
|
|
97
|
+
const SHOW_CHROME = `() => {
|
|
98
|
+
document.querySelector('deck-root')?.shadowRoot?.getElementById('rik-measure-chrome')?.remove();
|
|
99
|
+
}`;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Photograph each slide and measure where its ink sits.
|
|
103
|
+
* `goTo(index)` must leave the page on that slide, settled.
|
|
104
|
+
*/
|
|
105
|
+
export async function measureSlides(page, count, goTo) {
|
|
106
|
+
await page.evaluate((fn) => new Function('return ' + fn)()(), HIDE_CHROME);
|
|
107
|
+
const out = [];
|
|
108
|
+
for (let index = 1; index <= count; index++) {
|
|
109
|
+
await goTo(index);
|
|
110
|
+
const shot = await page.screenshot({ type: 'png' });
|
|
111
|
+
const dataUrl = 'data:image/png;base64,' + shot.toString('base64');
|
|
112
|
+
const ink = await page.evaluate(
|
|
113
|
+
({ fn, url }) => new Function('return ' + fn)()(url),
|
|
114
|
+
{ fn: MEASURE_INK, url: dataUrl },
|
|
115
|
+
);
|
|
116
|
+
out.push({ index, ...ink });
|
|
117
|
+
}
|
|
118
|
+
await page.evaluate((fn) => new Function('return ' + fn)()(), SHOW_CHROME);
|
|
119
|
+
return out;
|
|
120
|
+
}
|