@dev.fast/whiteboard 0.5.0 → 0.5.2-preview.20261104.146
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/dist/{account-alias-Bw3PT3Qo.js → account-alias-BlYamXAG.js} +5 -5
- package/dist/{agent-cli-kHRrwO4n.js → agent-cli-D7fF5xmm.js} +9 -9
- package/dist/agent-cli-DLibAL4u.js +2 -0
- package/dist/{agent-client-CMacGbMp.js → agent-client-BCEbSPWU.js} +4 -4
- package/dist/{authoring-tools-CiLy46kh.js → authoring-tools-CkfxTVJm.js} +12 -8
- package/dist/{background-server-eZWXoVcI.js → background-server-BraP8pJ9.js} +4 -4
- package/dist/{background-server-BLNHbksB.js → background-server-CwToVN1g.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-Bxmiqutp.js → cli-BpadBaGU.js} +1 -1
- package/dist/{cli-runner-DQPuu7kJ.js → cli-runner-BebYF50a.js} +22 -22
- package/dist/cli.js +2 -2
- package/dist/{fuzzy-match-DZjeOaPY.js → fuzzy-match-CxcsLQGx.js} +3 -3
- package/dist/{headless-host-Dl2KZ9IT.js → headless-host-FAlbekFD.js} +561 -415
- package/dist/{headless-host-B4r0s1ks.js → headless-host-cLipIufH.js} +1 -1
- package/dist/{import-YAIU6rB0.js → import-CZ1958L4.js} +234 -62
- package/dist/{local-data-C58vzLZR.js → local-data-DdFkxFK_.js} +41 -16
- package/dist/{mcp-COYWiGVn.js → mcp-Claiy9xE.js} +9 -6
- package/dist/{package-paths-Cg6fnaGp.js → package-paths-Dk7MOXxN.js} +1 -1
- package/dist/{profile--BYIjPkc.js → profile-Bsgzi9PA.js} +2 -2
- package/dist/profile-BxdzBxHW.js +2 -0
- package/dist/{remote-attach-C1prLjmj.js → remote-attach-LGTpwpUv.js} +4 -4
- package/dist/{remote-extensions-Bw029iIS.js → remote-extensions-BvV29_gk.js} +1 -1
- package/dist/{remote-extensions-MUUgbLJn.js → remote-extensions-L95HNwIH.js} +2 -2
- package/dist/{remote-language-server-z3rdHcLL.js → remote-language-server-BAznkpeu.js} +4 -4
- package/dist/{remote-uninstall-B3Slwor1.js → remote-uninstall-CLP79Cuk.js} +5 -5
- package/dist/{request-origin-B3_XI2VA.js → request-origin-D6B-zrL8.js} +27 -5
- package/dist/{review-app-launcher-BZEbDKuK.js → review-app-launcher-tTe8bTwg.js} +2 -2
- package/dist/{review-home-paths-LdGS4SpR.js → review-home-paths-BmZX2qiz.js} +1 -1
- package/dist/{review-telemetry-x8u-9EZb.js → review-telemetry-DACDMfqq.js} +4 -4
- package/dist/server/desktop-host.js +26 -38
- package/dist/{server-discovery-WmuFl2S3.js → server-discovery-CV5qf1J8.js} +2 -2
- package/dist/sharing/index.d.ts +137 -14
- package/dist/sharing/index.js +1 -1
- package/dist/{src-B2b8cCoI.js → src-BQDlJXkG.js} +44 -5
- package/dist/{src-DLWol7GZ.js → src-CrrW7lcY.js} +16 -1
- package/dist/{stored-document-migration-iwIQ4l5G.js → stored-document-migration-D1wK4yID.js} +3 -3
- package/dist/{thread-state-B7L1mpN_.js → thread-state-WTqSborE.js} +1 -1
- package/dist/{tool-failure-RLR8m9hq.js → tool-failure-YOFGHJCZ.js} +319 -55
- package/instructions/animation.md +69 -5
- package/instructions/animations/before-after-structure/README.md +27 -0
- package/instructions/animations/before-after-structure/animation.html +89 -0
- package/instructions/animations/before-after-structure/block.json +10 -0
- package/instructions/animations/before-after-structure/frames.json +313 -0
- package/instructions/animations/cache-fill-evict/README.md +24 -0
- package/instructions/animations/cache-fill-evict/animation.html +81 -0
- package/instructions/animations/cache-fill-evict/block.json +13 -0
- package/instructions/animations/cache-fill-evict/frames.json +79 -0
- package/instructions/animations/cache-fill-evict/scenarios.json +17 -0
- package/instructions/animations/log-replay/README.md +25 -0
- package/instructions/animations/log-replay/animation.html +86 -0
- package/instructions/animations/log-replay/block.json +10 -0
- package/instructions/animations/log-replay/frames.json +143 -0
- package/instructions/animations/log-replay/scenarios.json +18 -0
- package/instructions/animations/queue-workers/README.md +23 -0
- package/instructions/animations/queue-workers/animation.html +107 -0
- package/instructions/animations/queue-workers/block.json +13 -0
- package/instructions/animations/queue-workers/frames.json +119 -0
- package/instructions/animations/queue-workers/scenarios.json +18 -0
- package/instructions/animations/race-two-lanes/README.md +22 -0
- package/instructions/animations/race-two-lanes/animation.html +97 -0
- package/instructions/animations/race-two-lanes/block.json +1 -0
- package/instructions/animations/race-two-lanes/frames.json +234 -0
- package/instructions/animations/race-two-lanes/scenarios.json +19 -0
- package/instructions/animations/request-across-layers/README.md +24 -0
- package/instructions/animations/request-across-layers/animation.html +85 -0
- package/instructions/animations/request-across-layers/block.json +17 -0
- package/instructions/animations/request-across-layers/frames.json +58 -0
- package/instructions/animations/request-across-layers/scenarios.json +12 -0
- package/instructions/animations/request-timing/README.md +22 -0
- package/instructions/animations/request-timing/animation.html +120 -0
- package/instructions/animations/request-timing/block.json +21 -0
- package/instructions/animations/request-timing/markers.json +32 -0
- package/instructions/animations/request-timing/scenarios.json +4 -0
- package/instructions/animations/retry-backoff/README.md +22 -0
- package/instructions/animations/retry-backoff/animation.html +122 -0
- package/instructions/animations/retry-backoff/block.json +10 -0
- package/instructions/animations/retry-backoff/frames.json +65 -0
- package/instructions/animations/retry-backoff/scenarios.json +12 -0
- package/instructions/animations/state-machine/README.md +24 -0
- package/instructions/animations/state-machine/animation.html +95 -0
- package/instructions/animations/state-machine/block.json +9 -0
- package/instructions/animations/state-machine/frames.json +79 -0
- package/instructions/animations/state-machine/scenarios.json +12 -0
- package/instructions/animations/video-platform-architecture/README.md +24 -0
- package/instructions/animations/video-platform-architecture/animation.html +76 -0
- package/instructions/animations/video-platform-architecture/block.json +17 -0
- package/instructions/animations/video-platform-architecture/frames.json +97 -0
- package/instructions/animations/video-platform-architecture/scenarios.json +28 -0
- package/instructions/whiteboard.d.ts +168 -14
- package/package.json +6 -6
- package/src/animation-stage-kit.ts +150 -10
- package/src/cli-runner.ts +4 -1
- package/src/fixtures/blocks/clip.json +19 -0
- package/src/review-api/README.md +1 -0
- package/src/review-api/agent-cli.ts +3 -2
- package/src/review-api/animation-drafts.ts +151 -47
- package/src/review-api/animation-references.ts +92 -0
- package/src/review-api/animation-render.ts +106 -0
- package/src/review-api/authoring-tools.ts +10 -3
- package/src/review-api/document-text.ts +27 -12
- package/src/review-api/http.ts +52 -6
- package/src/review-api/instructions.ts +1 -1
- package/src/review-api/mcp.ts +10 -5
- package/src/review-api/public-tools.ts +71 -2
- package/src/review-api/store.ts +89 -12
- package/src/server/desktop-server.ts +17 -5
- package/src/server/headless-host.ts +4 -0
- package/src/server/review-gateway.ts +1 -0
- package/src/server/review-server-core.ts +71 -12
- package/src/sharing/client.ts +129 -32
- package/src/sharing/export.ts +42 -5
- package/src/sharing/file.ts +8 -0
- package/src/sharing/host.ts +202 -73
- package/src/sharing/import.ts +125 -33
- package/src/ui-telemetry-events.ts +25 -1
- package/dist/agent-cli-B8KBHuh_.js +0 -2
- package/dist/profile-BjcXi-oH.js +0 -2
|
@@ -2,12 +2,15 @@ An `animation` component draws HTML you write on the whiteboard, in a sandboxed
|
|
|
2
2
|
|
|
3
3
|
For paths a reader can pick, such as a retry that succeeds or one that gives up, use `scenarios`: the whiteboard draws the picker, so do not draw your own buttons.
|
|
4
4
|
|
|
5
|
+
A `clip` component runs in the same stage with the same API, but it has no frames: it is one timeline the reader plays, pauses and jumps around by named markers. See "Clips: one timeline, played" below.
|
|
6
|
+
|
|
5
7
|
## Work in files
|
|
6
8
|
|
|
7
9
|
1. `session_checkout({ sessionId, targetId })` writes the animation to a folder: `animation.html`, `frames.json`, `scenarios.json` (once it has scenarios) and `block.json` (`title`, and `code` and `pins` when it shows code).
|
|
8
10
|
2. Edit those files with your file tools.
|
|
9
11
|
3. Publish the folder: `session_edit({ sessionId, edit: { type: "update", targetId, fromFiles: "<folder>" } })`. Only the files you changed are saved. Keep editing and publishing the same folder; it records what it last published in `checkout.json`.
|
|
10
12
|
4. Read it back with `session_get({ sessionId, targetId })`. Its script runs only where the whiteboard is open, a moment after you publish, so the read says whether it ran there without an error, threw (with the error), or has not run yet. Fix an error and publish again.
|
|
13
|
+
5. Look at it: `session_render({ sessionId, targetId, frame })` shows that frame (or a clip at that marker; omitted, the first) on the whiteboard open in Desktop, as the reader sees it, and returns it as a PNG image, with the error the script threw, if any. It also writes the PNG to the path the result names. Render each frame you changed and check the drawing is what you meant: cards where you placed them, arrows between the right parts, nothing clipped or overlapping. Do not take screenshots of the screen. The whiteboard must be open in Desktop: `session_create` opens it, and `session_open` opens an existing one.
|
|
11
14
|
|
|
12
15
|
For a new animation, write the same files into a folder of your own and publish with `{ type: "insert", fromFiles: "<folder>", parentId, afterId }`. If someone else changed the animation after your checkout, the publish is refused: check it out again.
|
|
13
16
|
|
|
@@ -15,7 +18,11 @@ For a new animation, write the same files into a folder of your own and publish
|
|
|
15
18
|
|
|
16
19
|
Before you write HTML, read the two files whose paths `session_checkout` and these instructions give: `whiteboard.css`, the classes and `--wb-*` variables every animation loads, and `whiteboard.d.ts`, the `whiteboard` API. Use the classes and variables rather than your own colors, sizes or fonts, so the animation matches the board in light and dark.
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Draw for a stage 720px wide: the width of the board's reading column, and the narrowest the stage gets. Fit that width with nothing past its right edge, so the reader never scrolls sideways on a full-size board: a scene fits three columns of cards, with room for link labels between them. A wider board or fullscreen gives the stage more room, and a narrower window keeps it at 720px and scrolls it sideways, so do not shrink the drawing for small widths. A scene keeps its columns at any width.
|
|
22
|
+
|
|
23
|
+
Start from the reference closest to what you are drawing, listed under References at the end of these instructions: copy its folder into your draft, then change the data, the prose and the code links.
|
|
24
|
+
|
|
25
|
+
There are three ways to draw.
|
|
19
26
|
|
|
20
27
|
### A scene: cards and messages
|
|
21
28
|
|
|
@@ -24,8 +31,8 @@ For a few services, processes or stores with messages moving between them, use `
|
|
|
24
31
|
```html
|
|
25
32
|
<script>
|
|
26
33
|
const s = whiteboard.scene({ columns: 2 });
|
|
27
|
-
const client = s.actor({ id: "client", at: [1, 1], title: "Client" });
|
|
28
|
-
const api = s.actor({ id: "api", at: [2, 1], title: "API" });
|
|
34
|
+
const client = s.actor({ id: "client", at: [1, 1], title: "Client", form: "window" });
|
|
35
|
+
const api = s.actor({ id: "api", at: [2, 1], title: "API", form: "bars" });
|
|
29
36
|
|
|
30
37
|
s.send(client, api, "GET", "send");
|
|
31
38
|
s.on("send", api, { note: "timed out", tone: "removed" });
|
|
@@ -34,7 +41,21 @@ For a few services, processes or stores with messages moving between them, use `
|
|
|
34
41
|
</script>
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
A scene names frame keys, so a new scenario means new frames and a few new lines like these.
|
|
44
|
+
A scene names frame keys, so a new scenario means new frames and a few new lines like these. While a frame shows, each message it sends is the label of its edge and the edge takes the marker colour, however the reader reached the frame; stepping into the frame rides a chip from the sender that settles into the label. A card's `id` is its element's id, so your own CSS can address it as `#api`.
|
|
45
|
+
|
|
46
|
+
Give each card the silhouette of what it stands for, so a reader tells the parts apart before reading a title: `form: "window"` for a client, `"bars"` for a service or worker of this codebase, `"cylinder"` for a database, `"drum"` for a cache, `"stack"` for a queue or topic, `"pill"` for a start or end, `"cut"` for a branch. A third-party service or API gets `outline: "dashed"`. `color` sets a border colour at rest (`teal` for stores, `violet` for queues, `accent` for the part under discussion); a frame's `tone` takes over while set. The same `data-form`, `data-outline` and `data-color` work on a `wb-node` in a hand-made layout.
|
|
47
|
+
|
|
48
|
+
Draw a boundary around cards that belong together with `s.group({ id, at: [column, row, rowSpan, colSpan], title: "SEARCH SERVICE", color })`: a washed box behind the cards in those cells, titled in its corner. On a board that reviews a change, mark what it did to a part with `change: "added" | "modified" | "removed"` on the card: its dot and left edge take the change colour at rest, and a removed part's title is struck. When the cards carry marks a reader must decode, add `s.legend([{ form: "cylinder", label: "Database" }, { change: "modified", label: "Changed here" }])`, one row under the scene; name only what matters.
|
|
49
|
+
|
|
50
|
+
### A hand-made layout: lanes, axes, graphs
|
|
51
|
+
|
|
52
|
+
When the drawing is not a grid of cards (attempts on a time axis, two lanes interleaving, a state graph), lay out your own elements with the kit (`wb-node`, `wb-lane`, `wb-table`) and connect them:
|
|
53
|
+
|
|
54
|
+
- `whiteboard.arrow("#caller", "#service", { enter: "a1" })` draws an SVG arrow that follows the two elements. A string here is a CSS selector; in a scene's methods it is a card id.
|
|
55
|
+
- `whiteboard.token({ label: "GET" })` and `whiteboard.travel(token, arrow, "a1")` ride a chip along the arrow during that frame.
|
|
56
|
+
- `whiteboard.timeline("a1").add(".wait", { scaleX: [0, 1], duration: 600 })` adds tweens that play when the reader steps into the frame and land at their end on a jump.
|
|
57
|
+
|
|
58
|
+
Declare arrows, tokens, travels and timelines while the script first runs, naming the frames they play in; they cannot be added from a frame handler. `data-enter="a1"`, `data-exit="a3"`, `data-only="a1 a3"` and `data-highlight="a1 a3"` on any element show, hide or ring it in those frames with no script.
|
|
38
59
|
|
|
39
60
|
### Your own HTML: draw each frame from its data
|
|
40
61
|
|
|
@@ -65,7 +86,19 @@ Draw the frame in full, then animate what is new since the previous frame:
|
|
|
65
86
|
</script>
|
|
66
87
|
```
|
|
67
88
|
|
|
68
|
-
Write each frame's data in full, not as a change from the one before: a reader can jump straight to any frame. `whiteboard.animate` skips the motion on a jump and under reduced motion, and returning its animations makes playback wait for them.
|
|
89
|
+
Write each frame's data in full, not as a change from the one before: a reader can jump straight to any frame. `whiteboard.animate` skips the motion on a jump and under reduced motion, and returning its animations makes playback wait for them. Give every keyframe its start value (`[{ opacity: 0 }, { opacity: 1 }]`, not `[{ opacity: 1 }]`): a jump finishes the animation, and a missing start lands wrong.
|
|
90
|
+
|
|
91
|
+
Something that moves between frames (a job changing queue slot, a record regrouped under a new parent) is drawn the same way: measure every element's position, redraw, then animate each moved element from its old position to `transform: none`. Measure everything before starting any animation; one that has started already reports its transform.
|
|
92
|
+
|
|
93
|
+
`whiteboard.frames` and `frame.index` follow the active scenario, so a frame outside it is not listed. Key your drawing on `frame.data`, not on positions.
|
|
94
|
+
|
|
95
|
+
## Painting in
|
|
96
|
+
|
|
97
|
+
When you write an animation, the reader watches it arrive: its first frame paints in, part by part, instead of appearing at once. It paints only then, not when the board loads with it already there, and not under reduced motion. The test harness and a cold load do not paint, so you see it only by watching your own write land on the board.
|
|
98
|
+
|
|
99
|
+
- By default the kit's parts (cards, nodes, chips, badges, table rows, lanes, labels, peeks, tokens) fade in one after another in document order, and arrows draw along their path, all within about a second and a half. A part inside another goes in with it.
|
|
100
|
+
- `data-wb-paint="1"` orders a part: numbered parts first, the rest in document order. `data-wb-paint="none"` shows an element and everything in it at once. `data-wb-paint-style="fade"`, `"draw"` (an SVG line), `"wipe"` (left to right) or `"none"` picks the effect. Any element with either attribute paints on its own.
|
|
101
|
+
- `whiteboard.on("paint", ({ frame, instant }) => ...)` replaces the default painter: draw the first frame in yourself and return the animations, as a frame handler does. It also runs with `instant: true` when the block does not paint in; leave everything in its end state then. `whiteboard.timeline("paint")` is a timeline that plays once as the block paints in and lands at its end otherwise; it also replaces the default painter. Do not name a frame `paint`.
|
|
69
102
|
|
|
70
103
|
## Link the code
|
|
71
104
|
|
|
@@ -74,3 +107,34 @@ Name the ranges the animation explains in `code`, then link the drawing to them:
|
|
|
74
107
|
- `data-wb-open="retry"` (or `code: "retry"` on a scene card) opens the code beside the whiteboard when the reader clicks that element, and names it when they hover. Put it on the part of the drawing the code explains.
|
|
75
108
|
- A frame's `code` lists the ranges that frame shows. Your script reads it as `frame.code`, so one script can mark what to open in every frame.
|
|
76
109
|
- `<div data-wb-code="retry">` (or `scene.peek`) shows a range inside the drawing instead.
|
|
110
|
+
|
|
111
|
+
## Clips: one timeline, played
|
|
112
|
+
|
|
113
|
+
Pick a clip when the motion itself is the point and the timing carries the meaning: a token crossing a system, two requests racing, a queue draining, a timeout firing while a reply is on its way. Pick an animation when the reader needs to compare discrete states, one frame against the next.
|
|
114
|
+
|
|
115
|
+
A clip's folder holds `markers.json` where an animation's holds `frames.json`; `session_checkout` writes whichever the block has, and publishing a folder with `markers.json` makes a clip. A marker is `{ key, title, caption?, code? }`: a named moment the reader jumps to and the tour stops at. The strip under the clip shows each title, so keep titles to a few words. Markers have no `data` or `duration`: the script decides when each one happens.
|
|
116
|
+
|
|
117
|
+
Build the timeline while the script first runs, and place every marker on it:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<script>
|
|
121
|
+
whiteboard
|
|
122
|
+
.clip()
|
|
123
|
+
.marker("sent")
|
|
124
|
+
.add(".packet", { left: ["10%", "50%"], duration: 700 })
|
|
125
|
+
.marker("accepted")
|
|
126
|
+
.add(".packet", { left: ["50%", "90%"], duration: 900 })
|
|
127
|
+
.marker("stored");
|
|
128
|
+
</script>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `whiteboard.clip()` returns a timeline with `add`, `set` and `call`, as `whiteboard.timeline` does, and `.marker(key)`, which places a declared marker at the timeline's end so far, or at an anime.js position (`.marker("timeout", 1500)`). Numeric positions count from the clip's start. Markers are labels on the timeline, so later calls can start at one: `.add(".retry", { opacity: [0, 1] }, "timeout")`.
|
|
132
|
+
- A marker the script leaves unplaced shows an error under the clip, naming it.
|
|
133
|
+
- With `scenarios` (`[{ key, label }]` in `scenarios.json`), each scenario is a timeline of its own: call `whiteboard.clip("fast")`, `whiteboard.clip("slow")`, once each, and place every marker on each. A `whiteboard.clip()` without a key serves every scenario that has no timeline of its own.
|
|
134
|
+
- Write each tween with its start value (`left: ["10%", "50%"]`, not `left: "50%"`), and make what differs between beats a `set` or a tween on the timeline: jumping back to a marker then undoes everything after it.
|
|
135
|
+
- `whiteboard.on("stored", ({ marker, instant }) => ...)` runs when the playhead crosses that marker playing forward, or lands on it after a jump with `instant: true`; `"*"` runs on every marker. Use it for what a tween cannot draw, such as changing text, and set the whole state for that marker, since the reader can jump straight to it. `whiteboard.markers` lists the markers with their `at` (milliseconds) once placed, and `whiteboard.marker` is the one the playhead last reached.
|
|
136
|
+
- In a clip, `data-enter`, `data-exit`, `data-only`, `data-highlight` and `scene.on` name markers instead of frames, and follow the marker the playhead last reached.
|
|
137
|
+
- `whiteboard.frames`, `frame`, `goto`, `next`, `prev`, `setSequence`, `timeline(key)`, `travel` and `scene.send` belong to frames and throw in a clip. Move tokens with tweens on the clip instead.
|
|
138
|
+
- Under reduced motion the clip never animates: Play steps from marker to marker, holding each for a few seconds and landing on its time at once, and a jump lands at once. Marker handlers get `instant: true`.
|
|
139
|
+
- Painting in and code linking work as for an animation: the clip paints in at time 0, before anything plays, and a marker's `code` gives its tour stop, as a frame's does.
|
|
140
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Before/after structure
|
|
2
|
+
|
|
3
|
+
A flat list of orders regroups into a map keyed by customer: the same records slide from the old shape into the new one, and tones mark the key, the new containers and the field that goes away.
|
|
4
|
+
|
|
5
|
+
## Use when
|
|
6
|
+
|
|
7
|
+
A change reshapes data rather than behavior: a list becomes a map or tree, fields move into a nested object, a type gains or drops a level. The reviewer needs to see where each existing value lands, not just the new type.
|
|
8
|
+
|
|
9
|
+
## Shows
|
|
10
|
+
|
|
11
|
+
1. The before shape, as nested cards.
|
|
12
|
+
2. The field that drives the change (here the grouping key), toned `modified`.
|
|
13
|
+
3. The records moving into their new containers, which appear toned `added`.
|
|
14
|
+
4. The field the new shape makes redundant, toned `removed`.
|
|
15
|
+
5. The after shape, plain.
|
|
16
|
+
|
|
17
|
+
## Technique
|
|
18
|
+
|
|
19
|
+
Data-driven. Each frame's `data.root` is the whole structure as a tree of nodes `{id, title, note?, tone?, open?, layout?, fields?, children?}`. One `whiteboard.on("*", ...)` handler redraws the tree, then animates the difference with FLIP: it measures every `[data-id]` before and after the redraw and slides kept elements from their old box with `whiteboard.animate` (a child subtracts its parent's slide). New elements fade in; a new container that receives moved elements fades only its own chrome. `whiteboard.animate` lands the end state on a jump and under reduced motion. A node's `open` becomes `data-wb-open` only in frames whose `code` lists it.
|
|
20
|
+
|
|
21
|
+
## Adapt
|
|
22
|
+
|
|
23
|
+
- Rewrite `frames.json`: keep each node's `id` stable across frames, since the id is what makes an element travel instead of reappearing.
|
|
24
|
+
- Set `layout: "row"` on a node whose children should sit side by side; the default stacks them.
|
|
25
|
+
- Put tones on what changed in that frame only, and leave the last frame plain.
|
|
26
|
+
- Name your ranges in `block.json` `code`, list them in the frames that show the transformation, and set `open` on the node the code builds.
|
|
27
|
+
- `animation.html` should not need changes.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.structure { padding: var(--wb-space-16) var(--wb-space-24); }
|
|
3
|
+
.node { width: max-content; max-width: 100%; min-width: var(--wb-space-120); }
|
|
4
|
+
.structure > .node { width: auto; }
|
|
5
|
+
.children { display: flex; flex-direction: column; align-items: flex-start; gap: var(--wb-space-8); margin-top: var(--wb-space-6); }
|
|
6
|
+
.children[data-layout="row"] { flex-direction: row; flex-wrap: wrap; gap: var(--wb-space-12); }
|
|
7
|
+
.fields { flex-direction: column; align-items: flex-start; }
|
|
8
|
+
</style>
|
|
9
|
+
<div class="structure"></div>
|
|
10
|
+
<script>
|
|
11
|
+
const stage = document.querySelector(".structure");
|
|
12
|
+
|
|
13
|
+
function element(tag, className, text) {
|
|
14
|
+
const el = document.createElement(tag);
|
|
15
|
+
el.className = className;
|
|
16
|
+
if (text !== undefined) el.textContent = text;
|
|
17
|
+
return el;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// One card per node: a title, an optional type note, fields as chips, and nested children.
|
|
21
|
+
function draw(node, code) {
|
|
22
|
+
const card = element("div", "wb-card node");
|
|
23
|
+
card.dataset.id = node.id;
|
|
24
|
+
if (node.tone) card.dataset.tone = node.tone;
|
|
25
|
+
if (node.open && code.includes(node.open)) card.dataset.wbOpen = node.open;
|
|
26
|
+
|
|
27
|
+
const head = element("div", "wb-card-head");
|
|
28
|
+
head.append(element("i", "wb-card-dot"), element("span", "wb-card-title", node.title));
|
|
29
|
+
card.append(head);
|
|
30
|
+
if (node.note) card.append(element("div", "wb-card-note", node.note));
|
|
31
|
+
|
|
32
|
+
if (node.fields) {
|
|
33
|
+
const fields = element("div", "wb-items fields");
|
|
34
|
+
for (const field of node.fields) {
|
|
35
|
+
const chip = element("span", "wb-chip", `${field.name}: ${field.value}`);
|
|
36
|
+
chip.dataset.id = `${node.id}.${field.name}`;
|
|
37
|
+
if (field.tone) chip.dataset.tone = field.tone;
|
|
38
|
+
fields.append(chip);
|
|
39
|
+
}
|
|
40
|
+
card.append(fields);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (node.children) {
|
|
44
|
+
const children = element("div", "children");
|
|
45
|
+
children.dataset.layout = node.layout ?? "column";
|
|
46
|
+
children.append(...node.children.map((child) => draw(child, code)));
|
|
47
|
+
card.append(children);
|
|
48
|
+
}
|
|
49
|
+
return card;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rects = () =>
|
|
53
|
+
new Map([...stage.querySelectorAll("[data-id]")].map((el) => [el, el.getBoundingClientRect()]));
|
|
54
|
+
|
|
55
|
+
whiteboard.on("*", ({ frame }) => {
|
|
56
|
+
const was = new Map([...rects()].map(([el, rect]) => [el.dataset.id, rect]));
|
|
57
|
+
stage.replaceChildren(draw(frame.data.root, frame.code ?? []));
|
|
58
|
+
const now = rects();
|
|
59
|
+
|
|
60
|
+
// Slide each kept element from where it was (FLIP). A child rides its parent's
|
|
61
|
+
// slide, so it moves only by the part of its offset the parent does not cover.
|
|
62
|
+
const offsets = new Map();
|
|
63
|
+
return [...now].flatMap(([el, to]) => {
|
|
64
|
+
const inherited = offsets.get(el.parentElement.closest("[data-id]")) ?? [0, 0];
|
|
65
|
+
const from = was.get(el.dataset.id);
|
|
66
|
+
if (!from) {
|
|
67
|
+
offsets.set(el, inherited);
|
|
68
|
+
// A new container keeps the moved elements it now holds visible: fade only its own chrome.
|
|
69
|
+
const holds = [...el.querySelectorAll("[data-id]")].some((child) => was.has(child.dataset.id));
|
|
70
|
+
if (!holds) return [whiteboard.animate(el, [{ opacity: 0 }, { opacity: 1 }], 300)];
|
|
71
|
+
const chrome = [...el.children].filter((part) => !part.classList.contains("children"));
|
|
72
|
+
return [
|
|
73
|
+
whiteboard.animate(el, [{ borderColor: "transparent", backgroundColor: "transparent" }, {}], 300),
|
|
74
|
+
...chrome.map((part) => whiteboard.animate(part, [{ opacity: 0 }, { opacity: 1 }], 300)),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
const offset = [from.left - to.left, from.top - to.top];
|
|
78
|
+
offsets.set(el, offset);
|
|
79
|
+
const [x, y] = [offset[0] - inherited[0], offset[1] - inherited[1]];
|
|
80
|
+
if (Math.abs(x) < 1 && Math.abs(y) < 1) return [];
|
|
81
|
+
return [
|
|
82
|
+
whiteboard.animate(el, [{ transform: `translate(${x}px, ${y}px)` }, { transform: "none" }], {
|
|
83
|
+
duration: 550,
|
|
84
|
+
easing: whiteboard.theme["motion-settle"],
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"key": "before",
|
|
4
|
+
"title": "A flat list of orders",
|
|
5
|
+
"label": "BEFORE",
|
|
6
|
+
"caption": "Before the change, orders is one array, and every reader that needs a customer's orders filters it.",
|
|
7
|
+
"data": {
|
|
8
|
+
"root": {
|
|
9
|
+
"id": "root",
|
|
10
|
+
"title": "orders",
|
|
11
|
+
"note": "Order[]",
|
|
12
|
+
"layout": "row",
|
|
13
|
+
"children": [
|
|
14
|
+
{
|
|
15
|
+
"id": "o1",
|
|
16
|
+
"title": "o1",
|
|
17
|
+
"open": "order",
|
|
18
|
+
"fields": [
|
|
19
|
+
{ "name": "customer", "value": "acme" },
|
|
20
|
+
{ "name": "total", "value": 40 }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "o2",
|
|
25
|
+
"title": "o2",
|
|
26
|
+
"open": "order",
|
|
27
|
+
"fields": [
|
|
28
|
+
{ "name": "customer", "value": "globex" },
|
|
29
|
+
{ "name": "total", "value": 15 }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "o3",
|
|
34
|
+
"title": "o3",
|
|
35
|
+
"open": "order",
|
|
36
|
+
"fields": [
|
|
37
|
+
{ "name": "customer", "value": "acme" },
|
|
38
|
+
{ "name": "total", "value": 70 }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "o4",
|
|
43
|
+
"title": "o4",
|
|
44
|
+
"open": "order",
|
|
45
|
+
"fields": [
|
|
46
|
+
{ "name": "customer", "value": "globex" },
|
|
47
|
+
{ "name": "total", "value": 30 }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"key": "key",
|
|
56
|
+
"title": "customer becomes the key",
|
|
57
|
+
"label": "KEY",
|
|
58
|
+
"code": ["group"],
|
|
59
|
+
"caption": "groupByCustomer reads each order's customer field and uses it as the map key.",
|
|
60
|
+
"data": {
|
|
61
|
+
"root": {
|
|
62
|
+
"id": "root",
|
|
63
|
+
"title": "orders",
|
|
64
|
+
"note": "Order[]",
|
|
65
|
+
"layout": "row",
|
|
66
|
+
"children": [
|
|
67
|
+
{
|
|
68
|
+
"id": "o1",
|
|
69
|
+
"title": "o1",
|
|
70
|
+
"open": "order",
|
|
71
|
+
"fields": [
|
|
72
|
+
{ "name": "customer", "value": "acme", "tone": "modified" },
|
|
73
|
+
{ "name": "total", "value": 40 }
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "o2",
|
|
78
|
+
"title": "o2",
|
|
79
|
+
"open": "order",
|
|
80
|
+
"fields": [
|
|
81
|
+
{ "name": "customer", "value": "globex", "tone": "modified" },
|
|
82
|
+
{ "name": "total", "value": 15 }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "o3",
|
|
87
|
+
"title": "o3",
|
|
88
|
+
"open": "order",
|
|
89
|
+
"fields": [
|
|
90
|
+
{ "name": "customer", "value": "acme", "tone": "modified" },
|
|
91
|
+
{ "name": "total", "value": 70 }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "o4",
|
|
96
|
+
"title": "o4",
|
|
97
|
+
"open": "order",
|
|
98
|
+
"fields": [
|
|
99
|
+
{ "name": "customer", "value": "globex", "tone": "modified" },
|
|
100
|
+
{ "name": "total", "value": 30 }
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"key": "move",
|
|
109
|
+
"title": "Orders move under their customer",
|
|
110
|
+
"label": "GROUP",
|
|
111
|
+
"code": ["group"],
|
|
112
|
+
"caption": "One entry per customer, in first-seen order; each order keeps its place within its customer.",
|
|
113
|
+
"data": {
|
|
114
|
+
"root": {
|
|
115
|
+
"id": "root",
|
|
116
|
+
"title": "ordersByCustomer",
|
|
117
|
+
"note": "Map<CustomerId, Order[]>",
|
|
118
|
+
"open": "group",
|
|
119
|
+
"tone": "modified",
|
|
120
|
+
"layout": "row",
|
|
121
|
+
"children": [
|
|
122
|
+
{
|
|
123
|
+
"id": "g-acme",
|
|
124
|
+
"title": "acme",
|
|
125
|
+
"note": "Order[]",
|
|
126
|
+
"open": "group",
|
|
127
|
+
"tone": "added",
|
|
128
|
+
"children": [
|
|
129
|
+
{
|
|
130
|
+
"id": "o1",
|
|
131
|
+
"title": "o1",
|
|
132
|
+
"open": "order",
|
|
133
|
+
"fields": [
|
|
134
|
+
{ "name": "customer", "value": "acme" },
|
|
135
|
+
{ "name": "total", "value": 40 }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "o3",
|
|
140
|
+
"title": "o3",
|
|
141
|
+
"open": "order",
|
|
142
|
+
"fields": [
|
|
143
|
+
{ "name": "customer", "value": "acme" },
|
|
144
|
+
{ "name": "total", "value": 70 }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "g-globex",
|
|
151
|
+
"title": "globex",
|
|
152
|
+
"note": "Order[]",
|
|
153
|
+
"open": "group",
|
|
154
|
+
"tone": "added",
|
|
155
|
+
"children": [
|
|
156
|
+
{
|
|
157
|
+
"id": "o2",
|
|
158
|
+
"title": "o2",
|
|
159
|
+
"open": "order",
|
|
160
|
+
"fields": [
|
|
161
|
+
{ "name": "customer", "value": "globex" },
|
|
162
|
+
{ "name": "total", "value": 15 }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "o4",
|
|
167
|
+
"title": "o4",
|
|
168
|
+
"open": "order",
|
|
169
|
+
"fields": [
|
|
170
|
+
{ "name": "customer", "value": "globex" },
|
|
171
|
+
{ "name": "total", "value": 30 }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"key": "trim",
|
|
182
|
+
"title": "Order drops its customer field",
|
|
183
|
+
"label": "TRIM",
|
|
184
|
+
"code": ["order"],
|
|
185
|
+
"caption": "The key already names the customer, so Order no longer carries it.",
|
|
186
|
+
"data": {
|
|
187
|
+
"root": {
|
|
188
|
+
"id": "root",
|
|
189
|
+
"title": "ordersByCustomer",
|
|
190
|
+
"note": "Map<CustomerId, Order[]>",
|
|
191
|
+
"open": "group",
|
|
192
|
+
"tone": "modified",
|
|
193
|
+
"layout": "row",
|
|
194
|
+
"children": [
|
|
195
|
+
{
|
|
196
|
+
"id": "g-acme",
|
|
197
|
+
"title": "acme",
|
|
198
|
+
"note": "Order[]",
|
|
199
|
+
"open": "group",
|
|
200
|
+
"children": [
|
|
201
|
+
{
|
|
202
|
+
"id": "o1",
|
|
203
|
+
"title": "o1",
|
|
204
|
+
"open": "order",
|
|
205
|
+
"tone": "modified",
|
|
206
|
+
"fields": [
|
|
207
|
+
{ "name": "customer", "value": "acme", "tone": "removed" },
|
|
208
|
+
{ "name": "total", "value": 40 }
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "o3",
|
|
213
|
+
"title": "o3",
|
|
214
|
+
"open": "order",
|
|
215
|
+
"tone": "modified",
|
|
216
|
+
"fields": [
|
|
217
|
+
{ "name": "customer", "value": "acme", "tone": "removed" },
|
|
218
|
+
{ "name": "total", "value": 70 }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"id": "g-globex",
|
|
225
|
+
"title": "globex",
|
|
226
|
+
"note": "Order[]",
|
|
227
|
+
"open": "group",
|
|
228
|
+
"children": [
|
|
229
|
+
{
|
|
230
|
+
"id": "o2",
|
|
231
|
+
"title": "o2",
|
|
232
|
+
"open": "order",
|
|
233
|
+
"tone": "modified",
|
|
234
|
+
"fields": [
|
|
235
|
+
{ "name": "customer", "value": "globex", "tone": "removed" },
|
|
236
|
+
{ "name": "total", "value": 15 }
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "o4",
|
|
241
|
+
"title": "o4",
|
|
242
|
+
"open": "order",
|
|
243
|
+
"tone": "modified",
|
|
244
|
+
"fields": [
|
|
245
|
+
{ "name": "customer", "value": "globex", "tone": "removed" },
|
|
246
|
+
{ "name": "total", "value": 30 }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"key": "after",
|
|
257
|
+
"title": "After: orders keyed by customer",
|
|
258
|
+
"label": "AFTER",
|
|
259
|
+
"code": ["group", "order"],
|
|
260
|
+
"caption": "A customer's orders are one lookup away, and no order can disagree with the customer it sits under.",
|
|
261
|
+
"data": {
|
|
262
|
+
"root": {
|
|
263
|
+
"id": "root",
|
|
264
|
+
"title": "ordersByCustomer",
|
|
265
|
+
"note": "Map<CustomerId, Order[]>",
|
|
266
|
+
"open": "group",
|
|
267
|
+
"layout": "row",
|
|
268
|
+
"children": [
|
|
269
|
+
{
|
|
270
|
+
"id": "g-acme",
|
|
271
|
+
"title": "acme",
|
|
272
|
+
"note": "Order[]",
|
|
273
|
+
"open": "group",
|
|
274
|
+
"children": [
|
|
275
|
+
{
|
|
276
|
+
"id": "o1",
|
|
277
|
+
"title": "o1",
|
|
278
|
+
"open": "order",
|
|
279
|
+
"fields": [{ "name": "total", "value": 40 }]
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"id": "o3",
|
|
283
|
+
"title": "o3",
|
|
284
|
+
"open": "order",
|
|
285
|
+
"fields": [{ "name": "total", "value": 70 }]
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "g-globex",
|
|
291
|
+
"title": "globex",
|
|
292
|
+
"note": "Order[]",
|
|
293
|
+
"open": "group",
|
|
294
|
+
"children": [
|
|
295
|
+
{
|
|
296
|
+
"id": "o2",
|
|
297
|
+
"title": "o2",
|
|
298
|
+
"open": "order",
|
|
299
|
+
"fields": [{ "name": "total", "value": 15 }]
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"id": "o4",
|
|
303
|
+
"title": "o4",
|
|
304
|
+
"open": "order",
|
|
305
|
+
"fields": [{ "name": "total", "value": 30 }]
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Read-through cache with LRU eviction
|
|
2
|
+
|
|
3
|
+
A scene with a caller, a bounded cache and a slow source that walks a miss and fill, a hit, and an eviction when the cache is full.
|
|
4
|
+
|
|
5
|
+
## Use when
|
|
6
|
+
|
|
7
|
+
A change adds or alters a cache in front of a slower dependency: its read-through path, its capacity, or what it evicts and when.
|
|
8
|
+
|
|
9
|
+
## Shows
|
|
10
|
+
|
|
11
|
+
- Miss: the request falls through to the source, the row is stored as the newest entry, then returned.
|
|
12
|
+
- Hit: the value comes from memory, the source is marked not called, and the entry moves to the newest end.
|
|
13
|
+
- Full: the oldest entry is marked, then leaves as the loaded one enters; the cache stays at capacity.
|
|
14
|
+
|
|
15
|
+
## Technique
|
|
16
|
+
|
|
17
|
+
Scene-based. `whiteboard.scene` places three cards; `scene.store` holds the entries as `cache.item(label, { enter, exit, tone })`. `scene.send` moves each request and reply, and `scene.on(frame, part, { text, tone })` changes notes and tones from a frame on. Items keep their order, so each scenario seeds its own entries, and a moved entry is one item exiting while another enters. The cache card opens code with `code:`; a `whiteboard.on("*")` handler points it at the first name in `frame.code`, so clicking it during an eviction opens the eviction range. The cache is a `drum` and Postgres a `cylinder`, both in `teal`, so the two stores read as stores beside the handler's plain box.
|
|
18
|
+
|
|
19
|
+
## Adapt
|
|
20
|
+
|
|
21
|
+
- Rename the cards and keys to your caller, cache and source; keep labels short (chips about 12 characters).
|
|
22
|
+
- Change the capacity by seeding more items, and the policy by which item exits (oldest for LRU or FIFO, least used for LFU).
|
|
23
|
+
- Add a scenario such as a stale entry or a failed load with new frame keys and a few `send` and `on` lines.
|
|
24
|
+
- Point `code` in block.json at your real ranges and list them in each frame's `code`.
|