wand-decks-kit 0.8.0 → 0.8.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/SKILL.md ADDED
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: wand-decks
3
+ description: Rebrand, merge, edit or author a PowerPoint deck on the Wand design system. Use when a .pptx is attached for restyling, or a deck is wanted from a brief. Outputs a client-ready .pptx and QA gates.
4
+ compatibility: Requires Node.js 18+ and code execution enabled in claude.ai (Settings > Features). No network access needed - dependencies are bundled. Python 3 is used to read a source .pptx and to embed fonts; without it the kit can still author and check a deck, but the output will render in Arial on machines without Geist.
5
+ license: Proprietary - Wand AI
6
+ metadata:
7
+ version: "0.7.0"
8
+ ---
9
+
10
+
11
+ # Wand decks
12
+
13
+ ## First: find the kit and stand in it
14
+
15
+ This file ships inside the kit, so if you are reading it you are close. The kit
16
+ arrives one of three ways — work out which, then `cd` into the kit's folder and
17
+ run everything from there:
18
+
19
+ 1. **Installed from npm** (the usual way): it is at `node_modules/wand-decks-kit/`.
20
+ If it is not there yet: `npm install wand-decks-kit@latest --no-audit --no-fund`.
21
+ 2. **Attached to the chat as a zip**: unzip it and enter the folder it makes.
22
+ 3. **You are already inside it**: `wand.js` is next to this file.
23
+
24
+ Then confirm the ground you are standing on:
25
+
26
+ ```bash
27
+ node wand.js doctor
28
+ ```
29
+
30
+ If none of the three work — npm unreachable, nothing attached — **stop and say
31
+ so, and ask the user to attach the kit zip.** Never improvise a deck, and never
32
+ report checks you did not run; the gates are the only reason to trust the
33
+ output.
34
+
35
+ Every path below is relative to the kit folder.
36
+
37
+ A deck engine: 28 layouts, real font metrics, embedded Geist, and six gates that
38
+ each catch something the others cannot.
39
+
40
+ **Read `PROMPT.md` before making any design decision.** It is the source of
41
+ truth on canvas, colour, type, layout choice and content rules. This file is
42
+ only the operating procedure.
43
+
44
+ ## Available scripts
45
+
46
+ - **`wand.js`** — the front door - doctor, rebuild, layouts, glyphs, validate, check, preview, ship, spec, inspect
47
+ - **`build.js`** — spec -> .pptx, or --check, or --preview
48
+ - **`tools/extract.py`** — read a source deck: verbatim dump, geometry, analysis
49
+ - **`embed_fonts.py`** — embed Geist unsubsetted, and carry the spec inside the deck
50
+ - **`qa_coverage.py`** — diff the verbatim dump against the built deck
51
+
52
+ ## Setup
53
+
54
+ None beyond the doctor check above. Dependencies come with the package —
55
+ npm resolves them on install, and the zip carries them vendored.
56
+
57
+
58
+ ```bash
59
+ node wand.js doctor
60
+ ```
61
+
62
+ If `node_modules` is somehow missing, `npm install` in this directory.
63
+
64
+ ## The four jobs
65
+
66
+ ### Rebrand a deck they attached
67
+
68
+ ```bash
69
+ node wand.js rebuild <their-deck>.pptx # phases 0-3
70
+ # ...you write work/deck.json...
71
+ node wand.js ship work/deck.json # phases 4-6, prints the gate table
72
+ ```
73
+
74
+ `rebuild` normalises the source onto the 10in grid, dumps every verbatim string
75
+ and writes `work/PLAN.md` — per slide: the source's shape, a layout shortlist
76
+ with reasons, the verbatim text grouped by frame, and a stub with the required
77
+ fields. Choose the layout yourself; the shortlist is ranked by shape and cannot
78
+ see meaning.
79
+
80
+ ### Author from a brief
81
+
82
+ Skip `rebuild`. Write the spec against `node wand.js layouts` and
83
+ `examples/template.json`, then `ship`. Coverage is skipped — there is no source
84
+ to cover — and it says so.
85
+
86
+ ### Merge several decks
87
+
88
+ `node wand.js rebuild` each one into its own work directory, then write a single
89
+ spec. Ask which deck leads if they have not said. Flag duplicates rather than
90
+ silently dropping them, and list every merge in the review note.
91
+
92
+ ### Edit a deck this kit built
93
+
94
+ ```bash
95
+ node wand.js spec <their-deck>.pptx work/deck.json
96
+ ```
97
+
98
+ Decks built here carry their own spec inside the file. Change only what was
99
+ asked, then `ship`. If the deck carries no spec it was not built here — treat it
100
+ as a rebrand.
101
+
102
+ ## Work in fast loops, ship once
103
+
104
+ `node wand.js validate` and `node wand.js check` run in seconds; `ship` runs the
105
+ whole pipeline. Iterate spec edits against the first two, and run `ship` when
106
+ they are clean — never as your discovery tool.
107
+
108
+ When one error reveals a pattern — a field on the wrong layout, a missing
109
+ optional — **sweep the entire spec for that pattern before re-running anything**.
110
+ Paying a full cycle per instance is the slow way to learn the same fact
111
+ repeatedly.
112
+
113
+ ## Rules that decide whether the output is usable
114
+
115
+ 1. **Faithful means verbatim.** No consolidating slides, no paraphrasing to
116
+ tighten, no restructuring. Reflowing to fit is fine; rewording is not.
117
+ Paraphrase passes the schema check and passes the eye — only coverage catches
118
+ it.
119
+ 2. **A spec has no coordinates, colours or font sizes.** If a slide needs
120
+ geometry the layouts do not have, add a layout. Never leak positioning into
121
+ content.
122
+ 3. **Vary the layouts.** Three of the same in a row is a failure even if each is
123
+ correct. Across 15 slides aim for at least eight distinct ones, alternating
124
+ dark and light. `validate` warns when you do not.
125
+ 4. **Icons are named.** `node wand.js glyphs` lists all 75. Pick by meaning.
126
+ They recolour to the theme, so there is no polarity to get wrong.
127
+ 5. **Never invent content.** If the source is ambiguous, ask.
128
+
129
+ ## Useful commands
130
+
131
+ | | |
132
+ |---|---|
133
+ | `node wand.js layouts [name]` | the catalog, and any layout's fields |
134
+ | `node wand.js glyphs [search]` | the named icon set |
135
+ | `node wand.js validate <spec>` | fields, types, item caps — run before building |
136
+ | `node wand.js check <spec>` | fit in real Geist, bounds, assets |
137
+ | `node wand.js preview <spec>` | writes `preview/trace.js` for the browser preview |
138
+ | `node wand.js inspect <deck>` | parts, relationships, embedded fonts |
139
+
140
+ ## Deliver
141
+
142
+ The `.pptx` first, then a review note: every split or merge, every fallback
143
+ mapping, anything tight after fitting, and **everything dropped from the
144
+ verbatim dump**. A rebuild that drops a string without saying so is not done.
145
+
146
+ Tell them the gate table honestly. The visual gate is the one no script passes —
147
+ say plainly that nobody has looked at the slides yet if nobody has.
package/build.js CHANGED
@@ -129,7 +129,11 @@ function check(spec) {
129
129
  (bad ? ` — ${bad} overflow${bad > 1 ? "s" : ""} above` : " — clean"));
