wand-decks-kit 0.9.0 → 0.10.0

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 CHANGED
@@ -11,6 +11,35 @@ is the workflow and the design system; the skill is the source of truth for comm
11
11
  ---
12
12
 
13
13
 
14
+
15
+ ## Session decks — the Kearney standard (0.10)
16
+
17
+ `Wand_x_Kearney_SF_Leadership_Session_29Sep2026_v2.pptx` is the reference for a
18
+ deck that is *run in a room*. What makes it that quality is not the slides
19
+ alone:
20
+
21
+ - **A spine.** Five Parts, each opened by a divider with a client-specific
22
+ *working question*, and a session slide that reads as a clock (minutes per
23
+ part). The kit: `divider` gains `part`, and uses the deck's own section art
24
+ (`divider-art.png`); `coDelivery` / `agenda` items gain `meta`.
25
+ - **A run-of-show in the notes.** Every content slide carries its time window,
26
+ CORE when it carries the argument, what to say, and who in the room will test
27
+ it; the session slide carries checkpoints and what to cut if late. The kit:
28
+ `notes` on any slide, written through pptxgenjs `addNotes`.
29
+ - **Backups behind the close**, hidden and uncounted. The kit: `backup: true`.
30
+ - **Two slides every Wand session uses**: the today-vs-with-Wand flow (the
31
+ agents' steps filled) and the transformation timeline. The kit:
32
+ `flowCompare`, `timeline`.
33
+
34
+ Where the reference is weaker than the kit, the kit wins: it embeds fonts
35
+ subsetted (`saveSubsetFonts="1"`, which garbles digits in exported PDFs), and
36
+ 1,834 text runs are Calibri. `audit` flags both on it.
37
+
38
+ **Edits are commands, not rewrites.** `wand.js show` and `wand.js edit` exist
39
+ because a salesperson's changes are small and the old edit path cost two full
40
+ copies of the spec in the chat. A spec's `confidential` now reaches the footer
41
+ — it never did; every deck said STRICTLY CONFIDENTIAL.
42
+
14
43
  ## The guideline deck (0.9)
15
44
 
16
45
  Since 2026-09-23 the design source of truth is a real deck —
package/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Rebrand, merge, edit or author a PowerPoint deck on the Wand design
4
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
5
  license: Proprietary - Wand AI
6
6
  metadata:
7
- version: "0.9.0"
7
+ version: "0.10.0"
8
8
  ---
9
9
 
10
10
 
@@ -34,16 +34,27 @@ output.
34
34
 
35
35
  Every path below is relative to the kit folder.
36
36
 
37
- A deck engine: 28 layouts, real font metrics, embedded Geist, and six gates that
38
- each catch something the others cannot.
37
+ A deck engine: 35 layouts, real font metrics, embedded Geist, speaker notes,
38
+ and gates that each catch something the others cannot.
39
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.
40
+ ## Read only what the job needs
41
+
42
+ Sales teams run this. Every file you read and every spec you write is paid for
43
+ in the chat, so the job decides the reading:
44
+
45
+ | Job | Read | Never |
46
+ |---|---|---|
47
+ | Edit a deck this kit built | `node wand.js show <deck>` (one line per slide), then `show <deck> <n>` for the slide you change | read or rewrite the whole spec; read `PROMPT.md` |
48
+ | Edit a deck built elsewhere | the slide you change, then `audit` | restyle anything unasked |
49
+ | Rebuild, merge, author | `PROMPT.md` once, `work/PLAN.md`, `node wand.js layouts` | re-read them per slide |
50
+
51
+ Look at the contact sheet once; open a single slide only where something looks
52
+ off. `PROMPT.md` is the source of truth on design — read it for design
53
+ decisions, not for edits.
43
54
 
44
55
  ## Available scripts
45
56
 
46
- - **`wand.js`** — the front door - doctor, rebuild, layouts, glyphs, validate, check, preview, ship, spec, inspect
57
+ - **`wand.js`** — the front door - doctor, rebuild, layouts, glyphs, validate, check, preview, ship, show, edit, audit, spec, inspect
47
58
  - **`build.js`** — spec -> .pptx, or --check, or --preview
48
59
  - **`tools/extract.py`** — read a source deck: verbatim dump, geometry, analysis
49
60
  - **`embed_fonts.py`** — embed Geist unsubsetted, and carry the spec inside the deck
@@ -70,7 +81,7 @@ thing, and paid a full second pass. This is not optional for a rebuild, a merge
70
81
  or an adaptation; only "author from a brief" with fewer than eight slides may
71
82
  go straight to the spec.
72
83
 
73
- ## The six jobs
84
+ ## The seven jobs
74
85
 
75
86
  ### Rebrand a deck they attached
76
87
 
@@ -139,13 +150,42 @@ partner slide". They want *that*, and nothing else touched.
139
150
 
140
151
  ### Edit a deck this kit built
141
152
 
153
+ Decks built here carry their spec inside the file, so an edit is one command —
154
+ not reading the spec, not writing it back:
155
+
142
156
  ```bash
143
- node wand.js spec <their-deck>.pptx work/deck.json
157
+ node wand.js show deck.pptx # one line per slide
158
+ node wand.js show deck.pptx 4 # slide 4's fields, only if you need them
159
+ node wand.js edit deck.pptx --set '1.tag=Presented to Acme · 6 October 2026' \
160
+ --set 2.items.4.meta='40 min' \
161
+ --set '7.notes.text=Lead with the Foundation phase.'
144
162
  ```
145
163
 
146
- Decks built here carry their own spec inside the file. Change only what was
147
- asked, then `ship`. If the deck carries no spec it was not built here — treat it
148
- as a rebrand.
164
+ Paths are 1-based, the way the room counts: `2.items.4.meta` is slide 2, item 4.
165
+ Also `--del N`, `--move N M`, `--add N '<slide json>'`, `--dry` to preview. The
166
+ result is saved as the next version (`Deck_v2.pptx` → `Deck_v3.pptx`), every
167
+ gate runs, the original is never overwritten. If `show` says the deck carries
168
+ no spec, it was not built here — use the job above.
169
+
170
+ ### Build a session deck
171
+
172
+ When the deck will be *run* in a room — a leadership session, a workshop, a
173
+ partner meeting — give it the shape of the Kearney example (the reference for
174
+ this kind of deck, `examples/session.json`):
175
+
176
+ - **Cover** with `presentedTo`. **Session slide** next: `coDelivery` with a
177
+ `meta` duration on every item ("15 min") — the room reads it as a clock.
178
+ - **Part dividers** (`divider` with `part: "Part II"` and `body: "Working
179
+ question: …"`) — one question per part, written for this client.
180
+ - **Speaker notes on every content slide**: `notes: { time: "00:21 to 00:25",
181
+ core: true, text, watch }`. `time` is the run-of-show window; `core` marks the
182
+ slides that carry the argument; `watch` names who in the room will push. The
183
+ session slide's notes carry the checkpoints and what to cut if late.
184
+ - **Backups after the closing**, `backup: true` — hidden, uncounted ("31 of 31",
185
+ not "of 34"), footer says BACKUP.
186
+
187
+ Timings, attendee names and "watch for" lines come from the user or the brief.
188
+ Never invent them — leave `notes` out rather than guess.
149
189
 
150
190
  ## Spend tool calls like they are metered — they are
151
191
 
@@ -213,6 +253,8 @@ combined one covers it.
213
253
  | `node wand.js check <spec>` | fit in real Geist, bounds, assets |
214
254
  | `node wand.js preview <spec>` | writes `preview/trace.js` for the browser preview |
215
255
  | `node wand.js inspect <deck>` | parts, relationships, embedded fonts |
256
+ | `node wand.js show <deck> [n]` | the deck in one line per slide, or one slide's fields |
257
+ | `node wand.js edit <deck> --set N.path=v` | change a kit-built deck in one command; saves `_vN+1`, runs every gate |
216
258
  | `node wand.js audit <deck> [--dump f]` | every spec-free gate on any .pptx, including the **brand** gate (fonts, colours, canvas, footer) — use on decks edited outside the kit |
217
259
 
218
260
  ## Deliver
Binary file
package/build.js CHANGED
@@ -30,16 +30,27 @@ function loadSpec(p) {
30
30
  /** Build the .pptx. */
31
31
  function build(spec, out) {
32
32
  const pres = W.newDeck(pptxgen);
33
- const total = spec.slides.length;
33
+ // Backups are hidden and uncounted: the room sees 31 of 31, not 31 of 34.
34
+ const total = spec.slides.filter((x) => !x.backup).length;
35
+ let n = 0;
34
36
  spec.slides.forEach((sl, i) => {
35
37
  const fn = W[sl.layout];
36
38
  if (typeof fn !== "function") throw new Error(`slide ${i + 1}: unknown layout "${sl.layout}"`);
37
- const args = { ...sl, n: i + 1, total };
39
+ const args = { ...sl, n: sl.backup ? null : ++n, total };
38
40
  if (!FULL_BLEED.has(sl.layout)) {
39
41
  args.deck = sl.deck || spec.deck;
40
42
  args.confidential = sl.confidential || spec.confidential;
41
43
  }
44
+ W.context({ confidential: args.confidential || "STRICTLY CONFIDENTIAL" });
45
+ const before = pres.slides.length;
42
46
  fn(pres, args);
47
+ // Not every layout returns its slide, so take the one it just added.
48
+ const slide = pres.slides.length > before ? pres.slides[pres.slides.length - 1] : null;
49
+ if (slide) {
50
+ const nt = W.notesText(sl.notes);
51
+ if (nt) slide.addNotes(nt);
52
+ if (sl.backup) slide.hidden = true;
53
+ }
43
54
  });
44
55
  return pres.writeFile({ fileName: out }).then(() => {
45
56
  console.log(`wrote ${out} (${total} slides)`);
package/docs/LAYOUTS.md CHANGED
@@ -17,18 +17,21 @@ full-bleed three) `deck` / `n` / `total` for the footer.
17
17
  | Layout | Params | Use for |
18
18
  |---|---|---|
19
19
  | `cover` | `eyebrow, titleLines[], body, kickerPlain, kickerBold` | Slide 1. Dark, full bleed, no footer |
20
- | `divider` | `eyebrow, titleText, body` | Section breaks and the agenda. Keeps the tag, drops the footer |
20
+ | `divider` | `eyebrow, titleText, body, part` | Section breaks and the agenda. Keeps the tag, drops the footer |
21
21
  | `closing` | `eyebrow, line1, line2, body, url` | Last slide. Centred, full bleed |
22
22
 
23
23
  ## Content
24
24
 
25
25
  | Layout | Params | Use for |
26
26
  |---|---|---|
27
- | `agenda` | `items[{title,body}]` | What we'll cover — 2–5 numbered cards (guideline S03) |
27
+ | `agenda` | `items[{title,body,meta}]` | What we'll cover — 2–5 numbered cards (guideline S03) |
28
28
  | `team` | `items[{name,role,initials}]` | Who's in the room — headshot slot per person (guideline S02) |
29
29
  | `fourCards` | `items[{title,body}]` | Demo objectives, pillars — four titled cards under a lede (S12) |
30
30
  | `deliverColumns` | `columns[{heading,lead,body}]` | How we deliver — three flat columns with an accent bar (S14) |
31
- | `coDelivery` | `items[{title,body}]` | Numbered rows in two columns, up to six (S13) |
31
+ | `coDelivery` | `items[{title,body,meta}]` | Numbered rows in two columns, up to six. With `meta: "15 min"` it is the session agenda (Kearney S02) |
32
+ | `flowCompare` | `lanes[{label,caption,steps[{who,step,human}]}]` | Today vs with Wand — two lanes of 3–5 steps; the machine steps are filled (Kearney S08) |
33
+ | `timeline` | `phases[{when,label,bullets}]` | A journey in 2–4 phases on a line (Kearney S16) |
34
+ | — every layout — | `notes`, `backup` | Speaker notes (`{time, core, text, watch}` or a string); `backup: true` hides the slide after the close |
32
35
  | `threeUp` | `items[{eyebrow,title,pill,body,emph}], arrows` | 3 comparable things. `arrows: true` for a progression |
33
36
  | `fourUpIcon` | `items[{icon,title,body}], kickerText` | 4 attributes or capabilities |
34
37
  | `sixUpIcon` | `items[{icon,title,body}]` | 6 features. Don't use it three slides running |
@@ -0,0 +1,92 @@
1
+ {
2
+ "deck": "WAND × CLIENT · LEADERSHIP SESSION",
3
+ "confidential": "WAND · CONFIDENTIAL",
4
+ "slides": [
5
+ { "layout": "cover", "tag": "Presented to Client · 29 September 2026",
6
+ "titleLines": ["Expand human possibility", "beyond human capacity."],
7
+ "body": "For all of history, what organizations could accomplish was constrained by the number of people available to do the work. AI labor changes that.",
8
+ "presentedTo": ["First Last, Managing Partner", "First Last, Head of Transformation"],
9
+ "notes": { "text": "On screen while the room settles. Frame it the way the client asked: a working conversation about a change in the nature of production, not a product pitch." } },
10
+
11
+ { "layout": "coDelivery", "tag": "The session", "titleText": "A working conversation, not a presentation.",
12
+ "items": [
13
+ { "title": "When software becomes labor", "body": "The threshold between a tool and a worker.", "meta": "15 min" },
14
+ { "title": "The organization has to change", "body": "From the individual agent to the Hybrid Organization.", "meta": "10 min" },
15
+ { "title": "Why transformations succeed, or die", "body": "Resistance is operating-model design.", "meta": "10 min" },
16
+ { "title": "From architecture to production", "body": "Inside an agent, then one real process, live.", "meta": "35 min" },
17
+ { "title": "When capacity decouples from labor", "body": "From the firm to the economy.", "meta": "5 min" },
18
+ { "title": "Wand × Client", "body": "Designing the Hybrid Organization together.", "meta": "10 min" }
19
+ ],
20
+ "notes": { "time": "00:00, under one minute", "text": "Walk the spine, not the rows. 90 minutes core, then 30 minutes of open discussion. Checkpoints: 00:20 Part II, 00:40 Part IV, 01:15 Part V. If late, compress Part III and Part V — never the live walkthrough." } },
21
+
22
+ { "layout": "divider", "titleText": "When software becomes labor",
23
+ "part": "Part I", "body": "Working question: which of your problems are being treated as AI productivity when they are really a chance to add a new source of labor?",
24
+ "notes": { "time": "00:01 to 00:03", "text": "Read the working question aloud and leave it on screen. Do not answer it yet." } },
25
+
26
+ { "layout": "flowCompare", "tag": "Why three years of AI spend has not moved the operating line",
27
+ "titleText": "Every AI tool you own ends at a person.",
28
+ "lanes": [
29
+ { "label": "Today — AI as a tool", "caption": "Capacity = your people. Four of five steps wait on a person; that person is the ceiling.",
30
+ "steps": [ { "who": "Person", "step": "Notices", "human": true }, { "who": "Person", "step": "Gathers context", "human": true },
31
+ { "who": "Person", "step": "Instructs", "human": true }, { "who": "AI", "step": "Drafts" },
32
+ { "who": "Person", "step": "Checks", "human": true } ] },
33
+ { "label": "With Wand — AI as labor", "caption": "Capacity = people + agents. The person moves from every step to the one step that needs judgment.",
34
+ "steps": [ { "who": "Agent", "step": "Notices" }, { "who": "Agent", "step": "Plans" }, { "who": "Agent", "step": "Executes" },
35
+ { "who": "Agent", "step": "Grades" }, { "who": "Manager", "step": "Approves", "human": true } ] }
36
+ ],
37
+ "notes": { "time": "00:11 to 00:14", "core": true, "text": "This is the difference between an efficiency programme and a capacity programme. Point at the top row: every step waits on a person.", "watch": "whoever owns the current copilot rollout — expect them to defend adoption numbers." } },
38
+
39
+ { "layout": "fourCards", "tag": "Agent behavior", "titleText": "A worker, not just a good tool.",
40
+ "subtitleText": "A tool responds to prompts. A worker pursues goals, decides what matters, acts within constraints, and is judged by outcomes.",
41
+ "items": [
42
+ { "title": "Pursues goals", "body": "Carries objectives and KPIs like any employee, and works toward them without being asked." },
43
+ { "title": "Decides what matters", "body": "Notices, prioritizes and plans inside the boundaries policy sets." },
44
+ { "title": "Is graded", "body": "Every output reviewed against a baseline agreed before it started." },
45
+ { "title": "Gets better", "body": "Banks what it learns, so a year in it is categorically better, not older." }
46
+ ],
47
+ "notes": { "time": "00:43 to 00:47", "core": true, "text": "Land the word 'worker'. Everything after this slide assumes the room accepts it." } },
48
+
49
+ { "layout": "divider", "titleText": "Why transformations die",
50
+ "part": "Part III", "body": "Working question: what would your methodology look like if org redesign, workforce redesign and agent deployment were one program, not three?",
51
+ "notes": { "time": "00:30 to 00:31" } },
52
+
53
+ { "layout": "timeline", "tag": "Transformation journey", "titleText": "The transformation timeline.",
54
+ "subtitleText": "From the starting line, month over month, compounding.",
55
+ "phases": [
56
+ { "when": "3–6 months", "label": "Foundation", "bullets": ["A core transformation team of 7–10 people", "One registry covering people and agents", "5–10 core processes rebuilt for the dual workforce", "Dozens of agents in production with named managers"] },
57
+ { "when": "6–12 months", "label": "Scale", "bullets": ["Agents across dozens of core functions", "Management structure rebalanced; spans widened", "Hundreds of agents in production"] },
58
+ { "when": "12–24 months", "label": "Accelerate", "bullets": ["Most processes designed for a dual workforce from the start", "Whole functions run by agent teams", "Cost per unit of work structurally below the market"] }
59
+ ],
60
+ "notes": { "time": "00:37 to 00:40", "core": true, "text": "Timelines are directional. The commitment is the Foundation phase; everything after it is earned by the telemetry." } },
61
+
62
+ { "layout": "deliverColumns", "tag": "The build", "titleText": "The client designs the transformation. Wand operates it.",
63
+ "subtitleText": "Who does each part, and how it is governed.",
64
+ "columns": [
65
+ { "heading": "1 · Change management", "lead": "Led by your people, supported by ours", "body": "A small core team who pick where to start and carry the early wins back." },
66
+ { "heading": "2 · Deployment", "lead": "Led by our solution architect with your technical team", "body": "Cloud, deployment model, security review, and reaching the systems you run." },
67
+ { "heading": "3 · Building the workforce", "lead": "Led by our forward-deployed engineers", "body": "Build the agents and processes with your team, then shift to supporting as your people take it on." }
68
+ ],
69
+ "notes": { "time": "01:23 to 01:25", "text": "Their methodology, our operating system. Say it in that order." } },
70
+
71
+ { "layout": "fourCards", "tag": "Next steps", "titleText": "What happens next.",
72
+ "subtitleText": "This room builds the momentum. The partnership work happens in a smaller session, with named owners and dates.",
73
+ "items": [
74
+ { "title": "Partnership session", "body": "Named leads on both sides; one process chosen and its baseline written down." },
75
+ { "title": "Architecture deep-dive", "body": "Under NDA, for your technology team: deployment posture, identity, audit." },
76
+ { "title": "Sandbox access", "body": "A governed environment your team can build in, with our engineers alongside." },
77
+ { "title": "First client reunion", "body": "Where the first agents in production are shown against the baseline." }
78
+ ],
79
+ "notes": { "time": "01:29 to 01:30", "text": "Leave the room with one date agreed. Anything else is a follow-up email." } },
80
+
81
+ { "layout": "closing", "line1": "Thank you.", "body": "The operating system for the human + agent workforce.", "url": "September 2026 | Strictly Confidential" },
82
+
83
+ { "layout": "fourCards", "backup": true, "tag": "Why this is a category", "titleText": "Five advantages, not five features.",
84
+ "items": [
85
+ { "title": "Governance & orchestration", "body": "One policy, one identity, one audit trail across the whole estate." },
86
+ { "title": "Institutional IP", "body": "Every lesson compounds inside your perimeter, not ours." },
87
+ { "title": "Model agnostic", "body": "Any model, any cloud — no lock-in, sovereignty-friendly by design." },
88
+ { "title": "One registry", "body": "People and agents managed in the same system, by the same managers." }
89
+ ],
90
+ "notes": "BACKUP. Use only if the room asks why this is not another agent platform." }
91
+ ]
92
+ }
@@ -665,6 +665,106 @@
665
665
  }
666
666
  ]
667
667
  },
668
+ {
669
+ "layout": "flowCompare",
670
+ "tag": "Why three years of AI spend has not moved the operating line",
671
+ "titleText": "Every AI tool you own ends at a person.",
672
+ "lanes": [
673
+ {
674
+ "label": "Today — AI as a tool",
675
+ "caption": "Capacity = your people. Four of five steps wait on a person; that person is the ceiling.",
676
+ "steps": [
677
+ {
678
+ "who": "Person",
679
+ "step": "Notices",
680
+ "human": true
681
+ },
682
+ {
683
+ "who": "Person",
684
+ "step": "Gathers context",
685
+ "human": true
686
+ },
687
+ {
688
+ "who": "Person",
689
+ "step": "Instructs",
690
+ "human": true
691
+ },
692
+ {
693
+ "who": "AI",
694
+ "step": "Drafts"
695
+ },
696
+ {
697
+ "who": "Person",
698
+ "step": "Checks",
699
+ "human": true
700
+ }
701
+ ]
702
+ },
703
+ {
704
+ "label": "With Wand — AI as labor",
705
+ "caption": "Capacity = people + agents. The person moves from every step to the one step that needs judgment.",
706
+ "steps": [
707
+ {
708
+ "who": "Agent",
709
+ "step": "Notices"
710
+ },
711
+ {
712
+ "who": "Agent",
713
+ "step": "Plans"
714
+ },
715
+ {
716
+ "who": "Agent",
717
+ "step": "Executes"
718
+ },
719
+ {
720
+ "who": "Agent",
721
+ "step": "Grades"
722
+ },
723
+ {
724
+ "who": "Manager",
725
+ "step": "Approves",
726
+ "human": true
727
+ }
728
+ ]
729
+ }
730
+ ]
731
+ },
732
+ {
733
+ "layout": "timeline",
734
+ "tag": "Transformation journey",
735
+ "titleText": "The transformation timeline.",
736
+ "subtitleText": "From the starting line, month over month, compounding.",
737
+ "phases": [
738
+ {
739
+ "when": "3–6 months",
740
+ "label": "Foundation",
741
+ "bullets": [
742
+ "A core transformation team of 7–10 people",
743
+ "One registry covering people and agents",
744
+ "5–10 core processes rebuilt for the dual workforce",
745
+ "Dozens of agents in production with named managers"
746
+ ]
747
+ },
748
+ {
749
+ "when": "6–12 months",
750
+ "label": "Scale",
751
+ "bullets": [
752
+ "Agents across dozens of core functions",
753
+ "Management structure rebalanced; spans widened",
754
+ "Hundreds of agents in production"
755
+ ]
756
+ },
757
+ {
758
+ "when": "12–24 months",
759
+ "label": "Accelerate",
760
+ "bullets": [
761
+ "Most processes designed for a dual workforce from the start",
762
+ "Whole functions run by agent teams",
763
+ "Cost per unit of work structurally below the market"
764
+ ]
765
+ }
766
+ ]
767
+ },
668
768
  {
669
769
  "layout": "closing",
670
770
  "eyebrow": "THE CLOSE",
package/lib/pdf.js CHANGED
@@ -112,6 +112,20 @@ class Doc {
112
112
  return this;
113
113
  }
114
114
 
115
+ /**
116
+ * A filled triangle in its box, pointing up — or right when rotated 90°, the
117
+ * way a flow connector is drawn. Matches pptxgenjs's "triangle" + rotate.
118
+ */
119
+ triangle({ x, y, w, h, fill, rotate = 0 }) {
120
+ const o = this.page.ops;
121
+ const pts = rotate === 90
122
+ ? [[x, y], [x + w, y + h / 2], [x, y + h]]
123
+ : [[x, y + h], [x + w / 2, y], [x + w, y + h]];
124
+ o.push(`${rgb(fill || "000000")} rg`,
125
+ pts.map(([px, py], i) => `${num(px * PT)} ${num(this._y(py))} ${i ? "l" : "m"}`).join(" ") + " h f");
126
+ return this;
127
+ }
128
+
115
129
  line({ x1, y1, x2, y2, color = "000000", width = 0.75 }) {
116
130
  this.page.ops.push(`${rgb(color)} RG`, `${num(width)} w`,
117
131
  `${num(x1 * PT)} ${num(this._y(y1))} m ${num(x2 * PT)} ${num(this._y(y2))} l S`);
package/lib/render_pdf.js CHANGED
@@ -152,6 +152,7 @@ function drawShape(d, sh) {
152
152
  const stroke = sh.line && sh.line.width ? (sh.line.color || "000000").replace(/^#/, "") : null;
153
153
  const box = { x: sh.x || 0, y: sh.y || 0, w: sh.w || 0, h: sh.h || 0 };
154
154
  if (sh.type === "ellipse") return d.ellipse({ ...box, fill, stroke, lineWidth: (sh.line && sh.line.width) || 0.75 });
155
+ if (sh.type === "triangle") return d.triangle({ ...box, fill, rotate: sh.rotate || 0 });
155
156
  d.rect({
156
157
  ...box, fill, stroke,
157
158
  lineWidth: (sh.line && sh.line.width) || 0.75,
package/lib/schema.js CHANGED
@@ -27,6 +27,8 @@ const COMMON = {
27
27
  subtitleText: str({ note: "the lede under the title" }),
28
28
  deck: str({ note: "overrides the deck-level footer" }),
29
29
  confidential: str(),
30
+ notes: s("notes", { note: 'speaker notes — a string, or { time: "00:21 to 00:25", core: true, text, watch }' }),
31
+ backup: s("boolean", { note: "a hidden slide after the closing; its footer says BACKUP" }),
30
32
  };
31
33
 
32
34
  const L = {
@@ -43,7 +45,8 @@ const L = {
43
45
  divider: {
44
46
  use: "Section breaks. Always dark, keeps the tag, drops the footer. Prose goes in `body`",
45
47
  bleed: true,
46
- fields: { eyebrow: str(), titleText: s("title", req()), body: str() },
48
+ fields: { eyebrow: str(), titleText: s("title", req()), body: str(),
49
+ part: str({ note: '"Part II" — set before the body as "Part II · <body>"' }) },
47
50
  },
48
51
  closing: {
49
52
  use: "Last slide. Always dark, centred, full bleed",
@@ -54,7 +57,7 @@ const L = {
54
57
  /* ----------------------------------------------------- guideline (0.9) */
55
58
  agenda: {
56
59
  use: "What we'll cover — 2–5 numbered cards in a row",
57
- fields: { items: s("object[]", req({ min: 2, max: 5, of: { title: str(req()), body: str() } })) },
60
+ fields: { items: s("object[]", req({ min: 2, max: 5, of: { title: str(req()), body: str(), meta: str({ note: '"15 min"' }) } })) },
58
61
  },
59
62
  team: {
60
63
  use: "Who's in the room — 2–4 people, each with a headshot slot the user fills",
@@ -70,7 +73,20 @@ const L = {
70
73
  },
71
74
  coDelivery: {
72
75
  use: "Up to six numbered rows in two columns — a co-delivery model, operating principles",
73
- fields: { items: s("object[]", req({ min: 2, max: 6, of: { title: str(req()), body: str(req()) } })) },
76
+ fields: { items: s("object[]", req({ min: 2, max: 6, of: { title: str(req()), body: str(req()), meta: str({ note: '"15 min" — makes it the session agenda' }) } })) },
77
+ },
78
+ flowCompare: {
79
+ use: "Two lanes of steps — today vs with Wand. Mark the steps a person does `human: true`",
80
+ fields: { lanes: s("object[]", req({ min: 2, max: 2, of: {
81
+ label: str(req()), caption: str(),
82
+ steps: s("object[]", req({ min: 3, max: 5, of: { who: str(), step: str(req()), human: s("boolean") } })),
83
+ } })) },
84
+ },
85
+ timeline: {
86
+ use: "A journey in 2–4 phases on a line — months, a label, bullets",
87
+ fields: { phases: s("object[]", req({ min: 2, max: 4, of: {
88
+ when: str(), label: str(req()), bullets: s("string[]", { max: 6 }),
89
+ } })) },
74
90
  },
75
91
 
76
92
  /* --------------------------------------------------------------- content */
@@ -322,6 +338,7 @@ const TYPEOF = {
322
338
  "string[]": (v) => Array.isArray(v) && v.every((x) => typeof x === "string"),
323
339
  "object[]": (v) => Array.isArray(v) && v.every((x) => x && typeof x === "object" && !Array.isArray(x)),
324
340
  object: (v) => v && typeof v === "object" && !Array.isArray(v),
341
+ notes: (v) => typeof v === "string" || (v && typeof v === "object" && !Array.isArray(v)),
325
342
  };
326
343
 
327
344
  function checkFields(defs, value, path, errs, warns) {
@@ -395,6 +412,14 @@ function validate(spec) {
395
412
  seen.push(sl.layout);
396
413
  });
397
414
 
415
+ // backups live after the close — hidden, so they never interrupt the room
416
+ const closeAt = spec.slides.findIndex((x) => x.layout === "closing");
417
+ spec.slides.forEach((x, i) => {
418
+ if (x.backup && closeAt >= 0 && i < closeAt) warns.push(`S${i + 1} is a backup slide before the closing — move it after S${closeAt + 1}`);
419
+ if (x.notes && typeof x.notes === "object" && x.notes.time && !/\d\d:\d\d/.test(x.notes.time))
420
+ warns.push(`S${i + 1} notes.time "${x.notes.time}" is not a time window — use "00:21 to 00:25"`);
421
+ });
422
+
398
423
  // §5: vary the layouts, alternate the themes
399
424
  const distinct = new Set(seen).size;
400
425
  if (spec.slides.length >= 10 && distinct < 8) {
package/lib/trace.js CHANGED
@@ -52,7 +52,7 @@ function fakePres() {
52
52
  function trace(W, spec) {
53
53
  const pres = fakePres();
54
54
  pres.layout = "LAYOUT_16x9";
55
- const total = spec.slides.length;
55
+ const total = spec.slides.filter((x) => !x.backup).length;
56
56
  let n = 0;
57
57
  const errors = [];
58
58
 
@@ -63,12 +63,12 @@ function trace(W, spec) {
63
63
  return;
64
64
  }
65
65
  const fullBleed = ["cover", "divider", "closing"].includes(sl.layout);
66
- n += 1;
67
- const args = { ...sl, n, total };
66
+ const args = { ...sl, n: sl.backup ? null : ++n, total };
68
67
  if (!fullBleed) {
69
68
  args.deck = sl.deck || spec.deck;
70
69
  args.confidential = sl.confidential || spec.confidential;
71
70
  }
71
+ W.context({ confidential: args.confidential || "STRICTLY CONFIDENTIAL" });
72
72
  try { fn(pres, args); }
73
73
  catch (e) { errors.push(`slide ${i + 1} (${sl.layout}): ${e.message}`); }
74
74
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wand-decks-kit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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": {
@@ -90,6 +90,8 @@ header input[type="range"] { width: 120px; accent-color: var(--accent); }
90
90
 
91
91
  .sh.rect { border-radius: 0; }
92
92
  .sh.roundRect { border-radius: calc(var(--in) * .10); }
93
+ /* flow connectors: pptxgenjs triangle rotated 90 points right */
94
+ .sh.triangle { clip-path: polygon(0 0, 100% 50%, 0 100%); }
93
95
  .sh.ellipse { border-radius: 50%; }
94
96
 
95
97
  .sh.image { object-fit: contain; }
@@ -23,7 +23,9 @@ z = zipfile.ZipFile(deck)
23
23
  names = sorted((n for n in z.namelist() if re.fullmatch(r"ppt/slides/slide\d+\.xml", n)),
24
24
  key=lambda n: int(re.findall(r"\d+", n)[0]))
25
25
  pres = z.read("ppt/presentation.xml").decode()
26
- cx, cy = (int(v) for v in re.search(r'<p:sldSz cx="(\d+)" cy="(\d+)"', pres).groups())
26
+ # attribute order varies by writer — the Kearney deck writes cy before cx
27
+ tag = re.search(r"<p:sldSz[^>]*>", pres).group(0)
28
+ cx, cy = (int(re.search(rf'{k}="(\d+)"', tag).group(1)) for k in ("cx", "cy"))
27
29
  canvas_ok = abs(cx / 914400 - 10) < 0.01 and abs(cy / 914400 - 5.625) < 0.01
28
30
 
29
31
  bad = 0
package/wand.js CHANGED
@@ -10,11 +10,14 @@
10
10
  * node wand.js doctor what works here, and what does not
11
11
  * node wand.js rebuild source.pptx phases 0-3: extract, then the worksheet
12
12
  * node wand.js layouts [name] the catalog, with every field
13
- node wand.js glyphs [search] the named icon set
13
+ * node wand.js glyphs [search] the named icon set
14
14
  * node wand.js validate work/deck.json fields and counts, before the build
15
15
  * node wand.js preview work/deck.json browser preview at the 10in grid
16
16
  * node wand.js ship work/deck.json build .pptx and .pdf, then every gate
17
- node wand.js spec deck.pptx read back the spec a built deck carries
17
+ * node wand.js show deck.pptx [n] one line per slide, or slide n's fields
18
+ * node wand.js edit deck.pptx --set 4.titleText="..." patch a kit-built deck, rebuild, gate
19
+ * node wand.js audit any.pptx every spec-free gate, incl. brand, on any deck
20
+ * node wand.js spec deck.pptx read back the spec a built deck carries
18
21
  *
19
22
  * `rebuild` and `ship` are the two that matter. Everything else is a stage of
20
23
  * one of them, exposed so a failure can be re-run on its own.
@@ -254,6 +257,100 @@ function ship(specPath, outArg) {
254
257
  return gates.some(([, ok]) => ok === false) ? 1 : 0;
255
258
  }
256
259
 
260
+ /* --------------------------------------------------------------- show/edit */
261
+
262
+ /**
263
+ * Why these exist: a salesperson's edits are small — a date, a name, a title —
264
+ * and the old path cost two full copies of the spec in the context (read it,
265
+ * write it back) plus a rebuild. A 40 KB spec is ~10K tokens each way.
266
+ * `show` prints one line per slide, or one slide's fields; `edit` patches the
267
+ * spec the deck carries inside itself and ships. An edit costs a command line.
268
+ */
269
+ function specOf(file) {
270
+ const f = path.resolve(file);
271
+ if (f.endsWith(".json")) return JSON.parse(fs.readFileSync(f, "utf8"));
272
+ const z = readZip(f);
273
+ if (!z.has("wand/spec.json")) die(`${rel(f)} carries no spec — it was not built by this kit.\n` +
274
+ "Edit it directly with the pptx tools, then run `node wand.js audit` on it (SKILL.md, \"Edit a deck they attached\").");
275
+ return JSON.parse(z.text("wand/spec.json"));
276
+ }
277
+
278
+ function oneLine(sl) {
279
+ // titleText arrays are runs of one line; titleLines are separate lines
280
+ const t = sl.titleText || sl.titleLines || sl.line1 || "";
281
+ const title = (Array.isArray(t) ? t.join(sl.titleLines && !sl.titleText ? " " : "") : String(t)).replace(/\s+/g, " ");
282
+ const bits = [sl.backup ? "BACKUP" : null, sl.notes ? "notes" : null, sl.dark === false ? "light" : null].filter(Boolean);
283
+ return `${sl.layout.padEnd(15)} ${title.slice(0, 64)}${bits.length ? " [" + bits.join(", ") + "]" : ""}`;
284
+ }
285
+
286
+ function show(file, which) {
287
+ const spec = specOf(file);
288
+ if (which == null) {
289
+ console.log(`${spec.deck || "(untitled)"} — ${spec.slides.length} slides`);
290
+ spec.slides.forEach((sl, i) => console.log(` ${String(i + 1).padStart(2)} ${oneLine(sl)}`));
291
+ console.log("\nnode wand.js show <deck> <n> for one slide's fields");
292
+ return 0;
293
+ }
294
+ const sl = spec.slides[Number(which) - 1];
295
+ if (!sl) die(`no slide ${which} — the deck has ${spec.slides.length}`);
296
+ console.log(JSON.stringify(sl, null, 1));
297
+ return 0;
298
+ }
299
+
300
+ /** "4.items.2.body" sets slide 4, item 2, body. 1-based, the way the room counts. */
301
+ function setPath(spec, dotted, value) {
302
+ const [n, ...keys] = dotted.split(".");
303
+ let node = spec.slides[Number(n) - 1];
304
+ if (!node) die(`--set ${dotted}: no slide ${n}`);
305
+ if (!keys.length) die(`--set ${dotted}: name a field, e.g. ${n}.titleText`);
306
+ for (let i = 0; i < keys.length - 1; i++) {
307
+ const k = /^\d+$/.test(keys[i]) ? Number(keys[i]) - 1 : keys[i];
308
+ if (node[k] == null) node[k] = /^\d+$/.test(keys[i + 1]) ? [] : {};
309
+ node = node[k];
310
+ }
311
+ const last = keys[keys.length - 1];
312
+ node[/^\d+$/.test(last) ? Number(last) - 1 : last] = value;
313
+ }
314
+
315
+ /** Deck_v2.pptx -> Deck_v3.pptx; Deck.pptx -> Deck_v2.pptx. Never overwrite the input. */
316
+ function nextVersion(file) {
317
+ const ext = path.extname(file), base = file.slice(0, -ext.length);
318
+ const m = /_v(\d+)$/.exec(base);
319
+ return m ? `${base.slice(0, m.index)}_v${Number(m[1]) + 1}${ext}` : `${base}_v2${ext}`;
320
+ }
321
+
322
+ function edit(file, args) {
323
+ const spec = specOf(file);
324
+ const changed = [];
325
+ for (let i = 0; i < args.length; i++) {
326
+ const a = args[i];
327
+ if (a === "--set") {
328
+ const arg = args[++i] || "", eq = arg.indexOf("=");
329
+ if (eq < 1) die(`--set wants N.path=value, got "${arg}"`);
330
+ const key = arg.slice(0, eq), raw = arg.slice(eq + 1);
331
+ let val; try { val = JSON.parse(raw); } catch { val = raw; } // arrays, numbers, booleans parse; prose stays prose
332
+ setPath(spec, key, val); changed.push(`set ${key}`);
333
+ } else if (a === "--del") {
334
+ const n = Number(args[++i]); spec.slides.splice(n - 1, 1); changed.push(`deleted S${n}`);
335
+ } else if (a === "--move") {
336
+ const from = Number(args[++i]), to = Number(args[++i]);
337
+ const [sl] = spec.slides.splice(from - 1, 1); spec.slides.splice(to - 1, 0, sl); changed.push(`moved S${from} to ${to}`);
338
+ } else if (a === "--add") {
339
+ const at = Number(args[++i]); spec.slides.splice(at - 1, 0, JSON.parse(args[++i])); changed.push(`added S${at}`);
340
+ }
341
+ }
342
+ if (!changed.length) die("nothing to do — pass --set, --del, --move or --add");
343
+ console.log(changed.map((c) => " " + c).join("\n"));
344
+ const work = path.join(path.dirname(path.resolve(file)), "work");
345
+ fs.mkdirSync(work, { recursive: true });
346
+ const specPath = path.join(work, "deck.json");
347
+ fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
348
+ if (args.includes("--dry")) { show(specPath); return validate(specPath) ? 1 : 0; }
349
+ const oi = args.indexOf("-o");
350
+ const out = oi >= 0 ? path.resolve(args[oi + 1]) : nextVersion(path.resolve(file));
351
+ return ship(specPath, out);
352
+ }
353
+
257
354
  /**
258
355
  * Every gate that does not need a spec, on any .pptx.
259
356
  *
@@ -486,6 +583,16 @@ to.
486
583
  process.exit(0);
487
584
  }
488
585
 
586
+ case "show": {
587
+ if (!positional[0]) die("usage: node wand.js show <deck.pptx|deck.json> [slide]");
588
+ process.exit(show(positional[0], positional[1]));
589
+ }
590
+
591
+ case "edit": {
592
+ if (!positional[0]) die("usage: node wand.js edit <deck.pptx> --set N.path=value [--del N] [--move N M] [--add N '<json>'] [-o out.pptx] [--dry]");
593
+ process.exit(edit(positional[0], rest));
594
+ }
595
+
489
596
  case "inspect":
490
597
  if (!positional[0]) die("usage: node wand.js inspect <deck.pptx>");
491
598
  process.exit(inspect(path.resolve(positional[0])) ? 1 : 0);
package/wand_kit.js CHANGED
@@ -32,6 +32,7 @@ const T = {
32
32
  accentDeep: "6C5CE7", // accent text on light surfaces
33
33
  accentSoft: "9481FF", // accent text on dark: eyebrows, links, pills (text 22)
34
34
  accentPale: "DCD6F5", // quote marks, soft labels on dark (text 12)
35
+ accentLight:"AA9BFF", // divider sublines — "Part I · Working question" (Kearney example)
35
36
 
36
37
  bgDark: "240F48", // flat dark ground — the guideline paints no art on interior slides
37
38
  inkDark: "FFFFFF", // titles (text 101)
@@ -119,16 +120,29 @@ function chrome(slide, { tag, dark = true }) {
119
120
  });
120
121
  }
121
122
 
123
+ /**
124
+ * Per-slide context the builder sets before each layout runs. No layout passes
125
+ * `confidential` to footer() — thirty-odd call sites — so a spec's
126
+ * `confidential` never reached a single slide: every deck said "STRICTLY
127
+ * CONFIDENTIAL" whatever it asked for. The builder sets it here instead.
128
+ */
129
+ const CTX = { confidential: "STRICTLY CONFIDENTIAL" };
130
+ function context(c) { Object.assign(CTX, c); }
131
+
122
132
  /** Footer band. `n` and `total` are literal — never a field. */
123
- function footer(slide, { deck, n, total, dark = true, confidential = "STRICTLY CONFIDENTIAL" }) {
133
+ function footer(slide, { deck, n, total, dark = true, confidential = CTX.confidential }) {
124
134
  const c = dark ? T.footerDark : T.footerLight;
125
135
  // guideline: a 0.34" band at y 5.29, 7pt, "WAND · CONFIDENTIAL" left, "03/13" right
126
- slide.addText(`WAND · ${deck.toUpperCase()}`, {
136
+ const left = /^WAND\b/i.test(deck) ? deck.toUpperCase() : `WAND · ${deck.toUpperCase()}`;
137
+ slide.addText(left, {
127
138
  x: 0.42, y: 5.29, w: 4.58, h: 0.34, margin: 0, isTextBox: true,
128
139
  fontFace: F.reg, fontSize: 7, color: c, valign: "middle",
129
140
  });
130
141
  const pad = (v) => String(v).padStart(2, "0");
131
- slide.addText(`${confidential} · ${pad(n)}/${pad(total)}`, {
142
+ // A backup slide sits after the close and is hidden; it has no place in the
143
+ // count the room sees, so it says what it is instead of "32/31".
144
+ const tail = n == null ? "BACKUP" : `${pad(n)}/${pad(total)}`;
145
+ slide.addText(`${confidential} · ${tail}`, {
132
146
  x: 5.23, y: 5.29, w: 4.38, h: 0.34, margin: 0, isTextBox: true,
133
147
  fontFace: F.reg, fontSize: 7, color: c, align: "right", valign: "middle",
134
148
  });
@@ -322,27 +336,34 @@ function cover(pres, { eyebrow, titleLines, body, kickerPlain, kickerBold, tag,
322
336
  }
323
337
 
324
338
  /** L2 divider — dark only, full bleed, tag kept, no footer. */
325
- function divider(pres, { tag, eyebrow, titleText, body }) {
339
+ function divider(pres, { tag, eyebrow, titleText, body, part }) {
326
340
  const s = pres.addSlide();
327
341
  s.background = { color: T.bgDark };
328
- s.addImage({ path: `${ASSETS}/bg-statement-dark.png`, x: 0, y: 0, w: 10, h: 5.625 });
342
+ // Measured from the Kearney example: its own section art, a big line low on
343
+ // the left, and one subline — "Part I · Working question: …". A title that
344
+ // is itself the question (a full sentence) sits higher and smaller.
345
+ s.addImage({ path: `${ASSETS}/divider-art.png`, x: 0, y: 0, w: 10, h: 5.625 });
329
346
  chrome(s, { tag, dark: true });
347
+ const plain = Array.isArray(titleText) ? titleText.join("") : titleText;
348
+ const question = plain.length > 44;
349
+ const box = question ? { y: 1.55, h: 1.3, size: 28 } : { y: 2.12, h: 0.72, size: 33 };
350
+ const size = autoSize(plain, 8.25, box.h, { family: F.reg, fontSize: box.size, min: 22, lineSpacingMultiple: 1.08 });
330
351
  if (eyebrow) s.addText(eyebrow.toUpperCase(), {
331
- x: 0.38, y: 1.77, w: 8.9, h: 0.24, margin: 0, isTextBox: true,
332
- fontFace: F.med, fontSize: 12, color: T.accentSoft, valign: "middle",
352
+ x: 0.42, y: box.y - 0.34, w: 8.25, h: 0.24, margin: 0, isTextBox: true,
353
+ fontFace: F.med, fontSize: 10, color: T.accentSoft, valign: "middle",
333
354
  });
334
355
  const runs = Array.isArray(titleText)
335
- ? titleText.map((t, i) => ({
336
- text: t,
337
- options: i === 1
338
- ? { fontFace: F.semi, fontSize: 44, italic: true, color: T.accentSoft }
339
- : { fontFace: F.semi, fontSize: 44, color: T.white },
340
- }))
341
- : [{ text: titleText, options: { fontFace: F.semi, fontSize: 44, color: T.white } }];
342
- s.addText(runs, { x: 0.38, y: 2.09, w: 8.6, h: 0.77, margin: 0, isTextBox: true, valign: "top" });
343
- if (body) s.addText(body, {
344
- x: 0.38, y: 2.91, w: 5.81, h: 0.72, margin: 0, isTextBox: true,
345
- fontFace: F.reg, fontSize: 12, color: T.bodyDark, valign: "top", lineSpacingMultiple: 1.25,
356
+ ? titleText.map((t, i) => ({ text: t, options: i === 1
357
+ ? { fontFace: F.reg, fontSize: size, italic: true, color: T.accentSoft }
358
+ : { fontFace: F.reg, fontSize: size, color: T.white } }))
359
+ : [{ text: titleText, options: { fontFace: F.reg, fontSize: size, color: T.white } }];
360
+ s.addText(runs, { x: 0.42, y: box.y, w: 8.25, h: box.h, margin: 0, isTextBox: true,
361
+ valign: question ? "top" : "bottom", lineSpacingMultiple: 1.08 });
362
+ const sub = [part, body].filter(Boolean).join(" · ");
363
+ if (sub) s.addText(sub, {
364
+ x: 0.42, y: box.y + box.h + 0.1, w: 5.6, h: 0.55, margin: 0, isTextBox: true,
365
+ fontFace: F.reg, fontSize: autoSize(sub, 5.6, 0.55, { family: F.reg, fontSize: 10, min: 8, lineSpacingMultiple: 1.3 }),
366
+ color: T.accentLight, valign: "top", lineSpacingMultiple: 1.3,
346
367
  });
347
368
  return s;
348
369
  }
@@ -1043,8 +1064,8 @@ function agenda(pres, { tag, titleText, subtitleText, items, dark = true, deck,
1043
1064
  list.forEach((it, i) => {
1044
1065
  const cx = 0.42 + i * (w + gap);
1045
1066
  card(s, { x: cx, y: cy, w, h: ch, dark, variant: "solid" });
1046
- s.addText(String(i + 1).padStart(2, "0"), {
1047
- x: cx + 0.15, y: cy + 0.14, w: 1.0, h: 0.15, margin: 0, isTextBox: true,
1067
+ s.addText(String(i + 1).padStart(2, "0") + (it.meta ? " · " + it.meta : ""), {
1068
+ x: cx + 0.15, y: cy + 0.14, w: w - 0.3, h: 0.15, margin: 0, isTextBox: true,
1048
1069
  fontFace: F.semi, fontSize: 8, color: T.accentSoft, valign: "middle",
1049
1070
  });
1050
1071
  s.addText(it.title, {
@@ -1172,23 +1193,134 @@ function coDelivery(pres, { tag, titleText, subtitleText, items, dark = true, de
1172
1193
  x: cx + 0.55, y: cy + 0.12, w: cw - 0.7, h: 0.19, margin: 0, isTextBox: true,
1173
1194
  fontFace: F.semi, fontSize: 10, color: th.ink, valign: "middle",
1174
1195
  });
1175
- s.addText(it.body, {
1176
- x: cx + 0.55, y: cy + 0.35, w: cw - 0.7, h: ch - 0.42, margin: 0, isTextBox: true,
1177
- fontFace: F.reg, fontSize: autoSize(it.body, cw - 0.7, ch - 0.42, { family: F.reg, fontSize: 8, min: 6.5, lineSpacingMultiple: 1.2 }),
1178
- color: th.body, valign: "top", lineSpacingMultiple: 1.2,
1196
+ // `meta` is the run-of-show time, "15 min" — set apart in accent so the
1197
+ // room can read the agenda as a clock (Kearney's session slide).
1198
+ const bodyRuns = [{ text: it.body, options: { color: th.body } }];
1199
+ if (it.meta) bodyRuns.push({ text: " · " + it.meta, options: { color: T.accentSoft, bold: true } });
1200
+ s.addText(bodyRuns, {
1201
+ x: cx + 0.55, y: cy + 0.35, w: cw - 0.7, h: ch - 0.42, margin: 0, isTextBox: true, fontFace: F.reg,
1202
+ fontSize: autoSize(it.body + (it.meta ? " · " + it.meta : ""), cw - 0.7, ch - 0.42, { family: F.reg, fontSize: 8, min: 6.5, lineSpacingMultiple: 1.2 }),
1203
+ valign: "top", lineSpacingMultiple: 1.2,
1179
1204
  });
1180
1205
  });
1181
1206
  if (deck) footer(s, { deck, n, total, dark });
1182
1207
  return s;
1183
1208
  }
1184
1209
 
1210
+
1211
+ /**
1212
+ * Two lanes of steps — "today" against "with Wand" (Kearney S08, Israir "The
1213
+ * Ceiling"). The point is where the person sits: every step, or only the one
1214
+ * that needs judgment. lanes: [{ label, caption, steps: [{ who, step, human }] }]
1215
+ */
1216
+ function flowCompare(pres, { tag, titleText, subtitleText, lanes, dark = true, deck, n, total }) {
1217
+ const s = pres.addSlide();
1218
+ background(s, dark); chrome(s, { tag, dark });
1219
+ const th = theme(dark);
1220
+ title(s, titleText, { dark, y: 0.78, h: 0.5 });
1221
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.26, h: 0.3 });
1222
+ const laneY = [1.74, 3.40];
1223
+ lanes.slice(0, 2).forEach((lane, li) => {
1224
+ const y = laneY[li];
1225
+ s.addText(lane.label.toUpperCase(), {
1226
+ x: 0.42, y, w: 3.2, h: 0.18, margin: 0, isTextBox: true,
1227
+ fontFace: F.med, fontSize: 7.5, color: T.accentSoft, valign: "middle",
1228
+ });
1229
+ s.addShape("rect", { x: 0.42 + Math.min(3.2, lane.label.length * 0.062) + 0.12, y: y + 0.085, w: 9.17 - Math.min(3.2, lane.label.length * 0.062) - 0.12, h: 0.01,
1230
+ fill: { color: dark ? T.cardStroke : T.divider }, line: { width: 0 } });
1231
+ const steps = lane.steps.slice(0, 5), gap = 0.30;
1232
+ const w = (9.17 - gap * (steps.length - 1)) / steps.length, cy = y + 0.30, ch = 0.73;
1233
+ steps.forEach((st, i) => {
1234
+ const cx = 0.42 + i * (w + gap);
1235
+ // Kearney's reading: the machine steps are the filled ones, so the eye sees
1236
+ // how much of the line the agents carry and where the person still sits.
1237
+ if (st.human) card(s, { x: cx, y: cy, w, h: ch, dark, variant: "solid" });
1238
+ else s.addShape("roundRect", { x: cx, y: cy, w, h: ch, rectRadius: radius(w, ch),
1239
+ fill: { color: dark ? T.cardStroke : T.accent }, line: { color: T.accent, width: 1 } });
1240
+ if (st.who) s.addText(st.who.toUpperCase(), {
1241
+ x: cx + 0.14, y: cy + 0.1, w: w - 0.28, h: 0.16, margin: 0, isTextBox: true,
1242
+ fontFace: F.med, fontSize: 6.5, color: st.human ? T.accentSoft : T.accentPale, valign: "middle",
1243
+ });
1244
+ s.addText(st.step, {
1245
+ x: cx + 0.14, y: cy + 0.32, w: w - 0.28, h: 0.32, margin: 0, isTextBox: true,
1246
+ fontFace: F.semi, fontSize: autoSize(st.step, w - 0.28, 0.32, { family: F.semi, fontSize: 11, min: 8 }),
1247
+ color: st.human ? th.ink : T.white, valign: "middle",
1248
+ });
1249
+ if (i < steps.length - 1) s.addShape("triangle", {
1250
+ x: cx + w + gap / 2 - 0.05, y: cy + ch / 2 - 0.07, w: 0.11, h: 0.14, rotate: 90,
1251
+ fill: { color: T.accent }, line: { width: 0 },
1252
+ });
1253
+ });
1254
+ if (lane.caption) s.addText(lane.caption, {
1255
+ x: 0.42, y: cy + ch + 0.1, w: 9.17, h: 0.24, margin: 0, isTextBox: true,
1256
+ fontFace: F.reg, fontSize: autoSize(lane.caption, 9.17, 0.24, { family: F.reg, fontSize: 8.5, min: 7 }),
1257
+ italic: true, color: th.body, valign: "middle",
1258
+ });
1259
+ });
1260
+ if (deck) footer(s, { deck, n, total, dark });
1261
+ return s;
1262
+ }
1263
+
1264
+ /**
1265
+ * A journey in phases on a line — "The transformation timeline" (Kearney S16).
1266
+ * phases: [{ when, label, bullets: [..] }], 2–4 of them.
1267
+ */
1268
+ function timeline(pres, { tag, titleText, subtitleText, phases, dark = true, deck, n, total }) {
1269
+ const s = pres.addSlide();
1270
+ background(s, dark); chrome(s, { tag, dark });
1271
+ const th = theme(dark);
1272
+ title(s, titleText, { dark, y: 0.78, h: 0.5 });
1273
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.30, h: 0.3 });
1274
+ const list = phases.slice(0, 4), gap = list.length === 4 ? 0.34 : 0.62;
1275
+ const w = (9.14 - gap * (list.length - 1)) / list.length, x0 = 0.44;
1276
+ const ly = 2.30, d = 0.30;
1277
+ s.addShape("rect", { x: x0 + d / 2, y: ly + d / 2 - 0.01, w: 9.14 - d, h: 0.02,
1278
+ fill: { color: T.accent }, line: { width: 0 } });
1279
+ list.forEach((ph, i) => {
1280
+ const cx = x0 + i * (w + gap);
1281
+ s.addShape("ellipse", { x: cx, y: ly, w: d, h: d,
1282
+ fill: { color: dark ? T.bgDark : T.bgLight }, line: { color: T.accent, width: 2 } });
1283
+ s.addShape("ellipse", { x: cx + 0.09, y: ly + 0.09, w: d - 0.18, h: d - 0.18,
1284
+ fill: { color: T.accent }, line: { width: 0 } });
1285
+ const cy = 2.86;
1286
+ if (ph.when) s.addText(ph.when, {
1287
+ x: cx, y: cy, w, h: 0.2, margin: 0, isTextBox: true,
1288
+ fontFace: F.reg, fontSize: 8.5, color: dark ? T.mutedDark : T.mutedLight, valign: "middle",
1289
+ });
1290
+ s.addText(ph.label.toUpperCase(), {
1291
+ x: cx, y: cy + 0.22, w, h: 0.22, margin: 0, isTextBox: true,
1292
+ fontFace: F.semi, fontSize: 10, color: dark ? T.accentPale : T.accentDeep, valign: "middle",
1293
+ });
1294
+ const bullets = (ph.bullets || []).map((b) => "• " + b).join("\n");
1295
+ if (bullets) s.addText(bullets, {
1296
+ x: cx, y: cy + 0.52, w, h: 1.62, margin: 0, isTextBox: true,
1297
+ fontFace: F.reg, fontSize: autoSize(bullets, w, 1.62, { family: F.reg, fontSize: 8.5, min: 6.5, lineSpacingMultiple: 1.3 }),
1298
+ color: th.body, valign: "top", lineSpacingMultiple: 1.3, paraSpaceAfter: 3,
1299
+ });
1300
+ });
1301
+ if (deck) footer(s, { deck, n, total, dark });
1302
+ return s;
1303
+ }
1304
+
1305
+ /**
1306
+ * Speaker notes, in the run-of-show convention the Kearney example uses:
1307
+ * the time window first, "CORE" when the slide carries the argument, then what
1308
+ * to say, then who in the room will push on it. A plain string passes through.
1309
+ */
1310
+ function notesText(nt) {
1311
+ if (!nt) return "";
1312
+ if (typeof nt === "string") return nt;
1313
+ return [nt.time, nt.core ? "CORE" : null, nt.text, nt.watch ? "Watch for: " + nt.watch : null]
1314
+ .filter(Boolean).join("\n");
1315
+ }
1316
+
1185
1317
  module.exports = {
1186
- VERSION: "0.9.0",
1318
+ VERSION: "0.10.0",
1187
1319
  T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
1188
1320
  background, chrome, footer, title, subtitle, kicker, contentTop,
1189
1321
  card, iconTile, pill,
1190
1322
  cover, divider, closing,
1191
- agenda, team, fourCards, deliverColumns, coDelivery,
1323
+ agenda, team, fourCards, deliverColumns, coDelivery, flowCompare, timeline, notesText, context,
1192
1324
  threeUp, fourUpIcon, sixUpIcon, rows, parity, statQuad, statGrid, band,
1193
1325
  colCards, featureStats, barRank, bridge, heroQuad, productMap,
1194
1326