footprint-storydeck 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1 — footprint-narration is now footprint-story
4
+
5
+ - **footprint-narration is now footprint-story.** StoryDeck's shared narration code (shown-vs-spoken text, captions,
6
+ chapters, the voice cache) comes from `footprint-story` 0.3.0, the same package under its new name: what StoryDeck
7
+ and footprint-storyreel share, now with a `/motion` door too. Nothing a deck says, shows or voices changes. A project
8
+ that imports `footprint-narration` itself beside StoryDeck should move to `footprint-story` (the two doors export
9
+ exactly what they did).
10
+
11
+ ## 0.8.0 — a part can have its own look (data-theme)
12
+
13
+ - **A part can have its own look: `data-theme`.** `<section … data-theme="ledger">` in clicks.html puts
14
+ `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
15
+ tokens and pieces — a talk in several parts, each dressed for its subject. A theme is one lower-case name (letters,
16
+ digits, dashes; 40 at most), refused otherwise with the part's name. A part without one is drawn byte for byte as
17
+ before.
18
+ The title and closing slides are parts, so they can have a look of their own. `check` warns (`theme-unstyled`) when a
19
+ part asks for a theme that theme.css never selects, and `review` ends a click's first line with its theme. The README
20
+ shows how the page around the slide follows the part on screen (`:root:has([data-deck-active][data-theme=…])`).
21
+
3
22
  ## 0.7.0 — a deck from a folder, an editing loop, co-editing and wire motion
4
23
 
5
24
  - **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
@@ -240,7 +240,7 @@ through it the same way.
240
240
  A talk lives on after the room: someone opens the deck alone, or it becomes a video. The words were in the
241
241
  speaker notes all along, so narration is data on each click, and the library turns it into speech, captions and
242
242
  timing — the same words, three uses. The rules underneath — what a voice says for a number or a symbol, the clip
243
- cache, the captions and YouTube's chapter rule — are [footprint-narration](https://github.com/footprintjs/footprint-narration)'s,
243
+ cache, the captions and YouTube's chapter rule — are [footprint-story](https://github.com/footprintjs/footprint-story)'s,
244
244
  shared with footprint-storyreel; StoryDeck adds what is the deck's own:
245
245
 
246
246
  - **what the voice says** — `narrationText(notes)`: the notes as a voice reads them. An acronym stays as written
@@ -249,7 +249,7 @@ shared with footprint-storyreel; StoryDeck adds what is the deck's own:
249
249
  another script its reading), numbers become words, a lone symbol that means something is said (`SAY`: & + = < > ×
250
250
  % and Greek letters), any other word without a letter is a pause that keeps its sentence end (a forced aligner
251
251
  needs letters in every word), and a line after `[draft]` (a bridge you wrote for the deck) is said, and shown apart;
252
- - **a clip per click, cached by what it says** — `footprint-narration/voice` (Node). A clip's key is a hash of the
252
+ - **a clip per click, cached by what it says** — `footprint-story/voice` (Node). A clip's key is a hash of the
253
253
  voice settings and the exact words, so editing one note re-voices that click and nothing else, and an interrupted
254
254
  run keeps every batch it finished. The voice is a port (`{ name, synthesize(scenes, { work }) }`); `chatterboxKit`
255
255
  is one adapter — a local voice kit (Chatterbox, words aligned with MMS_FA; nothing is uploaded). `footprint-storydeck/voice`
@@ -259,7 +259,7 @@ shared with footprint-storyreel; StoryDeck adds what is the deck's own:
259
259
  the browser's where a click has none — captions lighting sentence by sentence (styles: `footprint-storydeck/narration.css`).
260
260
 
261
261
  ```js
262
- import { voiceClips, chatterboxKit } from 'footprint-narration/voice';
262
+ import { voiceClips, chatterboxKit } from 'footprint-story/voice';
263
263
  import { narrationText } from 'footprint-storydeck/narration';
264
264
  import { embedClips } from 'footprint-storydeck/voice';
265
265
  import { notesData, notesRuntime, listenRuntime } from 'footprint-storydeck/narration-html';
@@ -290,7 +290,7 @@ would drop one entrance frame in six at 30 fps, and a test with the real ffmpeg
290
290
  ```js
291
291
  import { chromium } from 'playwright-core';
292
292
  import { renderVideo, deckStageDriver } from 'footprint-storydeck/video';
293
- import { clipFor } from 'footprint-narration/voice';
293
+ import { clipFor } from 'footprint-story/voice';
294
294
  import { narrationText, writtenText } from 'footprint-storydeck/narration';
295
295
 
296
296
  const steps = clicks.map(c => {
@@ -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`.
@@ -537,9 +547,9 @@ const { theme, toggle, setTheme } = useTheme(); // flips an html class + persi
537
547
  | `planFocus` · `focusStep` · `readFocus` · `FOCUS` | focus: per click, what it is about and what happens to the rest (also `footprint-storydeck/focus`) |
