wand-decks-kit 0.8.1 → 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 CHANGED
@@ -99,6 +99,17 @@ Decks built here carry their own spec inside the file. Change only what was
99
99
  asked, then `ship`. If the deck carries no spec it was not built here — treat it
100
100
  as a rebrand.
101
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
+
102
113
  ## Rules that decide whether the output is usable
103
114
 
104
115
  1. **Faithful means verbatim.** No consolidating slides, no paraphrasing to
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.1",
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.1",
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,