footprint-storydeck 0.5.0 → 0.6.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +33 -3
  3. package/dist/BlogView.js +48 -0
  4. package/dist/PostView.js +170 -0
  5. package/dist/ScrollyView.js +93 -0
  6. package/dist/SlideDeck.js +49 -0
  7. package/dist/SlideFigure.js +24 -0
  8. package/dist/ThemeToggle.js +13 -0
  9. package/dist/content/loadPost.js +25 -0
  10. package/dist/content/markdown.js +23 -0
  11. package/dist/context.js +14 -0
  12. package/dist/figure.js +6 -0
  13. package/dist/focus.js +339 -0
  14. package/dist/focusHtml.js +235 -0
  15. package/dist/index.js +59 -0
  16. package/dist/narration.js +11 -0
  17. package/dist/narrationHtml.js +243 -0
  18. package/dist/pptx.js +280 -0
  19. package/dist/pptxCheck.js +156 -0
  20. package/dist/pptxParts.js +198 -0
  21. package/dist/scopeDeckCss.js +6 -0
  22. package/dist/sections.js +52 -0
  23. package/dist/slug.js +6 -0
  24. package/dist/tour.js +225 -0
  25. package/dist/useTheme.js +35 -0
  26. package/dist/video.js +244 -0
  27. package/dist/voice.js +35 -0
  28. package/dist/xml.js +106 -0
  29. package/dist/zip.js +133 -0
  30. package/index.d.ts +51 -6
  31. package/package.json +16 -18
  32. package/storydeck.css +1 -0
  33. package/BlogView.jsx +0 -52
  34. package/PostView.jsx +0 -229
  35. package/ScrollyView.jsx +0 -112
  36. package/SlideDeck.jsx +0 -64
  37. package/SlideFigure.jsx +0 -30
  38. package/ThemeToggle.jsx +0 -24
  39. package/content/loadPost.js +0 -36
  40. package/content/markdown.js +0 -27
  41. package/context.jsx +0 -15
  42. package/focus.js +0 -387
  43. package/focusHtml.js +0 -281
  44. package/index.js +0 -22
  45. package/narration.js +0 -25
  46. package/narrationHtml.js +0 -156
  47. package/pptx.js +0 -313
  48. package/pptxCheck.js +0 -167
  49. package/pptxParts.js +0 -262
  50. package/scopeDeckCss.js +0 -8
  51. package/sections.js +0 -72
  52. package/slug.js +0 -9
  53. package/tour.js +0 -206
  54. package/useTheme.js +0 -37
  55. package/video.js +0 -236
  56. package/voice.js +0 -41
  57. package/xml.js +0 -123
  58. package/zip.js +0 -155
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — a figure in any framework, and compiled JavaScript
4
+
5
+ - **`StoryFigure`: the Scroll lens drives a figure it does not know how to draw.** A section's `figure` may
6
+ now be `{ mount(el), seek(beat), dispose() }` beside the React render function it already took. The lens
7
+ mounts it once into a plain element in the pinned stage, asks it to `seek` with the beat's plain coordinates
8
+ (`{ key, index, sectionKey, step }`) on every beat change, and prints the sentence a seek resolves to under
9
+ the figure (a refusal, verbatim) until a later seek answers; only the latest seek's answer is shown.
10
+ `isStoryFigure` is exported. Additive: render-function figures and HTML steps are unchanged.
11
+ - **The package ships compiled JavaScript.** `dist/` holds each source file compiled on its own (JSX to
12
+ `react/jsx-runtime`, relative imports with `.js`, `'use client'` kept), so plain Node and any bundler load
13
+ it with no transform: the `.jsx` workarounds consumers needed (`transpilePackages`, a test runner's
14
+ inline list) are no longer required. Types stay the hand-kept `*.d.ts`. `npm run check:dist` loads every
15
+ door in plain Node and refuses a package that ships source JSX; CI runs it.
16
+
3
17
  ## 0.5.0 — PowerPoint, the exploded view, and a tour for decks nobody presents
4
18
 
5
19
  - **`footprint-storydeck/pptx`: the deck as a PowerPoint file.** `exportPptx` makes a slide per click: its settled
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - **Watch** — an animated, click-through slide deck.
8
8
 
9
9
  Same content, your lens. It's the footprintjs idea — *one canonical record, you choose the view* —
