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.
- package/CHANGELOG.md +61 -0
- package/README.md +158 -9
- package/dist/chunks/{index-rgqxg226.js → index-8dqzx1yf.js} +44 -5
- package/dist/chunks/{index-ac6c756n.js → index-pc811tpa.js} +3524 -847
- package/dist/cli.js +3691 -963
- package/dist/core/index.js +3 -1
- package/dist/index.js +8 -4
- package/dist/node/index.js +2 -2
- package/dist/types/core/layout/nodes.d.ts +7 -0
- package/dist/types/core/layout/overlays.d.ts +58 -0
- package/dist/types/core/layout/storyscan.d.ts +40 -0
- package/dist/types/core/page/css.d.ts +0 -1
- package/dist/types/core/page/types.d.ts +2 -2
- package/dist/types/core/render/App.d.ts +4 -0
- package/dist/types/core/render/Hud.d.ts +22 -0
- package/dist/types/core/render/css.d.ts +24 -2
- package/dist/types/core/render/index.d.ts +9 -0
- package/dist/types/core/render/smil.d.ts +1 -1
- package/dist/types/core/scene.d.ts +59 -0
- package/dist/types/core/spec.d.ts +93 -3
- package/dist/types/core/story/acts.d.ts +108 -0
- package/dist/types/core/story/compile.d.ts +20 -0
- package/dist/types/core/story/content-state.d.ts +9 -0
- package/dist/types/core/story/content.d.ts +47 -3
- package/dist/types/core/story/ease.d.ts +11 -0
- package/dist/types/core/story/state.d.ts +68 -2
- package/dist/types/core/story/types.d.ts +137 -9
- package/dist/types/core/suggest.d.ts +2 -0
- package/dist/types/generated/meta.d.ts +1 -1
- package/dist/types/generated/viewer.d.ts +1 -1
- package/dist/types/node/index.d.ts +2 -1
- package/dist/types/node/snapshot.d.ts +12 -3
- package/dist/types/theme/tokens.d.ts +14 -5
- package/dist/types/theme/tones.d.ts +38 -0
- package/docs/spec.md +232 -3
- package/examples/stories/payment-retry.acts.architecture.json +340 -0
- package/examples/stories/request-colours.dataflow.json +68 -0
- package/examples/stories/secret-broker.architecture.json +126 -0
- package/package.json +2 -1
- package/schema/storyink.schema.json +189 -6
- 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><img></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 `
|
|
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.
|
|
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-
|
|
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
|
|
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
|
-
|
|
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) };
|