storyink 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 (41) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +158 -9
  3. package/dist/chunks/{index-rgqxg226.js → index-8dqzx1yf.js} +44 -5
  4. package/dist/chunks/{index-ac6c756n.js → index-pc811tpa.js} +3524 -847
  5. package/dist/cli.js +3691 -963
  6. package/dist/core/index.js +3 -1
  7. package/dist/index.js +8 -4
  8. package/dist/node/index.js +2 -2
  9. package/dist/types/core/layout/nodes.d.ts +7 -0
  10. package/dist/types/core/layout/overlays.d.ts +58 -0
  11. package/dist/types/core/layout/storyscan.d.ts +40 -0
  12. package/dist/types/core/page/css.d.ts +0 -1
  13. package/dist/types/core/page/types.d.ts +2 -2
  14. package/dist/types/core/render/App.d.ts +4 -0
  15. package/dist/types/core/render/Hud.d.ts +22 -0
  16. package/dist/types/core/render/css.d.ts +24 -2
  17. package/dist/types/core/render/index.d.ts +9 -0
  18. package/dist/types/core/render/smil.d.ts +1 -1
  19. package/dist/types/core/scene.d.ts +59 -0
  20. package/dist/types/core/spec.d.ts +93 -3
  21. package/dist/types/core/story/acts.d.ts +108 -0
  22. package/dist/types/core/story/compile.d.ts +20 -0
  23. package/dist/types/core/story/content-state.d.ts +9 -0
  24. package/dist/types/core/story/content.d.ts +47 -3
  25. package/dist/types/core/story/ease.d.ts +11 -0
  26. package/dist/types/core/story/state.d.ts +68 -2
  27. package/dist/types/core/story/types.d.ts +137 -9
  28. package/dist/types/core/suggest.d.ts +2 -0
  29. package/dist/types/generated/meta.d.ts +1 -1
  30. package/dist/types/generated/viewer.d.ts +1 -1
  31. package/dist/types/node/index.d.ts +2 -1
  32. package/dist/types/node/snapshot.d.ts +12 -3
  33. package/dist/types/theme/tokens.d.ts +14 -5
  34. package/dist/types/theme/tones.d.ts +38 -0
  35. package/docs/spec.md +232 -3
  36. package/examples/stories/payment-retry.acts.architecture.json +340 -0
  37. package/examples/stories/request-colours.dataflow.json +68 -0
  38. package/examples/stories/secret-broker.architecture.json +126 -0
  39. package/package.json +2 -1
  40. package/schema/storyink.schema.json +189 -6
  41. package/skill/SKILL.md +18 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - **Payload label track** — a pulse label now appears at departure parked clear of the source box,
6
+ holds until the dot catches up, moves with the dot, parks clear of the destination box and stays
7
+ fully visible until the dot touches it, then fades out over 0.2 s after arrival (was: faded out
8
+ before arrival and near every box). Multi-hop routes hand off between hops with a short fade
9
+ instead of drawing over the intermediate node. One rule (`pulseLabelWindows` /
10
+ `pulseLabelAnchor`) drives playback, the animated SVG and the labelled flight timing, whose
11
+ legible share is now the share where the label follows the dot.
12
+ - **Readable payload labels in flight** — a pulse with a `label` now flies slower and at a steady
13
+ cruise (not `inOutCubic`), so its text stays legible while it travels: reading budget 0.6 s +
14
+ 0.05 s per character over the legible share of the flight, clamped to 1.2–3.2 s, never faster
15
+ than before. An explicit pulse `duration` still wins; unlabelled pulses are unchanged
16
+ (byte-identical output). Shared by playback, step moves, snapshots and the animated SVG.
17
+ - **Tones: colour carries meaning** — five tones (`note` blue · `warn` gold · `risk` rose · `good`
18
+ sage · `neutral` ink), one source shared with page tones (`theme/tones.ts`). Pulses take
19
+ `tone`, a payload `label` (mono, rides beside the dot, fades clear of nodes; layer gaps widen for
20
+ it), `land: "edge"` (the label stays as the arriving edge's label), `tint` (the target takes the
21
+ tone on arrival) and `stain` (each wire keeps the tone once the dot has passed). New `tone` step
22
+ field `{ ids, to, stagger? }` for nodes (all kinds), groups and edges: tinted face, toned
23
+ stroke / tag / detail, toned wire / ports / label, toned group border; 0.35 s crossfade; `to:
24
+ null` clears; `stagger` sweeps. `set` works on plain graph nodes (`label`, `detail`, `tag`, and a
25
+ detail-line `tone`), `status` on plain nodes draws the glyph at the start of the detail line;
26
+ boxes fit every version (no resize mid-story). `glow: { ids, tone }` glows in a tone. Caption
27
+ `*emphasis*` in the accent (`\*` literal). Timeline `tones`, frame `tone` / `litTone`, pulse
28
+ `tone` / `label`, caption `em`; HTML viewer, static SVG, snapshots, beat sheets and the
29
+ animated SVG (opacity-only tone layers) agree. Diagnostics with did-you-mean (tones, ids, pulse
30
+ fields), warnings for clipped labels, `tint` / `stain` without a tone, `land` without a label,
31
+ no-op tones, plain nodes still running at the end. Schema, docs ("Tones"), example
32
+ `examples/stories/request-colours.dataflow.json`. Specs without these features render
33
+ byte-identically.
34
+ - **Acts: problem → rewind → fix** — `story.acts` (2–6 `{ id, label, tone? }`) and step fields
35
+ `act` / `enter` (`rewind` default · `cut` · `continue`) tell several scenarios on one stage
36
+ (graph diagrams). Elements say `in: [acts]`, or `delta` decides (removed → first act, added →
37
+ later acts); act stories drop the delta look. Positions are shared; each edge is routed only
38
+ against the boxes it coexists with, so a first-act wire runs straight through the slot of a
39
+ node the fix inserts (lint ignores pairs that never coexist). A rewind plays the previous act
40
+ backwards (eased, `clamp(len / 4, 0.6, 1.5)` s, captions hidden; HTML glitch + blur, plain in
41
+ the animated SVG), a cut dips; then a morph retracts leaving wires, fades leaving boxes, reveals
42
+ entering ones and draws entering wires. Later acts start from the previous act's start state.
43
+ Act chip ("● label", "◀◀ rewind") on the HTML stage and in the animated SVG header. `#act=<id>`,
44
+ `#sheet=acts`, `snapshot --act / --sheet acts` (tool `act`, `sheet: "acts"`), `render --svg
45
+ --act`. Timeline `acts`, frame `act`. Diagnostics with did-you-mean. Examples
46
+ `examples/stories/secret-broker.architecture.json` and
47
+ `examples/stories/payment-retry.acts.architecture.json`.
48
+ - **Overlays: annotations, toasts, HUD** — `annotations: [{ id, on, side?, text, tone?, in? }]`:
49
+ one mono line on a node's left edge, above or below it, its room reserved by the layout (edge
50
+ labels avoid it); story `reveal` / `hide` / `show` / `type` (char or word, caret) / `set` (`text`,
51
+ `tone`) / `clear` / `tone` take annotation ids; a typed-first annotation is hidden until typed.
52
+ Step `toast` (`{ id?, near, title?, text, tone?, for? }`) and `dismiss` (ids or `"all"`):
53
+ floating cards placed deterministically at layout time (above the anchor, then shifted, then a
54
+ row higher, then below; never over nodes, groups, edge labels, annotations or toasts up at the
55
+ same time; the viewBox grows to fit), fade + rise + scale in, fade out; `tone` steps reach them;
56
+ warnings for toasts still up when an act or the story ends. `story.hud: [{ id, label, value?,
57
+ tone?, prefix?, suffix?, at? }]`: stage metrics driven by `counter` steps, toned and revealed
58
+ like elements; HTML stage overlay below the act chip, a header row in the animated SVG, a line on
59
+ beat / act sheet tiles. Rewinds and act resets cover all three. Timeline `toasts` / `hud`, frame
60
+ `toasts`; schema, docs ("Annotations", "Toasts", "HUD metrics"). The secret-broker and
61
+ payment-retry examples use them; `bun run gallery:stories` builds a local showcase
62
+ (`examples/stories/out/index.html`). Specs without these features render byte-identically.
63
+
3
64
  ## 0.5.0
