wand-kit 0.2.0 → 0.3.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/PROMPT.md +86 -5
- package/SKILL.md +41 -4
- package/START-HERE.md +6 -0
- package/examples/storyboard/job.mjs +72 -0
- package/examples/storyboard/script.txt +6 -0
- package/examples/talk/agents-demo.html +69 -0
- package/examples/talk/agents-demo.poster-next.png +0 -0
- package/examples/talk/agents-demo.poster.png +0 -0
- package/examples/talk/job.mjs +85 -0
- package/lib/board.js +152 -0
- package/lib/brand.js +3 -0
- package/lib/components.js +151 -5
- package/lib/engine.js +32 -6
- package/lib/html_export.js +35 -4
- package/lib/runtime_check.js +108 -0
- package/lib/talk.js +620 -0
- package/package.json +2 -2
- package/templates/storyboard.mjs +33 -0
- package/wand.js +52 -9
package/PROMPT.md
CHANGED
|
@@ -143,6 +143,9 @@ The components (`node wand.js components`; `kit/lib/components.js`):
|
|
|
143
143
|
| `stats`, `delta(from, to)` | figures, before-and-after |
|
|
144
144
|
| `steps(items)` | next steps with arrows |
|
|
145
145
|
| `callout`, `list`, `chapter`, `img`, `icon`, `tile` | the rest |
|
|
146
|
+
| `enter(kind, i)`, `step(n, html)`, `countup(text)`, `stat({ count })` | motion in a talk (§12) |
|
|
147
|
+
| `video({ src, poster })`, `demo({ src, poster })` | a clip or a live page on a slide (§12) |
|
|
148
|
+
| `storyboard({ title, shots })`, `sketch({ people, apps, agents })` | a board from a script, and a quick frame for it (§13) |
|
|
146
149
|
|
|
147
150
|
**Composition is free.** A job writes the markup it needs from these, adds its
|
|
148
151
|
own CSS on top (custom properties only) and may write raw markup where no
|
|
@@ -184,7 +187,9 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
184
187
|
6. **review** — look at `out/sheet.png`, then at any page the gates named.
|
|
185
188
|
Read the pages as the reader will: does each carry its claim?
|
|
186
189
|
7. **ship** — `node wand.js ship <job> [--pptx]`: the build, the one-file HTML,
|
|
187
|
-
the PowerPoint.
|
|
190
|
+
the PowerPoint. A deck's HTML is a talk (§12), a storyboard's plays (§13),
|
|
191
|
+
and either runtime is driven before it ships. `--brand next` (or `current`)
|
|
192
|
+
builds the same job on the other brand into `out-<brand>/`. The review note says what was changed from the source and
|
|
188
193
|
why, which gates ran, and what a human still has to decide.
|
|
189
194
|
|
|
190
195
|
## 7. Gates — every build, every job
|
|
@@ -202,7 +207,13 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
202
207
|
| pdf text | the PDF's text misses a line, or carries an em dash | |
|
|
203
208
|
| contrast (warn) | text under WCAG AA against its background, chrome excepted | |
|
|
204
209
|
| measure (warn) | a paragraph over the surface's characters-per-line | |
|
|
210
|
+
| images | an image did not load | an image that loaded and is the wrong one |
|
|
211
|
+
| media | a demo, a clip or its poster is missing | a demo that loads and misbehaves |
|
|
205
212
|
| console | a page error | |
|
|
213
|
+
| runtime (ship, decks) | a slide or step the keyboard cannot reach, the overview or notes not opening, the presenter view out of step, a demo that never loads | how the motion feels |
|
|
214
|
+
| timing (warn, boards) | the board is more than 10% off its `target` | the real voiceover's pace |
|
|
215
|
+
| frames (warn, boards) | a frame is still to draw | a frame that is drawn and wrong |
|
|
216
|
+
| player (ship, boards) | the player does not run, scrub, or caption | |
|
|
206
217
|
|
|
207
218
|
The gates read the DOM Chromium laid out for the PDF; nothing is inferred from
|
|
208
219
|
the spec. What no gate sees: the argument, the pacing, and whether the right
|
|
@@ -248,8 +259,78 @@ component was chosen. That is the review.
|
|
|
248
259
|
1. **DOCX export.** Declined for now: Word cannot hold these pages. If needed,
|
|
249
260
|
a semantic export (headings, prose, tables) as an editable-text version,
|
|
250
261
|
labelled as such.
|
|
251
|
-
2. **The sandbox's browser.**
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
2. **The sandbox's browser.** Confirmed 2026-10-19: claude.ai's sandbox carries
|
|
263
|
+
Playwright's Chromium in `/opt/pw-browsers`, which `findChrome()` reads, and
|
|
264
|
+
a page renders there exactly as it does on the Mac.
|
|
265
|
+
3. **The talk runtime** from the BofA deck: carried into every slide deck's HTML
|
|
266
|
+
in 0.3.0 (§12). The chapter panel is still a component for a job to draw.
|
|
267
|
+
4. **A scratch voice.** The Wand OS film timed its board against a voice from
|
|
268
|
+
macOS `say`; the chat sandbox has none, so timing here is words at `wpm`.
|
|
269
|
+
A recorded voiceover passed as `audio` leads the player.
|
|
255
270
|
4. **Email and banner surfaces**: declared in §1's spirit, not yet in the table.
|
|
271
|
+
|
|
272
|
+
## 12. Talks: motion and the presenter
|
|
273
|
+
|
|
274
|
+
Every slide deck's one-file HTML is a talk, with the runtime ported from the
|
|
275
|
+
BofA deck (`kit/lib/talk.js`): a crossfade between slides, entrances, steps,
|
|
276
|
+
media, the presenter view in a second window kept in step, the talk clock and
|
|
277
|
+
its pace, notes, the overview. The PDF, the PNGs and the PowerPoint never see
|
|
278
|
+
any of it: they show every slide at its end state, every step in, every
|
|
279
|
+
figure final, every poster.
|
|
280
|
+
|
|
281
|
+
- **Entrances, not effects.** By default (`talk: { motion: "auto" }`) a slide
|
|
282
|
+
that marks nothing has its blocks rise in, staggered. Mark a slide yourself
|
|
283
|
+
when one thing should lead: `enter("pop", 0)` on it and the rest follow.
|
|
284
|
+
`talk: { motion: "none" }` stills a deck; `motion: "none"` on a page stills
|
|
285
|
+
one slide. One orchestrated moment a slide; nothing loops, nothing bounces.
|
|
286
|
+
- **Kinds.** `rise` (the default), `fade`, `pop` for a mark or a figure,
|
|
287
|
+
`grow` for a bar going up, `widen` for a bar going across, `draw` for a
|
|
288
|
+
line that tells a trend (an SVG path with `pathLength="1"`), `count` for a
|
|
289
|
+
figure the room should watch arrive (`stat({ count: true })`, `countup()`).
|
|
290
|
+
- **Steps** when the order of the argument is the point: `step(1, html)`, then
|
|
291
|
+
2, then 3. Never more than four on a slide. Going back undoes the last step.
|
|
292
|
+
- **Notes on every slide of a talk**, and `seconds` on each when the talk has a
|
|
293
|
+
slot: the presenter view shows the slide's window and whether you are on
|
|
294
|
+
pace. `talk: { minutes }` gives the clock a total when slides carry none.
|
|
295
|
+
- **A demo** is one self-contained HTML file (no network, no relative assets),
|
|
296
|
+
mounted in a frame when its slide comes up and the next one preloaded. It
|
|
297
|
+
may expose `window.wandDemo = { play, pause, reset }` for the D and R keys,
|
|
298
|
+
and dress itself from `window.wandEmbed.brand` (bg, card, stroke, ink, text,
|
|
299
|
+
label, accent), which the deck fills from its brand. Its poster is required:
|
|
300
|
+
`node wand.js poster demo.html --play 3 --brand <the deck's>`.
|
|
301
|
+
- **A clip** plays muted and looping while its slide is on; MP4 (H.264) plays
|
|
302
|
+
in every browser. The HTML carries it, so it carries its size: over 60 MB
|
|
303
|
+
warns.
|
|
304
|
+
- **The presenter's keys:** arrows, Space and PgUp/PgDn move (a clicker works),
|
|
305
|
+
P opens the presenter view, N the notes, G the overview, T the clock, B
|
|
306
|
+
blanks the screen, F goes full screen, D and R play and restart media, a
|
|
307
|
+
number then Enter jumps, ? lists them.
|
|
308
|
+
|
|
309
|
+
## 13. Storyboards
|
|
310
|
+
|
|
311
|
+
A storyboard turns a script into a film's plan (`storyboard()`, generalised
|
|
312
|
+
from the Wand OS film in `runs/2026-10-06-wand-os-video`): a cover, the run
|
|
313
|
+
of show, then a page a shot with its number, name and timecode, the
|
|
314
|
+
voiceover word for word, one to four frames on their cue words, and the
|
|
315
|
+
direction (the line, camera, motion, on screen, sound, into next).
|
|
316
|
+
|
|
317
|
+
- **The script is the content file**, one voiceover line a shot. Split a long
|
|
318
|
+
paragraph where the edit will cut. Coverage then proves every word of the
|
|
319
|
+
script is on the board.
|
|
320
|
+
- **Timing is scratch timing:** each line lasts its words at `wpm` (150 for a
|
|
321
|
+
conversational read, 160 to 170 for a promo), with `lead` before and `tail`
|
|
322
|
+
after; a shot without voiceover `hold`s; `seconds` on a shot overrides the
|
|
323
|
+
estimate once a real read exists. `target` warns when the board is more
|
|
324
|
+
than 10% off. `timing.csv` and `captions.srt` ship with the board.
|
|
325
|
+
- **A frame is drawn four ways:** `sketch` (people as dots, software as
|
|
326
|
+
labelled boxes, agents as sparks: the script's own vocabulary, in the
|
|
327
|
+
brand's colours), the job's own SVG on brand roles, an image (a product
|
|
328
|
+
screen, a demo poster), or left to draw, which the frames gate counts. The
|
|
329
|
+
first frame is on screen from the cut; the others land on their cues.
|
|
330
|
+
- **The HTML plays the board:** the pages to read, and a player (P, or the
|
|
331
|
+
button) that cross-fades the frames on their cues with the voiceover as the
|
|
332
|
+
caption, a track of every shot to scrub, and a title-safe overlay. An
|
|
333
|
+
`audio` file, a scratch or final read, leads the clock when given.
|
|
334
|
+
- **Both brands:** the board is one job; `--brand next` builds it on the
|
|
335
|
+
website's brand beside the first. Frames drawn as sketches or on brand roles
|
|
336
|
+
follow; an image frame is per brand when it shows the brand.
|
package/SKILL.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wand-design
|
|
3
|
-
description: Rebrand or create Wand material on the current or
|
|
3
|
+
description: Rebrand or create Wand material on the current or new brand - decks and talks, documents, case studies, storyboards from a script, social cards. Use when a file is attached or branded work is wanted.
|
|
4
4
|
compatibility: Requires Node.js 18+, code execution enabled in claude.ai (Settings > Features), and a Chromium the kit can find (Playwright's cache or the system). No network needed once installed. Python 3 embeds fonts in the .pptx; without it the deck ships with the faces named, not embedded.
|
|
5
5
|
license: Proprietary - Wand AI
|
|
6
6
|
metadata:
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.3.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
@@ -45,12 +45,18 @@ the default without asking; when their words ask for the other, pass
|
|
|
45
45
|
says which. The rule lives in the kit, not here, so it can change without
|
|
46
46
|
anyone reinstalling the skill.
|
|
47
47
|
|
|
48
|
+
**The same piece on the other brand:** `node wand.js ship <job> --brand next`
|
|
49
|
+
(or `current`) builds the job again on that brand, into `out-<brand>/`
|
|
50
|
+
beside the first. Nothing in the job changes.
|
|
51
|
+
|
|
48
52
|
## Read only what the job needs
|
|
49
53
|
|
|
50
54
|
| Job | Read |
|
|
51
55
|
|---|---|
|
|
52
56
|
| A document, a two-pager, a case study | `PROMPT.md` §5 and `templates/page.mjs`; `examples/overview/job.mjs` for a finished one |
|
|
53
|
-
| A deck
|
|
57
|
+
| A deck | §5 and `templates/slide.mjs` |
|
|
58
|
+
| An animated deck, a talk, a live demo | §12 and `templates/slide.mjs`; `examples/talk/job.mjs` for a finished one |
|
|
59
|
+
| A storyboard from a script | §13 and `templates/storyboard.mjs`; `examples/storyboard/job.mjs` |
|
|
54
60
|
| Social cards, a banner | `templates/card.mjs` |
|
|
55
61
|
| Edit a job this kit built | the job's `job.mjs` only, then `build` |
|
|
56
62
|
|
|
@@ -85,6 +91,35 @@ Author from the client piece: remove the client's figures, names, quotes and
|
|
|
85
91
|
vendors; put the client's name and its people in `denylist` so the gate proves
|
|
86
92
|
they are gone. Say in the note what was cut and what was generalised.
|
|
87
93
|
|
|
94
|
+
### An animated deck or a talk
|
|
95
|
+
|
|
96
|
+
Every deck's HTML already animates: blocks rise in, slides crossfade, and the
|
|
97
|
+
presenter has notes, a clock, an overview and a presenter view (P). What a
|
|
98
|
+
talk adds:
|
|
99
|
+
|
|
100
|
+
1. Notes on every slide, and `seconds` on each when the talk has a slot.
|
|
101
|
+
2. Motion where it carries the argument (PROMPT.md §12): `step(n, html)` for
|
|
102
|
+
a reveal in order, `stat({ count: true })` for a figure to watch arrive,
|
|
103
|
+
`enter("draw")` on a trend line, `enter("grow", i)` on bars. Restraint: one
|
|
104
|
+
moment a slide.
|
|
105
|
+
3. A live demo: one self-contained HTML file, then its poster on the deck's
|
|
106
|
+
brand: `node wand.js poster demo.html --play 3 --brand <brand>`, and
|
|
107
|
+
`c.demo({ src, poster })` on the slide. A clip: `c.video({ src, poster })`.
|
|
108
|
+
4. `ship` drives the HTML like a presenter before it ships (the runtime row).
|
|
109
|
+
Deliver the HTML as the deck to present, with the PDF and .pptx beside it.
|
|
110
|
+
|
|
111
|
+
### A storyboard from a script
|
|
112
|
+
|
|
113
|
+
1. The script into `content.txt`, one voiceover line a shot (split where the
|
|
114
|
+
edit will cut). `node wand.js new work/<name> --surface storyboard`.
|
|
115
|
+
2. **Write the shot list in the chat first**: act, name, the line, and what
|
|
116
|
+
each frame shows. Then the job: `c.storyboard({ title, wpm, target, shots })`.
|
|
117
|
+
3. Frames: `sketch` for a quick one in the brand's colours, the job's own SVG,
|
|
118
|
+
an image, or left to draw (the frames gate counts them).
|
|
119
|
+
4. `ship`: the board as PDF and .pptx, `timing.csv`, `captions.srt`, and the
|
|
120
|
+
HTML with its player. Say in the note that timing is scratch timing at the
|
|
121
|
+
`wpm` given until a real read exists, and which frames are still to draw.
|
|
122
|
+
|
|
88
123
|
### Social cards
|
|
89
124
|
|
|
90
125
|
`--surface square` or `og`; one `c.page()` per card; `out/pages/pNN.png` are
|
|
@@ -110,8 +145,10 @@ table has been checked by nobody.
|
|
|
110
145
|
|
|
111
146
|
## Available scripts
|
|
112
147
|
|
|
113
|
-
- **`wand.js`** — doctor, new, build, check, ship, watch, gallery, components, brands
|
|
148
|
+
- **`wand.js`** — doctor, new, build, check, ship, watch, poster, gallery, components, brands
|
|
114
149
|
- **`lib/engine.js`** — the render and the gates
|
|
115
150
|
- **`lib/components.js`** — the vocabulary and its CSS
|
|
116
151
|
- **`lib/pptx.js`**, **`tools/embed_fonts.py`** — the PowerPoint export
|
|
117
152
|
- **`lib/html_export.js`** — the one-file HTML with its runtime
|
|
153
|
+
- **`lib/talk.js`**, **`lib/board.js`** — the talk and the storyboard runtimes
|
|
154
|
+
- **`lib/runtime_check.js`** — the HTML driven like a presenter, at ship
|
package/START-HERE.md
CHANGED
|
@@ -6,8 +6,14 @@ node wand.js new work/acme --surface letter # the default brand; doct
|
|
|
6
6
|
# write work/acme/job.mjs (pages(c)) and, for a rebuild, work/acme/content.txt
|
|
7
7
|
node wand.js build work/acme/job.mjs # gates, PDF, a PNG per page, out/sheet.png
|
|
8
8
|
node wand.js ship work/acme/job.mjs --pptx # + one-file HTML, PowerPoint
|
|
9
|
+
node wand.js ship work/acme/job.mjs --brand next # the same job on the other brand, in out-next/
|
|
9
10
|
```
|
|
10
11
|
|
|
12
|
+
A deck's HTML is a talk: entrances, steps, a presenter view (P). A demo needs
|
|
13
|
+
a poster: `node wand.js poster demo.html --play 3`. A storyboard starts from
|
|
14
|
+
`node wand.js new work/film --surface storyboard`; its HTML plays the board.
|
|
15
|
+
`examples/talk/` and `examples/storyboard/` are finished ones.
|
|
16
|
+
|
|
11
17
|
`node wand.js components` lists the vocabulary; `examples/overview/job.mjs` is
|
|
12
18
|
a finished document and `examples/gallery/job.mjs` every component on one
|
|
13
19
|
sheet (`node wand.js gallery --brand next` draws it on the website's brand).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storyboard example — a 30-second film from a six-line script, the
|
|
3
|
+
* regression for the storyboard component and lib/board.js.
|
|
4
|
+
*
|
|
5
|
+
* The script is the content file, one voiceover line a shot, so coverage
|
|
6
|
+
* proves every word of it is on the board. Frames show each way a frame can
|
|
7
|
+
* be drawn: a sketch in the brand's colours (people, software, agents), the
|
|
8
|
+
* job's own SVG, an image (the talk example's demo poster), and one left to
|
|
9
|
+
* draw, which the frames gate counts.
|
|
10
|
+
*
|
|
11
|
+
* node wand.js ship examples/storyboard/job.mjs the current brand
|
|
12
|
+
* node wand.js ship examples/storyboard/job.mjs --brand next the website's, in out-next/
|
|
13
|
+
*
|
|
14
|
+
* Outputs: the board as PDF and PNGs, timing.csv, captions.srt, and one HTML
|
|
15
|
+
* file with the board to read and a player that runs it against the clock.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
name: "The dual workforce, a 30-second film",
|
|
19
|
+
file: "Wand_Dual_Workforce_Storyboard",
|
|
20
|
+
brand: "current",
|
|
21
|
+
surface: "slide",
|
|
22
|
+
tag: "Wand · Storyboard",
|
|
23
|
+
footer: "Wand AI · Draft for review",
|
|
24
|
+
content: "script.txt",
|
|
25
|
+
css: `
|
|
26
|
+
.frame-art .own .ring{fill:none;stroke:var(--stroke);stroke-width:6}
|
|
27
|
+
.frame-art .own .core{fill:var(--accent)}
|
|
28
|
+
.frame-art .own text{fill:var(--ink);font-family:var(--face-display);font-size:96px}
|
|
29
|
+
`,
|
|
30
|
+
pages: (c) => c.storyboard({
|
|
31
|
+
title: "The dual workforce",
|
|
32
|
+
lede: "A 30-second film for the website and LinkedIn. Six shots, three acts.",
|
|
33
|
+
wpm: 160,
|
|
34
|
+
target: 30,
|
|
35
|
+
shots: [
|
|
36
|
+
{ act: "Today", name: "Two things", vo: c.L(1),
|
|
37
|
+
line: "Every company is people plus software; we show both, plainly.",
|
|
38
|
+
camera: "Locked off, wide.", motion: "People dots settle in; three app boxes slide up.", sound: "Low pad comes in.", next: "Cut on 'software'.",
|
|
39
|
+
frames: [
|
|
40
|
+
{ sketch: { people: 6, label: "People" }, caption: "The team, as the script draws it: dots." },
|
|
41
|
+
{ cue: "software", sketch: { people: 6, apps: ["CRM", "ERP", "Email"], label: "People and software" }, caption: "The systems arrive beside them." },
|
|
42
|
+
] },
|
|
43
|
+
{ act: "Today", name: "Software waits", vo: c.L(2),
|
|
44
|
+
line: "Software only moves when a person moves it.",
|
|
45
|
+
camera: "Slow push in on the boxes.", motion: "An arrow from the people to each box, one at a time.", onscreen: "30 years", sound: "A single click per arrow.", next: "Hold, then dip.",
|
|
46
|
+
frames: [{ cue: "waited", sketch: { people: 3, apps: ["CRM", "ERP"], label: "Every task starts with a person" } }] },
|
|
47
|
+
{ act: "The arrival", name: "A new kind of worker", vo: c.L(3),
|
|
48
|
+
line: "The turn of the film: something new enters the frame.",
|
|
49
|
+
camera: "Pull out to reveal the right third.", motion: "A spark draws itself in, then pulses once.", sound: "Rising tone, then silence.", next: "Match cut on the spark.",
|
|
50
|
+
frames: [{ cue: "worker", art: `<svg class="own" viewBox="0 0 1920 1080"><circle class="ring" cx="960" cy="500" r="230"/><circle class="ring" cx="960" cy="500" r="330"/><path class="core" d="M960 380Q990 470 1080 500Q990 530 960 620Q930 530 840 500Q930 470 960 380Z"/><text x="960" y="930" text-anchor="middle">A new worker</text></svg>`, caption: "The job's own SVG, on brand roles." }] },
|
|
51
|
+
{ act: "The arrival", name: "Agents at work", vo: c.L(4),
|
|
52
|
+
line: "Agents do the work inside the systems people already use.",
|
|
53
|
+
camera: "Over the shoulder of the boxes.", motion: "Sparks move into the boxes; the boxes light up.", onscreen: "Read · Decide · Act", sound: "Three soft ticks.", next: "Cut.",
|
|
54
|
+
frames: [
|
|
55
|
+
{ sketch: { apps: ["CRM", "ERP", "Email"], agents: 3, label: "Agents inside the systems" } },
|
|
56
|
+
{ cue: "act", img: c.brand === "next" ? "../talk/agents-demo.poster-next.png" : "../talk/agents-demo.poster.png", caption: "The live product, as it looks on screen." },
|
|
57
|
+
] },
|
|
58
|
+
{ act: "Wand", name: "A place in the organisation", vo: c.L(5),
|
|
59
|
+
line: "Wand is what makes an agent a colleague: identity, manager, budget, record.",
|
|
60
|
+
camera: "Locked off.", motion: "Four labels type in under the spark, one per beat of the voiceover.", onscreen: "Identity · Manager · Budget · Record", sound: "One tick per label.", next: "Dissolve.",
|
|
61
|
+
frames: [
|
|
62
|
+
{ cue: "identity", sketch: { agents: 1, apps: ["Identity", "Manager"], label: "An agent, provisioned" } },
|
|
63
|
+
{ cue: "budget", sketch: { agents: 1, apps: ["Budget", "Record"], label: "Governed like a person" } },
|
|
64
|
+
{ cue: "record", caption: "End card for the governance beat: to draw." },
|
|
65
|
+
] },
|
|
66
|
+
{ act: "Wand", name: "One workforce", vo: c.L(6),
|
|
67
|
+
line: "The close: people and agents in one picture, then the wordmark.",
|
|
68
|
+
camera: "Slow pull out to the full frame.", motion: "Dots and sparks gather into one grid; the wordmark fades up.", sound: "Pad resolves.", next: "End.",
|
|
69
|
+
frames: [{ cue: "workforce", sketch: { people: 6, agents: 4, label: "One workforce", arrows: false } }] },
|
|
70
|
+
],
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Every company runs on two things: people and software.
|
|
2
|
+
For thirty years, software waited for people to tell it what to do.
|
|
3
|
+
Now a new kind of worker has arrived.
|
|
4
|
+
AI agents that read, decide and act, inside the systems you already run.
|
|
5
|
+
Wand gives each one a place in your organisation: an identity, a manager, a budget, a record.
|
|
6
|
+
One workforce. People and agents, working as one.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Agents at work</title>
|
|
6
|
+
<!--
|
|
7
|
+
A live demo for a talk deck: requests arrive, agents take them, a person
|
|
8
|
+
approves the ones that need a person. Self-contained, as a demo must be to
|
|
9
|
+
travel inside the deck. It dresses itself in the brand the deck hands it
|
|
10
|
+
(window.wandEmbed.brand) and answers the deck's D and R keys through
|
|
11
|
+
window.wandDemo = { play, pause, reset }.
|
|
12
|
+
-->
|
|
13
|
+
<style>
|
|
14
|
+
:root { --bg:#1A1033; --card:#291D5A; --stroke:#5B4BB0; --ink:#FFFFFF; --text:#F5F5F5; --label:#746B98; --accent:#9481FF; }
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
html, body { margin: 0; height: 100%; }
|
|
17
|
+
body { background: var(--bg); color: var(--text); font: 15px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; overflow: hidden; }
|
|
18
|
+
main { height: 100%; display: grid; grid-template-columns: 1fr 1.25fr 1fr; gap: 18px; padding: 26px; }
|
|
19
|
+
section { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; border: 1px solid var(--stroke); border-radius: 14px; background: color-mix(in srgb, var(--card) 55%, transparent); }
|
|
20
|
+
h2 { margin: 0 0 4px; font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--label); display: flex; justify-content: space-between; }
|
|
21
|
+
h2 b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
|
22
|
+
.lane { display: flex; flex-direction: column; gap: 8px; overflow: hidden; flex: 1; }
|
|
23
|
+
.task { padding: 10px 12px; border-radius: 10px; background: var(--card); border: 1px solid var(--stroke); color: var(--ink); font-size: 14px; display: flex; justify-content: space-between; gap: 10px; animation: in 380ms cubic-bezier(.16,1,.3,1) both; }
|
|
24
|
+
.task i { font-style: normal; color: var(--label); font-size: 12px; white-space: nowrap; }
|
|
25
|
+
.task.agent { border-color: var(--accent); }
|
|
26
|
+
.task.agent i { color: var(--accent); }
|
|
27
|
+
.task.human i { color: var(--ink); }
|
|
28
|
+
.bar { height: 3px; border-radius: 2px; background: var(--stroke); overflow: hidden; margin-top: 6px; }
|
|
29
|
+
.bar > span { display: block; height: 100%; width: 0; background: var(--accent); }
|
|
30
|
+
@keyframes in { from { opacity: 0; transform: translateY(8px); } }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<main>
|
|
35
|
+
<section><h2>Incoming <b id="n-in">0</b></h2><div class="lane" id="in"></div></section>
|
|
36
|
+
<section><h2>Agents working <b id="n-work">0</b></h2><div class="lane" id="work"></div></section>
|
|
37
|
+
<section><h2>Done <b id="n-done">0</b></h2><div class="lane" id="done"></div></section>
|
|
38
|
+
</main>
|
|
39
|
+
<script>
|
|
40
|
+
(() => {
|
|
41
|
+
const E = window.wandEmbed || {}, B = E.brand || {};
|
|
42
|
+
for (const k of ["bg", "card", "stroke", "ink", "text", "label", "accent"]) if (B[k]) document.documentElement.style.setProperty("--" + k, B[k]);
|
|
43
|
+
const KINDS = ["Invoice match", "Refund request", "Address change", "Claim review", "Contract renewal", "Access request", "Vendor onboarding", "Policy question"];
|
|
44
|
+
const lanes = { in: document.getElementById("in"), work: document.getElementById("work"), done: document.getElementById("done") };
|
|
45
|
+
let seq = 0, timer = null, tick = 0, done = 0;
|
|
46
|
+
const count = () => { for (const k of ["in", "work", "done"]) document.getElementById("n-" + k).textContent = k === "done" ? done : lanes[k].children.length; };
|
|
47
|
+
const card = (txt, cls, tag) => { const d = document.createElement("div"); d.className = "task " + cls; d.innerHTML = "<span>" + txt + "</span><i>" + tag + "</i>"; return d; };
|
|
48
|
+
function step() {
|
|
49
|
+
tick++;
|
|
50
|
+
if (lanes.in.children.length < 6) lanes.in.append(card(KINDS[seq++ % KINDS.length] + " #" + (1040 + seq), "", "queued"));
|
|
51
|
+
if (tick % 2 === 0 && lanes.in.firstElementChild && lanes.work.children.length < 4) {
|
|
52
|
+
const t = lanes.in.firstElementChild, name = t.firstElementChild.textContent, human = seq % 5 === 0;
|
|
53
|
+
t.remove(); const w = card(name, human ? "human" : "agent", human ? "needs a person" : "agent"); const bar = document.createElement("div"); bar.className = "bar"; bar.innerHTML = "<span></span>"; w.firstElementChild.append(bar); lanes.work.append(w);
|
|
54
|
+
}
|
|
55
|
+
[...lanes.work.children].forEach((w) => { const s = w.querySelector(".bar > span"); const v = (parseFloat(s.style.width) || 0) + (w.classList.contains("human") ? 9 : 22); s.style.width = Math.min(100, v) + "%"; if (v >= 100) { w.remove(); done++; lanes.done.prepend(card(w.firstElementChild.firstChild.textContent, w.classList.contains("human") ? "human" : "agent", w.classList.contains("human") ? "approved" : "resolved")); if (lanes.done.children.length > 7) lanes.done.lastElementChild.remove(); } });
|
|
56
|
+
count();
|
|
57
|
+
}
|
|
58
|
+
const api = {
|
|
59
|
+
play() { if (!timer) timer = setInterval(step, E.motion === false ? 900 : 450); },
|
|
60
|
+
pause() { clearInterval(timer); timer = null; },
|
|
61
|
+
reset() { api.pause(); for (const l of Object.values(lanes)) l.replaceChildren(); seq = 0; tick = 0; done = 0; for (let i = 0; i < 8; i++) step(); },
|
|
62
|
+
};
|
|
63
|
+
window.wandDemo = api;
|
|
64
|
+
api.reset();
|
|
65
|
+
if (E.motion !== false) api.play();
|
|
66
|
+
})();
|
|
67
|
+
</script>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The talk example — an animated deck, the regression for lib/talk.js.
|
|
3
|
+
*
|
|
4
|
+
* Every motion the runtime knows, once: entrances staggered in for a slide
|
|
5
|
+
* that marks nothing (`talk.motion: "auto"`), figures that count up, a chart
|
|
6
|
+
* whose line draws and whose bars grow, steps the presenter brings in one at
|
|
7
|
+
* a time, and a live demo in a frame with its poster for the static exports.
|
|
8
|
+
* `seconds` on each slide gives the presenter's clock a pace to keep.
|
|
9
|
+
*
|
|
10
|
+
* node wand.js ship examples/talk/job.mjs --pptx the current brand
|
|
11
|
+
* node wand.js ship examples/talk/job.mjs --pptx --brand next the website's, in out-next/
|
|
12
|
+
* node wand.js poster examples/talk/agents-demo.html --play 3 [--brand next]
|
|
13
|
+
*
|
|
14
|
+
* The demo's poster is per brand, because the demo dresses itself in the
|
|
15
|
+
* deck's colours: agents-demo.poster.png and agents-demo.poster-next.png.
|
|
16
|
+
*/
|
|
17
|
+
import { DEFAULT_BRAND } from "../../lib/brand.js";
|
|
18
|
+
|
|
19
|
+
const brandOf = (c) => c.brand || DEFAULT_BRAND;
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: "Agents at work",
|
|
23
|
+
file: "Wand_Agents_at_Work_Talk",
|
|
24
|
+
brand: "current",
|
|
25
|
+
surface: "slide",
|
|
26
|
+
tag: "Wand",
|
|
27
|
+
footer: "Wand AI · Example talk",
|
|
28
|
+
talk: { motion: "auto", minutes: 8 },
|
|
29
|
+
css: `
|
|
30
|
+
.chart{width:100%;height:auto;display:block}
|
|
31
|
+
.chart .axis{stroke:var(--rule);stroke-width:2}
|
|
32
|
+
.chart .line{fill:none;stroke:var(--accent);stroke-width:6;stroke-linecap:round;stroke-linejoin:round}
|
|
33
|
+
.chart .bar{fill:var(--emph)}
|
|
34
|
+
.chart text{fill:var(--soft);font-family:var(--face-body);font-size:22px}
|
|
35
|
+
.demo-wrap{margin-top:calc(var(--m-gap) * .6);width:66%}
|
|
36
|
+
`,
|
|
37
|
+
pages: (c) => [
|
|
38
|
+
c.cover({
|
|
39
|
+
label: "Example talk",
|
|
40
|
+
title: `Agents at <em>work</em>`,
|
|
41
|
+
lede: "What changes when software stops waiting to be told.",
|
|
42
|
+
meta: "An animated deck from the Wand studio",
|
|
43
|
+
notes: "Open on the question. Nothing on this slide needs explaining; let the title land.",
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
c.page({ eyebrow: "The shift", seconds: 60, notes: "Left is every company today. Right is what we deploy. Pause on the word 'colleagues'.", body: `
|
|
47
|
+
${c.h1(`Two <em>models</em>`)}
|
|
48
|
+
${c.cols(2, [
|
|
49
|
+
c.card({ tag: "Today", title: "Software waits", level: "h3", body: [`People read, decide and act. Software holds the records and waits to be told.`] }),
|
|
50
|
+
c.card({ tag: "With agents", title: "Software works", level: "h3", emph: true, body: [`Agents read, decide and act inside the same systems, and people manage them like colleagues.`] }),
|
|
51
|
+
])}
|
|
52
|
+
` }),
|
|
53
|
+
|
|
54
|
+
c.page({ eyebrow: "What it adds up to", seconds: 50, notes: "The three figures count up as the slide arrives. Read the first one out; the room reads the rest.", body: `
|
|
55
|
+
${c.h1("In the first year")}
|
|
56
|
+
${c.stats([{ v: "62%", k: "of routine requests closed by agents", hero: true, count: true }, { v: "4,800", k: "hours a month back to the team", count: true }, { v: "3.5x", k: "faster from request to resolution", count: true }])}
|
|
57
|
+
` }),
|
|
58
|
+
|
|
59
|
+
c.page({ eyebrow: "How it rolls out", seconds: 90, notes: "Three steps; press once for each. Do not jump ahead: each step is the reason for the next.", body: `
|
|
60
|
+
${c.h1("One step at a time")}
|
|
61
|
+
${c.cols(3, [
|
|
62
|
+
c.step(1, c.card({ tag: "Step 1", title: "Give agents an identity", body: ["Every agent is provisioned like a person: an owner, access, a record."] })),
|
|
63
|
+
c.step(2, c.card({ tag: "Step 2", title: "Start where the volume is", body: ["Pick the work that is high in volume and low in judgement."] })),
|
|
64
|
+
c.step(3, c.card({ tag: "Step 3", title: "Widen what they may decide", body: ["Raise the limits as the record earns it."], emph: true })),
|
|
65
|
+
])}
|
|
66
|
+
` }),
|
|
67
|
+
|
|
68
|
+
c.page({ eyebrow: "The curve", seconds: 60, notes: "The line draws itself: resolution time falling as agents take the volume. The bars are the agents' share.", body: `
|
|
69
|
+
${c.h1("Time to resolve, month by month")}
|
|
70
|
+
<svg class="chart" viewBox="0 0 1600 520" role="img" aria-label="Resolution time falls while the agents' share of requests rises">
|
|
71
|
+
<line class="axis" x1="60" y1="460" x2="1560" y2="460"/>
|
|
72
|
+
${[18, 26, 35, 44, 52, 58, 62].map((v, i) => `<rect class="bar"${c.enter("grow", i)} x="${120 + i * 205}" y="${460 - v * 5}" width="110" height="${v * 5}"/>`).join("")}
|
|
73
|
+
<path class="line" pathLength="1"${c.enter("draw", 2)} d="M175 120 L380 170 L585 230 L790 300 L995 350 L1200 385 L1405 400"/>
|
|
74
|
+
${["M1", "M2", "M3", "M4", "M5", "M6", "M7"].map((m, i) => `<text x="${175 + i * 205}" y="500" text-anchor="middle">${m}</text>`).join("")}
|
|
75
|
+
</svg>
|
|
76
|
+
` }),
|
|
77
|
+
|
|
78
|
+
c.page({ eyebrow: "Live", seconds: 120, notes: "The demo is live: press D to pause or play it, R to restart. Esc gives the keys back to the deck.", body: `
|
|
79
|
+
${c.h1(`Watch them <em>work</em>`, "md")}
|
|
80
|
+
<div class="demo-wrap">${c.demo({ src: "agents-demo.html", poster: brandOf(c) === "next" ? "agents-demo.poster-next.png" : "agents-demo.poster.png", label: "Agents at work, live" })}</div>
|
|
81
|
+
` }),
|
|
82
|
+
|
|
83
|
+
c.closing({ pair: "Wand", title: "Agents at work", url: "www.wand.ai", eyebrow: "Thank you" }),
|
|
84
|
+
],
|
|
85
|
+
};
|
package/lib/board.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/board.js — the runtime a storyboard carries in its one-file HTML.
|
|
3
|
+
*
|
|
4
|
+
* Generalised from the Wand OS film's board-o-matic
|
|
5
|
+
* (runs/2026-10-06-wand-os-video/storyboard/build.mjs): the shot pages stay
|
|
6
|
+
* readable as a scroll, and a player runs the frames against the timeline the
|
|
7
|
+
* storyboard component computed, cross-fading on each frame's cue, with the
|
|
8
|
+
* shot's voiceover as the caption and a track of every shot to scrub. With an
|
|
9
|
+
* audio file (a scratch or final voiceover) the sound leads and the frames
|
|
10
|
+
* follow it; without one, the clock does.
|
|
11
|
+
*
|
|
12
|
+
* Its controls read `--ui-*`, the brand's dark roles, so it dresses itself in
|
|
13
|
+
* whichever brand the board is on.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function boardShell(board) {
|
|
17
|
+
return {
|
|
18
|
+
before: `<button class="board-play" type="button" data-ui="play-board" aria-label="Play the storyboard (P)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5v15l12-7.5z"/></svg>Play the board</button><div class="board-pages">`,
|
|
19
|
+
after: `</div>
|
|
20
|
+
<div class="player" role="dialog" aria-modal="true" aria-label="Storyboard player" hidden>
|
|
21
|
+
<div class="pl-stage"><div class="pl-frames"></div><div class="pl-shot"></div><div class="pl-safe" hidden></div></div>
|
|
22
|
+
<p class="pl-vo" aria-live="off"></p>
|
|
23
|
+
<div class="pl-bar">
|
|
24
|
+
<button class="pl-btn" type="button" data-pl="toggle" aria-label="Play or pause (Space)"></button>
|
|
25
|
+
<span class="pl-tc"><b>0:00.0</b> / <i></i></span>
|
|
26
|
+
<div class="pl-track" role="slider" aria-label="Timeline" tabindex="0"></div>
|
|
27
|
+
<button class="pl-btn txt" type="button" data-pl="safe" aria-pressed="false">Safe area</button>
|
|
28
|
+
<button class="pl-btn txt" type="button" data-pl="close">Close</button>
|
|
29
|
+
</div>
|
|
30
|
+
${board.audio ? `<audio class="pl-audio" preload="auto" src="${board.audio}"></audio>` : ""}
|
|
31
|
+
</div>`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const BOARD_CSS = `<style id="wand-board">
|
|
36
|
+
html,body{margin:0}
|
|
37
|
+
body.board-on{background:var(--ui-bg);color:var(--ui-text);font-family:var(--ui-face),system-ui,sans-serif;-webkit-font-smoothing:antialiased}
|
|
38
|
+
body.board-on button{font:inherit;color:inherit}
|
|
39
|
+
[hidden]{display:none!important}
|
|
40
|
+
.board-pages{padding:72px 0 32px}
|
|
41
|
+
.board-pages>.page{margin:0 auto 24px;transform-origin:top center;transform:scale(var(--k,1));box-shadow:0 2px 24px color-mix(in srgb,var(--ui-bg) 70%,transparent)}
|
|
42
|
+
.board-play{position:fixed;top:16px;right:20px;z-index:20;display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border:1px solid var(--ui-stroke);border-radius:10px;background:var(--ui-card);color:var(--ui-ink);cursor:pointer;font-weight:600;font-size:14px}
|
|
43
|
+
.board-play svg{width:14px;height:14px;fill:currentColor}
|
|
44
|
+
.board-play:hover{border-color:var(--ui-accent)}
|
|
45
|
+
.player{position:fixed;inset:0;z-index:50;display:grid;grid-template-rows:1fr auto auto;gap:14px;padding:24px 28px 18px;background:var(--ui-bg)}
|
|
46
|
+
.pl-stage{position:relative;container-type:size;min-height:0;display:grid;place-items:center}
|
|
47
|
+
.pl-frames{position:relative;width:min(100cqw,100cqh * 16 / 9);aspect-ratio:16/9;overflow:hidden;border-radius:12px;background:var(--ui-card);box-shadow:0 0 0 1px var(--ui-stroke)}
|
|
48
|
+
.pl-frames>.pf{position:absolute;inset:0;opacity:0;transition:opacity 380ms cubic-bezier(.65,0,.35,1)}
|
|
49
|
+
.pl-frames>.pf.on{opacity:1}
|
|
50
|
+
.pl-frames>.pf>.frame-art{position:absolute;inset:0;border:0;border-radius:0;aspect-ratio:auto}
|
|
51
|
+
.pl-shot{position:absolute;left:calc(50% - min(100cqw,100cqh * 16 / 9) / 2 + 16px);top:calc(50% - min(100cqw,100cqh * 16 / 9) * 9 / 32 + 14px);padding:5px 10px;border-radius:8px;background:color-mix(in srgb,var(--ui-bg) 78%,transparent);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ui-label);font-variant-numeric:tabular-nums}
|
|
52
|
+
.pl-safe{position:absolute;width:min(100cqw,100cqh * 16 / 9);aspect-ratio:16/9;pointer-events:none;background:
|
|
53
|
+
linear-gradient(var(--ui-accent),var(--ui-accent)) 5% 5%/90% 1px no-repeat,linear-gradient(var(--ui-accent),var(--ui-accent)) 5% 95%/90% 1px no-repeat,
|
|
54
|
+
linear-gradient(var(--ui-accent),var(--ui-accent)) 5% 5%/1px 90% no-repeat,linear-gradient(var(--ui-accent),var(--ui-accent)) 95% 5%/1px 90% no-repeat;opacity:.7}
|
|
55
|
+
.pl-vo{margin:0 auto;max-width:min(1100px,92vw);min-height:3.2em;text-align:center;font-size:clamp(15px,1.6vw,22px);line-height:1.5;color:var(--ui-ink);transition:opacity 200ms}
|
|
56
|
+
.pl-vo.off{opacity:.35}
|
|
57
|
+
.pl-bar{display:flex;align-items:center;gap:12px}
|
|
58
|
+
.pl-btn{display:inline-grid;place-items:center;min-width:36px;height:36px;padding:0 10px;border:1px solid var(--ui-stroke);border-radius:9px;background:var(--ui-card);color:var(--ui-ink);cursor:pointer}
|
|
59
|
+
.pl-btn svg{width:14px;height:14px;fill:currentColor}
|
|
60
|
+
.pl-btn.txt{font-size:13px}
|
|
61
|
+
.pl-btn[aria-pressed="true"]{border-color:var(--ui-accent);color:var(--ui-accent)}
|
|
62
|
+
.pl-tc{font-size:13px;color:var(--ui-label);font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
63
|
+
.pl-tc b{color:var(--ui-ink);font-weight:600}
|
|
64
|
+
.pl-tc i{font-style:normal}
|
|
65
|
+
.pl-track{position:relative;flex:1;display:flex;gap:2px;height:34px;cursor:pointer;outline-offset:3px}
|
|
66
|
+
.pl-track>.seg{position:relative;flex:var(--d) 1 0;min-width:2px;border-radius:4px;background:var(--ui-card);border:1px solid var(--ui-stroke);overflow:hidden}
|
|
67
|
+
.pl-track>.seg.act{border-left:2px solid var(--ui-accent)}
|
|
68
|
+
.pl-track>.seg>span{position:absolute;left:5px;top:50%;transform:translateY(-50%);font-size:10px;color:var(--ui-label);white-space:nowrap;pointer-events:none}
|
|
69
|
+
.pl-track>.seg.here{background:color-mix(in srgb,var(--ui-accent) 22%,var(--ui-card))}
|
|
70
|
+
.pl-track>.head{position:absolute;top:-4px;bottom:-4px;width:2px;background:var(--ui-ink);left:var(--x,0%);pointer-events:none}
|
|
71
|
+
.pl-track:focus-visible,.pl-btn:focus-visible,.board-play:focus-visible{outline:3px solid var(--ui-accent)}
|
|
72
|
+
@media (prefers-reduced-motion:reduce){.pl-frames>.pf{transition:none}}
|
|
73
|
+
@media print{.board-play,.player{display:none!important}.board-pages{padding:0}.board-pages>.page{margin:0;transform:none;box-shadow:none;break-after:page}body.board-on{background:none}}
|
|
74
|
+
</style>`;
|
|
75
|
+
|
|
76
|
+
export const BOARD_JS = String.raw`(() => {
|
|
77
|
+
"use strict";
|
|
78
|
+
const $ = (s, r = document) => r.querySelector(s);
|
|
79
|
+
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
|
|
80
|
+
const B = document.body, W = +B.dataset.w;
|
|
81
|
+
const D = JSON.parse($("#deck-data").textContent);
|
|
82
|
+
const shots = D.shots, total = D.total;
|
|
83
|
+
const pages = $$(".board-pages > .page");
|
|
84
|
+
B.classList.add("board-on");
|
|
85
|
+
const fit = () => { const k = Math.min(1, (innerWidth - 48) / W); pages.forEach((p) => { p.style.setProperty("--k", k); p.style.marginBottom = 24 - p.offsetHeight * (1 - k) + "px"; }); };
|
|
86
|
+
addEventListener("resize", fit); fit();
|
|
87
|
+
|
|
88
|
+
const tc = (t) => Math.floor(t / 60) + ":" + (t % 60).toFixed(1).padStart(4, "0");
|
|
89
|
+
const player = $(".player"), framesEl = $(".pl-frames"), voEl = $(".pl-vo"), shotEl = $(".pl-shot"), track = $(".pl-track");
|
|
90
|
+
const audio = $(".pl-audio");
|
|
91
|
+
const PLAY = '<svg viewBox="0 0 24 24"><path d="M7 4.5v15l12-7.5z"/></svg>', PAUSE = '<svg viewBox="0 0 24 24"><path d="M6 4.5h4v15H6zM14 4.5h4v15h-4z"/></svg>';
|
|
92
|
+
|
|
93
|
+
// every frame, in time order, cloned from its shot page
|
|
94
|
+
const FR = [];
|
|
95
|
+
shots.forEach((s, si) => s.frames.forEach((f) => {
|
|
96
|
+
const src = $('.frame[data-frame="' + f.id + '"] .frame-art');
|
|
97
|
+
const pf = document.createElement("div"); pf.className = "pf"; pf.dataset.id = f.id;
|
|
98
|
+
if (src) pf.append(src.cloneNode(true));
|
|
99
|
+
framesEl.append(pf); FR.push({ ...f, si, el: pf });
|
|
100
|
+
}));
|
|
101
|
+
FR.sort((a, b) => a.t - b.t);
|
|
102
|
+
$(".pl-tc i").textContent = tc(total);
|
|
103
|
+
shots.forEach((s, i) => {
|
|
104
|
+
const seg = document.createElement("div"); seg.className = "seg" + (i === 0 || s.act !== shots[i - 1].act ? " act" : "");
|
|
105
|
+
seg.style.setProperty("--d", Math.max(0.1, s.dur)); seg.title = String(s.n).padStart(2, "0") + " " + s.name;
|
|
106
|
+
seg.innerHTML = "<span>" + String(s.n).padStart(2, "0") + "</span>"; track.append(seg);
|
|
107
|
+
});
|
|
108
|
+
const head = document.createElement("i"); head.className = "head"; track.append(head);
|
|
109
|
+
|
|
110
|
+
let t = 0, playing = false, t0 = 0, last = -1;
|
|
111
|
+
const shotAt = (x) => { let i = shots.findIndex((s) => x < s.out); return i < 0 ? shots.length - 1 : i; };
|
|
112
|
+
function paint() {
|
|
113
|
+
const si = shotAt(t), s = shots[si];
|
|
114
|
+
let fi = -1; for (let i = 0; i < FR.length; i++) if (FR[i].t <= t + 1e-6) fi = i;
|
|
115
|
+
if (fi < 0) fi = 0;
|
|
116
|
+
if (fi !== last) { FR.forEach((f, i) => f.el.classList.toggle("on", i === fi)); last = fi; }
|
|
117
|
+
shotEl.textContent = "Shot " + String(s.n).padStart(2, "0") + (s.name ? " · " + s.name : "");
|
|
118
|
+
if (voEl.dataset.n !== String(s.n)) { voEl.textContent = s.vo || ""; voEl.dataset.n = s.n; }
|
|
119
|
+
voEl.classList.toggle("off", !(t >= s.voIn && t <= s.voOut));
|
|
120
|
+
$(".pl-tc b").textContent = tc(t);
|
|
121
|
+
head.style.setProperty("--x", (t / total) * 100 + "%");
|
|
122
|
+
$$(".seg", track).forEach((g, i) => g.classList.toggle("here", i === si));
|
|
123
|
+
}
|
|
124
|
+
function frame() {
|
|
125
|
+
if (!playing) return;
|
|
126
|
+
t = audio && !audio.paused ? audio.currentTime : (performance.now() - t0) / 1000;
|
|
127
|
+
if (t >= total) { t = total; pause(); }
|
|
128
|
+
paint(); if (playing) requestAnimationFrame(frame);
|
|
129
|
+
}
|
|
130
|
+
function play() { if (t >= total) t = 0; playing = true; t0 = performance.now() - t * 1000; if (audio) { audio.currentTime = t; audio.play().catch(() => {}); } $('[data-pl="toggle"]').innerHTML = PAUSE; requestAnimationFrame(frame); }
|
|
131
|
+
function pause() { playing = false; audio?.pause(); $('[data-pl="toggle"]').innerHTML = PLAY; paint(); }
|
|
132
|
+
function seek(x) { t = Math.max(0, Math.min(total, x)); t0 = performance.now() - t * 1000; if (audio) audio.currentTime = t; paint(); }
|
|
133
|
+
function open() { player.hidden = false; seek(t); $('[data-pl="toggle"]').innerHTML = PLAY; $('[data-pl="toggle"]').focus(); }
|
|
134
|
+
function close() { pause(); player.hidden = true; }
|
|
135
|
+
track.addEventListener("click", (e) => { const r = track.getBoundingClientRect(); seek(((e.clientX - r.left) / r.width) * total); });
|
|
136
|
+
document.addEventListener("click", (e) => {
|
|
137
|
+
if (e.target.closest('[data-ui="play-board"]')) { open(); play(); return; }
|
|
138
|
+
const b = e.target.closest("[data-pl]"); if (!b) return;
|
|
139
|
+
const a = b.dataset.pl;
|
|
140
|
+
if (a === "toggle") playing ? pause() : play();
|
|
141
|
+
if (a === "close") close();
|
|
142
|
+
if (a === "safe") { const s = $(".pl-safe"); s.hidden = !s.hidden; b.setAttribute("aria-pressed", String(!s.hidden)); }
|
|
143
|
+
});
|
|
144
|
+
addEventListener("keydown", (e) => {
|
|
145
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
146
|
+
if (player.hidden) { if (e.key === "p" || e.key === "P") { e.preventDefault(); open(); play(); } return; }
|
|
147
|
+
const si = shotAt(t);
|
|
148
|
+
const map = { " ": () => (playing ? pause() : play()), Escape: close, ArrowRight: () => seek(shots[Math.min(shots.length - 1, si + 1)].in), ArrowLeft: () => seek(t - shots[si].in > 0.6 ? shots[si].in : shots[Math.max(0, si - 1)].in), Home: () => seek(0), End: () => seek(total) };
|
|
149
|
+
const fn = map[e.key]; if (!fn) return; e.preventDefault(); fn();
|
|
150
|
+
});
|
|
151
|
+
window.wandBoard = { open, close, play, pause, seek, get t() { return t; }, get playing() { return playing; }, total, frames: FR.length };
|
|
152
|
+
})();`;
|
package/lib/brand.js
CHANGED
|
@@ -128,7 +128,10 @@ function brandCss(tokens, roles, fonts, extras = {}) {
|
|
|
128
128
|
`@font-face{font-family:"${f.family}";font-weight:${f.weight};font-style:${f.italic ? "italic" : "normal"};src:url("${url(path.join(fonts, f.file))}")}`).join("\n");
|
|
129
129
|
const F = tokens.faces, S = tokens.shape;
|
|
130
130
|
const face = (k) => `--face-${k}:"${F[k].family}";--w-${k}:${F[k].weight};--tr-${k}:${F[k].tracking}em;--lh-${k}:${F[k].leading}`;
|
|
131
|
+
// the talk and board runtimes' own controls, dressed in the brand's dark roles
|
|
132
|
+
const d = roles.dark, ui = { bg: d.track, card: d.paper, stroke: d.stroke, ink: d.ink, text: d.text, label: d.muted, accent: d.accent };
|
|
131
133
|
return `${faces}
|
|
134
|
+
:root{${Object.entries(ui).map(([k, v]) => `--ui-${k}:#${v}`).join(";")};--ui-face:"${F.body.family}"}
|
|
132
135
|
.page{${vars(roles.light)};${Object.keys(extras).length ? vars(extras) + ";" : ""}${face("display")};${face("body")};${face("label")};--caps-label:${F.label.caps ? "uppercase" : "none"};
|
|
133
136
|
--radius-k:${S.radius};--card-stroke:${S.cardStroke}px;--emph-stroke:${S.emphStroke}px}
|
|
134
137
|
.page.dark{${vars(roles.dark)}}`;
|