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 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). `check --layout`
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`.
@@ -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)];
@@ -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
@@ -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;
@@ -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.7.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",