4
65
 
5
66
  - **Scrollytelling and slides: viewer** — figure API `beats()` and `moveTo({ beat } | { t },
package/README.md CHANGED
@@ -69,6 +69,11 @@ It also works without JavaScript.
69
69
  <td valign="top"><a href="#slides"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/pr-review.deck.slide3-build4.dark.png"><img alt="Present mode: slide 3 of 8 mid-build, the diff node lit and the new queue highlighted" src="docs/gallery/pr-review.deck.slide3-build4.light.png" width="260"></picture></a><br><b>Slides</b><br><sub>present any page; builds step the figure</sub></td>
70
70
  <td valign="top"><a href="#animated-svg-readmes-and-prs"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/auth-session-to-jwt.sequence.animated.dark.svg"><img alt="Sequence change story, animated: session lookups replaced by a signed JWT" src="docs/gallery/auth-session-to-jwt.sequence.animated.light.svg" width="260"></picture></a><br><b>Animated SVG</b><br><sub>SMIL inside a plain <code>&lt;img&gt;</code></sub></td>
71
71
  </tr>
72
+ <tr>
73
+ <td valign="top"><a href="#tones-acts-and-overlays"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/request-colours.story.pulse.dark.png"><img alt="A gold pulse with its payload label charge $42.00 in flight" src="docs/gallery/request-colours.story.pulse.light.png" width="260"></picture></a><br><b>Tones and payload labels</b><br><sub>colour carries meaning</sub></td>
74
+ <td valign="top"><a href="#tones-acts-and-overlays"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/payment-retry.story.acts.dark.png"><img alt="Act sheet: today the order is lost, with a retry queue it is paid" src="docs/gallery/payment-retry.story.acts.light.png" width="260"></picture></a><br><b>Acts</b><br><sub>problem → rewind → fix</sub></td>
75
+ <td valign="top"><a href="#tones-acts-and-overlays"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/gallery/secret-broker.story.toasts.dark.png"><img alt="Approval toasts piling up with a prompts HUD" src="docs/gallery/secret-broker.story.toasts.light.png" width="260"></picture></a><br><b>Toasts, annotations, HUD</b><br><sub>overlays on the stage</sub></td>
76
+ </tr>
72
77
  </table>
73
78
 
74
79
  ## Diagram types
@@ -247,6 +252,130 @@ Spec: [storyboard](docs/spec.md#storyboard-story-opt-in), [beats](docs/spec.md#b
247
252
  [motion](docs/spec.md#motion-mode-storymotion), [reduced motion](docs/spec.md#reduced-motion-play-steps),
248
253
  [follow camera](docs/spec.md#follow-camera-storycamera).
249
254
 
255
+ ## Tones, acts and overlays
256
+
257
+ > **Available since 0.6.0.** Everything in this section needs storyink 0.6.0 or later; earlier
258
+ > versions do not know these fields. See [CHANGELOG.md](CHANGELOG.md#060).
259
+
260
+ Three example stories in [examples/stories/](examples/stories) show the new pieces:
261
+ [request-colours](examples/stories/request-colours.dataflow.json) (tones and payload labels),
262
+ [secret-broker](examples/stories/secret-broker.architecture.json) (acts, toasts, annotation, HUD)
263
+ and [payment-retry](examples/stories/payment-retry.acts.architecture.json) (a code change told as
264
+ two acts).
265
+
266
+ <picture>
267
+ <source media="(prefers-color-scheme: dark)" srcset="docs/gallery/request-colours.story.animated.dark.svg">
268
+ <img alt="One order, four colours: a blue request with its payload label, a gold charge, a rose decline that spreads, a sage response that lands on the wire (animated)" src="docs/gallery/request-colours.story.animated.light.svg">
269
+ </picture>
270
+
271
+ **Tones: colour carries meaning.** Five tones, the same as page tones: `note` (blue, data in
272
+ flight), `warn` (gold, pending), `risk` (rose, failure), `good` (sage, done), `neutral` (ink). A
273
+ pulse takes a `tone` and a payload `label`; `tint` colours its target on arrival, `stain` keeps the
274
+ tone on each wire it has passed, and `land: "edge"` leaves the label on the wire as its edge label.
275
+ The `tone` step colours nodes, groups, edges (and toasts, annotations, HUD metrics) with an
276
+ optional `stagger` for a contagion sweep; `to: null` clears. `set` / `status` work on plain graph
277
+ nodes (`detail` + `tone`, a spinner / check / cross on the detail line), and `*phrase*` in a caption
278
+ is drawn in the accent.
279
+
280
+ ```json
281
+ { "caption": "Gold: the order is *pending* while payments decides",
282
+ "status": { "id": "orders", "to": "running" },
283
+ "set": { "id": "orders", "detail": "charging card…", "tone": "warn" },
284
+ "pulse": { "edge": "orders->payments", "tone": "warn", "label": "charge $42.00", "tint": true } },
285
+ { "tone": { "ids": ["payments", "orders->payments", "orders->alerts"], "to": "risk", "stagger": 0.15 } },
286
+ { "pulse": { "route": ["client->gateway", "gateway->orders"], "reverse": true, "tone": "good",
287
+ "label": "201 Created", "stain": true, "land": "edge" } }
288
+ ```
289
+
290
+ <picture>
291
+ <source media="(prefers-color-scheme: dark)" srcset="docs/gallery/request-colours.story.pulse.dark.png">
292
+ <img alt="Mid-flight: the gold charge $42.00 label held clear of the payments box while the dot arrives" src="docs/gallery/request-colours.story.pulse.light.png" width="720">
293
+ </picture>
294
+
295
+ **Payload labels you can read in flight.** A label follows a *label track*: it **waits** parked
296
+ clear of the source box until the dot catches up, **follows** the dot, then **waits** clear of the
297
+ destination box until the dot touches it, and fades out after arrival (multi-hop routes hand off
298
+ between hops instead of drawing over the middle node). A labelled pulse also flies **slower**, at
299
+ a steady cruise: 0.6 s + 0.05 s per character of reading time over the part of the flight where
300
+ the label moves, clamped to 1.2–3.2 s and never faster than an unlabelled pulse; an explicit
301
+ `duration` wins. Labels over 32 characters are clipped with "…". Reduced motion has no flights, so
302
+ no labels.
303
+
304
+ <picture>
305
+ <source media="(prefers-color-scheme: dark)" srcset="docs/gallery/secret-broker.story.acts.dark.png">
306
+ <img alt="Act sheet: raw CLI (prompts: 4, the raw key in the agent's chat, rose) beside with broker (prompts: 0, a reference in the same slot, sage)" src="docs/gallery/secret-broker.story.acts.light.png">
307
+ </picture>
308
+
309
+ **Acts: problem → rewind → fix.** `story.acts` (2–6, graph diagrams) puts several scenarios on one
310
+ stage. A step with `act` starts the next one; by default (`"enter": "rewind"`) the previous act
311
+ plays backwards to its start, then the topology morphs in place (leaving wires retract, a node
312
+ fades in in the gap, new wires draw on); the act's own steps then send the request through the fix.
313
+ The rewind only plays the previous act backwards: nothing replays act 1's steps for you, so write
314
+ the second pass as steps of its own (`"cut"` and `"continue"` are the other entries). Elements say which acts they are in with `in: [actIds]`; in
315
+ an act story `delta: "removed"` means the first act only and `delta: "added"` every later act, so
316
+ a change diagram becomes a before / after (as in
317
+ [payment-retry](examples/stories/payment-retry.acts.architecture.json)). Positions come from one
318
+ layout of every act, so nothing jumps; each wire is routed only against the boxes it shares a
319
+ stage with. Act tiles have their own sheet and anchors: `#act=<id>` seeks to an act's settled end,
320
+ `#sheet=acts` / `snapshot --sheet acts` lays the acts side by side.
321
+
322
+ ```json
323
+ "nodes": [ { "id": "broker", "label": "secret broker", "in": ["after"] }, … ],
324
+ "edges": [ { "id": "ask", "from": "agent", "to": "cli", "in": ["before"] }, … ],
325
+ "story": {
326
+ "acts": [ { "id": "before", "label": "raw CLI", "tone": "risk" },
327
+ { "id": "after", "label": "with broker", "tone": "good" } ],
328
+ "steps": [ …, { "act": "after", "caption": "Same request, with a secret broker in between" }, … ]
329
+ }
330
+ ```
331
+
332
+ <picture>
333
+ <source media="(prefers-color-scheme: dark)" srcset="docs/gallery/secret-broker.story.toasts.dark.png">
334
+ <img alt="Four approval toasts piled up above the vault CLI, the act chip raw CLI and the HUD prompts: 4" src="docs/gallery/secret-broker.story.toasts.light.png" width="720">
335
+ </picture>
336
+
337
+ **Annotations, toasts and HUD metrics.** An **annotation** is one mono line attached above (or
338
+ below) a node; it can be typed, `set` to a new text and tone, and after a rewind the same slot can
339
+ hold the opposite (the leaked key in act 1, a reference in act 2). A **toast** is a small card near
340
+ a node (`toast` step, gone after `for` seconds or at a `dismiss`); slots are placed by the layout so
341
+ a pile never overlaps nodes, labels or each other. A **HUD** metric is a big `label: value` number
342
+ on the stage, driven by the existing `counter` step: the number that drops between the problem and
343
+ the fix.
344
+
345
+ ```json
346
+ "annotations": [ { "id": "chat", "on": "agent", "text": "chat ▸ sk-live-7Hq2Zx9mK4pL…", "tone": "risk" } ],
347
+ "story": {
348
+ "hud": [ { "id": "prompts", "label": "prompts", "value": 0 } ],
349
+ "steps": [
350
+ { "toast": { "id": "p1", "near": "cli", "title": "Allow access?", "text": "vault CLI wants 1 secret", "tone": "warn" },
351
+ "counter": { "id": "prompts", "to": 1 } },
352
+ { "dismiss": "all", "type": { "id": "chat", "cps": 24 } },
353
+ { "toast": { "near": "cli", "title": "approved once ✓", "text": "no more prompts this session", "tone": "good", "for": 2.8 } }
354
+ ]
355
+ }
356
+ ```
357
+
358
+ **Where each piece shows.** The HTML viewer, snapshots, beat sheets and the animated SVG share one
359
+ timeline, so rewinds, morphs, toasts and labels play everywhere a story plays. The act chip and the
360
+ HUD are pinned to the HTML stage; the animated SVG draws them in its header row; beat and act sheet
361
+ tiles show the HUD in their caption line. The **static SVG** has no header, so it shows neither (tones,
362
+ annotations and the end state are there); `render --svg x.svg --act <id>` writes one act's settled
363
+ end. `--act` applies only to `--svg` (open the HTML with `#act=<id>` instead). A
364
+ toast still up at the end of an act or the story is a warning.
365
+
366
+ ```sh
367
+ storyink render examples/stories/secret-broker.architecture.json -o broker.html \
368
+ --svg broker.before.svg --act before
369
+ storyink render examples/stories/secret-broker.architecture.json -o broker.html \
370
+ --animated-svg broker.svg --theme both
371
+ storyink snapshot broker.html --sheet acts -o shots
372
+ ```
373
+
374
+ Spec: [tones](docs/spec.md#tones-colour-carries-meaning), [acts](docs/spec.md#acts-problem--rewind--fix-storyacts),
375
+ [annotations](docs/spec.md#annotations-annotations), [toasts](docs/spec.md#toasts-toast-dismiss),
376
+ [HUD](docs/spec.md#hud-metrics-storyhud). All three examples, with captures and sheets:
377
+ `bun run gallery:stories` (writes `examples/stories/out/index.html`).
378
+
250
379
  ## Rich nodes and content steps
251
380
 
252
381
  For agent sessions, generated code and tool servers: `panel` (a window of rows), `code` (a
@@ -892,6 +1021,7 @@ to `https://unpkg.com/storyink/schema/storyink.schema.json`) for editor completi
892
1021
  | `direction` | `TB` (`TD`) `BT` `LR` `RL` | graphs; omit to pick TB or LR by aspect (closest to 16:10) |
893
1022
  | `style` | `{ "arrowheads": bool, "legend": bool }` | graph arrowheads (default off); the change legend (default: on when any element has a delta) |
894
1023
  | `story` | `"auto"`, `"changes"` or [an object](#story) | opt-in |
1024
+ | `annotations` | `[{ "id", "on", "side", "text", "tone", "in" }]` | graphs; one mono line on a node ([overlays](#tones-acts-and-overlays)) |
895
1025
  | `change` | `{ "base", "head", "title", "url" }` | `base → head` (else `title`) above the diagram |
896
1026
  | `changes` | object | written by `--changes`; not hand-written |
897
1027
  | `$schema` | string | ignored by the renderer |
@@ -919,6 +1049,7 @@ to `https://unpkg.com/storyink/schema/storyink.schema.json`) for editor completi
919
1049
  | `stack` | `chip` | 1–3 sheets peeking below |
920
1050
  | `diff` | `code` | unified hunk text, `{ "file", "lines", "context", "max" }`, or resolved `{ "file", "hunks" }` |
921
1051
  | `delta` `stat` `summary` `files` | all | [change diagrams](#change-diagrams) |
1052
+ | `in` | all | act ids the node is on stage in ([acts](#tones-acts-and-overlays)) |
922
1053
 
923
1054
  | edge field | value |
924
1055
  | --- | --- |
@@ -928,6 +1059,7 @@ to `https://unpkg.com/storyink/schema/storyink.schema.json`) for editor completi
928
1059
  | `style` | `solid` (default) `dashed` `thick` |
929
1060
  | `arrow` | `end` (default) `none` `both`; drawn with `"style": { "arrowheads": true }` |
930
1061
  | `delta` `emphasis` `summary` `files` | [change diagrams](#change-diagrams) |
1062
+ | `in` | act ids (also limited to the acts both ends are in) |
931
1063
 
932
1064
  | group field | value |
933
1065
  | --- | --- |
@@ -937,6 +1069,7 @@ to `https://unpkg.com/storyink/schema/storyink.schema.json`) for editor completi
937
1069
  | `direction` | inner layout direction (best-effort) |
938
1070
  | `bare` | label only, no box (a column heading) |
939
1071
  | `delta` | [change diagrams](#change-diagrams) |
1072
+ | `in` | act ids |
940
1073
 
941
1074
  ### Sequence
942
1075
 
@@ -965,6 +1098,8 @@ Message references (`start`, `end`, `after`) are 0-based indices or message ids.
965
1098
  | `camera` | `follow` `fit` | `follow` |
966
1099
  | `spotlight` | `true` or `"veil"` | off |
967
1100
  | `rewind` | `tape` `glitch` | `tape` (the HTML replay effect) |
1101
+ | `acts` | 2–6 `{ "id", "label", "tone" }` | none; graphs ([acts](#tones-acts-and-overlays)) |
1102
+ | `hud` | `[{ "id", "label", "value", "tone", "prefix", "suffix", "at" }]`; `at`: `top-right` `bottom-left` | none |
968
1103
 
969
1104
  `"story": "auto"` and `"story": "changes"` are shorthands for `{ "steps": "auto" }` and
970
1105
  `{ "steps": "changes" }`.
@@ -976,21 +1111,25 @@ Message references (`start`, `end`, `after`) are 0-based indices or message ids.
976
1111
  | `id` | string | names the step (beat sheets, `__storyink.steps`, scrolly `at`) |
977
1112
  | `at` | seconds, or `"+x"` | start time; default `"+0"` after the previous step ends; with reading holds an absolute `at` is a minimum |
978
1113
  | `reveal` | id or list | appears (fade and rise): nodes, groups, edges, notes `note-<i>`, frames `frame-<i>` |
979
- | `pulse` | edge id, `"from->to"`, a list, `{ "route": [...] }`, `{ "edge", "duration", "reverse", "delay" }` | a dot travels the wire and the target glows |
1114
+ | `pulse` | edge id, `"from->to"`, a list, `{ "route": [...] }`, `{ "edge", "duration", "reverse", "delay" }`; also `"tone"`, `"label"`, `"land": "edge"`, `"tint"`, `"stain"` | a dot travels the wire and the target glows |
980
1115
  | `highlight` | ids, or `{ "ids", "for" }` | flood glow and text flash |
981
1116
  | `caption` | string | a line under the title |
982
- | `counter` | `{ "id", "to" }` or a list | rolls a node counter |
1117
+ | `counter` | `{ "id", "to" }` or a list | rolls a node counter (or a HUD metric) |
983
1118
  | `stop` | string | a chapter: scrubber tick, beat-sheet title, Shift+→ / ← stop |
984
1119
  | `hold` | seconds, 0–60 | the reading hold after this beat (not scaled by `pace`) |
985
1120
  | `type` | id, or `{ "id", "by", "cps", "duration" }` | typewriter on a code node or a row (`by`: `char` `word`) |
986
- | `set` | `{ "id", "code" }`, `{ "id", "text", "detail", "tag" }`, `{ "id", "label" }` | crossfades to new content |
1121
+ | `set` | `{ "id", "code" }`, `{ "id", "text", "detail", "tag" }`, `{ "id", "label" }`; also `{ "id", "label", "detail", "tag", "tone" }` on a plain node, `{ "id", "text", "tone" }` on an annotation | crossfades to new content |
987
1122
  | `clear` | id or list | fades content out |
988
1123
  | `line` | `"code#2"`, `"code#2-4"`, `"pay#+14"`, `{ "id", "lines" }`, `{ "id", "hunk" }`, `{ "id", "off": true }` | the active-line bar |
989
- | `status` | `{ "id": "node#row", "to" }` | `none` `running` `done` `error` |
1124
+ | `status` | `{ "id": "node#row", "to" }` (also a plain node) | `none` `running` `done` `error` |
990
1125
  | `dim`, `undim` | ids, or `{ "ids", "to" }` | lower to a level (default 0.42) / back to full |
991
1126
  | `hide`, `show` | ids | take away / bring back something already there |
992
1127
  | `wire`, `unwire` | edge, or `{ "edge", "duration" }` | draw an edge on / retract it |
993
- | `glow`, `unglow` | ids | a persistent glow |
1128
+ | `glow`, `unglow` | ids (`glow` also `{ "ids", "tone" }`) | a persistent glow |
1129
+ | `tone` | `{ "ids", "to", "stagger" }` or a list; `to`: `note` `warn` `risk` `good` `neutral` or `null` | colours nodes, groups, edges, annotations, toasts, HUD metrics |
1130
+ | `toast` | `{ "id", "near", "title", "text", "tone", "for" }` or a list | a card near a node |
1131
+ | `dismiss` | toast ids or `"all"` | removes toasts |
1132
+ | `act`, `enter` | an act id; `rewind` (default) `cut` `continue` | starts that act |
994
1133
  | `focus` | id or list | where the follow camera, spotlight and veil aim |
995
1134
  | `change` | id or list | plays these elements' deltas |
996
1135
  | `apply` | id, `{ "id", "hunk", "cps" }`, or a list | plays a diff code node's change |
@@ -1028,6 +1167,7 @@ storyink render <in.json|in.mmd|-> [-o out.html] [--svg out.svg] [--theme light|
1028
1167
  [--motion full|reduced|system] story playback motion (default full; system = OS setting)
1029
1168
  [--camera follow|fit] viewer camera while playing (default follow)
1030
1169
  [--pace N] reading holds after each beat × N (default 0.6; 0 = none)
1170
+ [--act <id>] act stories: --svg shows that act's settled end
1031
1171
  [--animated-svg out.svg [--theme light|dark|both] [--once] [--font system|embed]]
1032
1172
  animated SVG (SMIL) for READMEs / PRs: plays inside <img>, no script
1033
1173
  [--changes changes.json|x.diff|x.patch] resolve files / stat / diff nodes from a diff
@@ -1038,7 +1178,7 @@ storyink diff [<range>|<base> [<head>]] [--staged] [--patch file|-] [-o changes.
1038
1178
  parse git diff (default: working tree vs merge base with main/master)
1039
1179
  storyink mermaid <in.mmd> [-o out.json]
1040
1180
  storyink validate <in|page.json> [--json]
1041
- storyink snapshot <out.html> [--theme light,dark] [--width N] [--sheet [themes|beats]|--no-sheet]
1181
+ storyink snapshot <out.html> [--theme light,dark] [--width N] [--sheet [themes|beats|acts]|--no-sheet] [--act <id>]
1042
1182
  [--at 0.5,1.2,end] [--motion reduced] [--camera follow] [--pace N] [--scale 2] [-o dir] [--json]
1043
1183
  [--rail] [--drawer <id|path>] narration rail / change drawer in the --at captures
1044
1184
  [--figure <id>] page HTML: snapshot one figure (default: full-page captures)
@@ -1061,7 +1201,7 @@ input (`x.json` → `x.html`). `--theme both` needs `--animated-svg`. `--pace` t
1061
1201
  | `storyink_diff` | `range`, `base`, `head`, `staged`, `patch` (diff text), `paths` (pathspecs), `output` |
1062
1202
  | `storyink_from_mermaid` | `mermaid` (required), `output` |
1063
1203
  | `storyink_validate` | `spec`, `mermaid` or `path` |
1064
- | `storyink_snapshot` | `html` (required); `themes`; `width`; `outDir`; `at`; `sheet` (`themes` `beats` `none`); `image` (`overview` `full` `none`); `maxImageSize` (256–2048, default 1024); `motion` (`full` `reduced`); `camera` (`fit` `follow`); `pace`; `rail`; `drawer`; `figure`; `slides`; `builds`; `scrolly` |
1204
+ | `storyink_snapshot` | `html` (required); `themes`; `width`; `outDir`; `at`; `sheet` (`themes` `beats` `acts` `none`); `act`; `image` (`overview` `full` `none`); `maxImageSize` (256–2048, default 1024); `motion` (`full` `reduced`); `camera` (`fit` `follow`); `pace`; `rail`; `drawer`; `figure`; `slides`; `builds`; `scrolly` |
1065
1205
 
1066
1206
  ### Viewer keys
1067
1207
 
@@ -1096,7 +1236,8 @@ otherwise), the wheel scrolls the page and Ctrl / ⌘ + wheel or a pinch zooms.
1096
1236
  | `camera` | `follow` `fit` | the follow camera; with `t`, `follow` shows the followed view |
1097
1237
  | `pace` | a number | the reading-hold pace |
1098
1238
  | `static` | `1` | story runtime off, the final frame (page-wide on pages) |
1099
- | `sheet` | `light,dark` or `beats` | both themes side by side, or one tile per beat |
1239
+ | `sheet` | `light,dark`, `beats` or `acts` | both themes side by side, one tile per beat, or one tile per act |
1240
+ | `act` | an act id | seeks, paused, to that act's settled end |
1100
1241
  | `rail` | `0` `1` | hides / shows the narration rail |
1101
1242
  | `drawer` | an element id or file path | opens the drawer on load |
1102
1243
  | `fig` | a figure id | pages: scopes `t`, `camera`, `motion`, `pace`, `drawer`, `rail`, `autoplay` and `static` to that figure |
@@ -1161,7 +1302,7 @@ From npm, pinned or not:
1161
1302
 
1162
1303
  ```jsonc
1163
1304
  // opencode.json (global ~/.config/opencode/ or project .opencode/)
1164
- { "plugins": ["storyink"] } // or "storyink@0.5.0"
1305
+ { "plugins": ["storyink"] } // or "storyink@0.6.0"
1165
1306
  ```
1166
1307
 
1167
1308
  From a local checkout, for development: build first, then point `plugins` at the **directory**
@@ -1243,6 +1384,10 @@ file):
1243
1384
  [batch-email.changes](docs/gallery/batch-email.changes.dataflow.animated.light.svg) · [auth-session-to-jwt](docs/gallery/auth-session-to-jwt.sequence.animated.light.svg) · [payment-retry](docs/gallery/payment-retry.architecture.animated.light.svg) ·
1244
1385
  [etl-dedupe](docs/gallery/etl-dedupe.dataflow.animated.light.svg) · [cache-layer](docs/gallery/cache-layer.architecture.animated.light.svg) · [rate-limit-plugin](docs/gallery/rate-limit-plugin.architecture.animated.light.svg) ·
1245
1386
  [storyink-core](docs/gallery/storyink-core.architecture.animated.light.svg) (resolved with `--changes`)
1387
+ - **Tones, acts and overlays** (`bun run gallery:stories --docs`): [request-colours animated](docs/gallery/request-colours.story.animated.light.svg) ·
1388
+ [request-colours mid-flight](docs/gallery/request-colours.story.pulse.light.png) · [secret-broker animated](docs/gallery/secret-broker.story.animated.light.svg) ·
1389
+ [secret-broker act sheet](docs/gallery/secret-broker.story.acts.light.png) · [secret-broker toasts](docs/gallery/secret-broker.story.toasts.light.png) ·
1390
+ [payment-retry act sheet](docs/gallery/payment-retry.story.acts.light.png)
1246
1391
 
1247
1392
  `bun run gallery` renders every example and Mermaid sample and writes a light and a dark PNG per
1248
1393
  example to `docs/gallery/`, plus a beat sheet for each story (`storyink-*` change examples are
@@ -1250,6 +1395,9 @@ resolved against [the 0.4.0 diff](examples/changes/storyink-0.4.0.changes.json))
1250
1395
  `bun run gallery:animated` writes the animated SVGs of the story examples
1251
1396
  (`*.animated.light.svg` / `*.animated.dark.svg`), and `bun run gallery:showcase` the PNGs of the
1252
1397
  HTML-only features (rail, drawer, page, scrolly, slides). `--only name,…` writes just those.
1398
+ `bun run gallery:stories` renders [examples/stories/](examples/stories) (HTML, SVGs, captures,
1399
+ beat and act sheets) to the git-ignored `examples/stories/out/`; `--docs` also copies the
1400
+ `*.story.*` pairs above to `docs/gallery/`.
1253
1401
  `bun run verify:smil` checks the animated SVGs in headless Chrome (frame parity with the viewer's
1254
1402
  frames, `<img>` playback, embedded font).
1255
1403
 
@@ -1272,6 +1420,7 @@ bun run build # dist/ (ESM for node, .d.ts, CLI with a node shebang
1272
1420
  bun run gallery # docs/gallery/*.png (needs Chrome / Playwright headless shell)
1273
1421
  bun run gallery:animated # docs/gallery/*.animated.{light,dark}.svg
1274
1422
  bun run gallery:showcase # docs/gallery PNGs of the rail, drawer, pages, scrolly and slides
1423
+ bun run gallery:stories # examples/stories/out/ (ignored); --docs: docs/gallery/*.story.*
1275
1424
  bun run verify:smil # animated SVGs: frame parity, <img> playback, embedded font
1276
1425
  bun run verify:viewer # every viewer control with real mouse input, full and reduced motion
1277
1426
  bun run verify:narrate # narration rail and change drawer
@@ -1,6 +1,8 @@
1
1
  import {
2
+ actEndTime,
2
3
  animatedSvg,
3
4
  beatTimes,
5
+ closest,
4
6
  detectMermaid,
5
7
  fromMermaid,
6
8
  isPageSpec,
@@ -10,9 +12,10 @@ import {
10
12
  renderPageHtml,
11
13
  renderSvg,
12
14
  resolvePageChanges,
15
+ toScene,
13
16
  validate,
14
17
  validatePage
15
- } from "./index-ac6c756n.js";
18
+ } from "./index-pc811tpa.js";
16
19
 
17
20
  // src/node/index.ts
18
21
  import fs4 from "node:fs";
@@ -457,13 +460,16 @@ async function snapshot(htmlPath, opts = {}) {
457
460
  const tl = pinPace !== undefined ? recompilePace(scene, pinPace) : scene.timeline;
458
461
  const headerH = (scene.subtitle ? 128 : 100) + (tl ? 54 : 0);
459
462
  const ats = opts.at?.length ? opts.at : opts.t ? [opts.t === "end" ? "end" : Number(opts.t)] : ["end"];
460
- const tq = (at) => tl ? `&t=${at === "end" ? "end" : +at.toFixed(3)}` : "";
463
+ const actId = opts.act && tl?.acts?.some((a) => a.id === opts.act) ? opts.act : undefined;
464
+ if (opts.act && !actId)
465
+ return { code: 1, error: `unknown act "${opts.act}"${tl?.acts ? ` (acts: ${tl.acts.map((a) => a.id).join(", ")})` : " (the story has no acts)"}` };
466
+ const tq = (at) => tl ? actId && at === "end" ? `&act=${encodeURIComponent(actId)}` : `&t=${at === "end" ? "end" : +at.toFixed(3)}` : "";
461
467
  const mq = opts.motion ? `&motion=${opts.motion}` : "";
462
468
  const follow = opts.camera === "follow" && !!tl;
463
469
  const cq = follow ? "&camera=follow" : "";
464
470
  const dq = `${opts.rail ? "&rail=1" : ""}${opts.drawer ? `&drawer=${encodeURIComponent(opts.drawer)}` : ""}`;
465
471
  const panes = !!(opts.rail || opts.drawer);
466
- const sheetMode = opts.sheet === false ? false : opts.sheet === "beats" ? "beats" : "themes";
472
+ const sheetMode = opts.sheet === false ? false : opts.sheet === "beats" ? "beats" : opts.sheet === "acts" ? tl?.acts ? "acts" : false : "themes";
467
473
  const W = Math.round(Math.max(500, opts.width ?? Math.min(1600, vb.w + 64)));
468
474
  const s = Math.min(1, (W - 64) / vb.w);
469
475
  const H = Math.round(headerH + vb.h * s + 64 + 8);
@@ -504,6 +510,7 @@ async function snapshot(htmlPath, opts = {}) {
504
510
  const captures = [];
505
511
  let sheetCap;
506
512
  let beatCaps = [];
513
+ const actCaps = [];
507
514
  let previews = [];
508
515
  let beatCount;
509
516
  const measure = async (hash, w) => {
@@ -575,6 +582,12 @@ async function snapshot(htmlPath, opts = {}) {
575
582
  out.push({ path: partFile(k, n2), ...r, shows: `beat tiles ${a + 1}–${b + 1} of ${tileCount} (part ${k + 1}/${n2}), ${theme}` });
576
583
  }
577
584
  }
585
+ } else if (sheetMode === "acts" && tl?.acts) {
586
+ const colW = Math.max(500, Math.min(1000, vb.w + 48));
587
+ const sw = colW * tl.acts.length;
588
+ const sh = Math.round(headerH + 46 + vb.h * (colW - 48) / vb.w + 36);
589
+ const r = await shootSmall(`theme=${theme}&chrome=0&sheet=acts`, sw, sh, partFile(0, 1), maxSize, maxBytes);
590
+ out.push({ path: partFile(0, 1), ...r, shows: `${tl.acts.map((a) => a.label).join(" | ")}: each act's settled end, ${theme}` });
578
591
  } else if (sheetCap && sheetMode === "themes") {
579
592
  const colW = Math.max(500, Math.min(1000, vb.w + 48));
580
593
  const sw = colW * themes.length;
@@ -595,7 +608,7 @@ async function snapshot(htmlPath, opts = {}) {
595
608
  try {
596
609
  for (const theme of themes)
597
610
  for (const at of ats) {
598
- const tag = `${at === "end" ? "" : `.t${+at.toFixed(2)}`}${opts.motion === "reduced" ? ".reduced" : ""}${follow ? ".follow" : ""}${opts.rail ? ".rail" : ""}${opts.drawer ? ".drawer" : ""}`;
611
+ const tag = `${at === "end" ? actId ? `.act-${actId.replace(/[^\w-]/g, "_")}` : "" : `.t${+at.toFixed(2)}`}${opts.motion === "reduced" ? ".reduced" : ""}${follow ? ".follow" : ""}${opts.rail ? ".rail" : ""}${opts.drawer ? ".drawer" : ""}`;
599
612
  const png = path3.join(outDir, `${base}.${theme}${tag}.png`);
600
613
  const [cw, ch] = follow || panes ? [FW, Math.round(FW * 0.5625)] : [W, H];
601
614
  const ms = await shoot(`theme=${theme}&chrome=0${tq(at)}${mq}${cq}${dq}`, png, cw, ch);
@@ -659,6 +672,17 @@ async function snapshot(htmlPath, opts = {}) {
659
672
  }
660
673
  }
661
674
  beatCaps = beats;
675
+ if (sheetMode === "acts" && tl?.acts) {
676
+ const n2 = tl.acts.length;
677
+ const colW = Math.max(500, Math.min(1000, vb.w + 48));
678
+ const sw = colW * n2;
679
+ const sh = Math.round(headerH + 46 + vb.h * (colW - 48) / vb.w + 36);
680
+ for (const theme of themes) {
681
+ const png = path3.join(outDir, `${base}.acts.${theme}.png`);
682
+ const ms = await shoot(`theme=${theme}&chrome=0&sheet=acts`, png, sw, sh);
683
+ actCaps.push({ theme: "acts", png, sha256: sha256(png), bytes: fs3.statSync(png).size, width: sw * scale, height: sh * scale, ms });
684
+ }
685
+ }
662
686
  if (tl)
663
687
  beatCount = beatTimes(tl).length;
664
688
  if (sheetMode === "themes" && themes.length > 1) {
@@ -707,6 +731,7 @@ async function snapshot(htmlPath, opts = {}) {
707
731
  captures,
708
732
  ...sheetCap ? { sheet: sheetCap } : {},
709
733
  ...beatCaps.length ? { beats: beatCaps } : {},
734
+ ...actCaps.length ? { acts: actCaps } : {},
710
735
  ...previews.length ? { previews } : {},
711
736
  ...tl ? { story: { duration: tl.duration, steps: tl.steps.length } } : {},
712
737
  ...lint ? { lint } : {},
@@ -966,7 +991,21 @@ function writeDiagram(input, out) {
966
991
  res.html = { path: path4.resolve(out.html), bytes: Buffer.byteLength(html) };
967
992
  }
968
993
  if (out.svg) {
969
- const svg = renderSvg(v.spec, out.theme ? { theme: out.theme } : {});
994
+ let t;
995
+ let src = v.spec;
996
+ if (out.act) {
997
+ const scene = toScene(v.spec);
998
+ const acts = scene.timeline?.acts;
999
+ t = scene.timeline ? actEndTime(scene.timeline, out.act) : undefined;
1000
+ if (t === undefined) {
1001
+ const guess = acts ? closest(out.act, acts.map((a) => a.id)) : undefined;
1002
+ res.ok = false;
1003
+ res.diagnostics = [...res.diagnostics, { severity: "error", path: "act", message: `unknown act "${out.act}"`, hint: acts ? `${guess ? `did you mean "${guess}"? ` : ""}acts: ${acts.map((a) => a.id).join(", ")}` : "the story has no acts" }];
1004
+ return res;
1005
+ }
1006
+ src = scene;
1007
+ }
1008
+ const svg = renderSvg(src, { ...out.theme ? { theme: out.theme } : {}, ...t !== undefined ? { t } : {} });
970
1009
  fs4.mkdirSync(path4.dirname(out.svg), { recursive: true });
971
1010
  fs4.writeFileSync(out.svg, svg);
972
1011
  res.svg = { path: path4.resolve(out.svg), bytes: Buffer.byteLength(svg) };