538
548
  | `readPieces` · `readStep` · `drawFocus` · `wrapStage` · `slideClass` · `focusOverlay` · `focusRuntime` | focus, written into slide HTML (also `footprint-storydeck/focus-html`; styles in `footprint-storydeck/focus.css`) |
539
549
  | `idleTour` · `tourPlan` · `tourText` | tour: a deck (or a figure) that shows its point while nobody is using it, and its plan in data and in one line (also `footprint-storydeck/tour`) |
540
- | `narrationText` · `writtenText` | narration: what a click says (also `footprint-storydeck/narration`); the rules beneath, sentences, captions and chapters are `footprint-narration`'s |
550
+ | `narrationText` · `writtenText` | narration: what a click says (also `footprint-storydeck/narration`); the rules beneath, sentences, captions and chapters are `footprint-story`'s |
541
551
  | `notesData` · `notesRuntime` · `listenRuntime` | narration in the page: notes, presenter window, listen mode (also `footprint-storydeck/narration-html`; styles in `footprint-storydeck/narration.css`) |
542
- | `embedClips` | `footprint-storydeck/voice` (Node only): a deck's clips inside one page; the clip cache, the voice port and its local-kit adapter are `footprint-narration/voice`'s |
552
+ | `embedClips` | `footprint-storydeck/voice` (Node only): a deck's clips inside one page; the clip cache, the voice port and its local-kit adapter are `footprint-story/voice`'s |
543
553
  | `renderVideo` · `planVideo` · `deckStageDriver` · `ffmpegMajor` | `footprint-storydeck/video` (Node only, ffmpeg 5.0+): the deck as a narrated video, with captions and chapters |
544
554
  | `exportPptx` · `pptxParts` · `zip` · `unzip` · `checkPptx` | `footprint-storydeck/pptx` (Node only): the deck as a PowerPoint file — a slide per click, its picture, notes and clip — through a footprintjs flowchart that records why |
545
555
  | `loadDeck` · `buildDeck` · `serveDeck` · `outline` · `reviewClick` · `checkDeck` · `layoutCheck` · `initDeck` · `serveMcp` · `runCli` | `footprint-storydeck/deck` (Node only): a deck folder made into one page, served for editing, read back as text — the `storydeck` command (also `npx footprint-storydeck`); its page's base styles are `footprint-storydeck/deck.css` |
@@ -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/dist/narration.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spokenMap, autoRules } from "footprint-narration";
1
+ import { spokenMap, autoRules } from "footprint-story";
2
2
  const AUTO = autoRules();
3
3
  const writtenText = (notes) => (Array.isArray(notes) ? notes : [notes ?? ""]).join(" ").replace(/\[draft\]\s*/g, "").replace(/\s+/g, " ").trim();
