wand-kit 0.3.1 → 0.3.3
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 +49 -5
- package/SKILL.md +18 -7
- package/brands/current/tokens.json +10 -2
- package/brands/next/tokens.json +14 -2
- package/examples/storyboard/job.mjs +3 -3
- package/lib/components.js +45 -9
- package/lib/engine.js +51 -0
- package/lib/surfaces.js +4 -1
- package/lib/talk.js +37 -4
- package/package.json +1 -1
- package/wand.js +1 -1
package/PROMPT.md
CHANGED
|
@@ -80,13 +80,24 @@ the ground's values, so one component draws on both.
|
|
|
80
80
|
| `label` | 6C5CE7 | A78BFA | tags, section numbers |
|
|
81
81
|
| `emph` / `emphInk` / `emphText` / `emphAccent` | 251C63 / FFF / DCD6F5 / AA9BFF | 4D2E8C / FFF / DCD6F5 / C4B5F4 | the emphasis card and what sits on it |
|
|
82
82
|
| `stroke` / `rule` / `track` / `surface` | E4E0F2 / E4E0F2 / E9E6F5 / EBE8F8 | 5B4BB0 / 3A2E63 / 1A1033 / 1A1033 | card strokes, hairlines, meter tracks, tiles |
|
|
83
|
+
| `second` / `secondSm` | 6C5CE7 / 6C5CE7 | 9481FF / 9481FF | a title's phrase, large / small (on this brand, the accent itself) |
|
|
84
|
+
| `gradeLow` / `gradeHigh` | 240F48 / F8F6FB | the same | the photograph grade: where blacks lift to, where highlights settle |
|
|
83
85
|
|
|
84
86
|
The next brand fills the same roles from the website (gold the one accent,
|
|
85
|
-
violets as grounds, beige as paper); see `brand-next/tokens.json`.
|
|
87
|
+
violets as grounds, beige as paper); see `brand-next/tokens.json`. Its
|
|
88
|
+
`second` is a quiet tone, not gold (7B7B79 on light, 9D99A8 on dark, large
|
|
89
|
+
type only; `secondSm`, the text grey, below that size).
|
|
86
90
|
|
|
87
91
|
**Rules.**
|
|
88
92
|
- One accent. The accent phrase in a title is the single most recognisable
|
|
89
93
|
device in the system: one per title, never two, the sentence period in ink.
|
|
94
|
+
- **On the next brand a title has two tones and gold is rare.** The phrase
|
|
95
|
+
takes `second`, a quieter tone of the ink; gold stays for the pages that
|
|
96
|
+
carry the brand's art (the cover, a statement, the close), for figures and
|
|
97
|
+
for the spark. The website's own headings carry no gold at all, and the
|
|
98
|
+
wand.ai rebuild (October 2026) read as a template while every heading's
|
|
99
|
+
second line was gold: when gold is everywhere it means nothing. On the
|
|
100
|
+
current brand `second` is the accent, so nothing changes there.
|
|
90
101
|
- One emphasis card per row, at most one or two per page.
|
|
91
102
|
- Light pages carry no art. Dark pages carry art on the cover, the closing and
|
|
92
103
|
a statement; a dark content page is flat.
|
|
@@ -142,6 +153,7 @@ The components (`node wand.js components`; `kit/lib/components.js`):
|
|
|
142
153
|
| `quote({ text, by, emph })`, `who` | testimonial cards, name over role, no photographs |
|
|
143
154
|
| `stats`, `delta(from, to)` | figures, before-and-after |
|
|
144
155
|
| `steps(items)` | next steps with arrows |
|
|
156
|
+
| `photo(name, { pos })` | a brand photograph filling its box, with the brand's grade |
|
|
145
157
|
| `callout`, `list`, `chapter`, `img`, `icon`, `tile` | the rest |
|
|
146
158
|
| `enter(kind, i)`, `step(n, html)`, `countup(text)`, `stat({ count })` | motion in a talk (§12) |
|
|
147
159
|
| `video({ src, poster })`, `demo({ src, poster })` | a clip or a live page on a slide (§12) |
|
|
@@ -165,6 +177,26 @@ decide, and that make or break a page:
|
|
|
165
177
|
never the place for the argument.
|
|
166
178
|
- **Equal heights in a row** (`stretch`), titles at one height, and the empty
|
|
167
179
|
space at the foot of a page shared by its sections, never left under one.
|
|
180
|
+
- **One arc.** Night, then day, then first light: the dark cover with its
|
|
181
|
+
art, the argument on light pages, one dark statement where the piece turns,
|
|
182
|
+
and the close. A piece that switches ground on every page reads as a
|
|
183
|
+
slideshow of boxes.
|
|
184
|
+
- **No composition three pages running.** The same heading, then the same
|
|
185
|
+
three cards, page after page, is what makes a set of pages read as a
|
|
186
|
+
template; the variety gate names the run. Vary it: a statement set large,
|
|
187
|
+
a split with a photograph, a figure, a page that is one exhibit.
|
|
188
|
+
- **Photographs large or not at all**, through `photo()`: full bleed or a full
|
|
189
|
+
column, never a thumbnail; architecture and light from the brand's set,
|
|
190
|
+
never stock (a person at a desk, a stethoscope, a form, a highway). Every
|
|
191
|
+
photograph takes the brand's one grade, so they read as one shoot.
|
|
192
|
+
- **An exhibit on hairlines, not in a box.** A diagram or a flow sits on the
|
|
193
|
+
ground with rules; a frame around it reads as a dashboard widget.
|
|
194
|
+
- **A slide balances and sizes itself.** Its body sits in the space under the
|
|
195
|
+
title, not in the top quarter; a slide whose content fills under 55% of it
|
|
196
|
+
takes the type scale a step up (`.big`) when that still fits, and the space
|
|
197
|
+
gate names any slide still under 40%. A thin slide wants its proof, a
|
|
198
|
+
figure or a visual, or to be joined to the next; `autosize: false` on a page
|
|
199
|
+
keeps it as composed.
|
|
168
200
|
|
|
169
201
|
## 6. Pipeline
|
|
170
202
|
|
|
@@ -185,7 +217,12 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
185
217
|
per page, the contact sheet. `watch` does this on every save in under a
|
|
186
218
|
second with the browser kept warm.
|
|
187
219
|
6. **review** — look at `out/sheet.png`, then at any page the gates named.
|
|
188
|
-
Read the pages as the reader will: does each carry its claim?
|
|
220
|
+
Read the pages as the reader will: does each carry its claim? Then read
|
|
221
|
+
the sheet as a creative director would, the questions the build prints
|
|
222
|
+
under it: is one page memorable, and which; is the accent rare;
|
|
223
|
+
are photographs large or absent; are hairlines and type doing the
|
|
224
|
+
separating. A piece where no page would stop someone who has seen a
|
|
225
|
+
hundred decks is not finished: make one page that does.
|
|
189
226
|
7. **ship** — `node wand.js ship <job> [--pptx]`: the build, the one-file HTML,
|
|
190
227
|
the PowerPoint. A deck's HTML is a talk (§12), a storyboard's plays (§13),
|
|
191
228
|
and either runtime is driven before it ships. `--brand next` (or `current`)
|
|
@@ -197,6 +234,8 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
197
234
|
| Gate | Fails when | Blind to |
|
|
198
235
|
|---|---|---|
|
|
199
236
|
| fit | anything in the content box runs past it, under the footer, or off the sides | a page that fits and reads badly |
|
|
237
|
+
| space (warn, slides) | a slide still uses under 40% of its content area after sizing itself | whether the content deserves the slide |
|
|
238
|
+
| variety (warn) | three pages running share one composition (the same blocks in the same order) | two pages composed alike but far apart |
|
|
200
239
|
| coverage | a line of `content.txt` is not on the page (whole, or in its declared parts), less the `cut` lines | a line that is present and in the wrong place |
|
|
201
240
|
| overlap | one text box sits on another | text on art |
|
|
202
241
|
| brand | a computed colour, border or fill that is not a token or a declared theme colour | gradients and images |
|
|
@@ -286,7 +325,11 @@ figure final, every poster.
|
|
|
286
325
|
- **Kinds.** `rise` (the default), `fade`, `pop` for a mark or a figure,
|
|
287
326
|
`grow` for a bar going up, `widen` for a bar going across, `draw` for a
|
|
288
327
|
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()`)
|
|
328
|
+
figure the room should watch arrive (`stat({ count: true })`, `countup()`),
|
|
329
|
+
`words` for a title whose words rise out of their lines (every title does
|
|
330
|
+
this under `motion: "auto"`), `lit` for a statement that lights word by
|
|
331
|
+
word, `window` for a photograph that opens like a window. A page's art
|
|
332
|
+
(the cover's light, a statement, the close) comes up like first light.
|
|
290
333
|
- **Steps** when the order of the argument is the point: `step(1, html)`, then
|
|
291
334
|
2, then 3. Never more than four on a slide. Going back undoes the last step.
|
|
292
335
|
- **Notes on every slide of a talk**, and `seconds` on each when the talk has a
|
|
@@ -322,9 +365,10 @@ direction (the line, camera, motion, on screen, sound, into next).
|
|
|
322
365
|
after; a shot without voiceover `hold`s; `seconds` on a shot overrides the
|
|
323
366
|
estimate once a real read exists. `target` warns when the board is more
|
|
324
367
|
than 10% off. `timing.csv` and `captions.srt` ship with the board.
|
|
325
|
-
- **A frame is drawn
|
|
368
|
+
- **A frame is drawn five ways:** `sketch` (people as dots, software as
|
|
326
369
|
labelled boxes, agents as sparks: the script's own vocabulary, in the
|
|
327
|
-
brand's colours), the
|
|
370
|
+
brand's colours), `endcard` (the wordmark on the brand's ground, a line
|
|
371
|
+
under it when given), the job's own SVG on brand roles, an image (a product
|
|
328
372
|
screen, a demo poster), or left to draw, which the frames gate counts. The
|
|
329
373
|
first frame is on screen from the cut; the others land on their cues.
|
|
330
374
|
- **The HTML plays the board:** the pages to read, and a player (P, or the
|
package/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Rebrand or create Wand material on the current or new brand - decks
|
|
|
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.3.
|
|
7
|
+
version: "0.3.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
@@ -94,7 +94,9 @@ beside the first. Nothing in the job changes.
|
|
|
94
94
|
in `edits` (no em dashes: swap them for commas or semicolons there) and
|
|
95
95
|
every line left out in `cut`.
|
|
96
96
|
4. `node wand.js build work/<name>/job.mjs` until every hard gate is ok. Look
|
|
97
|
-
at `out/sheet.png`. Open a page only where something looks off.
|
|
97
|
+
at `out/sheet.png`. Open a page only where something looks off. Then answer
|
|
98
|
+
the review questions the build prints under the gates, as a creative
|
|
99
|
+
director would; if the variety row names a run of pages, recompose it.
|
|
98
100
|
5. `node wand.js ship work/<name>/job.mjs --pptx` for a deck; without `--pptx`
|
|
99
101
|
for a document. Deliver the PDF (and the .pptx, and the .html) with the
|
|
100
102
|
review note.
|
|
@@ -105,6 +107,10 @@ Skip `content.txt` (delete the `content:` line in the job). Write the page list
|
|
|
105
107
|
with its claims first, then the pages. Coverage is skipped and says so; the
|
|
106
108
|
review reads the argument instead. Three sections a page, not four.
|
|
107
109
|
|
|
110
|
+
On the next brand, a title's phrase is its quiet second tone and gold is kept
|
|
111
|
+
for the cover, a statement, the close and the figures; photographs go through
|
|
112
|
+
`c.photo("walk")` (the brand's set, graded), large or not at all.
|
|
113
|
+
|
|
108
114
|
### Make it generic (an industry version of a client piece)
|
|
109
115
|
|
|
110
116
|
Author from the client piece: remove the client's figures, names, quotes and
|
|
@@ -120,12 +126,16 @@ talk adds:
|
|
|
120
126
|
1. Notes on every slide, and `seconds` on each when the talk has a slot.
|
|
121
127
|
2. Motion where it carries the argument (PROMPT.md §12): `step(n, html)` for
|
|
122
128
|
a reveal in order, `stat({ count: true })` for a figure to watch arrive,
|
|
123
|
-
`enter("draw")` on a trend line, `enter("grow", i)` on bars
|
|
124
|
-
|
|
129
|
+
`enter("draw")` on a trend line, `enter("grow", i)` on bars, `enter("lit")`
|
|
130
|
+
on a statement, `enter("window")` on a photograph. Titles rise word by word
|
|
131
|
+
on their own. Restraint: one moment a slide.
|
|
125
132
|
3. A live demo: one self-contained HTML file, then its poster on the deck's
|
|
126
133
|
brand: `node wand.js poster demo.html --play 3 --brand <brand>`, and
|
|
127
134
|
`c.demo({ src, poster })` on the slide. A clip: `c.video({ src, poster })`.
|
|
128
|
-
4.
|
|
135
|
+
4. Slides balance and size themselves; if the space row names one, it is
|
|
136
|
+
thin: add its proof or a figure, or join it to the next, rather than
|
|
137
|
+
stretching what is there.
|
|
138
|
+
5. `ship` drives the HTML like a presenter before it ships (the runtime row).
|
|
129
139
|
Deliver the HTML as the deck to present, with the PDF and .pptx beside it.
|
|
130
140
|
|
|
131
141
|
### A storyboard from a script
|
|
@@ -134,8 +144,9 @@ talk adds:
|
|
|
134
144
|
edit will cut). `node wand.js new work/<name> --surface storyboard`.
|
|
135
145
|
2. **Write the shot list in the chat first**: act, name, the line, and what
|
|
136
146
|
each frame shows. Then the job: `c.storyboard({ title, wpm, target, shots })`.
|
|
137
|
-
3. Frames: `sketch` for a quick one in the brand's colours,
|
|
138
|
-
an image, or left to draw (the
|
|
147
|
+
3. Frames: `sketch` for a quick one in the brand's colours, `endcard: true`
|
|
148
|
+
for the wordmark close, the job's own SVG, an image, or left to draw (the
|
|
149
|
+
frames gate counts them). A held close is a shot with no `vo` and a `hold`.
|
|
139
150
|
4. `ship`: the board as PDF and .pptx, `timing.csv`, `captions.srt`, and the
|
|
140
151
|
HTML with its player. Say in the note that timing is scratch timing at the
|
|
141
152
|
`wpm` given until a real read exists, and which frames are still to draw.
|
|
@@ -54,7 +54,11 @@
|
|
|
54
54
|
"rule": "E4E0F2",
|
|
55
55
|
"track": "E9E6F5",
|
|
56
56
|
"surface": "EBE8F8",
|
|
57
|
-
"emphLabel": "AA9BFF"
|
|
57
|
+
"emphLabel": "AA9BFF",
|
|
58
|
+
"second": "6C5CE7",
|
|
59
|
+
"secondSm": "6C5CE7",
|
|
60
|
+
"gradeLow": "240F48",
|
|
61
|
+
"gradeHigh": "F8F6FB"
|
|
58
62
|
},
|
|
59
63
|
"dark": {
|
|
60
64
|
"bg": "240F48",
|
|
@@ -74,7 +78,11 @@
|
|
|
74
78
|
"rule": "3A2E63",
|
|
75
79
|
"track": "1A1033",
|
|
76
80
|
"surface": "1A1033",
|
|
77
|
-
"emphLabel": "C4B5F4"
|
|
81
|
+
"emphLabel": "C4B5F4",
|
|
82
|
+
"second": "9481FF",
|
|
83
|
+
"secondSm": "9481FF",
|
|
84
|
+
"gradeLow": "240F48",
|
|
85
|
+
"gradeHigh": "F8F6FB"
|
|
78
86
|
}
|
|
79
87
|
},
|
|
80
88
|
"faces": {
|
package/brands/next/tokens.json
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"hex": "C4C1CA",
|
|
35
35
|
"from": "white @ 70% on violet200, lists inside dark cards"
|
|
36
36
|
},
|
|
37
|
+
"secondDark": {
|
|
38
|
+
"hex": "9D99A8",
|
|
39
|
+
"from": "white @ 55% on violet300 - a title's quiet second tone on dark (5.7:1)"
|
|
40
|
+
},
|
|
37
41
|
"footDark": {
|
|
38
42
|
"hex": "888394",
|
|
39
43
|
"from": "white @ 45% on violet300"
|
|
@@ -60,7 +64,11 @@
|
|
|
60
64
|
"rule": "D5CEC2",
|
|
61
65
|
"track": "D5CEC2",
|
|
62
66
|
"surface": "EBE8E2",
|
|
63
|
-
"emphLabel": "B79A5E"
|
|
67
|
+
"emphLabel": "B79A5E",
|
|
68
|
+
"second": "7B7B79",
|
|
69
|
+
"secondSm": "686766",
|
|
70
|
+
"gradeLow": "261D3D",
|
|
71
|
+
"gradeHigh": "F1EFEB"
|
|
64
72
|
},
|
|
65
73
|
"dark": {
|
|
66
74
|
"bg": "261D3D",
|
|
@@ -80,7 +88,11 @@
|
|
|
80
88
|
"rule": "504666",
|
|
81
89
|
"track": "504666",
|
|
82
90
|
"surface": "3A314E",
|
|
83
|
-
"emphLabel": "C4C1CA"
|
|
91
|
+
"emphLabel": "C4C1CA",
|
|
92
|
+
"second": "9D99A8",
|
|
93
|
+
"secondSm": "BEBBC5",
|
|
94
|
+
"gradeLow": "261D3D",
|
|
95
|
+
"gradeHigh": "F1EFEB"
|
|
84
96
|
}
|
|
85
97
|
},
|
|
86
98
|
"faces": {
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* The script is the content file, one voiceover line a shot, so coverage
|
|
6
6
|
* proves every word of it is on the board. Frames show each way a frame can
|
|
7
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
|
|
9
|
-
*
|
|
8
|
+
* job's own SVG, an image (the talk example's demo poster), and an end card
|
|
9
|
+
* (the wordmark on the brand's ground).
|
|
10
10
|
*
|
|
11
11
|
* node wand.js ship examples/storyboard/job.mjs the current brand
|
|
12
12
|
* node wand.js ship examples/storyboard/job.mjs --brand next the website's, in out-next/
|
|
@@ -61,7 +61,7 @@ export default {
|
|
|
61
61
|
frames: [
|
|
62
62
|
{ cue: "identity", sketch: { agents: 1, apps: ["Identity", "Manager"], label: "An agent, provisioned" } },
|
|
63
63
|
{ cue: "budget", sketch: { agents: 1, apps: ["Budget", "Record"], label: "Governed like a person" } },
|
|
64
|
-
{ cue: "record",
|
|
64
|
+
{ cue: "record", endcard: "Identity · Manager · Budget · Record", caption: "The governance beat lands on the wordmark." },
|
|
65
65
|
] },
|
|
66
66
|
{ act: "Wand", name: "One workforce", vo: c.L(6),
|
|
67
67
|
line: "The close: people and agents in one picture, then the wordmark.",
|
package/lib/components.js
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* content lines. A job's `pages(c)` composes with them and may add its own
|
|
19
19
|
* CSS on top; nothing stops it writing raw markup where no component fits.
|
|
20
20
|
*/
|
|
21
|
+
import fs from "node:fs";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { esc } from "./content.js";
|
|
23
|
-
import { url } from "./brand.js";
|
|
24
|
+
import { url, brandDir, BRANDS } from "./brand.js";
|
|
24
25
|
|
|
25
26
|
export const ARROW = `<svg class="arrow deco" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 1 L9 5 L2 9 Z"/></svg>`;
|
|
26
27
|
|
|
@@ -52,11 +53,11 @@ export function bind({ brand, surface, job, content, dir: jobDir = process.cwd()
|
|
|
52
53
|
* view; `seconds` is the slide's share of the talk, for the presenter's
|
|
53
54
|
* pace; `motion: "none"` keeps a slide still when the deck animates.
|
|
54
55
|
*/
|
|
55
|
-
const page = ({ eyebrow = "", dark = false, art = null, shade = dark, cls: k = "", body = "", notes = "", chrome = true, seconds = 0, motion = "" } = {}) => {
|
|
56
|
+
const page = ({ eyebrow = "", dark = false, art = null, shade = dark, cls: k = "", body = "", notes = "", chrome = true, seconds = 0, motion = "", autosize = true } = {}) => {
|
|
56
57
|
n += 1;
|
|
57
58
|
const artSrc = art ? (/[\/.]/.test(art) && !brand.tokens.art[art] ? url(art) : brand.artUrl(art)) : null;
|
|
58
59
|
const eb = job.tag && eyebrow ? `${esc(job.tag)}<span class="bar">|</span>${esc(eyebrow)}` : esc(job.tag || eyebrow || "");
|
|
59
|
-
return `<section class="page ${dark ? "dark" : "light"}${cls(k)}" data-n="${n}"${notes ? ` data-notes="${esc(notes)}"` : ""}${seconds ? ` data-seconds="${seconds}"` : ""}${motion ? ` data-motion="${esc(motion)}"` : ""}>
|
|
60
|
+
return `<section class="page ${dark ? "dark" : "light"}${cls(k)}" data-n="${n}"${notes ? ` data-notes="${esc(notes)}"` : ""}${seconds ? ` data-seconds="${seconds}"` : ""}${motion ? ` data-motion="${esc(motion)}"` : ""}${autosize ? "" : ' data-size="fixed"'}>
|
|
60
61
|
${artSrc ? `<img class="art deco" src="${artSrc}" alt="">${shade ? `<div class="shade deco"></div>` : ""}` : ""}
|
|
61
62
|
${chrome ? `<div class="eyebrow">${eb}</div>${mark(dark)}` : ""}
|
|
62
63
|
<div class="content">${body}</div>
|
|
@@ -130,13 +131,29 @@ ${chrome ? `<footer><span>${esc(job.footer || "")}</span><span class="pn">${Stri
|
|
|
130
131
|
/** A big outlined numeral in a dark panel on the right (the deck's chapter pages). */
|
|
131
132
|
const chapter = (num) => `<div class="chapter deco"><span>${esc(num)}</span></div>`;
|
|
132
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A photograph, filling its box and given the brand's one grade: blacks lift toward the dark ground,
|
|
136
|
+
* highlights settle toward the light one, so photographs from different shoots read as one set.
|
|
137
|
+
* `src` is a photograph of the brands' set by name ("walk", "corridor"; `brand-next/assets/photos`, which
|
|
138
|
+
* the current brand shares, graded on its own grounds) or a path.
|
|
139
|
+
*/
|
|
140
|
+
const photo = (src, { cls: k = "", alt = "", pos = "" } = {}) => {
|
|
141
|
+
let href = /^(file|https?|data):/.test(src) ? src : url(src);
|
|
142
|
+
if (!/[\/.]/.test(src)) {
|
|
143
|
+
const dirs = [brand.assets, ...BRANDS.filter((b) => b !== brand.name).map((b) => { try { return path.join(brandDir(b), "assets"); } catch { return null; } }).filter(Boolean)];
|
|
144
|
+
const hit = dirs.map((d) => path.join(d, "photos", `${src}.jpg`)).find((f) => fs.existsSync(f));
|
|
145
|
+
if (!hit) throw new Error(`photo("${src}"): no photograph of that name in ${dirs.map((d) => path.join(d, "photos")).join(" or ")}`);
|
|
146
|
+
href = url(hit);
|
|
147
|
+
}
|
|
148
|
+
return `<figure class="photo${cls(k)}"><img src="${href}" alt="${esc(alt)}"${pos ? ` style="object-position:${esc(pos)}"` : ""}><span class="grade"></span></figure>`;
|
|
149
|
+
};
|
|
133
150
|
const img = (src, k = "", alt = "") => `<img class="img${cls(k)}" src="${/^(file|https?|data):/.test(src) ? src : url(src)}" alt="${esc(alt)}">`;
|
|
134
151
|
const icon = (name, k = "") => brand.glyph(name).replace("<svg", `<svg class="icon deco${cls(k)}" aria-hidden="true"`);
|
|
135
152
|
const tile = (name) => `<div class="tile deco">${icon(name)}</div>`;
|
|
136
153
|
|
|
137
154
|
/* ---------------------------------------------------------- motion */
|
|
138
155
|
|
|
139
|
-
/** An entrance, as attributes for raw markup: `<div${c.enter("pop", 2)}>`. Kinds: rise (default), fade, pop, grow (bars, up), widen (bars, across), draw (an SVG path with pathLength="1"), count. */
|
|
156
|
+
/** An entrance, as attributes for raw markup: `<div${c.enter("pop", 2)}>`. Kinds: rise (default), fade, pop, grow (bars, up), widen (bars, across), draw (an SVG path with pathLength="1"), count, words (a title rising word by word), lit (a statement lighting word by word), window (a photograph opening). */
|
|
140
157
|
const enter = (kind = "", i = null) => ` data-a="${esc(kind)}"${i != null ? ` style="--i:${+i}"` : ""}`;
|
|
141
158
|
/** Held back until the presenter moves on; steps of one number come in together. */
|
|
142
159
|
const step = (nth, html, { a = "", tag: t = "div", cls: k = "" } = {}) => `<${t} data-step="${+nth}"${a ? ` data-a="${esc(a)}"` : ""}${k ? ` class="${k}"` : ""}>${html}</${t}>`;
|
|
@@ -190,7 +207,8 @@ ${chrome ? `<footer><span>${esc(job.footer || "")}</span><span class="pn">${Stri
|
|
|
190
207
|
* on its `cue` word. The timeline is kept for the CSV, the captions and the
|
|
191
208
|
* player in the HTML.
|
|
192
209
|
* A shot: { act, name, vo, line, camera, motion, onscreen, sound, next,
|
|
193
|
-
* lead, tail, hold, seconds, frames: [{ cue, caption, art | img | sketch }] }
|
|
210
|
+
* lead, tail, hold, seconds, frames: [{ cue, caption, art | img | sketch | endcard }] }
|
|
211
|
+
* `endcard: true` draws the wordmark on the brand's ground; a string adds a line under it.
|
|
194
212
|
*/
|
|
195
213
|
const storyboard = ({ title, lede = "", wpm = 150, target = 0, shots = [], dark = true, cover: withCover = true, index = true, audio = "" }) => {
|
|
196
214
|
let t = 0;
|
|
@@ -204,14 +222,16 @@ ${chrome ? `<footer><span>${esc(job.footer || "")}</span><span class="pn">${Stri
|
|
|
204
222
|
const at = f.cue && vo ? vo.toLowerCase().indexOf(String(f.cue).toLowerCase()) : -1;
|
|
205
223
|
// the first frame is on screen from the cut; the others land on their cue words
|
|
206
224
|
const ft = j === 0 ? inT : at >= 0 ? voIn + voDur * (at / vo.length) : voIn + (voDur * j) / all.length;
|
|
207
|
-
return { ...f, id: `${String(i + 1).padStart(2, "0")}${"ABCD"[j]}`, t: +ft.toFixed(1), placeholder: !(f.art || f.img || f.sketch) };
|
|
225
|
+
return { ...f, id: `${String(i + 1).padStart(2, "0")}${"ABCD"[j]}`, t: +ft.toFixed(1), placeholder: !(f.art || f.img || f.sketch || f.endcard) };
|
|
208
226
|
});
|
|
209
227
|
return { ...s, n: i + 1, vo, in: +inT.toFixed(1), voIn: +voIn.toFixed(1), voOut: +voOut.toFixed(1), out: t, dur: +(t - inT).toFixed(1), frames: fr };
|
|
210
228
|
});
|
|
211
229
|
const total = t;
|
|
212
230
|
job.__board = { wpm, target, total, audio: audio ? path.resolve(jobDir, audio) : "", shots: timeline.map(({ n, act, name, vo, in: i, voIn, voOut, out, dur, frames }) => ({ n, act: act || "", name: name || "", vo, in: i, voIn, voOut, out, dur, frames: frames.map(({ id, t: ft, cue, placeholder }) => ({ id, t: ft, cue: cue || "", placeholder })) })) };
|
|
213
231
|
|
|
214
|
-
|
|
232
|
+
// an end card is the wordmark on the brand's ground, with a line under it when endcard is a string
|
|
233
|
+
const endcard = (f) => `<div class="endcard"><img src="${brand.artUrl(dark ? "wordmarkOnDark" : "wordmarkOnLight")}" alt="Wand">${typeof f.endcard === "string" ? `<span>${esc(f.endcard)}</span>` : ""}</div>`;
|
|
234
|
+
const art = (f) => f.art ? f.art : f.img ? `<img src="${fileUrl(f.img)}" alt="">` : f.sketch ? sketch(f.sketch) : f.endcard ? endcard(f) : `<div class="todo"><span>${esc(f.cue ? `on ‘${f.cue}’` : "frame")}</span></div>`;
|
|
215
235
|
const DIR = [["line", "The line"], ["camera", "Camera"], ["motion", "Motion"], ["onscreen", "On screen"], ["sound", "Sound"], ["next", "Into next"]];
|
|
216
236
|
const shotPage = (s) => page({ eyebrow: s.act || "", dark, cls: "shot", notes: s.vo, body: `
|
|
217
237
|
<div class="shot-head"><div class="shot-id"><span class="tag">Shot</span><span class="no">${String(s.n).padStart(2, "0")}</span></div>
|
|
@@ -232,7 +252,7 @@ ${DIR.some(([k]) => s[k]) ? `<dl class="direction">${DIR.filter(([k]) => s[k]).m
|
|
|
232
252
|
page, sec, h1: h("h1"), h2: h("h2"), h3: h("h3"), h4: h("h4"), h5: h("h5"),
|
|
233
253
|
lead: p("lead"), body: p("body"), note: p("note"), small: p("body sm"), tag, p: (html, k = "") => `<p${cls(k) ? ` class="${k}"` : ""}>${html}</p>`,
|
|
234
254
|
cols, card, chip, strip, phase, flow, table, who, quote, stat, stats, delta, steps, callout, list: listOf,
|
|
235
|
-
cover, closing, chapter, img, icon, tile, mark, wordmark,
|
|
255
|
+
cover, closing, chapter, img, photo, icon, tile, mark, wordmark,
|
|
236
256
|
asset: brand.assetUrl, art: brand.artUrl,
|
|
237
257
|
enter, step, countup, video, demo, sketch, storyboard,
|
|
238
258
|
brand: brand.name, surface: surface.name, count: () => n,
|
|
@@ -262,7 +282,11 @@ footer .pn{font-weight:500}
|
|
|
262
282
|
/* type */
|
|
263
283
|
h1,h2,h3,h4,h5,.display{font-family:var(--face-display);font-weight:var(--w-display);letter-spacing:var(--tr-display);line-height:var(--lh-display);color:var(--ink)}
|
|
264
284
|
h1{font-size:var(--t-h1)}h1.md{font-size:var(--t-h1md)}h2{font-size:var(--t-h2)}h3{font-size:var(--t-h3);line-height:1.22}h4{font-size:var(--t-h4);line-height:1.2}h5{font-size:var(--t-h5);line-height:1.25}
|
|
265
|
-
|
|
285
|
+
/* a title's phrase is its second tone (on the current brand, the accent itself); on the cover, a statement
|
|
286
|
+
and the close, the pages that carry the brand's art, it keeps the accent */
|
|
287
|
+
h1 em,h2 em,.display em{font-style:normal;color:var(--second)}
|
|
288
|
+
h3 em,h4 em,h5 em,.lede em{font-style:normal;color:var(--second-sm)}
|
|
289
|
+
.page:has(>.art) :is(h1,h2,h3,h4,h5,.display,.lede) em{color:var(--accent-text)}
|
|
266
290
|
.label{font-family:var(--face-display);font-weight:500;font-size:var(--t-label);letter-spacing:var(--tr-display);color:var(--label);line-height:1.2}
|
|
267
291
|
.label.sm{font-size:calc(var(--t-label) * .8)}
|
|
268
292
|
.lead{font-size:var(--t-lead);line-height:1.5;color:var(--ink)}
|
|
@@ -398,11 +422,20 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
398
422
|
.has-chapter .left{width:62%;display:flex;flex-direction:column;gap:var(--m-gap)}
|
|
399
423
|
/* images, icons */
|
|
400
424
|
.img{display:block;max-width:100%}
|
|
425
|
+
.photo{position:relative;margin:0;overflow:hidden;border-radius:calc(var(--m-radius) * var(--radius-k));background:var(--grade-high)}
|
|
426
|
+
.photo>img{display:block;width:100%;height:100%;object-fit:cover}
|
|
427
|
+
.photo>.grade{position:absolute;inset:0;pointer-events:none}
|
|
428
|
+
.photo>.grade::before,.photo>.grade::after{content:"";position:absolute;inset:0}
|
|
429
|
+
.photo>.grade::before{background:var(--grade-low);mix-blend-mode:lighten;opacity:.42}
|
|
430
|
+
.photo>.grade::after{background:var(--grade-high);mix-blend-mode:darken;opacity:.55}
|
|
401
431
|
.figure{border-radius:calc(var(--m-radius) * var(--radius-k));overflow:hidden}
|
|
402
432
|
.icon{width:1.25em;height:1.25em;stroke:currentColor;fill:none}
|
|
403
433
|
.tile{width:2.3em;height:2.3em;border-radius:calc(var(--m-radius) * .6 * var(--radius-k));background:var(--surface);display:flex;align-items:center;justify-content:center;color:var(--accent-text);flex:none;margin-bottom:.8em}
|
|
404
434
|
.card.emph .tile{background:var(--emph-accent);color:var(--emph)}
|
|
405
435
|
/* room */
|
|
436
|
+
/* a slide's body sits in the space under its title, not in its top quarter */
|
|
437
|
+
.surface-slide .page:not(.cover):not(.back):not(.shot):not(.run) .content>:is(h1,h2,.sec):first-child+*{margin-top:auto}
|
|
438
|
+
.surface-slide .page:not(.cover):not(.back):not(.shot):not(.run) .content>:last-child{margin-bottom:auto}
|
|
406
439
|
.roomy .content{gap:calc(var(--m-gap) * 1.3)}
|
|
407
440
|
.roomy .cols{margin:auto 0}
|
|
408
441
|
.roomy .lead{font-size:calc(var(--t-lead) * 1.15)}
|
|
@@ -448,6 +481,9 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
448
481
|
.frame figcaption b{color:var(--label);font-weight:600}
|
|
449
482
|
.frame figcaption span{display:block;color:var(--text);margin-top:.2em}
|
|
450
483
|
.frame .todo{position:absolute;inset:0;display:grid;place-items:center;border:2px dashed var(--rule);border-radius:inherit}
|
|
484
|
+
.frame-art .endcard{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5%;background:var(--bg);box-shadow:inset 0 0 0 2px var(--rule);border-radius:inherit}
|
|
485
|
+
.frame-art .endcard>img{position:static;width:30%;height:auto}
|
|
486
|
+
.frame-art .endcard>span{font-family:var(--face-body);font-size:var(--t-sm);color:var(--soft);letter-spacing:.04em}
|
|
451
487
|
.frame .todo span{font-size:var(--t-sm);color:var(--soft)}
|
|
452
488
|
.direction{margin:0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:calc(var(--m-gap) * .5) var(--m-col-gap);border-top:var(--card-stroke) solid var(--rule);padding-top:calc(var(--m-gap) * .6)}
|
|
453
489
|
.direction dt{font-family:var(--face-label);font-weight:var(--w-label);font-size:var(--t-tag);letter-spacing:.14em;text-transform:var(--caps-label);color:var(--label);margin-bottom:.3em}
|
package/lib/engine.js
CHANGED
|
@@ -113,6 +113,13 @@ function GATES({ min, chars, tokens, families, base }) {
|
|
|
113
113
|
motion: { a: document.querySelectorAll("[data-a]").length, steps: document.querySelectorAll("[data-step]").length, video: document.querySelectorAll(".media.video").length, demo: document.querySelectorAll(".media.demo").length, placeholders: document.querySelectorAll(".frame.placeholder").length } };
|
|
114
114
|
document.querySelectorAll("img").forEach((im) => { if (im.getAttribute("src") && !(im.complete && im.naturalWidth)) res.broken.push(decodeURIComponent(im.getAttribute("src").replace(/^file:\/\//, "")).split("/").slice(-2).join("/")); });
|
|
115
115
|
const seen = new Set();
|
|
116
|
+
// a page's composition, coarsely: the blocks of its content in order, and how many a grid holds
|
|
117
|
+
const kindOf = (el) => { const c = [...el.classList].filter((x) => !/^(deco|big|wide|stretch|centre|sm|md)$/.test(x)).slice(0, 2); return el.tagName.toLowerCase() + (c.length ? "." + c.join(".") : ""); };
|
|
118
|
+
res.sig = pages.map((pg) => {
|
|
119
|
+
const c = pg.querySelector(".content");
|
|
120
|
+
if (!c || pg.matches(".cover, .back, .shot, .run")) return null;
|
|
121
|
+
return [...c.children].filter((k) => !k.classList.contains("deco")).map((k) => kindOf(k) + (/\b(cols|strip|stats|steps|grid)\b/.test(k.className) ? `[${k.children.length}]` : "")).join(" ");
|
|
122
|
+
});
|
|
116
123
|
pages.forEach((pg, pi) => {
|
|
117
124
|
const n = pi + 1;
|
|
118
125
|
const content = pg.querySelector(".content");
|
|
@@ -172,6 +179,35 @@ function GATES({ min, chars, tokens, families, base }) {
|
|
|
172
179
|
return res;
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Runs inside the page. A slide whose content fills too little of it gets the
|
|
184
|
+
* type scale one step up (.big), kept only if nothing then overflows: a deck
|
|
185
|
+
* sizes itself to its slides, the same way on every run. Returns the pages
|
|
186
|
+
* it changed, and every slide's fill after.
|
|
187
|
+
*/
|
|
188
|
+
function AUTOSIZE() {
|
|
189
|
+
const fill = (pg) => {
|
|
190
|
+
const c = pg.querySelector(".content"); if (!c) return 1;
|
|
191
|
+
const kids = [...c.children].filter((k) => !k.classList.contains("deco") && k.getBoundingClientRect().height);
|
|
192
|
+
if (!kids.length) return 1;
|
|
193
|
+
const gap = parseFloat(getComputedStyle(c).rowGap) || 0;
|
|
194
|
+
return (kids.reduce((a, k) => a + k.getBoundingClientRect().height, 0) + gap * (kids.length - 1)) / c.getBoundingClientRect().height;
|
|
195
|
+
};
|
|
196
|
+
const overflows = (pg) => {
|
|
197
|
+
const c = pg.querySelector(".content"), box = c.getBoundingClientRect(), foot = pg.querySelector("footer")?.getBoundingClientRect();
|
|
198
|
+
const floor = foot ? Math.min(box.bottom, foot.top - 6) : box.bottom;
|
|
199
|
+
// as the fit gate reads it: an inline run's line box may rise above the content's top, a block may not
|
|
200
|
+
return [...c.querySelectorAll("*")].some((el) => { if (el.closest(".deco")) return false; const r = el.getBoundingClientRect(); return r.width && r.height && (r.bottom > floor + 0.5 || r.right > box.right + 1 || (getComputedStyle(el).display !== "inline" && r.top < box.top - 0.5)); }) || fill(pg) > 1;
|
|
201
|
+
};
|
|
202
|
+
const sized = [], fills = [];
|
|
203
|
+
document.querySelectorAll(".page").forEach((pg, i) => {
|
|
204
|
+
if (pg.matches(".cover, .back, .shot, .run, [data-size]")) return;
|
|
205
|
+
if (fill(pg) < 0.55 && !pg.classList.contains("big")) { pg.classList.add("big"); if (overflows(pg)) pg.classList.remove("big"); else sized.push(i + 1); }
|
|
206
|
+
fills.push({ page: i + 1, fill: fill(pg) });
|
|
207
|
+
});
|
|
208
|
+
return { sized, fills };
|
|
209
|
+
}
|
|
210
|
+
|
|
175
211
|
/* ------------------------------------------------------------------ build */
|
|
176
212
|
|
|
177
213
|
/**
|
|
@@ -209,6 +245,9 @@ export async function build(jobPath, { outputs: wantOutputs = true, outDir = nul
|
|
|
209
245
|
await page.evaluate(() => Promise.all([...document.images].map((i) => (i.complete ? null : new Promise((r) => { i.onload = i.onerror = r; })))));
|
|
210
246
|
await page.waitForTimeout(120);
|
|
211
247
|
|
|
248
|
+
// a slide deck sizes itself before anything is measured or drawn; the file keeps the sizes it chose
|
|
249
|
+
const auto = surface.kind === "slide" ? await page.evaluate(AUTOSIZE) : { sized: [], fills: [] };
|
|
250
|
+
if (auto.sized.length) fs.writeFileSync(htmlPath, "<!doctype html>\n" + await page.evaluate(() => document.documentElement.outerHTML));
|
|
212
251
|
const g = await page.evaluate(GATES, { min: surface.min, chars: surface.chars, tokens: [...brand.tokenSet], families: brand.families, base: surface.base });
|
|
213
252
|
const pageCount = g.fit.length;
|
|
214
253
|
const text = g.texts.join("\n");
|
|
@@ -231,6 +270,16 @@ export async function build(jobPath, { outputs: wantOutputs = true, outDir = nul
|
|
|
231
270
|
row(!deny.length, "names", job.denylist?.length ? (deny.length ? `found ${deny.join(", ")}` : `none of ${job.denylist.length} denied names`) : "no denylist", !job.denylist?.length);
|
|
232
271
|
row(!g.contrast.length, "contrast", g.contrast.length ? `${g.contrast.length} low: ` + g.contrast.slice(0, 3).map((o) => `p${o.page} ${o.el} ${o.ratio}:1 (needs ${o.need})`).join("; ") : "every text passes WCAG AA", true);
|
|
233
272
|
row(!g.measure.length, "measure", g.measure.length ? `${g.measure.length} long: ` + g.measure.slice(0, 3).map((o) => `p${o.page} ${o.el} ~${o.chars} chars`).join("; ") : `no line over ${surface.chars} characters`, true);
|
|
273
|
+
const sparse = auto.fills.filter((f) => f.fill < 0.4);
|
|
274
|
+
if (surface.kind === "slide") row(!sparse.length, "space", sparse.length ? `${sparse.map((f) => `p${f.page} ${Math.round(f.fill * 100)}%`).join(", ")} of the slide used: add the proof, a figure or a visual, or join it to the next` : `every slide uses its space${auto.sized.length ? ` (type a step up on p${auto.sized.join(", p")})` : ""}`, true);
|
|
275
|
+
// the same composition page after page is what makes a set of pages read as a template
|
|
276
|
+
const runs = [], composed = !job.__board && surface.kind !== "card";
|
|
277
|
+
if (composed) for (let i = 1, start = 0; i <= g.sig.length; i++) {
|
|
278
|
+
if (i < g.sig.length && g.sig[i] && g.sig[i] === g.sig[i - 1]) continue;
|
|
279
|
+
if (i - start >= 3 && g.sig[start]) runs.push([start + 1, i]);
|
|
280
|
+
start = i;
|
|
281
|
+
}
|
|
282
|
+
if (composed) row(!runs.length, "variety", runs.length ? runs.map(([a, z]) => `p${a}–p${z}`).join(", ") + " share one composition: vary it (a statement, a split, a figure, a photograph) or join them" : "no composition repeats three pages running", true);
|
|
234
283
|
row(!g.broken.length, "images", g.broken.length ? `${g.broken.length} did not load: ${g.broken.slice(0, 3).join(", ")}` : "every image loaded");
|
|
235
284
|
if (absent.length || g.motion.demo || g.motion.video) row(!absent.length, "media", absent.length ? `missing: ${absent.map((p) => path.basename(p)).join(", ")}` : `${g.motion.demo} demo${g.motion.demo === 1 ? "" : "s"}, ${g.motion.video} clip${g.motion.video === 1 ? "" : "s"}, each with its poster`);
|
|
236
285
|
const board = job.__board || null;
|
|
@@ -286,6 +335,8 @@ export function printReport(r) {
|
|
|
286
335
|
if (r.outputs.pdf) console.log(`\n ${path.relative(process.cwd(), r.outputs.pdf)} (${(fs.statSync(r.outputs.pdf).size / 1e6).toFixed(1)} MB, ${r.pages} page${r.pages === 1 ? "" : "s"}, ${r.brand} brand, ${r.surface}, ${(r.ms / 1000).toFixed(1)}s)`);
|
|
287
336
|
if (r.outputs.pptx) console.log(` ${path.relative(process.cwd(), r.outputs.pptx)} (${(fs.statSync(r.outputs.pptx).size / 1e6).toFixed(1)} MB)`);
|
|
288
337
|
if (r.outputs.single) console.log(` ${path.relative(process.cwd(), r.outputs.single)} (${(fs.statSync(r.outputs.single).size / 1e6).toFixed(1)} MB, one file, opens offline)`);
|
|
338
|
+
// what no gate sees: read the sheet the way a creative director would, before anyone else does
|
|
339
|
+
if (r.outputs.sheet) console.log(`\n review ${path.relative(process.cwd(), r.outputs.sheet)}, as a creative director:\n does every page carry its one claim? is one page memorable, and which?\n is the accent rare (cover, statement, close, figures)? photographs large or absent?\n hairlines and type doing the separating, not boxes?`);
|
|
289
340
|
console.log(r.ok ? "" : "\n FAILED — fix the rows marked FAIL\n");
|
|
290
341
|
}
|
|
291
342
|
|
package/lib/surfaces.js
CHANGED
|
@@ -57,6 +57,9 @@ export function surfaceCss(s) {
|
|
|
57
57
|
const kebab = (k) => k.replace(/[A-Z]/g, (x) => "-" + x.toLowerCase());
|
|
58
58
|
const m = Object.entries(s.m).map(([k, v]) => `--m-${kebab(k)}:${typeof v === "number" ? v + "px" : v}`).join(";");
|
|
59
59
|
const c = Object.entries(s.chrome).map(([k, v]) => `--c-${kebab(k)}:${v}px`).join(";");
|
|
60
|
+
// .big: the type scale one step up, which the engine gives a slide whose content fills too little of it
|
|
61
|
+
const big = Object.entries(s.t).map(([k, v]) => `--t-${k}:${+(v * 1.25).toFixed(1)}px`).join(";");
|
|
60
62
|
return `@page{size:${s.w}px ${s.h}px;margin:0}
|
|
61
|
-
.page{width:${s.w}px;height:${s.h}px;--x:${s.x}px;--top:${s.top}px;--bottom:${s.bottom}px;--base:${s.base}px;font-size:${s.base}px;${t};${m};${c}}
|
|
63
|
+
.page{width:${s.w}px;height:${s.h}px;--x:${s.x}px;--top:${s.top}px;--bottom:${s.bottom}px;--base:${s.base}px;font-size:${s.base}px;${t};${m};${c}}
|
|
64
|
+
.page.big{font-size:${+(s.base * 1.25).toFixed(1)}px;${big}}`;
|
|
62
65
|
}
|
package/lib/talk.js
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* What it adds to the pages, none of which the PDF, the PNGs or the .pptx see
|
|
11
11
|
* (they are read from the static render):
|
|
12
12
|
* - a crossfade between slides, the slide being left kept under the next;
|
|
13
|
-
* - entrances: `data-a` (rise, fade, pop, grow, widen, draw, count
|
|
14
|
-
* the stagger, played going forward; with
|
|
15
|
-
* marks nothing has its blocks staggered in
|
|
13
|
+
* - entrances: `data-a` (rise, fade, pop, grow, widen, draw, count, words,
|
|
14
|
+
* lit, window) with `--i` as the stagger, played going forward; with
|
|
15
|
+
* `motion: "auto"` a slide that marks nothing has its blocks staggered in
|
|
16
|
+
* for it, its title's words rising out of their lines, and a page's art
|
|
17
|
+
* comes up like first light;
|
|
16
18
|
* - steps: `data-step="n"` holds an element back until the presenter moves
|
|
17
19
|
* on, a group per number; going back undoes the last group;
|
|
18
20
|
* - media: a `.media.video` plays while its slide is on, a `.media.demo`
|
|
@@ -137,6 +139,19 @@ body.notes-open .viewport{grid-template-columns:1fr min(440px,34vw)}
|
|
|
137
139
|
@keyframes wa-grow{from{transform:scaleY(0)}}
|
|
138
140
|
@keyframes wa-widen{from{transform:scaleX(0)}}
|
|
139
141
|
@keyframes wa-draw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}
|
|
142
|
+
/* a title's words rise out of their lines; a statement lights word by word; a photograph opens like a window */
|
|
143
|
+
.wa-w{display:inline-block;overflow:clip;overflow-clip-margin:.14em;vertical-align:top;padding-bottom:.08em;margin-bottom:-.08em}
|
|
144
|
+
.wa-wi{display:inline-block}
|
|
145
|
+
.page.play [data-a="words"],.page.play [data-a="lit"]{animation:none}
|
|
146
|
+
.page.play [data-a="words"] .wa-wi{animation:wa-word 760ms var(--ease) both;animation-delay:calc(min(var(--i,0),12) * 70ms + min(var(--wi,0),24) * 40ms + 60ms)}
|
|
147
|
+
.page.play [data-a="lit"] .wa-wi{animation:wa-lit 420ms linear both;animation-delay:calc(min(var(--i,0),12) * 70ms + min(var(--wi,0),40) * 70ms + 240ms)}
|
|
148
|
+
.page.play [data-a="window"]{animation-name:wa-window;animation-duration:1300ms}
|
|
149
|
+
/* the art of a page (the cover's light, a statement, the close) comes up like first light */
|
|
150
|
+
.page.play>.art{animation:wa-dawn 1800ms var(--ease) both}
|
|
151
|
+
@keyframes wa-word{from{transform:translateY(105%)}}
|
|
152
|
+
@keyframes wa-lit{from{opacity:.16}}
|
|
153
|
+
@keyframes wa-window{from{clip-path:inset(16% 24%);transform:scale(1.06)}to{clip-path:inset(0)}}
|
|
154
|
+
@keyframes wa-dawn{from{opacity:0;transform:translateY(3%)}}
|
|
140
155
|
|
|
141
156
|
/* media: posters until the slide is on and the media is ready */
|
|
142
157
|
.talk-on .stage .media.video.playing>.poster,.talk-on .stage .media.demo.is-ready>.poster{opacity:0;transition:opacity 400ms var(--ease-io)}
|
|
@@ -270,11 +285,29 @@ export const TALK_JS = String.raw`(() => {
|
|
|
270
285
|
if (s.dataset.motion === "none" || $('[data-a]:not([data-a="count"]), [data-step]', s)) return;
|
|
271
286
|
const content = $(".content", s); if (!content) return;
|
|
272
287
|
let i = 0;
|
|
273
|
-
const mark = (el) => { if (el.classList.contains("deco") || el.matches("script, style")) return; if (!el.hasAttribute("data-a")) el.setAttribute("data-a", ""); el.style.setProperty("--i", i++); };
|
|
288
|
+
const mark = (el) => { if (el.classList.contains("deco") || el.matches("script, style")) return; if (!el.hasAttribute("data-a")) el.setAttribute("data-a", el.matches("h1, h2, .display") ? "words" : ""); el.style.setProperty("--i", i++); };
|
|
274
289
|
const walk = (el) => { for (const ch of el.children) { if (ch.matches(GROUPS) && ch.children.length > 1) { for (const g of ch.children) mark(g); } else if (ch.matches(".top, .bottom") ) walk(ch); else mark(ch); } };
|
|
275
290
|
walk(content);
|
|
276
291
|
});
|
|
277
292
|
|
|
293
|
+
/* the words of a rising title or a lit statement, each in its own line box (spaces stay text, so lines break as before) */
|
|
294
|
+
$$('[data-a="words"], [data-a="lit"]').forEach((el) => {
|
|
295
|
+
let n = 0;
|
|
296
|
+
const walk = (node) => [...node.childNodes].forEach((ch) => {
|
|
297
|
+
if (ch.nodeType === 3) {
|
|
298
|
+
const frag = document.createDocumentFragment();
|
|
299
|
+
ch.data.split(/(\s+)/).forEach((part) => {
|
|
300
|
+
if (!part) return;
|
|
301
|
+
if (/^\s+$/.test(part)) { frag.append(document.createTextNode(part)); return; }
|
|
302
|
+
const w = document.createElement("span"), i = document.createElement("span");
|
|
303
|
+
w.className = "wa-w"; i.className = "wa-wi"; i.style.setProperty("--wi", n++); i.textContent = part; w.append(i); frag.append(w);
|
|
304
|
+
});
|
|
305
|
+
ch.replaceWith(frag);
|
|
306
|
+
} else if (ch.nodeType === 1 && !ch.classList.contains("wa-w")) walk(ch);
|
|
307
|
+
});
|
|
308
|
+
walk(el);
|
|
309
|
+
});
|
|
310
|
+
|
|
278
311
|
/* ------------------------------------------------------------- the scale */
|
|
279
312
|
function fit(box) { const k = box.clientWidth / W; if (k) box.style.setProperty("--k", k); }
|
|
280
313
|
new ResizeObserver(() => { fit(stage); $$(".mini").forEach(fit); }).observe(stage);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wand-kit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "One HTML design surface for every sales and marketing material, on either Wand brand: decks that animate and present, documents, one-pagers, case studies, storyboards from a script, social cards. Every export (PDF, PNG, PowerPoint, one-file HTML) is read from the page that was reviewed.",
|
|
5
5
|
"license": "SEE LICENSE IN PROMPT.md",
|
|
6
6
|
"bin": {
|
package/wand.js
CHANGED
|
@@ -185,7 +185,7 @@ async function components() {
|
|
|
185
185
|
tag: "tag(text) a caps label", cols: "cols(n, [items], { stretch }) a grid", card: "card({ tag, title, level, body, out, emph, sm })", callout: "callout({ tag, text, emph })",
|
|
186
186
|
strip: "strip([labels], { on }) numbered chips with arrows", chip: "chip(i, label, state)", phase: "phase(i, label, line) chip then heading", flow: "flow({ label, nodes: [[who, verb, 'a'|'p']], note })",
|
|
187
187
|
table: "table({ head, rows, emph }) cells: string or { v, meter, cls }", quote: "quote({ text, by, emph })", who: "who(name, role)", stats: "stats([{ v, k, hero }])", delta: "delta(from, to)",
|
|
188
|
-
steps: "steps([{ title, when, body, emph }])", list: "list([items])", chapter: "chapter(num) the deck's numeral panel", img: "img(src, cls, alt)", icon: "icon(name) a brand glyph", tile: "tile(name) a glyph on a tile",
|
|
188
|
+
steps: "steps([{ title, when, body, emph }])", list: "list([items])", chapter: "chapter(num) the deck's numeral panel", img: "img(src, cls, alt)", photo: "photo(nameOrPath, { cls, alt, pos }) a photograph, filled and graded", icon: "icon(name) a brand glyph", tile: "tile(name) a glyph on a tile",
|
|
189
189
|
accent: "accent(text, phrase) the phrase in the accent colour", split: "split(line, sep)", L: "L(n) line n of content.txt", esc: "esc(s)", art: "art(role) a brand art url", asset: "asset(file)",
|
|
190
190
|
};
|
|
191
191
|
console.log("");
|