footprint-storydeck 0.7.0 → 0.8.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/CHANGELOG.md +11 -0
- package/README.md +11 -1
- package/dist/deck/check.js +11 -1
- package/dist/deck/compile.js +2 -1
- package/dist/deck/init.js +1 -0
- package/dist/deck/parse.js +10 -2
- package/dist/deck/review.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — a part can have its own look (data-theme)
|
|
4
|
+
|
|
5
|
+
- **A part can have its own look: `data-theme`.** `<section … data-theme="ledger">` in clicks.html puts
|
|
6
|
+
`data-theme="ledger"` on each slide of that part (plain or on a map), so a deck's theme.css can give one part its own
|
|
7
|
+
tokens and pieces — a talk in several parts, each dressed for its subject. A theme is one lower-case name (letters,
|
|
8
|
+
digits, dashes; 40 at most), refused otherwise with the part's name. A part without one is drawn byte for byte as
|
|
9
|
+
before.
|
|
10
|
+
The title and closing slides are parts, so they can have a look of their own. `check` warns (`theme-unstyled`) when a
|
|
11
|
+
part asks for a theme that theme.css never selects, and `review` ends a click's first line with its theme. The README
|
|
12
|
+
shows how the page around the slide follows the part on screen (`:root:has([data-deck-active][data-theme=…])`).
|
|
13
|
+
|
|
3
14
|
## 0.7.0 — a deck from a folder, an editing loop, co-editing and wire motion
|
|
4
15
|
|
|
5
16
|
- **A packet rides its wire's curve, and a wire shows itself in.** On a `path` click, a `pkt` packet now follows the
|
package/README.md
CHANGED
|
@@ -399,10 +399,20 @@ npx footprint-storydeck build my-talk # my-talk/index.html — one page, eve
|
|
|
399
399
|
ticks the line (`- [x]`). Pointing costs the person a second and the agent no screenshot. Each note is signed with a
|
|
400
400
|
key kept with you (`~/.storydeck/key`) for that deck's folder, so a feedback.md that came with someone else's deck is
|
|
401
401
|
never read as your requests: its lines are counted, never listed.
|
|
402
|
+
- **A part can have its own look.** `<section data-label="3 · Storage" … data-theme="ledger">` puts
|
|
403
|
+
`data-theme="ledger"` on each of that part's slides, so `theme.css` can dress it: redefine the tokens for it
|
|
404
|
+
(`[data-theme="ledger"] { --bg: …; --text: …; }`, and `:root.dark [data-theme="ledger"]` for dark) and give its pieces
|
|
405
|
+
their own style. Tokens inherit, so the chrome, the focus and every piece of that part follow. A theme is one
|
|
406
|
+
lower-case name (letters, digits and dashes); a part without one is drawn as before, byte for byte. The title and the
|
|
407
|
+
closing slide are parts too, so they can have their own look. To have the page around the slide (the letterbox)
|
|
408
|
+
follow the part on screen as well, select the active slide in the same rule:
|
|
409
|
+
`[data-theme="ledger"], :root:has([data-deck-active][data-theme="ledger"]) { … }`. `check` warns about a theme
|
|
410
|
+
that theme.css never selects (a misspelt name changes nothing), and `review` names each click's theme.
|
|
402
411
|
- **Read first, look last.** A reviewer — a person, or a model that pays for every screenshot — reads the deck as text:
|
|
403
412
|
`outline` (a line per click), `review --click 14` (its headline and words, what it lights, greys, brings in and moves,
|
|
404
413
|
its route, its notes), `check` (what to fix, each with its click and its fix: a headline over 12 words, pieces that
|
|
405
|
-
light just by coming in, a click that changes nothing, a piece never shown, clicks without notes
|
|
414
|
+
light just by coming in, a click that changes nothing, a piece never shown, clicks without notes, a theme theme.css
|
|
415
|
+
never styles). `check --layout`
|
|
406
416
|
opens the page in a browser, offline, and finds text that spills out of its box (it needs Playwright installed beside
|
|
407
417
|
StoryDeck: `npm i -D footprint-storydeck playwright-core`; without it, it says so and the other checks run). `mcp` serves the same tools to an AI assistant:
|
|
408
418
|
`claude mcp add storydeck -- npx footprint-storydeck mcp my-talk`.
|
package/dist/deck/check.js
CHANGED
|
@@ -43,6 +43,16 @@ function missingNotes(compiled) {
|
|
|
43
43
|
if (!without.length || without.length * 2 > compiled.clicks.length) return [];
|
|
44
44
|
return [problem("info", "no-notes", null, `click${without.length === 1 ? "" : "s"} ${without.map((c) => c.n).join(", ")} ha${without.length === 1 ? "s" : "ve"} no notes \u2014 the presenter view shows nothing there and listen mode skips ${without.length === 1 ? "it" : "them"}`)];
|
|
45
45
|
}
|
|
46
|
+
function unstyledThemes(compiled) {
|
|
47
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const c of compiled.clicks) {
|
|
49
|
+
if (!c.theme || seen.has(c.theme)) continue;
|
|
50
|
+
seen.add(c.theme);
|
|
51
|
+
const selects = new RegExp(`\\[\\s*data-theme\\s*[~|^$*]?=\\s*(["']?)${c.theme}\\1\\s*(?:[is]\\s*)?\\]`).test(compiled.themeCss ?? "");
|
|
52
|
+
if (!selects) out.push(problem("warn", "theme-unstyled", c, `part "${c.part}" asks for data-theme="${c.theme}", which theme.css never selects \u2014 fix the name, or give it its tokens there ([data-theme="${c.theme}"] { --bg: \u2026; })`));
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
46
56
|
function checkDeck(compiled) {
|
|
47
57
|
const out = [];
|
|
48
58
|
compiled.clicks.forEach((c, i) => {
|
|
@@ -50,7 +60,7 @@ function checkDeck(compiled) {
|
|
|
50
60
|
if (c.kind === "map") out.push(...litOnEntry(c, compiled.maps[c.map].pieces));
|
|
51
61
|
out.push(...noChange(c, compiled.clicks[i - 1]));
|
|
52
62
|
});
|
|
53
|
-
out.push(...neverShown(compiled), ...missingNotes(compiled));
|
|
63
|
+
out.push(...neverShown(compiled), ...missingNotes(compiled), ...unstyledThemes(compiled));
|
|
54
64
|
return out;
|
|
55
65
|
}
|
|
56
66
|
const failure = (error) => [problem("error", "compile", null, error.message)];
|
package/dist/deck/compile.js
CHANGED
|
@@ -73,7 +73,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
73
73
|
const head = (kicker, headline) => `<div class="head" data-chrome><div class="kicker">${esc(kicker)}</div>${headline ? `<h2>${headline}</h2>` : ""}</div>`;
|
|
74
74
|
const foot = (footer, n, total) => `<div class="foot" data-chrome><span>${esc(footer)}</span><span class="count">${n} / ${total}</span></div>`;
|
|
75
75
|
function drawClick(c, i, total, { footer, area, canvas }) {
|
|
76
|
-
const open = `<section data-label="${esc(c.label)}"`;
|
|
76
|
+
const open = `<section data-label="${esc(c.label)}"${c.part.theme ? ` data-theme="${esc(c.part.theme)}"` : ""}`;
|
|
77
77
|
if (c.kind === "plain") return `${open} class="slide plain">${head(c.part.kicker, "")}${c.inner}${foot(footer, i + 1, total)}</section>`;
|
|
78
78
|
return `${open} class="slide on-map ${slideClass(c.look)}">${head(c.part.kicker, c.headline)}<div class="map map-${c.map.name}">${wrapStage(drawFocus(c.map.html, c.look), c.look)}</div>${focusOverlay(c.look, { area, canvas: { w: canvas[0], h: canvas[1] } })}${foot(footer, i + 1, total)}</section>`;
|
|
79
79
|
}
|
|
@@ -108,6 +108,7 @@ function compileDeck(source, { dev = false } = {}) {
|
|
|
108
108
|
label: c.label,
|
|
109
109
|
part: c.part.label,
|
|
110
110
|
kicker: c.part.kicker,
|
|
111
|
+
theme: c.part.theme ?? null,
|
|
111
112
|
kind: c.kind,
|
|
112
113
|
map: c.kind === "map" ? c.map.name : null,
|
|
113
114
|
headline: plainText(c.headline),
|
package/dist/deck/init.js
CHANGED
|
@@ -24,6 +24,7 @@ const MAP = `<!--
|
|
|
24
24
|
const CLICKS = `<!--
|
|
25
25
|
The deck, click by click.
|
|
26
26
|
<section data-label="\u2026" data-kicker="\u2026"> one part of the story; without data-map it is one plain slide
|
|
27
|
+
<section \u2026 data-theme="ledger"> optional: its slides carry data-theme, so theme.css can dress that part
|
|
27
28
|
<section \u2026 data-map="map"> a part told on map.html: every <step> inside is ONE click
|
|
28
29
|
<step data-in="\u2026" data-out="\u2026" data-on="\u2026" data-focus="\u2026">
|
|
29
30
|
data-in pieces that come in (they stay) data-out pieces that leave
|
package/dist/deck/parse.js
CHANGED
|
@@ -84,6 +84,13 @@ function takeNotes(html) {
|
|
|
84
84
|
}
|
|
85
85
|
const without = (html, ranges) => ranges.reduceRight((out, r) => out.slice(0, r.start) + out.slice(r.end), html);
|
|
86
86
|
const where = (label) => `"${label}"`;
|
|
87
|
+
const THEME = /^[a-z][a-z0-9-]{0,39}$/;
|
|
88
|
+
function themeOf(label, attrs) {
|
|
89
|
+
const theme = attrOf(attrs, "data-theme");
|
|
90
|
+
if (theme === null) return null;
|
|
91
|
+
if (!THEME.test(theme)) throw new Error(`${where(label)}: data-theme="${theme}" \u2014 a theme is one lower-case name (letters, digits and dashes, at most 40), such as "ledger"`);
|
|
92
|
+
return theme;
|
|
93
|
+
}
|
|
87
94
|
function readMapStep(label, k, attrs, inner) {
|
|
88
95
|
const { html, notes } = takeNotes(inner);
|
|
89
96
|
let heads;
|
|
@@ -113,10 +120,11 @@ function parseClicks(html) {
|
|
|
113
120
|
seen.add(label);
|
|
114
121
|
const kicker = attrOf(attrs, "data-kicker") ?? "";
|
|
115
122
|
const map = attrOf(attrs, "data-map");
|
|
123
|
+
const theme = themeOf(label, attrs);
|
|
116
124
|
if (map === null) {
|
|
117
125
|
if (/<step\b/i.test(body)) throw new Error(`${where(label)} has <step> clicks but no data-map \u2014 name the map they are told on (data-map="map")`);
|
|
118
126
|
const { html: inner, notes } = takeNotes(body);
|
|
119
|
-
parts.push({ label, kicker, map: null, body: inner, notes });
|
|
127
|
+
parts.push({ label, kicker, theme, map: null, body: inner, notes });
|
|
120
128
|
continue;
|
|
121
129
|
}
|
|
122
130
|
if (!map.trim()) throw new Error(`${where(label)}: data-map is empty \u2014 name a map (a file in the deck, e.g. data-map="map")`);
|
|
@@ -129,7 +137,7 @@ function parseClicks(html) {
|
|
|
129
137
|
if (!steps.length) throw new Error(`${where(label)} is told on map "${map}" but has no <step> \u2014 a map part is one click per <step>`);
|
|
130
138
|
const outside = plainText(takeNotes(without(body, steps)).html);
|
|
131
139
|
if (outside) throw new Error(`${where(label)}: "${outside.slice(0, 60)}" is outside every <step> \u2014 put a headline or notes inside the step they belong to`);
|
|
132
|
-
parts.push({ label, kicker, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
140
|
+
parts.push({ label, kicker, theme, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
133
141
|
}
|
|
134
142
|
if (!parts.length) throw new Error('the clicks file has no <section> \u2014 a deck is one <section data-label="\u2026"> per part');
|
|
135
143
|
return parts;
|
package/dist/deck/review.js
CHANGED
|
@@ -2,6 +2,7 @@ import { wordCount } from "./parse.js";
|
|
|
2
2
|
const nameOf = (pieces, i) => pieces[i].keys[0];
|
|
3
3
|
const unique = (names) => [...new Set(names)];
|
|
4
4
|
const list = (names, max = 14) => names.length ? `${names.slice(0, max).join(", ")}${names.length > max ? `, \u2026 (${names.length - max} more)` : ""}` : "\u2014";
|
|
5
|
+
const themed = (c) => c.theme ? ` \xB7 theme ${c.theme}` : "";
|
|
5
6
|
const quote = (s, max = 160) => s.length > max ? `"${s.slice(0, max - 1)}\u2026"` : `"${s}"`;
|
|
6
7
|
function pieceGroups(click, pieces) {
|
|
7
8
|
const by = (pred) => unique(click.look.pieces.map((p, i) => pred(p) ? nameOf(pieces, i) : null).filter(Boolean));
|
|
@@ -56,10 +57,10 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
56
57
|
const lines = [];
|
|
57
58
|
const words = (n) => `${n} word${n === 1 ? "" : "s"}`;
|
|
58
59
|
if (c.kind === "plain") {
|
|
59
|
-
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)`);
|
|
60
|
+
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)${themed(c)}`);
|
|
60
61
|
lines.push(`text (${words(wordCount(c.text))}): ${quote(c.text, 300)}`);
|
|
61
62
|
} else {
|
|
62
|
-
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})`);
|
|
63
|
+
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})${themed(c)}`);
|
|
63
64
|
const hw = wordCount(c.headline);
|
|
64
65
|
lines.push(`headline: ${c.headline ? `${quote(c.headline)} (${words(hw)}${hw > maxWords ? `, over ${maxWords}` : ""})` : "\u2014"}`);
|
|
65
66
|
const pieces = compiled.maps[c.map].pieces;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "footprint-storydeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "One source, many lenses — render structured content as a Read article, a Scroll (scrollytelling) story, or a Watch deck; and make a deck from a folder with a live dev loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|