10
- applied to content. A React library; theme-agnostic; ships as source.
10
+ applied to content. A React library; theme-agnostic; ships compiled ESM that Node and any bundler load as-is.
11
11
 
12
12
  > Demo (dogfooded — storydeck explaining storydeck): **[footprintjs.github.io/storydeck](https://footprintjs.github.io/storydeck/)**
13
13
  > First consumer: the **[footprintjs blog](https://footprintjs.github.io/blog/)**.
@@ -58,6 +58,32 @@ A function cannot come from JSON, so `figure` is not part of the authoring model
58
58
  builds sections from post.json + Markdown, and the consumer attaches `figure` to them. The beat a
59
59
  figure is handed carries `sectionKey`, `step` and `index` — which beat it is being asked to draw.
60
60
 
61
+ ### A figure in any framework: `StoryFigure`
62
+
63
+ A section's `figure` may instead be a **`StoryFigure`** — three plain methods, no React:
64
+
65
+ ```ts
66
+ interface StoryFigure {
67
+ mount(el: HTMLElement): void; // draw into this element
68
+ seek(beat: StoryFigureBeat): Promise<string | void>; // the reader is on this beat: move there
69
+ dispose(): void; // take down what mount drew
70
+ }
71
+ // StoryFigureBeat = { key, index, sectionKey, step } — plain data
72
+ ```
73
+
74
+ ```js
75
+ const figure = { mount(el) { chart.attach(el); }, seek: (beat) => chart.goTo(beat.sectionKey), dispose() { chart.detach(); } };
76
+ <ScrollyView sections={sections.map((s) => ({ ...s, figure }))} />
77
+ ```
78
+
79
+ It is the seam for a figure the story should not have to know: a chart library, a canvas, another
80
+ framework's component. `ScrollyView` adapts it in a thin host — mounted once while it is on stage (a
81
+ different section's figure is disposed and the new one mounted), and asked to `seek` on every beat change.
82
+ **A seek that cannot move says why:** it resolves to a sentence, and the lens prints that sentence under the
83
+ figure, verbatim, until a later seek answers; it resolves nothing when it moved. Only the latest seek's
84
+ answer is shown, and a seek that rejects is shown as the lens's own sentence. The lens judges nothing — the
85
+ figure knows whether it can stand on a beat. `isStoryFigure(value)` is the check the lens uses.
86
+
61
87
  ## Focus: what a click is about, and what happens to the rest
62
88
 
63
89
  A talk often tells its story on **one picture** — a diagram, a page, a timeline — click by click. Each
@@ -434,8 +460,12 @@ const { theme, toggle, setTheme } = useTheme(); // flips an html class + persi
434
460
  | `renderVideo` · `planVideo` · `deckStageDriver` · `ffmpegMajor` | `footprint-storydeck/video` (Node only, ffmpeg 5.0+): the deck as a narrated video, with captions and chapters |
435
461
  | `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 |
436
462
 
437
- Types ship beside the source in `index.d.ts` — hand-kept (there is no build to generate them from),
438
- and here rather than in a consumer's shim, because a package's shape belongs in the package.
463
+ The package ships compiled JavaScript (`dist/`, built by `npm run build`: each source file compiled on its
464
+ own, JSX turned into `react/jsx-runtime` calls, every relative import given its `.js`, `'use client'` kept), so
465
+ plain Node and every bundler load it with no transform of their own; `npm run check:dist` loads every door in
466
+ plain Node and refuses a package that ships source JSX. Types ship in `index.d.ts` — hand-kept (nothing
467
+ type-checks the JSX to generate them from), and here rather than in a consumer's shim, because a package's
468
+ shape belongs in the package.
439
469
 
440
470
  ## Tests
441
471
 
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import SlideFigure from "./SlideFigure.js";
5
+ import { finalStep } from "./sections.js";
6
+ import { slugify } from "./slug.js";
7
+ function BlogView({ sections }) {
8
+ const [copied, setCopied] = useState("");
9
+ function copyLink(e, id) {
10
+ e.preventDefault();
11
+ const url = `${window.location.origin}${window.location.pathname}#${id}`;
12
+ if (window.location.hash !== `#${id}`) window.history.replaceState(null, "", `#${id}`);
13
+ try {
14
+ navigator.clipboard.writeText(url);
15
+ } catch (_) {
16
+ }
17
+ setCopied(id);
18
+ setTimeout(() => setCopied((c) => c === id ? "" : c), 1600);
19
+ }
20
+ return /* @__PURE__ */ jsx("article", { children: sections.map((s) => {
21
+ const id = slugify(s.key);
22
+ return /* @__PURE__ */ jsxs("section", { id, className: "post-section", children: [
23
+ /* @__PURE__ */ jsxs("div", { className: "section-head", children: [
24
+ /* @__PURE__ */ jsx(
25
+ "a",
26
+ {
27
+ className: "section-anchor",
28
+ href: `#${id}`,
29
+ onClick: (e) => copyLink(e, id),
30
+ title: "Copy link to this section",
31
+ "aria-label": `Copy link to \u201C${s.heading}\u201D`,
32
+ children: "#"
33
+ }
34
+ ),
35
+ /* @__PURE__ */ jsxs("div", { className: "section-head-text", children: [
36
+ /* @__PURE__ */ jsx("p", { className: "eyebrow", children: s.label }),
37
+ /* @__PURE__ */ jsx("h2", { children: s.heading })
38
+ ] }),
39
+ copied === id ? /* @__PURE__ */ jsx("span", { className: "section-copied", "aria-live": "polite", children: "link copied \u2713" }) : null
40
+ ] }),
41
+ /* @__PURE__ */ jsx(SlideFigure, { html: finalStep(s) }),
42
+ s.body ? /* @__PURE__ */ jsx("div", { className: "prose", dangerouslySetInnerHTML: { __html: s.body } }) : null
43
+ ] }, id);
44
+ }) });
45
+ }
46
+ export {
47
+ BlogView as default
48
+ };
@@ -0,0 +1,170 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import BlogView from "./BlogView.js";
5
+ import ScrollyView from "./ScrollyView.js";
6
+ import SlideDeck from "./SlideDeck.js";
7
+ const LENSES = [
8
+ { id: "read", label: "Read it" },
9
+ { id: "scrolly", label: "Scroll it" },
10
+ { id: "slides", label: "Watch it" }
11
+ ];
12
+ const ESCAPE_LENSES = LENSES.filter((l) => l.id !== "slides");
13
+ const NARROW_WATCH_MQ = "(max-width: 700px)";
14
+ function isNarrowViewport() {
15
+ return typeof window.matchMedia === "function" && window.matchMedia(NARROW_WATCH_MQ).matches;
16
+ }
17
+ function fullscreenElement() {
18
+ return document.fullscreenElement || document.webkitFullscreenElement || null;
19
+ }
20
+ function PostView({ post }) {
21
+ const [view, setView] = useState("read");
22
+ const [takeover, setTakeover] = useState(false);
23
+ const [fsApi, setFsApi] = useState(false);
24
+ const stageRef = useRef(null);
25
+ const viewRef = useRef("read");
26
+ const autoFsRef = useRef(false);
27
+ const content = post.sections.filter((s) => s.key !== "Title" && s.key !== "Close");
28
+ const isWatch = view === "slides";
29
+ const idle = useIdleFade(isWatch);
30
+ useEffect(() => {
31
+ viewRef.current = view;
32
+ }, [view]);
33
+ const enterFullscreen = useCallback((auto) => {
34
+ const el = stageRef.current;
35
+ if (!el) return;
36
+ autoFsRef.current = !!auto;
37
+ const request = el.requestFullscreen || el.webkitRequestFullscreen;
38
+ if (!request) {
39
+ setTakeover(true);
40
+ return;
41
+ }
42
+ let p;
43
+ try {
44
+ p = request.call(el);
45
+ } catch (_) {
46
+ setTakeover(true);
47
+ return;
48
+ }
49
+ if (p && typeof p.catch === "function") {
50
+ p.catch(() => setTakeover(true));
51
+ } else if (auto) {
52
+ setTimeout(() => {
53
+ if (!fullscreenElement() && viewRef.current === "slides" && autoFsRef.current) setTakeover(true);
54
+ }, 500);
55
+ }
56
+ }, []);
57
+ const leaveFullscreen = useCallback(() => {
58
+ autoFsRef.current = false;
59
+ setTakeover(false);
60
+ if (!fullscreenElement()) return;
61
+ const exit = document.exitFullscreen || document.webkitExitFullscreen;
62
+ if (!exit) return;
63
+ try {
64
+ const p = exit.call(document);
65
+ if (p && typeof p.catch === "function") p.catch(() => {
66
+ });
67
+ } catch (_) {
68
+ }
69
+ }, []);
70
+ const choose = useCallback((next) => {
71
+ setView(next);
72
+ const u = new URL(window.location.href);
73
+ if (next === "read") u.searchParams.delete("view");
74
+ else u.searchParams.set("view", next);
75
+ window.history.replaceState(null, "", u);
76
+ if (next === "slides") {
77
+ if (isNarrowViewport()) enterFullscreen(true);
78
+ } else {
79
+ leaveFullscreen();
80
+ }
81
+ }, [enterFullscreen, leaveFullscreen]);
82
+ useEffect(() => {
83
+ const v = new URLSearchParams(window.location.search).get("view");
84
+ if (v === "slides" || v === "scrolly") setView(v);
85
+ if (v === "slides" && isNarrowViewport()) enterFullscreen(true);
86
+ const el = stageRef.current;
87
+ setFsApi(!!(el && (el.requestFullscreen || el.webkitRequestFullscreen)));
88
+ }, [enterFullscreen]);
89
+ useEffect(() => {
90
+ function onFullscreenChange() {
91
+ if (!fullscreenElement() && viewRef.current === "slides" && autoFsRef.current) choose("read");
92
+ }
93
+ document.addEventListener("fullscreenchange", onFullscreenChange);
94
+ document.addEventListener("webkitfullscreenchange", onFullscreenChange);
95
+ return () => {
96
+ document.removeEventListener("fullscreenchange", onFullscreenChange);
97
+ document.removeEventListener("webkitfullscreenchange", onFullscreenChange);
98
+ };
99
+ }, [choose]);
100
+ useEffect(() => {
101
+ if (!isWatch) return;
102
+ function onKeyDown(e) {
103
+ if (e.key === "Escape") choose("read");
104
+ }
105
+ window.addEventListener("keydown", onKeyDown);
106
+ return () => window.removeEventListener("keydown", onKeyDown);
107
+ }, [isWatch, choose]);
108
+ return /* @__PURE__ */ jsxs("main", { id: "main", className: "wrap wide", children: [
109
+ /* @__PURE__ */ jsxs("header", { className: "post-head", children: [
110
+ /* @__PURE__ */ jsx("p", { className: "eyebrow", children: "Primer" }),
111
+ /* @__PURE__ */ jsx("h1", { children: post.title }),
112
+ /* @__PURE__ */ jsx("p", { className: "lead", children: post.description }),
113
+ /* @__PURE__ */ jsxs("p", { className: "byline", children: [
114
+ "By ",
115
+ post.author,
116
+ " \xB7 ",
117
+ (/* @__PURE__ */ new Date(post.date + "T00:00:00Z")).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" })
118
+ ] })
119
+ ] }),
120
+ /* @__PURE__ */ jsx("div", { className: "view-toggle", role: "group", "aria-label": "Choose a view", children: LENSES.map((l) => /* @__PURE__ */ jsx("button", { "aria-pressed": view === l.id, onClick: () => choose(l.id), children: l.label }, l.id)) }),
121
+ view === "read" ? /* @__PURE__ */ jsx(BlogView, { sections: content }) : null,
122
+ view === "scrolly" ? /* @__PURE__ */ jsx(ScrollyView, { sections: content }) : null,
123
+ /* @__PURE__ */ jsx("div", { ref: stageRef, className: `watch-stage${isWatch && takeover ? " is-takeover" : ""}`, children: isWatch ? /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ /* @__PURE__ */ jsxs("div", { className: `watch-lens-dock${idle ? " is-idle" : ""}`, role: "group", "aria-label": "Switch view", children: [
125
+ ESCAPE_LENSES.map((l) => /* @__PURE__ */ jsx("button", { type: "button", onClick: () => choose(l.id), children: l.label.replace(/ it$/, "") }, l.id)),
126
+ fsApi ? /* @__PURE__ */ jsx(
127
+ "button",
128
+ {
129
+ type: "button",
130
+ className: "watch-lens-fs",
131
+ "aria-label": "Toggle fullscreen",
132
+ title: "Full screen",
133
+ onClick: () => fullscreenElement() || takeover ? leaveFullscreen() : enterFullscreen(false),
134
+ children: "\u26F6"
135
+ }
136
+ ) : null,
137
+ /* @__PURE__ */ jsx("span", { className: "watch-lens-hint", title: "Press Esc to return to Read", "aria-hidden": "true", children: "Esc" })
138
+ ] }),
139
+ /* @__PURE__ */ jsx(SlideDeck, { steps: post.deckSteps, tour: post.tour })
140
+ ] }) : null })
141
+ ] });
142
+ }
143
+ function useIdleFade(active, ms = 2600) {
144
+ const [idle, setIdle] = useState(false);
145
+ useEffect(() => {
146
+ if (!active) {
147
+ setIdle(false);
148
+ return;
149
+ }
150
+ let timer = setTimeout(() => setIdle(true), ms);
151
+ function wake() {
152
+ setIdle(false);
153
+ clearTimeout(timer);
154
+ timer = setTimeout(() => setIdle(true), ms);
155
+ }
156
+ window.addEventListener("pointermove", wake);
157
+ window.addEventListener("touchstart", wake, { passive: true });
158
+ window.addEventListener("keydown", wake);
159
+ return () => {
160
+ clearTimeout(timer);
161
+ window.removeEventListener("pointermove", wake);
162
+ window.removeEventListener("touchstart", wake);
163
+ window.removeEventListener("keydown", wake);
164
+ };
165
+ }, [active, ms]);
166
+ return idle;
167
+ }
168
+ export {
169
+ PostView as default
170
+ };
@@ -0,0 +1,93 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import SlideFigure from "./SlideFigure.js";
5
+ import { slugify } from "./slug.js";
6
+ import { isStoryFigure } from "./figure.js";
7
+ function ScrollyView({ sections }) {
8
+ const beats = [];
9
+ sections.forEach((s) => {
10
+ s.steps.forEach((html, stepIdx) => {
11
+ beats.push({
12
+ key: `${slugify(s.key)}-${stepIdx}`,
13
+ index: beats.length,
14
+ html,
15
+ label: s.label,
16
+ heading: s.heading,
17
+ body: s.body,
18
+ // the section this beat belongs to, and which of its steps it is — what a live figure needs
19
+ // to know WHICH beat it is being asked to draw
20
+ sectionKey: s.key,
21
+ step: stepIdx,
22
+ firstOfSection: stepIdx === 0,
23
+ figure: isStoryFigure(s.figure) || typeof s.figure === "function" ? s.figure : null
24
+ });
25
+ });
26
+ });
27
+ const [active, setActive] = useState(0);
28
+ const refs = useRef([]);
29
+ useEffect(() => {
30
+ const io = new IntersectionObserver(
31
+ (entries) => {
32
+ entries.forEach((e) => {
33
+ if (e.isIntersecting) {
34
+ const i = Number(e.target.dataset.beat);
35
+ if (!Number.isNaN(i)) setActive(i);
36
+ }
37
+ });
38
+ },
39
+ { rootMargin: "-45% 0px -45% 0px", threshold: 0 }
40
+ // fire as a beat crosses the viewport centre
41
+ );
42
+ refs.current.forEach((el) => el && io.observe(el));
43
+ return () => io.disconnect();
44
+ }, [beats.length]);
45
+ const activeBeat = beats[active] || beats[0];
46
+ const live = beats.some((b) => b.figure !== null);
47
+ return /* @__PURE__ */ jsxs("div", { className: `scrolly${live ? " scrolly-live" : ""}`, children: [
48
+ /* @__PURE__ */ jsxs("div", { className: "scrolly-stage", children: [
49
+ activeBeat ? activeBeat.figure !== null ? isStoryFigure(activeBeat.figure) ? /* @__PURE__ */ jsx(FigureHost, { figure: activeBeat.figure, beat: activeBeat }) : /* @__PURE__ */ jsx("div", { className: "scrolly-stage-live", children: activeBeat.figure(activeBeat) }) : /* @__PURE__ */ jsx(SlideFigure, { html: activeBeat.html }, active) : null,
50
+ /* @__PURE__ */ jsx("div", { className: "scrolly-rail", "aria-hidden": "true", children: beats.map((b, i) => /* @__PURE__ */ jsx("span", { className: i === active ? "on" : "" }, b.key)) })
51
+ ] }),
52
+ /* @__PURE__ */ jsx("div", { className: "scrolly-flow", children: beats.map((b, i) => /* @__PURE__ */ jsxs("div", { className: "scrolly-beat", "data-beat": i, ref: (el) => refs.current[i] = el, children: [
53
+ b.firstOfSection ? /* @__PURE__ */ jsxs(Fragment, { children: [
54
+ /* @__PURE__ */ jsx("p", { className: "eyebrow", children: b.label }),
55
+ /* @__PURE__ */ jsx("h2", { children: b.heading }),
56
+ b.body ? /* @__PURE__ */ jsx("div", { className: "prose", dangerouslySetInnerHTML: { __html: b.body } }) : null
57
+ ] }) : null,
58
+ /* @__PURE__ */ jsx("div", { className: "scrolly-beat-figure", children: /* @__PURE__ */ jsx(SlideFigure, { html: b.html }) })
59
+ ] }, b.key)) })
60
+ ] });
61
+ }
62
+ const UNREACHED = "this figure could not move to this beat";
63
+ const useMountEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
64
+ function FigureHost({ figure, beat }) {
65
+ const host = useRef(null);
66
+ const [said, setSaid] = useState(null);
67
+ useMountEffect(() => {
68
+ figure.mount(host.current);
69
+ return () => figure.dispose();
70
+ }, [figure]);
71
+ const { key, index, sectionKey, step } = beat;
72
+ useEffect(() => {
73
+ let current = true;
74
+ new Promise((resolve) => resolve(figure.seek({ key, index, sectionKey, step }))).then(
75
+ (answer) => {
76
+ if (current) setSaid(typeof answer === "string" && answer !== "" ? answer : null);
77
+ },
78
+ () => {
79
+ if (current) setSaid(UNREACHED);
80
+ }
81
+ );
82
+ return () => {
83
+ current = false;
84
+ };
85
+ }, [figure, key, index, sectionKey, step]);
86
+ return /* @__PURE__ */ jsxs("div", { className: "scrolly-stage-live", children: [
87
+ /* @__PURE__ */ jsx("div", { className: "scrolly-figure", ref: host }),
88
+ said === null ? null : /* @__PURE__ */ jsx("p", { className: "scrolly-figure-said", role: "status", children: said })
89
+ ] });
90
+ }
91
+ export {
92
+ ScrollyView as default
93
+ };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { useBasePath } from "./context.js";
5
+ import { idleTour } from "./tour.js";
6
+ import { focusRuntime } from "./focusHtml.js";
7
+ function SlideDeck({ steps, tour }) {
8
+ const host = useRef(null);
9
+ const [copied, setCopied] = useState(false);
10
+ const basePath = useBasePath();
11
+ const tourKey = tour ? JSON.stringify(tour) : "";
12
+ function copySlideLink() {
13
+ try {
14
+ navigator.clipboard.writeText(window.location.href);
15
+ } catch (_) {
16
+ }
17
+ setCopied(true);
18
+ setTimeout(() => setCopied(false), 1600);
19
+ }
20
+ useEffect(() => {
21
+ const el = host.current;
22
+ if (!el) return;
23
+ focusRuntime();
24
+ const inner = (steps || []).join("\n");
25
+ el.innerHTML = `<deck-stage no-rail width="1920" height="1080">${inner}</deck-stage>`;
26
+ if (!document.querySelector("script[data-deck-stage]")) {
27
+ const sc = document.createElement("script");
28
+ sc.src = `${basePath}/deck-stage.js`;
29
+ sc.setAttribute("data-deck-stage", "");
30
+ document.body.appendChild(sc);
31
+ }
32
+ }, [steps]);
33
+ useEffect(() => {
34
+ const stage = host.current?.querySelector("deck-stage");
35
+ if (!tourKey || !stage) return void 0;
36
+ const t = idleTour(stage, typeof tour === "string" ? { stops: tour } : tour);
37
+ return () => t.stop();
38
+ }, [steps, tourKey]);
39
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
40
+ /* @__PURE__ */ jsx("div", { className: "deck-shell deck-scope", ref: host }),
41
+ /* @__PURE__ */ jsxs("div", { className: "deck-actions", children: [
42
+ /* @__PURE__ */ jsx("p", { className: "deck-hint", children: "\u2190 / \u2192 to navigate \xB7 Home / End to jump" }),
43
+ /* @__PURE__ */ jsx("button", { type: "button", className: "deck-copy", onClick: copySlideLink, children: copied ? "link copied \u2713" : "Copy link to this slide" })
44
+ ] })
45
+ ] });
46
+ }
47
+ export {
48
+ SlideDeck as default
49
+ };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ function SlideFigure({ html }) {
5
+ const box = useRef(null);
6
+ const canvas = useRef(null);
7
+ useEffect(() => {
8
+ const el = box.current, c = canvas.current;
9
+ if (!el || !c) return;
10
+ const fit = () => {
11
+ const s = el.clientWidth / 1920;
12
+ c.style.transform = `scale(${s})`;
13
+ el.style.height = 1080 * s + "px";
14
+ };
15
+ fit();
16
+ const ro = new ResizeObserver(fit);
17
+ ro.observe(el);
18
+ return () => ro.disconnect();
19
+ }, []);
20
+ return /* @__PURE__ */ jsx("div", { className: "slide-figure deck-scope", ref: box, "aria-hidden": "true", children: /* @__PURE__ */ jsx("div", { className: "slide-figure-canvas", ref: canvas, dangerouslySetInnerHTML: { __html: html } }) });
21
+ }
22
+ export {
23
+ SlideFigure as default
24
+ };
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useTheme } from "./useTheme.js";
4
+ function ThemeToggle() {
5
+ const { theme, toggle } = useTheme();
6
+ return /* @__PURE__ */ jsx("button", { className: "theme-toggle", onClick: toggle, "aria-label": `Switch to ${theme === "dark" ? "light" : "dark"} theme`, children: theme === "dark" ? /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: [
7
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4" }),
8
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19" })
9
+ ] }) : /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" }) }) });
10
+ }
11
+ export {
12
+ ThemeToggle as default
13
+ };
@@ -0,0 +1,25 @@
1
+ import { renderMarkdown, splitBodyByMarkers } from "./markdown.js";
2
+ import { compositeSlides } from "../sections.js";
3
+ function assemblePost({ meta, sections, bodyMd, deckSlides }) {
4
+ const byLabel = new Map(compositeSlides(deckSlides).map((s) => [s.label, s.html]));
5
+ const bodies = splitBodyByMarkers(bodyMd || "");
6
+ const builtSections = sections.map((s) => {
7
+ const steps = (s.slides || []).map((label) => byLabel.get(label)).filter(Boolean);
8
+ return {
9
+ key: s.key,
10
+ label: s.label || s.key,
11
+ heading: s.heading || s.label || s.key,
12
+ steps,
13
+ body: bodies[s.key] ? renderMarkdown(bodies[s.key]) : ""
14
+ };
15
+ });
16
+ return {
17
+ ...meta,
18
+ sections: builtSections,
19
+ deckSteps: deckSlides.map((s) => s.html)
20
+ // full deck, in order, for Watch
21
+ };
22
+ }
23
+ export {
24
+ assemblePost
25
+ };
@@ -0,0 +1,23 @@
1
+ import MarkdownIt from "markdown-it";
2
+ const md = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: false });
3
+ function renderMarkdown(src) {
4
+ return md.render(String(src || "").trim());
5
+ }
6
+ function splitBodyByMarkers(bodyMd) {
7
+ const out = {};
8
+ const re = /<!--\s*section:\s*([\w-]+)\s*-->/g;
9
+ const parts = [];
10
+ let m, last = null, lastIdx = 0;
11
+ while ((m = re.exec(bodyMd)) !== null) {
12
+ if (last !== null) parts.push([last, bodyMd.slice(lastIdx, m.index)]);
13
+ last = m[1];
14
+ lastIdx = re.lastIndex;
15
+ }
16
+ if (last !== null) parts.push([last, bodyMd.slice(lastIdx)]);
17
+ for (const [key, text] of parts) out[key] = text.trim();
18
+ return out;
19
+ }
20
+ export {
21
+ renderMarkdown,
22
+ splitBodyByMarkers
23
+ };
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const StoryDeckContext = createContext({ basePath: "" });
5
+ function StoryDeckProvider({ basePath = "", children }) {
6
+ return /* @__PURE__ */ jsx(StoryDeckContext.Provider, { value: { basePath }, children });
7
+ }
8
+ function useBasePath() {
9
+ return useContext(StoryDeckContext).basePath;
10
+ }
11
+ export {
12
+ StoryDeckProvider,
13
+ useBasePath
14
+ };
package/dist/figure.js ADDED
@@ -0,0 +1,6 @@
1
+ function isStoryFigure(value) {
2
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.mount === "function" && typeof value.seek === "function" && typeof value.dispose === "function";
3
+ }
4
+ export {
5
+ isStoryFigure
6
+ };