4
4
  function narrationText(notes, { spell, say } = {}) {
package/dist/pptx.js CHANGED
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
2
2
  import { closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
- import { clipFor } from "footprint-narration/voice";
5
+ import { clipFor } from "footprint-story/voice";
6
6
  import { narrationText, writtenText } from "./narration.js";
7
7
  import { planVideo } from "./video.js";
8
8
  import { pptxParts, pngSize, plainText, isSound, AUDIO, LANG_TAG, SOUND } from "./pptxParts.js";
@@ -49,13 +49,13 @@ function headOf(file) {
49
49
  }
50
50
  function soundProblem(clip, ext) {
51
51
  const { size, head } = headOf(clip.audio);
52
- if (!size) return `its clip ${clip.audio} is empty (0 bytes) \u2014 voice it again (footprint-narration/voice \xB7 voiceClips)`;
52
+ if (!size) return `its clip ${clip.audio} is empty (0 bytes) \u2014 voice it again (footprint-story/voice \xB7 voiceClips)`;
53
53
  return isSound(head, ext) ? null : `its clip ${clip.audio} is not a .${ext} file (no ${SOUND[ext].sign}) \u2014 voice it again, or convert it with ffmpeg`;
54
54
  }
55
55
  function clipProblem(c, { voiced, maxClip }) {
56
56
  const clip = c.clip, d = clip?.duration, ext = extOf(clip?.audio);
57
- if (!clip) return voiced && c.spoken.trim() ? ["unvoiced", "it says words but has no clip \u2014 voice it (footprint-narration/voice \xB7 voiceClips), or give its step its clip"] : null;
58
- if (!clip.audio || !existsSync(clip.audio)) return ["missing", `its clip ${clip.audio} is not there \u2014 voice it again (footprint-narration/voice \xB7 voiceClips)`];
57
+ if (!clip) return voiced && c.spoken.trim() ? ["unvoiced", "it says words but has no clip \u2014 voice it (footprint-story/voice \xB7 voiceClips), or give its step its clip"] : null;
58
+ if (!clip.audio || !existsSync(clip.audio)) return ["missing", `its clip ${clip.audio} is not there \u2014 voice it again (footprint-story/voice \xB7 voiceClips)`];
59
59
  if (!AUDIO[ext]) return ["unplayable", `its clip is .${ext} \u2014 PowerPoint plays .wav, .m4a and .mp3 (convert it with ffmpeg)`];
60
60
  const bytes = soundProblem(clip, ext);
61
61
  if (bytes) return ["unplayable", bytes];
package/dist/video.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import path from "node:path";
5
- import { captionCues, captionFile, readCaptions, youtubeChapters } from "footprint-narration";
5
+ import { captionCues, captionFile, readCaptions, youtubeChapters } from "footprint-story";
6
6
  const NUM = (n) => String(n).padStart(3, "0");
7
7
  const entry = (file) => `file '${path.resolve(file).replace(/'/g, "'\\''")}'`;
8
8
  const failure = (done) => done?.error ? done.error.message : done?.signal ? `signal ${done.signal}` : done?.status !== 0 ? `exit ${done?.status}` : null;
package/dist/voice.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import path from "node:path";
4
- import { clipFor } from "footprint-narration/voice";
4
+ import { clipFor } from "footprint-story/voice";
5
5
  const failure = (done) => done?.error ? done.error.message : done?.signal ? `signal ${done.signal}` : done?.status !== 0 ? `exit ${done?.status}` : null;
6
6
  function embedClips(texts, { cache, profile, run = spawnSync }) {
7
7
  cache = path.resolve(cache);
package/index.d.ts CHANGED
@@ -192,7 +192,7 @@ export * from './focusHtml';
192
192
  // Tour — a deck or a figure that shows its point by itself while nobody is using it (tour.d.ts).
193
193
  export * from './tour';
194
194
  // Narration — what a deck says on each click (narration.d.ts), in the page: notes, presenter, listen mode (narrationHtml.d.ts).
195
- // The rules a voice reads by, sentences, captions and chapters are footprint-narration's (its own types).
195
+ // The rules a voice reads by, sentences, captions and chapters are footprint-story's (its own types).
196
196
  // footprint-storydeck/voice, /video and /pptx (Node-only) carry their own types.
197
197
  export * from './narration';
198
198
  export * from './narrationHtml';
package/narration.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * storydeck · narration (narration.js) — what a deck says on each click. The rules a voice reads by, the sentences,
3
- * the captions and the chapters are footprint-narration's: import them from 'footprint-narration'.
3
+ * the captions and the chapters are footprint-story's: import them from 'footprint-story'.
4
4
  */
5
5
 
6
6
  /** The notes as written, for captions: [draft] marks dropped, white space collapsed. */
7
7
  export function writtenText(notes: string | readonly string[] | null | undefined): string;
8
- /** The notes as the voice should read them: footprint-narration's automatic rules over the written text (`spell`, `say` replace its default lists). */
8
+ /** The notes as the voice should read them: footprint-story's automatic rules over the written text (`spell`, `say` replace its default lists). */
9
9
  export function narrationText(notes: string | readonly string[] | null | undefined, options?: { spell?: Readonly<Record<string, string>>; say?: Readonly<Record<string, string>> }): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "footprint-storydeck",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",
@@ -118,7 +118,7 @@
118
118
  }
119
119
  },
120
120
  "dependencies": {
121
- "footprint-narration": "^0.1.0",
121
+ "footprint-story": "^0.3.0",
122
122
  "markdown-it": "^14.1.0"
123
123
  },
124
124
  "devDependencies": {
@@ -8,9 +8,9 @@ description: Build, focus, narrate and film talks and posts with StoryDeck — o
8
8
  A talk or a post is written once: **sections** of slide steps plus prose. StoryDeck shows it as an article
9
9
  (Read), a scrollytelling story (Scroll) or a click-through deck (Watch), decides per click what the audience
10
10
  looks at (focus), and turns the speaker notes into a voice, captions and a video (narration). Most of it is
11
- plain data and string builders; only `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` and `footprint-narration/voice` touch the
11
+ plain data and string builders; only `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` and `footprint-story/voice` touch the
12
12
  file system. The narration rules underneath (what a voice says for a number, captions, chapters, the clip cache)
13
- are footprint-narration's, a package StoryDeck depends on and shares with footprint-storyreel.
13
+ are footprint-story's, a package StoryDeck depends on and shares with footprint-storyreel.
14
14
 
15
15
  ## Pick the door
16
16
 
@@ -22,15 +22,15 @@ are footprint-narration's, a package StoryDeck depends on and shares with footpr
22
22
  | that focus written into slide HTML | `footprint-storydeck/focus-html` + `footprint-storydeck/focus.css`, `focusRuntime` once in the page | build + page |
23
23
  | a deck (or figure) that shows its point while nobody is using it (`idleTour`, its plan `tourPlan`, a notes line `tourText`) | `footprint-storydeck/tour`; declared as `<deck-stage data-tour="3 4 5 9:6s">` + `(${idleTour})()`, or `SlideDeck`'s `tour` / post.json's `tour` | build + page |
24
24
  | what each click says (`narrationText`, `writtenText`: `[draft]`-aware) | `footprint-storydeck/narration` | anywhere |
25
- | the rules beneath (`spokenMap`, `autoRules`, `SPELL`, `SAY`), sentences, captions, YouTube chapters | `footprint-narration` | anywhere |
25
+ | the rules beneath (`spokenMap`, `autoRules`, `SPELL`, `SAY`), sentences, captions, YouTube chapters | `footprint-story` | anywhere |
26
26
  | notes (N), presenter (P), listen mode (L) in the page | `footprint-storydeck/narration-html` + `footprint-storydeck/narration.css` | build + page |
27
- | a clip per click in a voice, cached (`voiceClips`, `clipFor`, `chatterboxKit`, `pickSteps`) | `footprint-narration/voice` | **Node only** |
27
+ | a clip per click in a voice, cached (`voiceClips`, `clipFor`, `chatterboxKit`, `pickSteps`) | `footprint-story/voice` | **Node only** |
28
28
  | those clips carried inside one page (`embedClips`) | `footprint-storydeck/voice` | **Node only** |
29
29
  | the deck as a narrated video | `footprint-storydeck/video` (+ ffmpeg 5.0 or newer on the path, Playwright's `chromium` passed in) | **Node only** |
30
30
  | the deck as PowerPoint (.pptx): a slide per click, its picture, notes and clip | `footprint-storydeck/pptx` — `exportPptx` (the same driver as the video; no ffmpeg; `npm i footprintjs` beside it, an optional peer) | **Node only** |
31
31
  | a deck from a folder: one page, a dev server that reloads on the same click, the deck read back as text | the `storydeck` command (`npx footprint-storydeck init · dev · outline · review · check · build · mcp`); `footprint-storydeck/deck` for scripts | **Node only** |
32
32
 
33
- Never import `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` or `footprint-narration/voice` into browser code: they use `node:fs`
33
+ Never import `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` or `footprint-story/voice` into browser code: they use `node:fs`
34
34
  and child processes.
35
35
  The main entry (`footprint-storydeck`) re-exports only what runs anywhere.
36
36
 
@@ -99,7 +99,7 @@ whole path .4 s).
99
99
  **Narrate a deck in a voice.**
100
100
 
101
101
  ```js
102
- import { voiceClips, chatterboxKit } from 'footprint-narration/voice';
102
+ import { voiceClips, chatterboxKit } from 'footprint-story/voice';
103
103
  import { narrationText } from 'footprint-storydeck/narration';
104
104
  import { embedClips } from 'footprint-storydeck/voice';
105
105
  import { notesData, notesRuntime, listenRuntime } from 'footprint-storydeck/narration-html';
@@ -113,14 +113,14 @@ page += notesData(clicks.map(c => c.notes)) + voice.tags + `<script>(${notesRunt
113
113
 
114
114
  The engine is a port — `{ name, synthesize(scenes, { work }) }`, scenes `{ id, text }`, results `{ id, audio,
115
115
  duration, words: [{ text, start, end }] }` — so a cloud voice or a test fake plugs in the same way as the local kit.
116
- A clip keeps its timed words (`clipFor(…).words`; `null` for a clip voiced before footprint-narration).
116
+ A clip keeps its timed words (`clipFor(…).words`; `null` for a clip voiced before footprint-story).
117
117
 
118
118
  **Render the narrated video.** A strip first, then the whole.
119
119
 
120
120
  ```js
121
121
  import { chromium } from 'playwright-core';
122
122
  import { renderVideo, deckStageDriver } from 'footprint-storydeck/video';
123
- import { clipFor } from 'footprint-narration/voice';
123
+ import { clipFor } from 'footprint-story/voice';
124
124
  import { narrationText, writtenText } from 'footprint-storydeck/narration';
125
125
 
126
126
  const steps = clicks.map(c => {
@@ -149,7 +149,7 @@ It is pictures + notes + sound, not editable PowerPoint shapes.
149
149
 
150
150
  - **A clip's key is its recipe**: the voice profile's settings and the exact spoken text. Change the words a
151
151
  voice reads — a note, or the `spell` list — and those clicks are voiced again; everything else stays cached.
152
- `spell` replaces the default list: extend it with `{ ...SPELL, K8s: 'K eight s' }` (`SPELL` from `footprint-narration`;
152
+ `spell` replaces the default list: extend it with `{ ...SPELL, K8s: 'K eight s' }` (`SPELL` from `footprint-story`;
153
153
  a value is words — a letter in every word, no digits, no comma, semicolon or colon at its end — or it is refused).
154
154
  - **Every word needs letters.** A lone symbol that means something is said (`SAY`: & + = < > × % and Greek
155
155
  letters; pass `say` to change it); any other (— → ... an emoji) is a pause that keeps its sentence end. The local
package/pptx.d.ts CHANGED
@@ -3,14 +3,14 @@
3
3
  * its clip. Node. The export is a footprintjs flowchart: every run leaves a record that explains it. footprintjs is an
4
4
  * optional peer dependency: `npm i footprintjs` beside StoryDeck to export.
5
5
  */
6
- import type { VoiceProfile } from 'footprint-narration/voice';
6
+ import type { VoiceProfile } from 'footprint-story/voice';
7
7
  import type { VideoClip, VideoDriver, VideoStep } from './video';
8
8
 
9
9
  /** A click to export: as renderVideo takes it, plus the slide's headline (the title, before the label). */
10
10
  export interface PptxStep extends VideoStep { headline?: string }
11
11
  /** A click of a deck: its label (and headline), and its notes — narration-html's notesData entry ({ now }), a string, or a list of strings. */
12
12
  export interface DeckClick { label?: string; headline?: string; notes?: string | readonly string[] | { now: readonly string[] } | null }
13
- /** Where a deck's clips are: footprint-narration/voice's cache and profile, and the reading rules the clips were voiced with. */
13
+ /** Where a deck's clips are: footprint-story/voice's cache and profile, and the reading rules the clips were voiced with. */
14
14
  export interface PptxVoice { cache: string; profile: VoiceProfile; spell?: Readonly<Record<string, string>>; say?: Readonly<Record<string, string>> }
15
15
  /** A click as exported: its number, title, notes as written, words as spoken, clip, and the seconds it stays (null: the speaker clicks). */
16
16
  export interface PlannedSlide {
package/video.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * storydeck · video (video.js) — a narrated deck as a video: every click on screen while its clip plays. Node + ffmpeg 5.0+.
3
3
  */
4
- import type { Clip } from 'footprint-narration/voice';
5
- import type { Chapters } from 'footprint-narration';
4
+ import type { Clip } from 'footprint-story/voice';
5
+ import type { Chapters } from 'footprint-story';
6
6
 
7
- /** A cached clip as the video uses it (`words` is not read: a clip from before footprint-narration has none). */
7
+ /** A cached clip as the video uses it (`words` is not read: a clip from before footprint-story has none). */
8
8
  export type VideoClip = Omit<Clip, 'words'> & { words?: Clip['words'] };
9
9
  /** A click to render: its label, its words as written and as spoken, and its clip (or null). */
10
10
  export interface VideoStep { label?: string; written?: string; spoken?: string; clip?: VideoClip | null }
package/voice.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * storydeck · voice (voice.js) — a deck's clips inside one page. Node. The clip cache, the voice port and the kit
3
- * adapter are footprint-narration's: import them from 'footprint-narration/voice'.
3
+ * adapter are footprint-story's: import them from 'footprint-story/voice'.
4
4
  */
5
- import type { VoiceProfile } from 'footprint-narration/voice';
5
+ import type { VoiceProfile } from 'footprint-story/voice';
6
6
 
7
7
  type Run = (cmd: string, args: string[], options?: object) => { status: number | null };
8
8