130
130
  console.log(`bounds check: ${oob ? oob + " shape(s) outside the content band — listed above" : "clean"}`);
131
131
  console.log(`asset check : ${missing ? missing + " missing — listed above" : `${seen.size} resolved`}`);
132
- return bad + oob + missing;
132
+ // A slide whose layout threw has no shapes, so nothing above can fail for
133
+ // it — a crash used to print an ERROR line and exit 0, and the spec author
134
+ // paid a full ship cycle to find out.
135
+ if (t.errors.length) console.log(`layout check: ${t.errors.length} slide(s) threw — listed above`);
136
+ return bad + oob + missing + t.errors.length;
133
137
  }
134
138
 
135
139
  /** The same trace, as a PDF. Fonts embedded, no LibreOffice, no conversion. */
package/lib/schema.js CHANGED
@@ -32,7 +32,7 @@ const COMMON = {
32
32
  const L = {
33
33
  /* ------------------------------------------------------------ structural */
34
34
  cover: {
35
- use: "Slide 1. Dark, full bleed, no footer",
35
+ use: "Slide 1. Always dark, full bleed, no footer. The lede goes in `body` — there is no subtitleText here",
36
36
  bleed: true,
37
37
  fields: {
38
38
  eyebrow: str(), body: str(), kickerPlain: str(), kickerBold: str(),
@@ -40,12 +40,12 @@ const L = {
40
40
  },
41
41
  },
42
42
  divider: {
43
- use: "Section breaks and the agenda. Keeps the tag, drops the footer",
43
+ use: "Section breaks. Always dark, keeps the tag, drops the footer. Prose goes in `body`",
44
44
  bleed: true,
45
45
  fields: { eyebrow: str(), titleText: s("title", req()), body: str() },
46
46
  },
47
47
  closing: {
48
- use: "Last slide. Centred, full bleed",
48
+ use: "Last slide. Always dark, centred, full bleed",
49
49
  bleed: true,
50
50
  fields: { eyebrow: str(), line1: str(req()), line2: str(), body: str(), url: str() },
51
51
  },
@@ -357,6 +357,18 @@ function validate(spec) {
357
357
  }
358
358
  if (sl.layout === "TODO") return;
359
359
  checkFields({ ...COMMON, ...def.fields }, sl, path, errs, warns);
360
+ // COMMON exempts fields every layout takes — but the full-bleed three do
361
+ // not take all of them, and a lede typed as subtitleText on a cover was
362
+ // silently dropped: valid name, wrong layout, no message, one ship cycle
363
+ // wasted finding out. Say it at validate time instead.
364
+ if (def.bleed) {
365
+ if (sl.subtitleText != null && !def.fields.subtitleText) {
366
+ errs.push(`${path}: ${sl.layout} does not render subtitleText — its lede goes in \`body\``);
367
+ }
368
+ if (sl.dark === false) {
369
+ warns.push(`${path}: ${sl.layout} is always dark — \`dark: false\` is ignored`);
370
+ }
371
+ }
360
372
  seen.push(sl.layout);
361
373
  });
362
374
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wand-decks-kit",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Rebuilds or authors a client-ready .pptx on the Wand design system: measured fit, embedded Geist, verbatim content.",
5
5
  "license": "SEE LICENSE IN PROMPT.md",
6
6
  "bin": {
package/wand_kit.js CHANGED
@@ -273,7 +273,7 @@ function cover(pres, { eyebrow, titleLines, body, kickerPlain, kickerBold, tag }
273
273
  s.addImage({ path: `${ASSETS}/bg-cover-dark.png`, x: 0, y: 0, w: 10, h: 5.625 });
274
274
  s.addImage({ path: `${ASSETS}/orb-cover.png`, x: 5.72, y: 0.38, w: 4.61, h: 4.61 });
275
275
  if (tag) chrome(s, { tag, dark: true });
276
- s.addText(eyebrow.toUpperCase(), {
276
+ if (eyebrow) s.addText(eyebrow.toUpperCase(), {
277
277
  x: 0.38, y: 1.24, w: 6.0, h: 0.4, margin: 0, isTextBox: true,
278
278
  fontFace: F.med, fontSize: 12, color: T.accentSoft, valign: "middle",
279
279
  });
@@ -290,10 +290,14 @@ function cover(pres, { eyebrow, titleLines, body, kickerPlain, kickerBold, tag }
290
290
  fontFace: F.reg, fontSize: 12.5, color: T.inkDark, valign: "top", lineSpacingMultiple: 1.12,
291
291
  });
292
292
  s.addShape("rect", { x: 0.38, y: 3.95, w: 0.60, h: 0.04, fill: { color: T.accent }, line: { width: 0 } });
293
- s.addText([
294
- { text: kickerPlain + " ", options: { fontFace: F.reg, fontSize: 10, color: T.inkDark } },
295
- { text: kickerBold, options: { fontFace: F.reg, fontSize: 10, bold: true, color: T.white } },
296
- ], { x: 0.39, y: 4.03, w: 6.0, h: 0.4, margin: 0, isTextBox: true, valign: "middle" });
293
+ // Concatenating an absent kicker used to paint the word "undefined" on the
294
+ // cover — visible only in a render, past every machine gate.
295
+ const kickerRuns = [
296
+ kickerPlain && { text: kickerPlain + " ", options: { fontFace: F.reg, fontSize: 10, color: T.inkDark } },
297
+ kickerBold && { text: kickerBold, options: { fontFace: F.reg, fontSize: 10, bold: true, color: T.white } },
298
+ ].filter(Boolean);
299
+ if (kickerRuns.length) s.addText(kickerRuns,
300
+ { x: 0.39, y: 4.03, w: 6.0, h: 0.4, margin: 0, isTextBox: true, valign: "middle" });
297
301
  return s;
298
302
  }
299
303
 
@@ -303,7 +307,7 @@ function divider(pres, { tag, eyebrow, titleText, body }) {
303
307
  s.background = { color: T.bgDark };
304
308
  s.addImage({ path: `${ASSETS}/bg-statement-dark.png`, x: 0, y: 0, w: 10, h: 5.625 });
305
309
  chrome(s, { tag, dark: true });
306
- s.addText(eyebrow.toUpperCase(), {
310
+ if (eyebrow) s.addText(eyebrow.toUpperCase(), {
307
311
  x: 0.38, y: 1.77, w: 8.9, h: 0.24, margin: 0, isTextBox: true,
308
312
  fontFace: F.med, fontSize: 12, color: T.accentSoft, valign: "middle",
309
313
  });
@@ -329,14 +333,14 @@ function closing(pres, { eyebrow, line1, line2, body, url }) {
329
333
  s.background = { color: T.bgDark };
330
334
  s.addImage({ path: `${ASSETS}/bg-statement-dark.png`, x: 0, y: 0, w: 10, h: 5.625 });
331
335
  s.addImage({ path: `${ASSETS}/wordmark-white.png`, x: 4.60, y: 0.98, w: 0.80, h: 0.22 });
332
- s.addText(eyebrow.toUpperCase(), {
336
+ if (eyebrow) s.addText(eyebrow.toUpperCase(), {
333
337
  x: 0.55, y: 1.85, w: 8.9, h: 0.3, margin: 0, isTextBox: true,
334
338
  fontFace: F.med, fontSize: 12, color: T.accentSoft, align: "center", valign: "middle",
335
339
  });
336
340
  s.addText([
337
- { text: line1 + "\n", options: { fontFace: F.reg, fontSize: 24, bold: true, color: T.white } },
338
- { text: line2, options: { fontFace: F.reg, fontSize: 24, bold: true, italic: true, color: T.accentSoft } },
339
- ], { x: 0.55, y: 2.19, w: 8.9, h: 1.0, margin: 0, isTextBox: true, align: "center", valign: "top", lineSpacingMultiple: 1.15 });
341
+ line1 && { text: line1 + (line2 ? "\n" : ""), options: { fontFace: F.reg, fontSize: 24, bold: true, color: T.white } },
342
+ line2 && { text: line2, options: { fontFace: F.reg, fontSize: 24, bold: true, italic: true, color: T.accentSoft } },
343
+ ].filter(Boolean), { x: 0.55, y: 2.19, w: 8.9, h: 1.0, margin: 0, isTextBox: true, align: "center", valign: "top", lineSpacingMultiple: 1.15 });
340
344
  if (body) s.addText(body, {
341
345
  x: 1.5, y: 3.23, w: 7.0, h: 0.7, margin: 0, isTextBox: true,
342
346
  fontFace: F.reg, fontSize: 12.5, color: T.bodyDark, align: "center", valign: "top", lineSpacingMultiple: 1.25,
@@ -363,7 +367,7 @@ function threeUp(pres, { tag, titleText, subtitleText, items, dark = true, arrow
363
367
  items.slice(0, 3).forEach((it, i) => {
364
368
  const cx = x[i], cy = 2.02;
365
369
  card(s, { x: cx, y: cy, w, h: 2.70, dark, variant: it.emph ? "emph" : "glass" });
366
- s.addText(it.eyebrow.toUpperCase(), {
370
+ if (it.eyebrow) s.addText(it.eyebrow.toUpperCase(), {
367
371
  x: cx + 0.22, y: cy + 0.22, w: w - 0.42, h: 0.20, margin: 0, isTextBox: true,
368
372
  fontFace: F.med, fontSize: 8, color: th.eyebrow, valign: "middle",
369
373
  });
@@ -917,6 +921,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
917
921
  if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.16, h: 0.28 });
918
922
  const th = theme(dark);
919
923
  const rail = (label, y, h) => {
924
+ if (!label) return;
920
925
  s.addShape("roundRect", {
921
926
  x: 0.37, y, w: 1.0, h, rectRadius: radius(1.0, h),
922
927
  fill: { color: dark ? T.cardMuted : T.surface }, line: { width: 0 },
@@ -994,7 +999,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
994
999
 
995
1000
 
996
1001
  module.exports = {
997
- VERSION: "0.8.0",
1002
+ VERSION: "0.8.2",
998
1003
  T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
999
1004
  background, chrome, footer, title, subtitle, kicker, contentTop,
1000
1005
  card, iconTile, pill,