storyink 0.1.0 → 0.2.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/README.md +24 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/chunks/{index-3y6t38gc.js → index-aq1sxjmx.js} +59 -16
- package/dist/chunks/index-xjaczbn5.js +5790 -0
- package/dist/cli.js +4702 -2874
- package/dist/core/index.js +18 -2
- package/dist/index.js +30 -6
- package/dist/node/index.js +2 -2
- package/dist/types/core/index.d.ts +4 -0
- package/dist/types/core/layout/nodes.d.ts +8 -0
- package/dist/types/core/render/App.d.ts +9 -0
- package/dist/types/core/render/Diagram.d.ts +4 -2
- package/dist/types/core/render/Story.d.ts +54 -0
- package/dist/types/core/render/index.d.ts +2 -0
- package/dist/types/core/scene.d.ts +13 -0
- package/dist/types/core/spec.d.ts +48 -2
- package/dist/types/core/story/auto.d.ts +12 -0
- package/dist/types/core/story/compile.d.ts +24 -0
- package/dist/types/core/story/ease.d.ts +22 -0
- package/dist/types/core/story/state.d.ts +36 -0
- package/dist/types/core/story/types.d.ts +142 -0
- package/dist/types/generated/meta.d.ts +1 -1
- package/dist/types/generated/rolling.d.ts +2 -0
- package/dist/types/generated/viewer.d.ts +1 -1
- package/dist/types/node/snapshot.d.ts +17 -4
- package/dist/types/theme/tokens.d.ts +71 -0
- package/docs/spec.md +46 -0
- package/examples/checkout.architecture.json +78 -2
- package/examples/oauth.sequence.json +197 -22
- package/package.json +5 -4
- package/schema/storyink.schema.json +226 -1
- package/skill/SKILL.md +29 -0
- package/dist/chunks/index-sd10277m.js +0 -4017
- package/docs/gallery/agent-run.lifecycle.dark.png +0 -0
- package/docs/gallery/agent-run.lifecycle.light.png +0 -0
- package/docs/gallery/analytics.dataflow.dark.png +0 -0
- package/docs/gallery/analytics.dataflow.light.png +0 -0
- package/docs/gallery/cache.sequence.dark.png +0 -0
- package/docs/gallery/cache.sequence.light.png +0 -0
- package/docs/gallery/checkout.architecture.dark.png +0 -0
- package/docs/gallery/checkout.architecture.light.png +0 -0
- package/docs/gallery/incident.flowchart.dark.png +0 -0
- package/docs/gallery/incident.flowchart.light.png +0 -0
- package/docs/gallery/oauth.sequence.dark.png +0 -0
- package/docs/gallery/oauth.sequence.light.png +0 -0
- package/docs/gallery/order.state.dark.png +0 -0
- package/docs/gallery/order.state.light.png +0 -0
- package/docs/gallery/release.workflow.dark.png +0 -0
- package/docs/gallery/release.workflow.light.png +0 -0
- /package/dist/chunks/{index-6hdgvcdm.js → index-90zf42zn.js} +0 -0
package/README.md
CHANGED
|
@@ -34,10 +34,10 @@ both and has the OpenCode plugin as its default export. Runs on Node ≥ 20 and
|
|
|
34
34
|
## 2. CLI
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
storyink render <in.json|in.mmd|-> [-o out.html] [--svg out.svg] [--theme light|dark]
|
|
37
|
+
storyink render <in.json|in.mmd|-> [-o out.html] [--svg out.svg] [--theme light|dark] [--story auto]
|
|
38
38
|
storyink mermaid <in.mmd> [-o out.json]
|
|
39
39
|
storyink validate <in> [--json]
|
|
40
|
-
storyink snapshot <out.html> [--theme light,dark] [--width N] [--no-sheet] [--scale 2] [-o dir] [--json]
|
|
40
|
+
storyink snapshot <out.html> [--theme light,dark] [--width N] [--sheet [themes|beats]|--no-sheet] [--at 0.5,1.2,end] [--scale 2] [-o dir] [--json]
|
|
41
41
|
storyink skill
|
|
42
42
|
```
|
|
43
43
|
|
|
@@ -90,6 +90,27 @@ npx storyink skill # prints the SKILL.md path and its content
|
|
|
90
90
|
|
|
91
91
|
An MCP server is planned.
|
|
92
92
|
|
|
93
|
+
## Storyboards (opt-in)
|
|
94
|
+
|
|
95
|
+
Add a `story` to play a diagram as a sequence of beats in the HTML viewer:
|
|
96
|
+
- nodes reveal;
|
|
97
|
+
- wires draw on under a travelling pulse;
|
|
98
|
+
- arrivals glow;
|
|
99
|
+
- captions type in;
|
|
100
|
+
- counters roll (via [@kitlangton/rolling-number](https://github.com/kitlangton/rolling-number)).
|
|
101
|
+
|
|
102
|
+
`"story": "auto"` (or `--story auto`) derives the beats from the graph or message order. Playback
|
|
103
|
+
has a click-to-play gate, play/pause, a tape-rewind replay and a scrubber with step and chapter
|
|
104
|
+
ticks. Space, ←/→ and R control it. When reduced motion is on, the viewer jumps to the final frame.
|
|
105
|
+
|
|
106
|
+
The final frame is always the static diagram. Every frame is a pure function of time
|
|
107
|
+
(`storyState(scene, timeline, t)`), so `#t=2.5` seeks exactly and
|
|
108
|
+
`storyink snapshot --at 1,2.5,end --sheet beats` renders stills and a beat contact sheet. The
|
|
109
|
+
receipt gates check that the end frame and the reduced-motion page match the static diagram. See
|
|
110
|
+
[docs/spec.md](docs/spec.md#storyboard-story-opt-in).
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
93
114
|
## Gallery
|
|
94
115
|
|
|
95
116
|
`bun run gallery` renders every example and Mermaid sample and writes one light and one dark PNG per
|
|
@@ -100,6 +121,7 @@ example to `docs/gallery/`.
|
|
|
100
121
|
|  |  |
|
|
101
122
|
|  |  |
|
|
102
123
|
|  |  |
|
|
124
|
+
|  |  |
|
|
103
125
|
|
|
104
126
|
## Spec
|
|
105
127
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -20,6 +20,18 @@ MIT License. Copyright (c) 2024 Motion B.V.; Copyright (c) 2018 Framer B.V. <htt
|
|
|
20
20
|
The viewer bundle inlined into each HTML file starts with a comment banner that carries
|
|
21
21
|
these notices and the MIT permission text.
|
|
22
22
|
|
|
23
|
+
## @kitlangton/rolling-number (bundled into the HTML viewer)
|
|
24
|
+
|
|
25
|
+
MIT License. Copyright (c) 2026 Kit Langton. <https://github.com/kitlangton/rolling-number>
|
|
26
|
+
Used unmodified from npm for live counter reels; its stylesheet is inlined when a story has
|
|
27
|
+
counters. The notice is also in the viewer bundle banner.
|
|
28
|
+
|
|
29
|
+
## Storyboard design
|
|
30
|
+
|
|
31
|
+
Timings and curves (beats, springs, pulse phases, rewind, gate) follow the numbers documented in
|
|
32
|
+
our own style study of the "OpenCode Reloaded" figures; the runtime is an independent
|
|
33
|
+
implementation. No code, shaders or figure content from anoma.ly are included.
|
|
34
|
+
|
|
23
35
|
## archify (design reference)
|
|
24
36
|
|
|
25
37
|
The spec's overall shape (one JSON document per diagram with `type`, `nodes`/`edges`/`groups`,
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
renderHtml,
|
|
5
5
|
renderSvg,
|
|
6
6
|
validate
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-xjaczbn5.js";
|
|
8
8
|
|
|
9
9
|
// src/node/index.ts
|
|
10
10
|
import fs3 from "node:fs";
|
|
@@ -187,12 +187,15 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
187
187
|
const scale = opts.scale ?? 1;
|
|
188
188
|
const timeoutMs = opts.timeoutMs ?? HARD_TIMEOUT;
|
|
189
189
|
const budget = opts.budgetMs ?? 3000;
|
|
190
|
-
const
|
|
190
|
+
const tl = scene.timeline;
|
|
191
|
+
const headerH = (scene.subtitle ? 128 : 100) + (tl ? 54 : 0);
|
|
192
|
+
const ats = opts.at?.length ? opts.at : opts.t ? [opts.t === "end" ? "end" : Number(opts.t)] : ["end"];
|
|
193
|
+
const tq = (at) => tl ? `&t=${at === "end" ? "end" : +at.toFixed(3)}` : "";
|
|
194
|
+
const sheetMode = opts.sheet === false ? false : opts.sheet === "beats" ? "beats" : "themes";
|
|
191
195
|
const W = Math.round(Math.max(500, opts.width ?? Math.min(1600, vb.w + 64)));
|
|
192
196
|
const s = Math.min(1, (W - 64) / vb.w);
|
|
193
197
|
const H = Math.round(headerH + vb.h * s + 64 + 8);
|
|
194
198
|
const url = (hash) => `${pathToFileURL(abs).href}#${hash}`;
|
|
195
|
-
const tHash = opts.t ? `&t=${encodeURIComponent(opts.t)}` : "";
|
|
196
199
|
const baseFlags = [
|
|
197
200
|
...browser.flavor === "chrome" ? ["--headless=new"] : [],
|
|
198
201
|
"--no-first-run",
|
|
@@ -214,7 +217,11 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
214
217
|
const shoot = async (hash, png, w, h) => {
|
|
215
218
|
fs2.rmSync(png, { force: true });
|
|
216
219
|
const args = [...baseFlags, fresh(), `--window-size=${w},${h}`, `--screenshot=${png}`, url(hash)];
|
|
217
|
-
|
|
220
|
+
let r = await run(browser.path, args, { timeoutMs, signal: opts.signal });
|
|
221
|
+
if ((!fs2.existsSync(png) || fs2.statSync(png).size === 0) && !opts.signal?.aborted) {
|
|
222
|
+
const retry = [...baseFlags, fresh(), `--window-size=${w},${h}`, `--screenshot=${png}`, url(hash)];
|
|
223
|
+
r = await run(browser.path, retry, { timeoutMs, signal: opts.signal });
|
|
224
|
+
}
|
|
218
225
|
if (!fs2.existsSync(png) || fs2.statSync(png).size === 0)
|
|
219
226
|
throw new Error(`screenshot failed (${r.ms} ms): ${r.stderr.split(`
|
|
220
227
|
`).filter((l) => l.trim()).slice(-3).join(" | ")}`);
|
|
@@ -223,19 +230,53 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
223
230
|
const gates = [];
|
|
224
231
|
const captures = [];
|
|
225
232
|
let sheetCap;
|
|
233
|
+
let beatCaps = [];
|
|
226
234
|
let lint;
|
|
227
235
|
try {
|
|
228
|
-
for (const theme of themes)
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
for (const theme of themes)
|
|
237
|
+
for (const at of ats) {
|
|
238
|
+
const tag = at === "end" ? "" : `.t${+at.toFixed(2)}`;
|
|
239
|
+
const png = path2.join(outDir, `${base}.${theme}${tag}.png`);
|
|
240
|
+
const ms = await shoot(`theme=${theme}&chrome=0${tq(at)}`, png, W, H);
|
|
241
|
+
captures.push({ theme, at, png, sha256: sha256(png), bytes: fs2.statSync(png).size, width: W * scale, height: H * scale, ms });
|
|
242
|
+
}
|
|
233
243
|
const first = captures[0];
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
const midT = tl ? ats.find((a) => a !== "end") ?? +(tl.duration / 2).toFixed(3) : "end";
|
|
245
|
+
const a1 = path2.join(tmpRoot, "same-1.png");
|
|
246
|
+
const a2 = path2.join(tmpRoot, "same-2.png");
|
|
247
|
+
await shoot(`theme=${first.theme}&chrome=0${tq(midT)}`, a1, W, H);
|
|
248
|
+
await shoot(`theme=${first.theme}&chrome=0${tq(midT)}`, a2, W, H);
|
|
249
|
+
const same = sha256(a1) === sha256(a2);
|
|
250
|
+
gates.push({ name: "deterministic", pass: same, detail: same ? `${first.theme} at t=${midT} captured twice: identical` : `${first.theme} at t=${midT} differs between runs` });
|
|
251
|
+
if (tl) {
|
|
252
|
+
const stat = path2.join(tmpRoot, "static.png");
|
|
253
|
+
const end = path2.join(tmpRoot, "end.png");
|
|
254
|
+
const red = path2.join(tmpRoot, "reduced.png");
|
|
255
|
+
await shoot(`theme=${first.theme}&chrome=0&static=1`, stat, W, H);
|
|
256
|
+
await shoot(`theme=${first.theme}&chrome=0&t=end`, end, W, H);
|
|
257
|
+
await shoot(`theme=${first.theme}&chrome=0&motion=reduced`, red, W, H);
|
|
258
|
+
const sStat = sha256(stat);
|
|
259
|
+
gates.push({ name: "end=static", pass: sha256(end) === sStat, detail: sha256(end) === sStat ? "t=end matches the static diagram" : "t=end differs from the static diagram" });
|
|
260
|
+
gates.push({ name: "reduced=static", pass: sha256(red) === sStat, detail: sha256(red) === sStat ? "reduced motion shows the static diagram" : "reduced motion differs from the static diagram" });
|
|
261
|
+
}
|
|
262
|
+
const beats = [];
|
|
263
|
+
if (sheetMode === "beats" && tl) {
|
|
264
|
+
const bw = 1440;
|
|
265
|
+
for (const theme of themes) {
|
|
266
|
+
const probe = await run(browser.path, [...baseFlags, fresh(), `--window-size=${bw},900`, "--dump-dom", url(`theme=${theme}&chrome=0&sheet=beats`)], {
|
|
267
|
+
timeoutMs,
|
|
268
|
+
untilStdout: /<\/html>\s*$/,
|
|
269
|
+
signal: opts.signal
|
|
270
|
+
});
|
|
271
|
+
const mh = /<html[^>]*data-content-height="(\d+)"/.exec(probe.stdout);
|
|
272
|
+
const bh = mh ? Number(mh[1]) : 1800;
|
|
273
|
+
const png = path2.join(outDir, `${base}.beats.${theme}.png`);
|
|
274
|
+
const ms = await shoot(`theme=${theme}&chrome=0&sheet=beats`, png, bw, bh);
|
|
275
|
+
beats.push({ theme: "beats", png, sha256: sha256(png), bytes: fs2.statSync(png).size, width: bw * scale, height: bh * scale, ms });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
beatCaps = beats;
|
|
279
|
+
if (sheetMode === "themes" && themes.length > 1) {
|
|
239
280
|
const colW = Math.max(500, Math.min(1000, vb.w + 48));
|
|
240
281
|
const sw = colW * themes.length;
|
|
241
282
|
const sh = Math.round(headerH + 46 + vb.h * (colW - 48) / vb.w + 36);
|
|
@@ -243,7 +284,7 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
243
284
|
try {
|
|
244
285
|
if (process.env.STORYINK_SHEET === "ffmpeg")
|
|
245
286
|
throw new Error("forced ffmpeg sheet");
|
|
246
|
-
const ms = await shoot(`sheet=${themes.join(",")}&chrome=0
|
|
287
|
+
const ms = await shoot(`sheet=${themes.join(",")}&chrome=0`, png, sw, sh);
|
|
247
288
|
sheetCap = { theme: "sheet", png, sha256: sha256(png), bytes: fs2.statSync(png).size, width: sw * scale, height: sh * scale, ms };
|
|
248
289
|
} catch (e) {
|
|
249
290
|
const ff = ffmpegSheet(captures.map((c) => c.png), png);
|
|
@@ -253,7 +294,7 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
253
294
|
gates.push({ name: "sheet", pass: false, detail: String(e.message) });
|
|
254
295
|
}
|
|
255
296
|
}
|
|
256
|
-
const dom = await run(browser.path, [...baseFlags, fresh(), `--window-size=${W},${H}`, "--dump-dom", url(`theme=${themes[0]}&chrome=0`)], {
|
|
297
|
+
const dom = await run(browser.path, [...baseFlags, fresh(), `--window-size=${W},${H}`, "--dump-dom", url(`theme=${themes[0]}&chrome=0${tq("end")}`)], {
|
|
257
298
|
timeoutMs,
|
|
258
299
|
untilStdout: /<\/html>\s*$/,
|
|
259
300
|
signal: opts.signal
|
|
@@ -278,6 +319,8 @@ async function snapshot(htmlPath, opts = {}) {
|
|
|
278
319
|
flags: [...baseFlags, "--user-data-dir=<fresh tmp>", `--window-size=${W},${H}`],
|
|
279
320
|
captures,
|
|
280
321
|
...sheetCap ? { sheet: sheetCap } : {},
|
|
322
|
+
...beatCaps.length ? { beats: beatCaps } : {},
|
|
323
|
+
...tl ? { story: { duration: tl.duration, steps: tl.steps.length } } : {},
|
|
281
324
|
...lint ? { lint } : {},
|
|
282
325
|
gates,
|
|
283
326
|
ok: gates.every((g) => g.pass),
|