wand-kit 0.3.2 → 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 +39 -3
- package/SKILL.md +11 -4
- package/brands/current/tokens.json +10 -2
- package/brands/next/tokens.json +14 -2
- package/lib/components.js +31 -4
- package/lib/engine.js +17 -0
- 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,20 @@ 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.
|
|
168
194
|
- **A slide balances and sizes itself.** Its body sits in the space under the
|
|
169
195
|
title, not in the top quarter; a slide whose content fills under 55% of it
|
|
170
196
|
takes the type scale a step up (`.big`) when that still fits, and the space
|
|
@@ -191,7 +217,12 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
191
217
|
per page, the contact sheet. `watch` does this on every save in under a
|
|
192
218
|
second with the browser kept warm.
|
|
193
219
|
6. **review** — look at `out/sheet.png`, then at any page the gates named.
|
|
194
|
-
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.
|
|
195
226
|
7. **ship** — `node wand.js ship <job> [--pptx]`: the build, the one-file HTML,
|
|
196
227
|
the PowerPoint. A deck's HTML is a talk (§12), a storyboard's plays (§13),
|
|
197
228
|
and either runtime is driven before it ships. `--brand next` (or `current`)
|
|
@@ -204,6 +235,7 @@ new ─▶ argue ─▶ content ─▶ compose ─▶ build ─▶ review ─▶
|
|
|
204
235
|
|---|---|---|
|
|
205
236
|
| fit | anything in the content box runs past it, under the footer, or off the sides | a page that fits and reads badly |
|
|
206
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 |
|
|
207
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 |
|
|
208
240
|
| overlap | one text box sits on another | text on art |
|
|
209
241
|
| brand | a computed colour, border or fill that is not a token or a declared theme colour | gradients and images |
|
|
@@ -293,7 +325,11 @@ figure final, every poster.
|
|
|
293
325
|
- **Kinds.** `rise` (the default), `fade`, `pop` for a mark or a figure,
|
|
294
326
|
`grow` for a bar going up, `widen` for a bar going across, `draw` for a
|
|
295
327
|
line that tells a trend (an SVG path with `pathLength="1"`), `count` for a
|
|
296
|
-
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.
|
|
297
333
|
- **Steps** when the order of the argument is the point: `step(1, html)`, then
|
|
298
334
|
2, then 3. Never more than four on a slide. Going back undoes the last step.
|
|
299
335
|
- **Notes on every slide of a talk**, and `seconds` on each when the talk has a
|
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,8 +126,9 @@ 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 })`.
|
|
@@ -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": {
|
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
|
|
|
@@ -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}>`;
|
|
@@ -235,7 +252,7 @@ ${DIR.some(([k]) => s[k]) ? `<dl class="direction">${DIR.filter(([k]) => s[k]).m
|
|
|
235
252
|
page, sec, h1: h("h1"), h2: h("h2"), h3: h("h3"), h4: h("h4"), h5: h("h5"),
|
|
236
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>`,
|
|
237
254
|
cols, card, chip, strip, phase, flow, table, who, quote, stat, stats, delta, steps, callout, list: listOf,
|
|
238
|
-
cover, closing, chapter, img, icon, tile, mark, wordmark,
|
|
255
|
+
cover, closing, chapter, img, photo, icon, tile, mark, wordmark,
|
|
239
256
|
asset: brand.assetUrl, art: brand.artUrl,
|
|
240
257
|
enter, step, countup, video, demo, sketch, storyboard,
|
|
241
258
|
brand: brand.name, surface: surface.name, count: () => n,
|
|
@@ -265,7 +282,11 @@ footer .pn{font-weight:500}
|
|
|
265
282
|
/* type */
|
|
266
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)}
|
|
267
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}
|
|
268
|
-
|
|
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)}
|
|
269
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}
|
|
270
291
|
.label.sm{font-size:calc(var(--t-label) * .8)}
|
|
271
292
|
.lead{font-size:var(--t-lead);line-height:1.5;color:var(--ink)}
|
|
@@ -401,6 +422,12 @@ table.grid{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums
|
|
|
401
422
|
.has-chapter .left{width:62%;display:flex;flex-direction:column;gap:var(--m-gap)}
|
|
402
423
|
/* images, icons */
|
|
403
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}
|
|
404
431
|
.figure{border-radius:calc(var(--m-radius) * var(--radius-k));overflow:hidden}
|
|
405
432
|
.icon{width:1.25em;height:1.25em;stroke:currentColor;fill:none}
|
|
406
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}
|
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");
|
|
@@ -265,6 +272,14 @@ export async function build(jobPath, { outputs: wantOutputs = true, outDir = nul
|
|
|
265
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);
|
|
266
273
|
const sparse = auto.fills.filter((f) => f.fill < 0.4);
|
|
267
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);
|
|
268
283
|
row(!g.broken.length, "images", g.broken.length ? `${g.broken.length} did not load: ${g.broken.slice(0, 3).join(", ")}` : "every image loaded");
|
|
269
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`);
|
|
270
285
|
const board = job.__board || null;
|
|
@@ -320,6 +335,8 @@ export function printReport(r) {
|
|
|
320
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)`);
|
|
321
336
|
if (r.outputs.pptx) console.log(` ${path.relative(process.cwd(), r.outputs.pptx)} (${(fs.statSync(r.outputs.pptx).size / 1e6).toFixed(1)} MB)`);
|
|
322
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?`);
|
|
323
340
|
console.log(r.ok ? "" : "\n FAILED — fix the rows marked FAIL\n");
|
|
324
341
|
}
|
|
325
342
|
|
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("");
|