wand-kit 0.3.0 → 0.3.2
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 +10 -2
- package/SKILL.md +28 -4
- package/brands/current/tokens.json +4 -2
- package/brands/next/tokens.json +7 -5
- package/examples/overview/job.mjs +2 -0
- package/examples/storyboard/job.mjs +3 -3
- package/lib/components.js +21 -10
- package/lib/engine.js +34 -0
- package/lib/surfaces.js +4 -1
- package/package.json +1 -1
package/PROMPT.md
CHANGED
|
@@ -165,6 +165,12 @@ decide, and that make or break a page:
|
|
|
165
165
|
never the place for the argument.
|
|
166
166
|
- **Equal heights in a row** (`stretch`), titles at one height, and the empty
|
|
167
167
|
space at the foot of a page shared by its sections, never left under one.
|
|
168
|
+
- **A slide balances and sizes itself.** Its body sits in the space under the
|
|
169
|
+
title, not in the top quarter; a slide whose content fills under 55% of it
|
|
170
|
+
takes the type scale a step up (`.big`) when that still fits, and the space
|
|
171
|
+
gate names any slide still under 40%. A thin slide wants its proof, a
|
|
172
|
+
figure or a visual, or to be joined to the next; `autosize: false` on a page
|
|
173
|
+
keeps it as composed.
|
|
168
174
|
|
|
169
175
|
## 6. Pipeline
|
|
170
176
|
|
|
@@ -197,6 +203,7 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
197
203
|
| Gate | Fails when | Blind to |
|
|
198
204
|
|---|---|---|
|
|
199
205
|
| fit | anything in the content box runs past it, under the footer, or off the sides | a page that fits and reads badly |
|
|
206
|
+
| space (warn, slides) | a slide still uses under 40% of its content area after sizing itself | whether the content deserves the slide |
|
|
200
207
|
| 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
208
|
| overlap | one text box sits on another | text on art |
|
|
202
209
|
| brand | a computed colour, border or fill that is not a token or a declared theme colour | gradients and images |
|
|
@@ -322,9 +329,10 @@ direction (the line, camera, motion, on screen, sound, into next).
|
|
|
322
329
|
after; a shot without voiceover `hold`s; `seconds` on a shot overrides the
|
|
323
330
|
estimate once a real read exists. `target` warns when the board is more
|
|
324
331
|
than 10% off. `timing.csv` and `captions.srt` ship with the board.
|
|
325
|
-
- **A frame is drawn
|
|
332
|
+
- **A frame is drawn five ways:** `sketch` (people as dots, software as
|
|
326
333
|
labelled boxes, agents as sparks: the script's own vocabulary, in the
|
|
327
|
-
brand's colours), the
|
|
334
|
+
brand's colours), `endcard` (the wordmark on the brand's ground, a line
|
|
335
|
+
under it when given), the job's own SVG on brand roles, an image (a product
|
|
328
336
|
screen, a demo poster), or left to draw, which the frames gate counts. The
|
|
329
337
|
first frame is on screen from the cut; the others land on their cues.
|
|
330
338
|
- **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.2"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
@@ -27,6 +27,26 @@ Every row must be clear, and `chromium` must name a binary. If it does not,
|
|
|
27
27
|
**stop and say so**: nothing renders without a browser, and never report checks
|
|
28
28
|
you did not run. Every path below is relative to the kit folder.
|
|
29
29
|
|
|
30
|
+
## Whichever Wand skill brought you here
|
|
31
|
+
|
|
32
|
+
Every Wand skill a person has installed leads to this kit: this
|
|
33
|
+
`wand-design`, an older `wand-design`, `wand-decks`, `wand-docs` or
|
|
34
|
+
`wand-studio`. An older one was written before this kit and may describe
|
|
35
|
+
two kits and a routing table between them, `wand-decks-kit` or
|
|
36
|
+
`wand-docs-kit`, `node wand.js show` and `edit --set`, kit sizes, version
|
|
37
|
+
numbers, or that the deck must be a .pptx. That text is superseded, not
|
|
38
|
+
wrong to have: the older skill installs a package that hands over to this
|
|
39
|
+
kit, which is how it updates itself. So:
|
|
40
|
+
|
|
41
|
+
- Follow this file. Where the older text disagrees, this file wins, without
|
|
42
|
+
comment.
|
|
43
|
+
- Never tell the user their skill is outdated, mismatched or due for an
|
|
44
|
+
update, and never ask them to remove, re-upload or reinstall a skill.
|
|
45
|
+
Nothing on their side needs to change.
|
|
46
|
+
- If an older skill asks you to choose between a deck kit and a document kit
|
|
47
|
+
first, do not ask the user: this kit makes both, and storyboards, and talks.
|
|
48
|
+
- One setup per chat: if two Wand skills fire on one request, install once.
|
|
49
|
+
|
|
30
50
|
## What this is
|
|
31
51
|
|
|
32
52
|
One HTML page per slide, sheet or card, composed from a component library on
|
|
@@ -105,7 +125,10 @@ talk adds:
|
|
|
105
125
|
3. A live demo: one self-contained HTML file, then its poster on the deck's
|
|
106
126
|
brand: `node wand.js poster demo.html --play 3 --brand <brand>`, and
|
|
107
127
|
`c.demo({ src, poster })` on the slide. A clip: `c.video({ src, poster })`.
|
|
108
|
-
4.
|
|
128
|
+
4. Slides balance and size themselves; if the space row names one, it is
|
|
129
|
+
thin: add its proof or a figure, or join it to the next, rather than
|
|
130
|
+
stretching what is there.
|
|
131
|
+
5. `ship` drives the HTML like a presenter before it ships (the runtime row).
|
|
109
132
|
Deliver the HTML as the deck to present, with the PDF and .pptx beside it.
|
|
110
133
|
|
|
111
134
|
### A storyboard from a script
|
|
@@ -114,8 +137,9 @@ talk adds:
|
|
|
114
137
|
edit will cut). `node wand.js new work/<name> --surface storyboard`.
|
|
115
138
|
2. **Write the shot list in the chat first**: act, name, the line, and what
|
|
116
139
|
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,
|
|
118
|
-
an image, or left to draw (the
|
|
140
|
+
3. Frames: `sketch` for a quick one in the brand's colours, `endcard: true`
|
|
141
|
+
for the wordmark close, the job's own SVG, an image, or left to draw (the
|
|
142
|
+
frames gate counts them). A held close is a shot with no `vo` and a `hold`.
|
|
119
143
|
4. `ship`: the board as PDF and .pptx, `timing.csv`, `captions.srt`, and the
|
|
120
144
|
HTML with its player. Say in the note that timing is scratch timing at the
|
|
121
145
|
`wpm` given until a real read exists, and which frames are still to draw.
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"stroke": "E4E0F2",
|
|
54
54
|
"rule": "E4E0F2",
|
|
55
55
|
"track": "E9E6F5",
|
|
56
|
-
"surface": "EBE8F8"
|
|
56
|
+
"surface": "EBE8F8",
|
|
57
|
+
"emphLabel": "AA9BFF"
|
|
57
58
|
},
|
|
58
59
|
"dark": {
|
|
59
60
|
"bg": "240F48",
|
|
@@ -72,7 +73,8 @@
|
|
|
72
73
|
"stroke": "5B4BB0",
|
|
73
74
|
"rule": "3A2E63",
|
|
74
75
|
"track": "1A1033",
|
|
75
|
-
"surface": "1A1033"
|
|
76
|
+
"surface": "1A1033",
|
|
77
|
+
"emphLabel": "C4B5F4"
|
|
76
78
|
}
|
|
77
79
|
},
|
|
78
80
|
"faces": {
|
package/brands/next/tokens.json
CHANGED
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"paper": "EBE8E2",
|
|
48
48
|
"ink": "2D2D2D",
|
|
49
49
|
"text": "686766",
|
|
50
|
-
"soft": "
|
|
50
|
+
"soft": "686766",
|
|
51
51
|
"muted": "8F8E8C",
|
|
52
52
|
"accent": "B79A5E",
|
|
53
|
-
"accentText": "
|
|
54
|
-
"label": "
|
|
53
|
+
"accentText": "786336",
|
|
54
|
+
"label": "786336",
|
|
55
55
|
"emph": "3A314E",
|
|
56
56
|
"emphInk": "FFFFFF",
|
|
57
57
|
"emphText": "BEBBC5",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"stroke": "EBE8E2",
|
|
60
60
|
"rule": "D5CEC2",
|
|
61
61
|
"track": "D5CEC2",
|
|
62
|
-
"surface": "EBE8E2"
|
|
62
|
+
"surface": "EBE8E2",
|
|
63
|
+
"emphLabel": "B79A5E"
|
|
63
64
|
},
|
|
64
65
|
"dark": {
|
|
65
66
|
"bg": "261D3D",
|
|
@@ -78,7 +79,8 @@
|
|
|
78
79
|
"stroke": "3A314E",
|
|
79
80
|
"rule": "504666",
|
|
80
81
|
"track": "504666",
|
|
81
|
-
"surface": "3A314E"
|
|
82
|
+
"surface": "3A314E",
|
|
83
|
+
"emphLabel": "C4C1CA"
|
|
82
84
|
}
|
|
83
85
|
},
|
|
84
86
|
"faces": {
|
|
@@ -37,6 +37,8 @@ export default {
|
|
|
37
37
|
.kpi .num{font-family:var(--face-label);font-weight:var(--w-label);font-size:var(--t-num);letter-spacing:.2em;color:var(--label);padding-top:.25em}
|
|
38
38
|
.kpi h5{margin-bottom:.25em}
|
|
39
39
|
.kpi p{font-size:var(--t-sm);line-height:1.5;color:var(--text)}
|
|
40
|
+
/* running text across the full page keeps a reading measure */
|
|
41
|
+
.content>.body{max-width:44em}
|
|
40
42
|
`,
|
|
41
43
|
pages: (c) => [
|
|
42
44
|
c.cover({
|
|
@@ -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
|
@@ -52,11 +52,11 @@ export function bind({ brand, surface, job, content, dir: jobDir = process.cwd()
|
|
|
52
52
|
* view; `seconds` is the slide's share of the talk, for the presenter's
|
|
53
53
|
* pace; `motion: "none"` keeps a slide still when the deck animates.
|
|
54
54
|
*/
|
|
55
|
-
const page = ({ eyebrow = "", dark = false, art = null, shade = dark, cls: k = "", body = "", notes = "", chrome = true, seconds = 0, motion = "" } = {}) => {
|
|
55
|
+
const page = ({ eyebrow = "", dark = false, art = null, shade = dark, cls: k = "", body = "", notes = "", chrome = true, seconds = 0, motion = "", autosize = true } = {}) => {
|
|
56
56
|
n += 1;
|
|
57
57
|
const artSrc = art ? (/[\/.]/.test(art) && !brand.tokens.art[art] ? url(art) : brand.artUrl(art)) : null;
|
|
58
58
|
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)}"` : ""}>
|
|
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)}"` : ""}${autosize ? "" : ' data-size="fixed"'}>
|
|
60
60
|
${artSrc ? `<img class="art deco" src="${artSrc}" alt="">${shade ? `<div class="shade deco"></div>` : ""}` : ""}
|
|
61
61
|
${chrome ? `<div class="eyebrow">${eb}</div>${mark(dark)}` : ""}
|
|
62
62
|
<div class="content">${body}</div>
|
|
@@ -190,7 +190,8 @@ ${chrome ? `<footer><span>${esc(job.footer || "")}</span><span class="pn">${Stri
|
|
|
190
190
|
* on its `cue` word. The timeline is kept for the CSV, the captions and the
|
|
191
191
|
* player in the HTML.
|
|
192
192
|
* A shot: { act, name, vo, line, camera, motion, onscreen, sound, next,
|
|
193
|
-
* lead, tail, hold, seconds, frames: [{ cue, caption, art | img | sketch }] }
|
|
193
|
+
* lead, tail, hold, seconds, frames: [{ cue, caption, art | img | sketch | endcard }] }
|
|
194
|
+
* `endcard: true` draws the wordmark on the brand's ground; a string adds a line under it.
|
|
194
195
|
*/
|
|
195
196
|
const storyboard = ({ title, lede = "", wpm = 150, target = 0, shots = [], dark = true, cover: withCover = true, index = true, audio = "" }) => {
|
|
196
197
|
let t = 0;
|
|
@@ -204,14 +205,16 @@ ${chrome ? `<footer><span>${esc(job.footer || "")}</span><span class="pn">${Stri
|
|
|
204
205
|
const at = f.cue && vo ? vo.toLowerCase().indexOf(String(f.cue).toLowerCase()) : -1;
|
|
205
206
|
// the first frame is on screen from the cut; the others land on their cue words
|
|
206
207
|
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) };
|
|
208
|
+
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
209
|
});
|
|
209
210
|
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
211
|
});
|
|
211
212
|
const total = t;
|
|
212
213
|
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
214
|
|
|
214
|
-
|
|
215
|
+
// an end card is the wordmark on the brand's ground, with a line under it when endcard is a string
|
|
216
|
+
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>`;
|
|
217
|
+
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
218
|
const DIR = [["line", "The line"], ["camera", "Camera"], ["motion", "Motion"], ["onscreen", "On screen"], ["sound", "Sound"], ["next", "Into next"]];
|
|
216
219
|
const shotPage = (s) => page({ eyebrow: s.act || "", dark, cls: "shot", notes: s.vo, body: `
|
|
217
220
|
<div class="shot-head"><div class="shot-id"><span class="tag">Shot</span><span class="no">${String(s.n).padStart(2, "0")}</span></div>
|
|
@@ -291,14 +294,16 @@ b,strong{font-weight:600}
|
|
|
291
294
|
.card.emph{background:var(--emph);border:var(--emph-stroke) solid var(--emph-accent);color:var(--emph-ink)}
|
|
292
295
|
.card.emph p,.card.emph .body,.card.emph .role,.card.emph .lead{color:var(--emph-text)}
|
|
293
296
|
.card.emph h3,.card.emph h4,.card.emph h5,.card.emph .name,.card.emph .lead-in,.card.emph .q p{color:var(--emph-ink)}
|
|
294
|
-
.card.emph
|
|
297
|
+
.card.emph em{color:var(--emph-accent)}
|
|
298
|
+
/* small text on an emphasis card takes its own role: gold reads at AA on one brand's card and not the other's */
|
|
299
|
+
.card.emph .tag,.card.emph .out,.card.emph .when{color:var(--emph-label,var(--emph-accent))}
|
|
295
300
|
.card.centred{display:flex;flex-direction:column;justify-content:center}
|
|
296
301
|
.card p{font-size:var(--t-sm);line-height:1.5;color:var(--text)}
|
|
297
302
|
.card .body{font-size:var(--t-body);line-height:var(--lh-body)}.card .body.sm{font-size:var(--t-sm);line-height:1.55}
|
|
298
303
|
.card .lead{font-size:var(--t-lead)}
|
|
299
304
|
.card h3{margin-bottom:.3em}.card h4{margin-bottom:.35em}.card h5{margin-bottom:.4em}
|
|
300
305
|
.card .sub{font-size:var(--t-sm);font-weight:500;color:var(--accent-text);margin-bottom:.5em}
|
|
301
|
-
.card.emph .sub{color:var(--emph-accent)}
|
|
306
|
+
.card.emph .sub{color:var(--emph-label,var(--emph-accent))}
|
|
302
307
|
.card .out{margin-top:auto;padding-top:.6em;font-weight:500;color:var(--accent-text)}
|
|
303
308
|
.card.fn,.card.flex{display:flex;flex-direction:column}
|
|
304
309
|
.card.fn h5{font-family:var(--face-label);font-weight:var(--w-label);font-size:calc(var(--t-tag) * 1.1);letter-spacing:.12em;text-transform:var(--caps-label);color:var(--ink);margin-bottom:.6em}
|
|
@@ -329,7 +334,7 @@ b,strong{font-weight:600}
|
|
|
329
334
|
.node{flex:1;display:flex;flex-direction:column;gap:.15em;padding:.7em 1em;border-radius:calc(var(--m-radius) * .7 * var(--radius-k));border:var(--m-stroke) solid var(--stroke);background:var(--paper)}
|
|
330
335
|
.node .who{font-family:var(--face-label);font-weight:var(--w-label);font-size:calc(var(--t-tag) * .92);letter-spacing:.16em;text-transform:var(--caps-label);color:var(--soft);display:block;border:0;padding:0}
|
|
331
336
|
.node .verb{font-family:var(--face-display);font-weight:var(--w-display);font-size:var(--t-sm);color:var(--ink);letter-spacing:var(--tr-display)}
|
|
332
|
-
.node.a{background:var(--emph);border-color:var(--emph)}.node.a .who{color:var(--emph-accent)}.node.a .verb{color:var(--emph-ink)}
|
|
337
|
+
.node.a{background:var(--emph);border-color:var(--emph)}.node.a .who{color:var(--emph-label,var(--emph-accent))}.node.a .verb{color:var(--emph-ink)}
|
|
333
338
|
.fnote{font-size:var(--t-sm);line-height:1.4;color:var(--text)}
|
|
334
339
|
/* tables */
|
|
335
340
|
table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}
|
|
@@ -379,7 +384,7 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
379
384
|
.cover .top{display:flex;flex-direction:column;gap:1.3em}
|
|
380
385
|
.cover h1{font-size:var(--t-display);line-height:1.02;letter-spacing:-.03em}
|
|
381
386
|
.lede{font-size:var(--t-lede);font-weight:500;line-height:1.45;color:var(--ink);max-width:88%}
|
|
382
|
-
.cover-body{font-size:var(--t-body);line-height:1.6;color:var(--text);max-width:80
|
|
387
|
+
.cover-body{font-size:var(--t-body);line-height:1.6;color:var(--text);max-width:min(80%,44em)}
|
|
383
388
|
.meta{font-size:var(--t-meta);color:var(--text);padding-top:1em;border-top:1px solid var(--rule);line-height:1.4}
|
|
384
389
|
.cover .bottom{display:flex;flex-direction:column;gap:1em}
|
|
385
390
|
.back .content{justify-content:center;gap:1.4em}
|
|
@@ -401,6 +406,9 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
401
406
|
.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}
|
|
402
407
|
.card.emph .tile{background:var(--emph-accent);color:var(--emph)}
|
|
403
408
|
/* room */
|
|
409
|
+
/* a slide's body sits in the space under its title, not in its top quarter */
|
|
410
|
+
.surface-slide .page:not(.cover):not(.back):not(.shot):not(.run) .content>:is(h1,h2,.sec):first-child+*{margin-top:auto}
|
|
411
|
+
.surface-slide .page:not(.cover):not(.back):not(.shot):not(.run) .content>:last-child{margin-bottom:auto}
|
|
404
412
|
.roomy .content{gap:calc(var(--m-gap) * 1.3)}
|
|
405
413
|
.roomy .cols{margin:auto 0}
|
|
406
414
|
.roomy .lead{font-size:calc(var(--t-lead) * 1.15)}
|
|
@@ -442,10 +450,13 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
442
450
|
.frame-art{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:calc(var(--m-radius) * .7 * var(--radius-k));background:var(--track);border:var(--card-stroke) solid var(--stroke)}
|
|
443
451
|
.frame-art>svg,.frame-art>img{position:absolute;inset:0;width:100%;height:100%;display:block}
|
|
444
452
|
.frame-art>img{object-fit:cover}
|
|
445
|
-
.frame figcaption{font-size:var(--t-note);line-height:1.4;color:var(--soft);font-variant-numeric:tabular-nums}
|
|
453
|
+
.frame figcaption{font-size:var(--t-note);line-height:1.4;color:var(--soft);font-variant-numeric:tabular-nums;max-width:44em}
|
|
446
454
|
.frame figcaption b{color:var(--label);font-weight:600}
|
|
447
455
|
.frame figcaption span{display:block;color:var(--text);margin-top:.2em}
|
|
448
456
|
.frame .todo{position:absolute;inset:0;display:grid;place-items:center;border:2px dashed var(--rule);border-radius:inherit}
|
|
457
|
+
.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}
|
|
458
|
+
.frame-art .endcard>img{position:static;width:30%;height:auto}
|
|
459
|
+
.frame-art .endcard>span{font-family:var(--face-body);font-size:var(--t-sm);color:var(--soft);letter-spacing:.04em}
|
|
449
460
|
.frame .todo span{font-size:var(--t-sm);color:var(--soft)}
|
|
450
461
|
.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)}
|
|
451
462
|
.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
|
@@ -172,6 +172,35 @@ function GATES({ min, chars, tokens, families, base }) {
|
|
|
172
172
|
return res;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Runs inside the page. A slide whose content fills too little of it gets the
|
|
177
|
+
* type scale one step up (.big), kept only if nothing then overflows: a deck
|
|
178
|
+
* sizes itself to its slides, the same way on every run. Returns the pages
|
|
179
|
+
* it changed, and every slide's fill after.
|
|
180
|
+
*/
|
|
181
|
+
function AUTOSIZE() {
|
|
182
|
+
const fill = (pg) => {
|
|
183
|
+
const c = pg.querySelector(".content"); if (!c) return 1;
|
|
184
|
+
const kids = [...c.children].filter((k) => !k.classList.contains("deco") && k.getBoundingClientRect().height);
|
|
185
|
+
if (!kids.length) return 1;
|
|
186
|
+
const gap = parseFloat(getComputedStyle(c).rowGap) || 0;
|
|
187
|
+
return (kids.reduce((a, k) => a + k.getBoundingClientRect().height, 0) + gap * (kids.length - 1)) / c.getBoundingClientRect().height;
|
|
188
|
+
};
|
|
189
|
+
const overflows = (pg) => {
|
|
190
|
+
const c = pg.querySelector(".content"), box = c.getBoundingClientRect(), foot = pg.querySelector("footer")?.getBoundingClientRect();
|
|
191
|
+
const floor = foot ? Math.min(box.bottom, foot.top - 6) : box.bottom;
|
|
192
|
+
// as the fit gate reads it: an inline run's line box may rise above the content's top, a block may not
|
|
193
|
+
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;
|
|
194
|
+
};
|
|
195
|
+
const sized = [], fills = [];
|
|
196
|
+
document.querySelectorAll(".page").forEach((pg, i) => {
|
|
197
|
+
if (pg.matches(".cover, .back, .shot, .run, [data-size]")) return;
|
|
198
|
+
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); }
|
|
199
|
+
fills.push({ page: i + 1, fill: fill(pg) });
|
|
200
|
+
});
|
|
201
|
+
return { sized, fills };
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
/* ------------------------------------------------------------------ build */
|
|
176
205
|
|
|
177
206
|
/**
|
|
@@ -209,6 +238,9 @@ export async function build(jobPath, { outputs: wantOutputs = true, outDir = nul
|
|
|
209
238
|
await page.evaluate(() => Promise.all([...document.images].map((i) => (i.complete ? null : new Promise((r) => { i.onload = i.onerror = r; })))));
|
|
210
239
|
await page.waitForTimeout(120);
|
|
211
240
|
|
|
241
|
+
// a slide deck sizes itself before anything is measured or drawn; the file keeps the sizes it chose
|
|
242
|
+
const auto = surface.kind === "slide" ? await page.evaluate(AUTOSIZE) : { sized: [], fills: [] };
|
|
243
|
+
if (auto.sized.length) fs.writeFileSync(htmlPath, "<!doctype html>\n" + await page.evaluate(() => document.documentElement.outerHTML));
|
|
212
244
|
const g = await page.evaluate(GATES, { min: surface.min, chars: surface.chars, tokens: [...brand.tokenSet], families: brand.families, base: surface.base });
|
|
213
245
|
const pageCount = g.fit.length;
|
|
214
246
|
const text = g.texts.join("\n");
|
|
@@ -231,6 +263,8 @@ export async function build(jobPath, { outputs: wantOutputs = true, outDir = nul
|
|
|
231
263
|
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
264
|
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
265
|
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);
|
|
266
|
+
const sparse = auto.fills.filter((f) => f.fill < 0.4);
|
|
267
|
+
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);
|
|
234
268
|
row(!g.broken.length, "images", g.broken.length ? `${g.broken.length} did not load: ${g.broken.slice(0, 3).join(", ")}` : "every image loaded");
|
|
235
269
|
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
270
|
const board = job.__board || null;
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wand-kit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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": {
|