wand-decks-kit 0.10.0 → 0.11.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 +13 -0
- package/SKILL.md +5 -1
- package/docs/LAYOUTS.md +1 -0
- package/examples/session.json +394 -66
- package/lib/layouts_consult.js +1 -1
- package/lib/pdf.js +17 -0
- package/lib/render_pdf.js +63 -0
- package/lib/schema.js +23 -0
- package/lib/trace.js +6 -1
- package/package.json +1 -1
- package/preview/preview.js +49 -1
- package/qa_coverage.py +7 -0
- package/wand.js +7 -0
- package/wand_kit.js +86 -17
package/PROMPT.md
CHANGED
|
@@ -35,6 +35,19 @@ Where the reference is weaker than the kit, the kit wins: it embeds fonts
|
|
|
35
35
|
subsetted (`saveSubsetFonts="1"`, which garbles digits in exported PDFs), and
|
|
36
36
|
1,834 text runs are Calibri. `audit` flags both on it.
|
|
37
37
|
|
|
38
|
+
**Charts are native (0.11).** Kearney drew its two charts as pictures; the
|
|
39
|
+
kit's `chart` writes a real PowerPoint chart, so the data stays editable, and
|
|
40
|
+
the trace records what it shows so the PDF and preview draw the same thing. The
|
|
41
|
+
gridline step is fixed at a quarter of a round maximum — PowerPoint otherwise
|
|
42
|
+
picks its own and the two renderers disagree.
|
|
43
|
+
|
|
44
|
+
**No outline is `line: { type: "none" }`.** Rendering the real `.pptx` for the
|
|
45
|
+
first time showed every accent bar and column carrying a 1pt `#333333` border:
|
|
46
|
+
pptxgenjs reads `line.width || 1`, so `width: 0` became its default outline. The
|
|
47
|
+
kit's own PDF read zero as zero, so no gate ever saw it — it is in the RAK and
|
|
48
|
+
SOMPO decks and in the site's template. `inspect` now fails a file carrying the
|
|
49
|
+
default outline.
|
|
50
|
+
|
|
38
51
|
**Edits are commands, not rewrites.** `wand.js show` and `wand.js edit` exist
|
|
39
52
|
because a salesperson's changes are small and the old edit path cost two full
|
|
40
53
|
copies of the spec in the chat. A spec's `confidential` now reaches the footer
|
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.
|
|
7
|
+
version: "0.11.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
|
|
@@ -183,6 +183,10 @@ this kind of deck, `examples/session.json`):
|
|
|
183
183
|
session slide's notes carry the checkpoints and what to cut if late.
|
|
184
184
|
- **Backups after the closing**, `backup: true` — hidden, uncounted ("31 of 31",
|
|
185
185
|
not "of 34"), footer says BACKUP.
|
|
186
|
+
- **Charts are native** — `chart` with `kind` line / area / bar, `labels`,
|
|
187
|
+
`series: [{ name, values, emph }]`, and up to two argument blocks in `side`.
|
|
188
|
+
The seller can change a number in PowerPoint (right-click, Edit Data). Figures
|
|
189
|
+
come from the brief; if they are illustrative, say so in the notes.
|
|
186
190
|
|
|
187
191
|
Timings, attendee names and "watch for" lines come from the user or the brief.
|
|
188
192
|
Never invent them — leave `notes` out rather than guess.
|
package/docs/LAYOUTS.md
CHANGED
|
@@ -31,6 +31,7 @@ full-bleed three) `deck` / `n` / `total` for the footer.
|
|
|
31
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
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
33
|
| `timeline` | `phases[{when,label,bullets}]` | A journey in 2–4 phases on a line (Kearney S16) |
|
|
34
|
+
| `chart` | `kind, labels, series[{name,values,emph}], yMax, yLabel, side[{title,body}], sidePlace` | A native, editable line / area / bar chart with its argument beside or below (Kearney S09, S25) |
|
|
34
35
|
| — every layout — | `notes`, `backup` | Speaker notes (`{time, core, text, watch}` or a string); `backup: true` hides the slide after the close |
|
|
35
36
|
| `threeUp` | `items[{eyebrow,title,pill,body,emph}], arrows` | 3 comparable things. `arrows: true` for a progression |
|
|
36
37
|
| `fourUpIcon` | `items[{icon,title,body}], kickerText` | 4 attributes or capabilities |
|
package/examples/session.json
CHANGED
|
@@ -2,91 +2,419 @@
|
|
|
2
2
|
"deck": "WAND × CLIENT · LEADERSHIP SESSION",
|
|
3
3
|
"confidential": "WAND · CONFIDENTIAL",
|
|
4
4
|
"slides": [
|
|
5
|
-
{
|
|
6
|
-
"
|
|
5
|
+
{
|
|
6
|
+
"layout": "cover",
|
|
7
|
+
"tag": "Presented to Client · 29 September 2026",
|
|
8
|
+
"titleLines": [
|
|
9
|
+
"Expand human possibility",
|
|
10
|
+
"beyond human capacity."
|
|
11
|
+
],
|
|
7
12
|
"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": [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
"presentedTo": [
|
|
14
|
+
"First Last, Managing Partner",
|
|
15
|
+
"First Last, Head of Transformation"
|
|
16
|
+
],
|
|
17
|
+
"notes": {
|
|
18
|
+
"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."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"layout": "coDelivery",
|
|
23
|
+
"tag": "The session",
|
|
24
|
+
"titleText": "A working conversation, not a presentation.",
|
|
12
25
|
"items": [
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
26
|
+
{
|
|
27
|
+
"title": "When software becomes labor",
|
|
28
|
+
"body": "The threshold between a tool and a worker.",
|
|
29
|
+
"meta": "15 min"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"title": "The organization has to change",
|
|
33
|
+
"body": "From the individual agent to the Hybrid Organization.",
|
|
34
|
+
"meta": "10 min"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"title": "Why transformations succeed, or die",
|
|
38
|
+
"body": "Resistance is operating-model design.",
|
|
39
|
+
"meta": "10 min"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"title": "From architecture to production",
|
|
43
|
+
"body": "Inside an agent, then one real process, live.",
|
|
44
|
+
"meta": "35 min"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"title": "When capacity decouples from labor",
|
|
48
|
+
"body": "From the firm to the economy.",
|
|
49
|
+
"meta": "5 min"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"title": "Wand × Client",
|
|
53
|
+
"body": "Designing the Hybrid Organization together.",
|
|
54
|
+
"meta": "10 min"
|
|
55
|
+
}
|
|
19
56
|
],
|
|
20
|
-
"notes": {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
57
|
+
"notes": {
|
|
58
|
+
"time": "00:00, under one minute",
|
|
59
|
+
"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."
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"layout": "divider",
|
|
64
|
+
"titleText": "When software becomes labor",
|
|
65
|
+
"part": "Part I",
|
|
66
|
+
"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?",
|
|
67
|
+
"notes": {
|
|
68
|
+
"time": "00:01 to 00:03",
|
|
69
|
+
"text": "Read the working question aloud and leave it on screen. Do not answer it yet."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"layout": "flowCompare",
|
|
74
|
+
"tag": "Why three years of AI spend has not moved the operating line",
|
|
27
75
|
"titleText": "Every AI tool you own ends at a person.",
|
|
28
76
|
"lanes": [
|
|
29
|
-
{
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
77
|
+
{
|
|
78
|
+
"label": "Today — AI as a tool",
|
|
79
|
+
"caption": "Capacity = your people. Four of five steps wait on a person; that person is the ceiling.",
|
|
80
|
+
"steps": [
|
|
81
|
+
{
|
|
82
|
+
"who": "Person",
|
|
83
|
+
"step": "Notices",
|
|
84
|
+
"human": true
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"who": "Person",
|
|
88
|
+
"step": "Gathers context",
|
|
89
|
+
"human": true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"who": "Person",
|
|
93
|
+
"step": "Instructs",
|
|
94
|
+
"human": true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"who": "AI",
|
|
98
|
+
"step": "Drafts"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"who": "Person",
|
|
102
|
+
"step": "Checks",
|
|
103
|
+
"human": true
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"label": "With Wand — AI as labor",
|
|
109
|
+
"caption": "Capacity = people + agents. The person moves from every step to the one step that needs judgment.",
|
|
110
|
+
"steps": [
|
|
111
|
+
{
|
|
112
|
+
"who": "Agent",
|
|
113
|
+
"step": "Notices"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"who": "Agent",
|
|
117
|
+
"step": "Plans"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"who": "Agent",
|
|
121
|
+
"step": "Executes"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"who": "Agent",
|
|
125
|
+
"step": "Grades"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"who": "Manager",
|
|
129
|
+
"step": "Approves",
|
|
130
|
+
"human": true
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"notes": {
|
|
136
|
+
"time": "00:11 to 00:14",
|
|
137
|
+
"core": true,
|
|
138
|
+
"text": "This is the difference between an efficiency programme and a capacity programme. Point at the top row: every step waits on a person.",
|
|
139
|
+
"watch": "whoever owns the current copilot rollout — expect them to defend adoption numbers."
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"layout": "chart",
|
|
144
|
+
"tag": "The experience gap",
|
|
145
|
+
"titleText": "A year in, an agent isn't older, it's categorically better.",
|
|
146
|
+
"kind": "area",
|
|
147
|
+
"labels": [
|
|
148
|
+
"M1",
|
|
149
|
+
"M2",
|
|
150
|
+
"M3",
|
|
151
|
+
"M4",
|
|
152
|
+
"M5",
|
|
153
|
+
"M6",
|
|
154
|
+
"M7",
|
|
155
|
+
"M8"
|
|
156
|
+
],
|
|
157
|
+
"series": [
|
|
158
|
+
{
|
|
159
|
+
"name": "Started twelve months earlier",
|
|
160
|
+
"values": [
|
|
161
|
+
18,
|
|
162
|
+
26,
|
|
163
|
+
35,
|
|
164
|
+
44,
|
|
165
|
+
55,
|
|
166
|
+
66,
|
|
167
|
+
78,
|
|
168
|
+
90
|
|
169
|
+
],
|
|
170
|
+
"emph": true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "Started later, same technology",
|
|
174
|
+
"values": [
|
|
175
|
+
2,
|
|
176
|
+
5,
|
|
177
|
+
9,
|
|
178
|
+
14,
|
|
179
|
+
20,
|
|
180
|
+
27,
|
|
181
|
+
35,
|
|
182
|
+
44
|
|
183
|
+
]
|
|
184
|
+
}
|
|
36
185
|
],
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
186
|
+
"sidePlace": "below",
|
|
187
|
+
"side": [
|
|
188
|
+
{
|
|
189
|
+
"title": "The asset",
|
|
190
|
+
"body": "Agents accumulate scored experience: what works in your business, which approach fails and why. It cannot be bought or copied, only run."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"title": "The arithmetic",
|
|
194
|
+
"body": "Starting in January rather than July is not six months — it is hundreds of improvement cycles, banked against starting from zero."
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"notes": {
|
|
198
|
+
"time": "00:14 to 00:20",
|
|
199
|
+
"core": true,
|
|
200
|
+
"text": "Illustrative curve. The gap widens because the earlier agent keeps banking lessons the later one has not yet met."
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"layout": "fourCards",
|
|
205
|
+
"tag": "Agent behavior",
|
|
206
|
+
"titleText": "A worker, not just a good tool.",
|
|
40
207
|
"subtitleText": "A tool responds to prompts. A worker pursues goals, decides what matters, acts within constraints, and is judged by outcomes.",
|
|
41
208
|
"items": [
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
209
|
+
{
|
|
210
|
+
"title": "Pursues goals",
|
|
211
|
+
"body": "Carries objectives and KPIs like any employee, and works toward them without being asked."
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"title": "Decides what matters",
|
|
215
|
+
"body": "Notices, prioritizes and plans inside the boundaries policy sets."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"title": "Is graded",
|
|
219
|
+
"body": "Every output reviewed against a baseline agreed before it started."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"title": "Gets better",
|
|
223
|
+
"body": "Banks what it learns, so a year in it is categorically better, not older."
|
|
224
|
+
}
|
|
46
225
|
],
|
|
47
|
-
"notes": {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
226
|
+
"notes": {
|
|
227
|
+
"time": "00:43 to 00:47",
|
|
228
|
+
"core": true,
|
|
229
|
+
"text": "Land the word 'worker'. Everything after this slide assumes the room accepts it."
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"layout": "divider",
|
|
234
|
+
"titleText": "Why transformations die",
|
|
235
|
+
"part": "Part III",
|
|
236
|
+
"body": "Working question: what would your methodology look like if org redesign, workforce redesign and agent deployment were one program, not three?",
|
|
237
|
+
"notes": {
|
|
238
|
+
"time": "00:30 to 00:31"
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"layout": "timeline",
|
|
243
|
+
"tag": "Transformation journey",
|
|
244
|
+
"titleText": "The transformation timeline.",
|
|
54
245
|
"subtitleText": "From the starting line, month over month, compounding.",
|
|
55
246
|
"phases": [
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
247
|
+
{
|
|
248
|
+
"when": "3–6 months",
|
|
249
|
+
"label": "Foundation",
|
|
250
|
+
"bullets": [
|
|
251
|
+
"A core transformation team of 7–10 people",
|
|
252
|
+
"One registry covering people and agents",
|
|
253
|
+
"5–10 core processes rebuilt for the dual workforce",
|
|
254
|
+
"Dozens of agents in production with named managers"
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"when": "6–12 months",
|
|
259
|
+
"label": "Scale",
|
|
260
|
+
"bullets": [
|
|
261
|
+
"Agents across dozens of core functions",
|
|
262
|
+
"Management structure rebalanced; spans widened",
|
|
263
|
+
"Hundreds of agents in production"
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"when": "12–24 months",
|
|
268
|
+
"label": "Accelerate",
|
|
269
|
+
"bullets": [
|
|
270
|
+
"Most processes designed for a dual workforce from the start",
|
|
271
|
+
"Whole functions run by agent teams",
|
|
272
|
+
"Cost per unit of work structurally below the market"
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
"notes": {
|
|
277
|
+
"time": "00:37 to 00:40",
|
|
278
|
+
"core": true,
|
|
279
|
+
"text": "Timelines are directional. The commitment is the Foundation phase; everything after it is earned by the telemetry."
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"layout": "chart",
|
|
284
|
+
"tag": "The economics",
|
|
285
|
+
"titleText": "Labor stops being a cost that scales with output.",
|
|
286
|
+
"subtitleText": "Human capacity scales by adding people. Agent capacity scales by provisioning compute.",
|
|
287
|
+
"kind": "line",
|
|
288
|
+
"labels": [
|
|
289
|
+
"Now",
|
|
290
|
+
"Y1",
|
|
291
|
+
"Y2",
|
|
292
|
+
"Y3"
|
|
293
|
+
],
|
|
294
|
+
"yMax": 100,
|
|
295
|
+
"yLabel": "Cost per unit of work (indexed)",
|
|
296
|
+
"series": [
|
|
297
|
+
{
|
|
298
|
+
"name": "Human-capped operation",
|
|
299
|
+
"values": [
|
|
300
|
+
100,
|
|
301
|
+
97,
|
|
302
|
+
95,
|
|
303
|
+
94
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"name": "Agent-capacity operation",
|
|
308
|
+
"values": [
|
|
309
|
+
100,
|
|
310
|
+
62,
|
|
311
|
+
38,
|
|
312
|
+
24
|
|
313
|
+
],
|
|
314
|
+
"emph": true
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
"side": [
|
|
318
|
+
{
|
|
319
|
+
"title": "A new factor in production",
|
|
320
|
+
"body": "The facilities that bolted machines onto the old operation got an efficiency gain and disappeared. The ones that won rebuilt the building and invented a new metric: output per machine-hour."
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"title": "And it does not stay internal",
|
|
324
|
+
"body": "Lower cost becomes lower price and better service. That takes share, share brings data, data makes the agents better."
|
|
325
|
+
}
|
|
59
326
|
],
|
|
60
|
-
"notes": {
|
|
61
|
-
|
|
62
|
-
|
|
327
|
+
"notes": {
|
|
328
|
+
"time": "01:18 to 01:20",
|
|
329
|
+
"core": true,
|
|
330
|
+
"text": "Figures are illustrative — replace them with the client's baseline before the session. The shape is the argument: one line is capped by headcount, the other by compute."
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"layout": "deliverColumns",
|
|
335
|
+
"tag": "The build",
|
|
336
|
+
"titleText": "The client designs the transformation. Wand operates it.",
|
|
63
337
|
"subtitleText": "Who does each part, and how it is governed.",
|
|
64
338
|
"columns": [
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
339
|
+
{
|
|
340
|
+
"heading": "1 · Change management",
|
|
341
|
+
"lead": "Led by your people, supported by ours",
|
|
342
|
+
"body": "A small core team who pick where to start and carry the early wins back."
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"heading": "2 · Deployment",
|
|
346
|
+
"lead": "Led by our solution architect with your technical team",
|
|
347
|
+
"body": "Cloud, deployment model, security review, and reaching the systems you run."
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"heading": "3 · Building the workforce",
|
|
351
|
+
"lead": "Led by our forward-deployed engineers",
|
|
352
|
+
"body": "Build the agents and processes with your team, then shift to supporting as your people take it on."
|
|
353
|
+
}
|
|
68
354
|
],
|
|
69
|
-
"notes": {
|
|
70
|
-
|
|
71
|
-
|
|
355
|
+
"notes": {
|
|
356
|
+
"time": "01:23 to 01:25",
|
|
357
|
+
"text": "Their methodology, our operating system. Say it in that order."
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"layout": "fourCards",
|
|
362
|
+
"tag": "Next steps",
|
|
363
|
+
"titleText": "What happens next.",
|
|
72
364
|
"subtitleText": "This room builds the momentum. The partnership work happens in a smaller session, with named owners and dates.",
|
|
73
365
|
"items": [
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
366
|
+
{
|
|
367
|
+
"title": "Partnership session",
|
|
368
|
+
"body": "Named leads on both sides; one process chosen and its baseline written down."
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"title": "Architecture deep-dive",
|
|
372
|
+
"body": "Under NDA, for your technology team: deployment posture, identity, audit."
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"title": "Sandbox access",
|
|
376
|
+
"body": "A governed environment your team can build in, with our engineers alongside."
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"title": "First client reunion",
|
|
380
|
+
"body": "Where the first agents in production are shown against the baseline."
|
|
381
|
+
}
|
|
78
382
|
],
|
|
79
|
-
"notes": {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
383
|
+
"notes": {
|
|
384
|
+
"time": "01:29 to 01:30",
|
|
385
|
+
"text": "Leave the room with one date agreed. Anything else is a follow-up email."
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"layout": "closing",
|
|
390
|
+
"line1": "Thank you.",
|
|
391
|
+
"body": "The operating system for the human + agent workforce.",
|
|
392
|
+
"url": "September 2026 | Strictly Confidential"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"layout": "fourCards",
|
|
396
|
+
"backup": true,
|
|
397
|
+
"tag": "Why this is a category",
|
|
398
|
+
"titleText": "Five advantages, not five features.",
|
|
84
399
|
"items": [
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
400
|
+
{
|
|
401
|
+
"title": "Governance & orchestration",
|
|
402
|
+
"body": "One policy, one identity, one audit trail across the whole estate."
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"title": "Institutional IP",
|
|
406
|
+
"body": "Every lesson compounds inside your perimeter, not ours."
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"title": "Model agnostic",
|
|
410
|
+
"body": "Any model, any cloud — no lock-in, sovereignty-friendly by design."
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"title": "One registry",
|
|
414
|
+
"body": "People and agents managed in the same system, by the same managers."
|
|
415
|
+
}
|
|
89
416
|
],
|
|
90
|
-
"notes": "BACKUP. Use only if the room asks why this is not another agent platform."
|
|
417
|
+
"notes": "BACKUP. Use only if the room asks why this is not another agent platform."
|
|
418
|
+
}
|
|
91
419
|
]
|
|
92
420
|
}
|
package/lib/layouts_consult.js
CHANGED
|
@@ -215,7 +215,7 @@ module.exports = function attach(W) {
|
|
|
215
215
|
});
|
|
216
216
|
s.addShape("roundRect", {
|
|
217
217
|
x: L + LAB, y: ry, w: COL, h: trh, rectRadius: radius(COL, trh),
|
|
218
|
-
fill: { color: dark ? T.cardMuted : T.surface }, line: {
|
|
218
|
+
fill: { color: dark ? T.cardMuted : T.surface }, line: { type: "none" },
|
|
219
219
|
});
|
|
220
220
|
s.addText(row.left, {
|
|
221
221
|
x: L + LAB + 0.18, y: ry, w: COL - 0.36, h: trh, margin: 0, isTextBox: true,
|
package/lib/pdf.js
CHANGED
|
@@ -126,6 +126,23 @@ class Doc {
|
|
|
126
126
|
return this;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** A filled polygon, optionally translucent — an area chart's fill. */
|
|
130
|
+
poly({ points, fill, opacity }) {
|
|
131
|
+
const o = this.page.ops;
|
|
132
|
+
const soft = opacity != null && opacity < 1;
|
|
133
|
+
if (soft) o.push("q", `/${this._alpha(opacity)} gs`);
|
|
134
|
+
o.push(`${rgb(fill || "000000")} rg`,
|
|
135
|
+
points.map(([px, py], i) => `${num(px * PT)} ${num(this._y(py))} ${i ? "l" : "m"}`).join(" ") + " h f");
|
|
136
|
+
if (soft) o.push("Q");
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Width of a string in inches, for centring a label without drawing it. */
|
|
141
|
+
measure(str, { stem, size }) {
|
|
142
|
+
const F = this._font(stem);
|
|
143
|
+
return (F.font.layout(String(str)).advanceWidth / F.font.unitsPerEm) * size / 72;
|
|
144
|
+
}
|
|
145
|
+
|
|
129
146
|
line({ x1, y1, x2, y2, color = "000000", width = 0.75 }) {
|
|
130
147
|
this.page.ops.push(`${rgb(color)} RG`, `${num(width)} w`,
|
|
131
148
|
`${num(x1 * PT)} ${num(this._y(y1))} m ${num(x2 * PT)} ${num(this._y(y2))} l S`);
|
package/lib/render_pdf.js
CHANGED
|
@@ -140,6 +140,7 @@ function renderTrace(trace, out, opts = {}) {
|
|
|
140
140
|
if (sh.kind === "shape") drawShape(d, sh);
|
|
141
141
|
else if (sh.kind === "image") drawImage(d, sh, missing);
|
|
142
142
|
else if (sh.kind === "text") drawText(d, sh);
|
|
143
|
+
else if (sh.kind === "chart") drawChart(d, sh);
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
fs.writeFileSync(out, d.toBuffer());
|
|
@@ -162,6 +163,68 @@ function drawShape(d, sh) {
|
|
|
162
163
|
});
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
/** Round up to a gridline-friendly maximum: 87 -> 100, 3.2 -> 4, 460 -> 500. */
|
|
167
|
+
function niceMax(v) {
|
|
168
|
+
if (!(v > 0)) return 1;
|
|
169
|
+
const p = Math.pow(10, Math.floor(Math.log10(v))), m = v / p;
|
|
170
|
+
return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10) * p;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The chart as PowerPoint draws it: legend on top, four gridlines, category
|
|
175
|
+
* labels under the plot. Not pixel-identical to PowerPoint's own layout — close
|
|
176
|
+
* enough that a slide which reads here reads there.
|
|
177
|
+
*/
|
|
178
|
+
function drawChart(d, sh) {
|
|
179
|
+
const c = sh.chart || {}, labels = c.labels || [], series = c.series || [];
|
|
180
|
+
const stem = "Geist-Regular", sz = 7;
|
|
181
|
+
const legendH = c.legend ? 0.26 : 0, padL = 0.40, padB = 0.26;
|
|
182
|
+
const px = sh.x + padL, py = sh.y + legendH + 0.06, pw = sh.w - padL - 0.08, ph = sh.h - legendH - padB - 0.06;
|
|
183
|
+
const max = c.yMax || niceMax(Math.max(0, ...series.flatMap((x) => x.values || [])));
|
|
184
|
+
const fmt = (v) => (Math.round(v * 100) / 100).toString();
|
|
185
|
+
for (let k = 0; k <= 4; k++) {
|
|
186
|
+
const gy = py + ph - (ph * k) / 4;
|
|
187
|
+
d.line({ x1: px, y1: gy, x2: px + pw, y2: gy, color: c.grid || "5B4BB0", width: 0.5 });
|
|
188
|
+
const lab = fmt((max * k) / 4);
|
|
189
|
+
d.text(lab, { x: px - 0.08 - d.measure(lab, { stem, size: sz }), y: gy + 0.03, stem, size: sz, color: c.ink || "BBBBBB" });
|
|
190
|
+
}
|
|
191
|
+
const n = labels.length, bar = c.kind === "bar";
|
|
192
|
+
const xAt = (i) => bar ? px + (pw / n) * (i + 0.5) : px + (n > 1 ? (pw * i) / (n - 1) : pw / 2);
|
|
193
|
+
const yAt = (v) => py + ph - (ph * Math.min(v, max)) / max;
|
|
194
|
+
labels.forEach((l, i) => {
|
|
195
|
+
const w = d.measure(l, { stem, size: sz });
|
|
196
|
+
const lx = Math.min(Math.max(xAt(i) - w / 2, px), px + pw - w);
|
|
197
|
+
d.text(l, { x: lx, y: py + ph + 0.17, stem, size: sz, color: c.ink || "BBBBBB" });
|
|
198
|
+
});
|
|
199
|
+
series.forEach((ser, si) => {
|
|
200
|
+
const col = (c.colors && c.colors[si]) || "8B7CF6", vals = ser.values || [];
|
|
201
|
+
if (bar) {
|
|
202
|
+
const slot = pw / n, group = slot * 0.62, bw = group / series.length;
|
|
203
|
+
vals.forEach((v, i) => {
|
|
204
|
+
const bx = px + slot * i + (slot - group) / 2 + bw * si;
|
|
205
|
+
d.rect({ x: bx, y: yAt(v), w: bw * 0.92, h: py + ph - yAt(v), fill: col });
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (c.kind === "area") {
|
|
210
|
+
d.poly({ points: [[xAt(0), py + ph], ...vals.map((v, i) => [xAt(i), yAt(v)]), [xAt(vals.length - 1), py + ph]],
|
|
211
|
+
fill: col, opacity: 0.45 });
|
|
212
|
+
}
|
|
213
|
+
if (c.kind === "area") return; // PowerPoint's area chart has no outline
|
|
214
|
+
for (let i = 1; i < vals.length; i++) {
|
|
215
|
+
d.line({ x1: xAt(i - 1), y1: yAt(vals[i - 1]), x2: xAt(i), y2: yAt(vals[i]), color: col, width: ser.emph ? 2.5 : 2 });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (c.legend) {
|
|
219
|
+
let lx = px;
|
|
220
|
+
series.forEach((ser, si) => {
|
|
221
|
+
d.rect({ x: lx, y: sh.y + 0.08, w: 0.1, h: 0.1, fill: (c.colors && c.colors[si]) || "8B7CF6" });
|
|
222
|
+
d.text(ser.name, { x: lx + 0.15, y: sh.y + 0.17, stem, size: 8, color: c.ink || "BBBBBB" });
|
|
223
|
+
lx += 0.15 + d.measure(ser.name, { stem, size: 8 }) + 0.3;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
165
228
|
function drawImage(d, sh, missing) {
|
|
166
229
|
const box = { x: sh.x || 0, y: sh.y || 0, w: sh.w || 0, h: sh.h || 0 };
|
|
167
230
|
if (sh.data) { // a recoloured glyph, inline
|
package/lib/schema.js
CHANGED
|
@@ -82,6 +82,19 @@ const L = {
|
|
|
82
82
|
steps: s("object[]", req({ min: 3, max: 5, of: { who: str(), step: str(req()), human: s("boolean") } })),
|
|
83
83
|
} })) },
|
|
84
84
|
},
|
|
85
|
+
chart: {
|
|
86
|
+
use: "A native chart the seller can edit in PowerPoint — line, area or bar — with up to two argument blocks beside or below it",
|
|
87
|
+
fields: {
|
|
88
|
+
kind: str({ note: '"line" (default), "area" or "bar"' }),
|
|
89
|
+
labels: s("string[]", req({ min: 2, max: 12, note: "the categories along the bottom" })),
|
|
90
|
+
series: s("object[]", req({ min: 1, max: 3, of: {
|
|
91
|
+
name: str(req()), values: s("number[]", req()), emph: s("boolean"),
|
|
92
|
+
} })),
|
|
93
|
+
yMax: s("number"), yLabel: str(),
|
|
94
|
+
side: s("object[]", { max: 2, of: { title: str(req()), body: str(req()) } }),
|
|
95
|
+
sidePlace: str({ note: '"right" (default) or "below"' }),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
85
98
|
timeline: {
|
|
86
99
|
use: "A journey in 2–4 phases on a line — months, a label, bullets",
|
|
87
100
|
fields: { phases: s("object[]", req({ min: 2, max: 4, of: {
|
|
@@ -336,6 +349,7 @@ const TYPEOF = {
|
|
|
336
349
|
boolean: (v) => typeof v === "boolean",
|
|
337
350
|
title: (v) => typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")),
|
|
338
351
|
"string[]": (v) => Array.isArray(v) && v.every((x) => typeof x === "string"),
|
|
352
|
+
"number[]": (v) => Array.isArray(v) && v.every((x) => typeof x === "number" && Number.isFinite(x)),
|
|
339
353
|
"object[]": (v) => Array.isArray(v) && v.every((x) => x && typeof x === "object" && !Array.isArray(x)),
|
|
340
354
|
object: (v) => v && typeof v === "object" && !Array.isArray(v),
|
|
341
355
|
notes: (v) => typeof v === "string" || (v && typeof v === "object" && !Array.isArray(v)),
|
|
@@ -412,6 +426,15 @@ function validate(spec) {
|
|
|
412
426
|
seen.push(sl.layout);
|
|
413
427
|
});
|
|
414
428
|
|
|
429
|
+
// a chart whose series do not match its labels draws a line that ends early
|
|
430
|
+
spec.slides.forEach((x, i) => {
|
|
431
|
+
if (x.layout !== "chart" || !Array.isArray(x.labels) || !Array.isArray(x.series)) return;
|
|
432
|
+
x.series.forEach((ser) => {
|
|
433
|
+
if (Array.isArray(ser.values) && ser.values.length !== x.labels.length)
|
|
434
|
+
errs.push(`S${i + 1} chart series "${ser.name}" has ${ser.values.length} values for ${x.labels.length} labels`);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
415
438
|
// backups live after the close — hidden, so they never interrupt the room
|
|
416
439
|
const closeAt = spec.slides.findIndex((x) => x.layout === "closing");
|
|
417
440
|
spec.slides.forEach((x, i) => {
|
package/lib/trace.js
CHANGED
|
@@ -18,7 +18,12 @@ function fakeSlide() {
|
|
|
18
18
|
addImage(o) { shapes.push({ kind: "image", ...o }); return api; },
|
|
19
19
|
addText(t, o) { shapes.push({ kind: "text", runs: normaliseRuns(t), ...o }); return api; },
|
|
20
20
|
addNotes() { return api; },
|
|
21
|
-
|
|
21
|
+
// a chart is recorded by what it shows, so the PDF and preview can draw
|
|
22
|
+
// the same thing PowerPoint will
|
|
23
|
+
addChart(type, data, o = {}) {
|
|
24
|
+
shapes.push({ kind: "chart", type, x: o.x, y: o.y, w: o.w, h: o.h, chart: o._wand || { kind: type, series: data } });
|
|
25
|
+
return api;
|
|
26
|
+
},
|
|
22
27
|
};
|
|
23
28
|
Object.defineProperty(api, "background", {
|
|
24
29
|
get() { return api._bg; },
|
package/package.json
CHANGED
package/preview/preview.js
CHANGED
|
@@ -95,7 +95,10 @@
|
|
|
95
95
|
|
|
96
96
|
const s = document.createElement("div");
|
|
97
97
|
const bg = sl.background && sl.background.color;
|
|
98
|
-
|
|
98
|
+
// by luminance, not by one hex: the dark ground moved from 0F0C26 to
|
|
99
|
+
// 240F48 in 0.9 and every dark slide previewed as light
|
|
100
|
+
const lum = bg ? (parseInt(bg.slice(0, 2), 16) * 299 + parseInt(bg.slice(2, 4), 16) * 587 + parseInt(bg.slice(4, 6), 16) * 114) / 1000 : 255;
|
|
101
|
+
s.className = "slide " + (lum < 90 ? "dark" : "light");
|
|
99
102
|
if (flag.checked) s.classList.add("flag");
|
|
100
103
|
if (grid.checked) s.classList.add("grid");
|
|
101
104
|
s.title = sl.layout;
|
|
@@ -109,6 +112,8 @@
|
|
|
109
112
|
el.src = sh.data ? "data:" + sh.data
|
|
110
113
|
: sh.path ? (T.assetsBase || "../assets/") + sh.path.split(/[\\/]assets[\\/]/).pop()
|
|
111
114
|
: "";
|
|
115
|
+
} else if (sh.kind === "chart") {
|
|
116
|
+
el = chartEl(sh);
|
|
112
117
|
} else if (sh.kind === "text") {
|
|
113
118
|
el = textEl(sh);
|
|
114
119
|
} else {
|
|
@@ -145,6 +150,49 @@
|
|
|
145
150
|
});
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
/** The same chart the PDF draws, as SVG in inches. */
|
|
154
|
+
function chartEl(sh) {
|
|
155
|
+
const c = sh.chart || {}, labels = c.labels || [], series = c.series || [];
|
|
156
|
+
const NS = "http://www.w3.org/2000/svg", W = sh.w, H = sh.h;
|
|
157
|
+
const svg = document.createElementNS(NS, "svg");
|
|
158
|
+
svg.setAttribute("class", "sh chart"); svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
|
159
|
+
const add = (tag, a, txt) => { const e = document.createElementNS(NS, tag); for (const k in a) e.setAttribute(k, a[k]); if (txt != null) e.textContent = txt; svg.appendChild(e); return e; };
|
|
160
|
+
const legendH = c.legend ? 0.26 : 0, padL = 0.40, padB = 0.26;
|
|
161
|
+
const px = padL, py = legendH + 0.06, pw = W - padL - 0.08, ph = H - legendH - padB - 0.06;
|
|
162
|
+
const all = series.flatMap((x) => x.values || []);
|
|
163
|
+
const nice = (v) => { if (!(v > 0)) return 1; const p = Math.pow(10, Math.floor(Math.log10(v))), m = v / p; return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10) * p; };
|
|
164
|
+
const max = c.yMax || nice(Math.max(0, ...all)), ink = "#" + (c.ink || "BBBBBB"), grid = "#" + (c.grid || "5B4BB0");
|
|
165
|
+
for (let k = 0; k <= 4; k++) {
|
|
166
|
+
const gy = py + ph - (ph * k) / 4;
|
|
167
|
+
add("line", { x1: px, y1: gy, x2: px + pw, y2: gy, stroke: grid, "stroke-width": 0.007 });
|
|
168
|
+
add("text", { x: px - 0.08, y: gy + 0.03, "text-anchor": "end", "font-size": 0.1, fill: ink, "font-family": "Geist" }, String(Math.round((max * k) / 4 * 100) / 100));
|
|
169
|
+
}
|
|
170
|
+
const n = labels.length, bar = c.kind === "bar";
|
|
171
|
+
const xAt = (i) => bar ? px + (pw / n) * (i + 0.5) : px + (n > 1 ? (pw * i) / (n - 1) : pw / 2);
|
|
172
|
+
const yAt = (v) => py + ph - (ph * Math.min(v, max)) / max;
|
|
173
|
+
labels.forEach((l, i) => add("text", { x: xAt(i), y: py + ph + 0.17, "text-anchor": "middle", "font-size": 0.1, fill: ink, "font-family": "Geist" }, l));
|
|
174
|
+
series.forEach((ser, si) => {
|
|
175
|
+
const col = "#" + ((c.colors && c.colors[si]) || "8B7CF6"), vals = ser.values || [];
|
|
176
|
+
if (bar) {
|
|
177
|
+
const slot = pw / n, group = slot * 0.62, bw = group / series.length;
|
|
178
|
+
vals.forEach((v, i) => add("rect", { x: px + slot * i + (slot - group) / 2 + bw * si, y: yAt(v), width: bw * 0.92, height: py + ph - yAt(v), fill: col }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const pts = vals.map((v, i) => `${xAt(i)},${yAt(v)}`).join(" ");
|
|
182
|
+
if (c.kind === "area") add("polygon", { points: `${xAt(0)},${py + ph} ${pts} ${xAt(vals.length - 1)},${py + ph}`, fill: col, "fill-opacity": 0.45 });
|
|
183
|
+
if (c.kind !== "area") add("polyline", { points: pts, fill: "none", stroke: col, "stroke-width": ser.emph ? 0.035 : 0.028 });
|
|
184
|
+
});
|
|
185
|
+
if (c.legend) {
|
|
186
|
+
let lx = px;
|
|
187
|
+
series.forEach((ser, si) => {
|
|
188
|
+
add("rect", { x: lx, y: 0.08, width: 0.1, height: 0.1, fill: "#" + ((c.colors && c.colors[si]) || "8B7CF6") });
|
|
189
|
+
add("text", { x: lx + 0.15, y: 0.17, "font-size": 0.11, fill: ink, "font-family": "Geist" }, ser.name);
|
|
190
|
+
lx += 0.15 + ser.name.length * 0.065 + 0.3;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return svg;
|
|
194
|
+
}
|
|
195
|
+
|
|
148
196
|
/** Browser-side overflow detection: does the laid-out text exceed its box? */
|
|
149
197
|
function markOverflow(slideEl) {
|
|
150
198
|
slideEl.querySelectorAll(".sh.text").forEach((el) => {
|
package/qa_coverage.py
CHANGED
|
@@ -62,6 +62,13 @@ def strings_from_pptx(path):
|
|
|
62
62
|
paras.append("".join(runs))
|
|
63
63
|
if paras:
|
|
64
64
|
out.append((i, " ".join(paras)))
|
|
65
|
+
# Chart series names and category labels live in ppt/charts, not on the
|
|
66
|
+
# slide. A rebuilt chart that drops a series name must still fail here.
|
|
67
|
+
for n in sorted(x for x in z.namelist() if re.fullmatch(r"ppt/charts/chart\d+\.xml", x)):
|
|
68
|
+
vals = re.findall(r"<c:v>([^<]*)</c:v>|<a:t>([^<]*)</a:t>", z.read(n).decode("utf8", "ignore"))
|
|
69
|
+
words = [a or b for a, b in vals if (a or b).strip() and not re.fullmatch(r"[-\d.,%]+", a or b)]
|
|
70
|
+
if words:
|
|
71
|
+
out.append((0, " ".join(words)))
|
|
65
72
|
return out
|
|
66
73
|
|
|
67
74
|
|
package/wand.js
CHANGED
|
@@ -456,6 +456,13 @@ function inspect(file) {
|
|
|
456
456
|
errs.push('fonts embedded but saveSubsetFonts is not "0" - digits garble in exported PDFs');
|
|
457
457
|
}
|
|
458
458
|
if (!fonts.length) errs.push("no embedded fonts - this renders as Arial on the client's machine");
|
|
459
|
+
// pptxgenjs writes its default outline — 1pt #333333 — whenever a shape asks
|
|
460
|
+
// for `line: { width: 0 }`. No Wand token is 333333, so this is always a leak.
|
|
461
|
+
let leaks = 0;
|
|
462
|
+
for (const name of z.names.filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n))) {
|
|
463
|
+
leaks += (z.text(name).match(/<a:ln w="12700"><a:solidFill><a:srgbClr val="333333"\/>/g) || []).length;
|
|
464
|
+
}
|
|
465
|
+
if (leaks) errs.push(`${leaks} shape(s) carry pptxgenjs's default 1pt #333333 outline — a layout used line:{width:0}; use line:{type:"none"}`);
|
|
459
466
|
errs.forEach((e) => console.log(` ERROR ${e}`));
|
|
460
467
|
console.log(`file : ${slides.length} slides, ${media.length} media, ${fonts.length} font parts / ` +
|
|
461
468
|
`${families} families` + (errs.length ? ` - ${errs.length} problem(s) above` : " - clean"));
|
package/wand_kit.js
CHANGED
|
@@ -233,6 +233,13 @@ function kicker(slide, text, { dark = true, y = 4.82 } = {}) {
|
|
|
233
233
|
|
|
234
234
|
/* ---------------------------------------------------------------- primitives */
|
|
235
235
|
|
|
236
|
+
/*
|
|
237
|
+
* Outlines: `line: { width: 0 }` does NOT mean none. pptxgenjs reads
|
|
238
|
+
* `width || 1`, so zero became its default 1pt #333333 border on every accent
|
|
239
|
+
* bar, rule and column — invisible on dark, a black box on light, and absent
|
|
240
|
+
* from the kit's own PDF, so no gate saw it. No outline is `{ type: "none" }`.
|
|
241
|
+
* `inspect` now fails a deck that carries the default outline.
|
|
242
|
+
*/
|
|
236
243
|
function card(slide, { x, y, w, h, dark = true, variant = "glass" }) {
|
|
237
244
|
const th = theme(dark);
|
|
238
245
|
let fill = th.cardFill, line = th.cardLine;
|
|
@@ -256,7 +263,7 @@ function card(slide, { x, y, w, h, dark = true, variant = "glass" }) {
|
|
|
256
263
|
function iconTile(slide, { x, y, size = 0.46, icon, dark = true }) {
|
|
257
264
|
slide.addShape("roundRect", {
|
|
258
265
|
x, y, w: size, h: size,
|
|
259
|
-
fill: { color: theme(dark).tile }, line: {
|
|
266
|
+
fill: { color: theme(dark).tile }, line: { type: "none" },
|
|
260
267
|
rectRadius: size * 0.30,
|
|
261
268
|
});
|
|
262
269
|
if (icon) {
|
|
@@ -576,7 +583,7 @@ function parity(pres, { tag, titleText, subtitleText, left, right, badge = "=",
|
|
|
576
583
|
});
|
|
577
584
|
});
|
|
578
585
|
});
|
|
579
|
-
s.addShape("ellipse", { x: 4.75, y: 2.89, w: 0.50, h: 0.50, fill: { color: T.accent }, line: {
|
|
586
|
+
s.addShape("ellipse", { x: 4.75, y: 2.89, w: 0.50, h: 0.50, fill: { color: T.accent }, line: { type: "none" } });
|
|
580
587
|
s.addText(badge, {
|
|
581
588
|
x: 4.75, y: 2.89, w: 0.50, h: 0.50, margin: 0, isTextBox: true,
|
|
582
589
|
fontFace: F.semi, fontSize: 17, color: T.white, align: "center", valign: "middle",
|
|
@@ -656,7 +663,7 @@ function band(pres, { tag, titleText, subtitleText, icon, runs, stats, quote, da
|
|
|
656
663
|
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.5 });
|
|
657
664
|
s.addShape("roundRect", {
|
|
658
665
|
x: 0.37, y: 1.78, w: 9.25, h: 0.86,
|
|
659
|
-
fill: { color: T.band }, line: {
|
|
666
|
+
fill: { color: T.band }, line: { type: "none" }, rectRadius: radius(9.25, 0.86),
|
|
660
667
|
});
|
|
661
668
|
iconTile(s, { x: 0.68, y: 2.00, size: 0.42, icon, dark: true });
|
|
662
669
|
s.addText(
|
|
@@ -804,12 +811,12 @@ function barRank(pres, { tag, titleText, subtitleText, bars, cards, footnote, da
|
|
|
804
811
|
const y = 2.10 + i * 0.64, TRACK = 5.40;
|
|
805
812
|
s.addShape("roundRect", {
|
|
806
813
|
x: 0.37, y, w: TRACK, h: 0.48, rectRadius: radius(TRACK, 0.48),
|
|
807
|
-
fill: { color: dark ? T.cardMuted : T.track }, line: {
|
|
814
|
+
fill: { color: dark ? T.cardMuted : T.track }, line: { type: "none" },
|
|
808
815
|
});
|
|
809
816
|
const fw = Math.max(0.30, Math.min(1, b.frac) * TRACK);
|
|
810
817
|
s.addShape("roundRect", {
|
|
811
818
|
x: 0.37, y, w: fw, h: 0.48, rectRadius: radius(fw, 0.48),
|
|
812
|
-
fill: { color: RAMP[i % RAMP.length] }, line: {
|
|
819
|
+
fill: { color: RAMP[i % RAMP.length] }, line: { type: "none" },
|
|
813
820
|
});
|
|
814
821
|
s.addText(b.label, {
|
|
815
822
|
x: 0.55, y, w: Math.max(0.60, fw - 0.26), h: 0.48, margin: 0, isTextBox: true,
|
|
@@ -865,12 +872,12 @@ function bridge(pres, { tag, titleText, subtitleText, eyebrow, rows: brows, stat
|
|
|
865
872
|
});
|
|
866
873
|
s.addShape("roundRect", {
|
|
867
874
|
x: 2.64, y: y + 0.06, w: TRACK, h: 0.22, rectRadius: radius(TRACK, 0.22),
|
|
868
|
-
fill: { color: dark ? T.cardMuted : T.track }, line: {
|
|
875
|
+
fill: { color: dark ? T.cardMuted : T.track }, line: { type: "none" },
|
|
869
876
|
});
|
|
870
877
|
const fw = Math.max(0.06, Math.min(1, r.frac) * TRACK);
|
|
871
878
|
s.addShape("roundRect", {
|
|
872
879
|
x: 2.64, y: y + 0.06, w: fw, h: 0.22, rectRadius: radius(fw, 0.22),
|
|
873
|
-
fill: { color: T.accent }, line: {
|
|
880
|
+
fill: { color: T.accent }, line: { type: "none" },
|
|
874
881
|
});
|
|
875
882
|
s.addText(r.value, {
|
|
876
883
|
x: 4.44, y, w: 0.95, h: 0.34, margin: 0, isTextBox: true,
|
|
@@ -921,7 +928,7 @@ function heroQuad(pres, { tag, titleText, subtitleText, hero, tiles, kickerText,
|
|
|
921
928
|
fontFace: F.reg, fontSize: 9, color: th.body, valign: "middle",
|
|
922
929
|
});
|
|
923
930
|
s.addShape("rect", { x: 0.67, y: 3.36, w: 3.30, h: 0.02,
|
|
924
|
-
fill: { color: dark ? T.cardMutedStroke : T.divider }, line: {
|
|
931
|
+
fill: { color: dark ? T.cardMutedStroke : T.divider }, line: { type: "none" } });
|
|
925
932
|
s.addText(hero.subValue, {
|
|
926
933
|
x: 0.67, y: 3.49, w: 3.4, h: 0.42, margin: 0, isTextBox: true,
|
|
927
934
|
fontFace: F.semi, fontSize: 26, color: th.ink, valign: "middle",
|
|
@@ -972,7 +979,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
|
|
|
972
979
|
if (!label) return;
|
|
973
980
|
s.addShape("roundRect", {
|
|
974
981
|
x: 0.37, y, w: 1.0, h, rectRadius: radius(1.0, h),
|
|
975
|
-
fill: { color: dark ? T.cardMuted : T.surface }, line: {
|
|
982
|
+
fill: { color: dark ? T.cardMuted : T.surface }, line: { type: "none" },
|
|
976
983
|
});
|
|
977
984
|
s.addText(label.toUpperCase(), {
|
|
978
985
|
x: 0.37, y, w: 1.0, h, margin: 0, isTextBox: true,
|
|
@@ -1152,8 +1159,8 @@ function deliverColumns(pres, { tag, titleText, subtitleText, columns, dark = fa
|
|
|
1152
1159
|
const gap = 0.23, w = (9.17 - gap * (list.length - 1)) / list.length, cy = 1.62, ch = 3.02;
|
|
1153
1160
|
list.forEach((c, i) => {
|
|
1154
1161
|
const cx = 0.42 + i * (w + gap);
|
|
1155
|
-
s.addShape("rect", { x: cx, y: cy, w, h: ch, fill: { color: dark ? T.cardSolid : T.surface }, line: {
|
|
1156
|
-
s.addShape("rect", { x: cx, y: cy, w, h: 0.04, fill: { color: T.accent }, line: {
|
|
1162
|
+
s.addShape("rect", { x: cx, y: cy, w, h: ch, fill: { color: dark ? T.cardSolid : T.surface }, line: { type: "none" } });
|
|
1163
|
+
s.addShape("rect", { x: cx, y: cy, w, h: 0.04, fill: { color: T.accent }, line: { type: "none" } });
|
|
1157
1164
|
s.addText(c.heading, {
|
|
1158
1165
|
x: cx + 0.2, y: cy + 0.15, w: w - 0.4, h: 0.24, margin: 0, isTextBox: true,
|
|
1159
1166
|
fontFace: F.semi, fontSize: 11, color: th.ink, valign: "middle",
|
|
@@ -1227,7 +1234,7 @@ function flowCompare(pres, { tag, titleText, subtitleText, lanes, dark = true, d
|
|
|
1227
1234
|
fontFace: F.med, fontSize: 7.5, color: T.accentSoft, valign: "middle",
|
|
1228
1235
|
});
|
|
1229
1236
|
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: {
|
|
1237
|
+
fill: { color: dark ? T.cardStroke : T.divider }, line: { type: "none" } });
|
|
1231
1238
|
const steps = lane.steps.slice(0, 5), gap = 0.30;
|
|
1232
1239
|
const w = (9.17 - gap * (steps.length - 1)) / steps.length, cy = y + 0.30, ch = 0.73;
|
|
1233
1240
|
steps.forEach((st, i) => {
|
|
@@ -1248,7 +1255,7 @@ function flowCompare(pres, { tag, titleText, subtitleText, lanes, dark = true, d
|
|
|
1248
1255
|
});
|
|
1249
1256
|
if (i < steps.length - 1) s.addShape("triangle", {
|
|
1250
1257
|
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: {
|
|
1258
|
+
fill: { color: T.accent }, line: { type: "none" },
|
|
1252
1259
|
});
|
|
1253
1260
|
});
|
|
1254
1261
|
if (lane.caption) s.addText(lane.caption, {
|
|
@@ -1275,13 +1282,13 @@ function timeline(pres, { tag, titleText, subtitleText, phases, dark = true, dec
|
|
|
1275
1282
|
const w = (9.14 - gap * (list.length - 1)) / list.length, x0 = 0.44;
|
|
1276
1283
|
const ly = 2.30, d = 0.30;
|
|
1277
1284
|
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: {
|
|
1285
|
+
fill: { color: T.accent }, line: { type: "none" } });
|
|
1279
1286
|
list.forEach((ph, i) => {
|
|
1280
1287
|
const cx = x0 + i * (w + gap);
|
|
1281
1288
|
s.addShape("ellipse", { x: cx, y: ly, w: d, h: d,
|
|
1282
1289
|
fill: { color: dark ? T.bgDark : T.bgLight }, line: { color: T.accent, width: 2 } });
|
|
1283
1290
|
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: {
|
|
1291
|
+
fill: { color: T.accent }, line: { type: "none" } });
|
|
1285
1292
|
const cy = 2.86;
|
|
1286
1293
|
if (ph.when) s.addText(ph.when, {
|
|
1287
1294
|
x: cx, y: cy, w, h: 0.2, margin: 0, isTextBox: true,
|
|
@@ -1302,6 +1309,68 @@ function timeline(pres, { tag, titleText, subtitleText, phases, dark = true, dec
|
|
|
1302
1309
|
return s;
|
|
1303
1310
|
}
|
|
1304
1311
|
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* A native, editable chart with its argument beside or below it (Kearney S09,
|
|
1315
|
+
* S25 — both hand-drawn there, so a seller could not change a number). The data
|
|
1316
|
+
* stays live in PowerPoint: right-click, Edit Data.
|
|
1317
|
+
* kind: "line" | "area" | "bar"; series: [{ name, values, emph }];
|
|
1318
|
+
* side: [{ title, body }] up to 2, placed "right" (default) or "below".
|
|
1319
|
+
*/
|
|
1320
|
+
function chart(pres, { tag, titleText, subtitleText, kind = "line", labels, series, yMax, yLabel,
|
|
1321
|
+
side, sidePlace = "right", dark = true, deck, n, total }) {
|
|
1322
|
+
const s = pres.addSlide();
|
|
1323
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1324
|
+
const th = theme(dark);
|
|
1325
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
1326
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.10, h: 0.3 });
|
|
1327
|
+
const blocks = (side || []).slice(0, 2);
|
|
1328
|
+
const below = blocks.length && sidePlace === "below";
|
|
1329
|
+
const box = !blocks.length ? { x: 0.42, y: 1.52, w: 9.17, h: 3.55 }
|
|
1330
|
+
: below ? { x: 0.42, y: 1.52, w: 9.17, h: 2.35 }
|
|
1331
|
+
: { x: 0.42, y: 1.52, w: 6.05, h: 3.55 };
|
|
1332
|
+
const list = series.slice(0, 3);
|
|
1333
|
+
const palette = dark ? [T.accent, T.accentPale, T.mutedDark] : [T.accentDeep, T.accentSoft, T.mutedLight];
|
|
1334
|
+
// the emphasised series takes the accent, the rest step down in weight
|
|
1335
|
+
const order = list.map((x, i) => ({ x, i })).sort((a, b) => (b.x.emph ? 1 : 0) - (a.x.emph ? 1 : 0));
|
|
1336
|
+
const colors = []; order.forEach(({ i }, k) => { colors[i] = palette[k]; });
|
|
1337
|
+
const ink = dark ? T.mutedDark : T.mutedLight, grid = dark ? T.cardStroke : T.divider;
|
|
1338
|
+
// Four gridlines on a round maximum, fixed here so PowerPoint does not pick
|
|
1339
|
+
// its own step (it chose 10 for a 0–90 series; the PDF drew 25).
|
|
1340
|
+
const peak = Math.max(0, ...list.flatMap((x) => x.values));
|
|
1341
|
+
const p10 = Math.pow(10, Math.floor(Math.log10(peak || 1))), mm = (peak || 1) / p10;
|
|
1342
|
+
const top = yMax || (mm <= 1 ? 1 : mm <= 2 ? 2 : mm <= 2.5 ? 2.5 : mm <= 5 ? 5 : 10) * p10;
|
|
1343
|
+
s.addChart(kind, list.map((x) => ({ name: x.name, labels, values: x.values })), {
|
|
1344
|
+
...box,
|
|
1345
|
+
chartColors: colors, chartColorsOpacity: kind === "area" ? 45 : undefined,
|
|
1346
|
+
lineSize: 2, lineDataSymbol: "none", barDir: "col", barGapWidthPct: 60,
|
|
1347
|
+
catAxisLabelColor: ink, catAxisLabelFontFace: F.reg, catAxisLabelFontSize: 8, catAxisLineShow: false,
|
|
1348
|
+
valAxisLabelColor: ink, valAxisLabelFontFace: F.reg, valAxisLabelFontSize: 8, valAxisLineShow: false,
|
|
1349
|
+
valAxisMinVal: 0, valAxisMaxVal: top, valAxisMajorUnit: top / 4, valGridLine: { color: grid, style: "solid", size: 0.5 },
|
|
1350
|
+
catGridLine: { style: "none" },
|
|
1351
|
+
showValAxisTitle: !!yLabel, valAxisTitle: yLabel, valAxisTitleColor: ink, valAxisTitleFontFace: F.reg, valAxisTitleFontSize: 8,
|
|
1352
|
+
showLegend: list.length > 1, legendPos: "t", legendFontFace: F.reg, legendFontSize: 8, legendColor: th.body,
|
|
1353
|
+
// recorded by the trace for the PDF and preview; ignored by pptxgenjs
|
|
1354
|
+
_wand: { kind, labels, series: list, colors, yMax: top, yLabel, ink, grid, legend: list.length > 1 },
|
|
1355
|
+
});
|
|
1356
|
+
const bx = below ? null : 6.75;
|
|
1357
|
+
blocks.forEach((b, i) => {
|
|
1358
|
+
const r = below ? { x: 0.42 + i * 4.69, y: 4.02, w: 4.48, h: 1.05 } : { x: bx, y: 1.62 + i * 1.72, w: 2.84, h: 1.55 };
|
|
1359
|
+
if (!below) card(s, { x: r.x - 0.14, y: r.y - 0.1, w: r.w + 0.14, h: r.h, dark, variant: "solid" });
|
|
1360
|
+
s.addText(b.title, {
|
|
1361
|
+
x: r.x, y: r.y, w: r.w - 0.14, h: 0.3, margin: 0, isTextBox: true,
|
|
1362
|
+
fontFace: F.semi, fontSize: 11, color: dark ? T.accentPale : T.accentDeep, valign: "top",
|
|
1363
|
+
});
|
|
1364
|
+
s.addText(b.body, {
|
|
1365
|
+
x: r.x, y: r.y + 0.34, w: r.w - 0.14, h: r.h - 0.44, margin: 0, isTextBox: true,
|
|
1366
|
+
fontFace: F.reg, fontSize: autoSize(b.body, r.w - 0.14, r.h - 0.44, { family: F.reg, fontSize: 8.5, min: 6.5, lineSpacingMultiple: 1.3 }),
|
|
1367
|
+
color: th.body, valign: "top", lineSpacingMultiple: 1.3,
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1371
|
+
return s;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1305
1374
|
/**
|
|
1306
1375
|
* Speaker notes, in the run-of-show convention the Kearney example uses:
|
|
1307
1376
|
* the time window first, "CORE" when the slide carries the argument, then what
|
|
@@ -1315,12 +1384,12 @@ function notesText(nt) {
|
|
|
1315
1384
|
}
|
|
1316
1385
|
|
|
1317
1386
|
module.exports = {
|
|
1318
|
-
VERSION: "0.
|
|
1387
|
+
VERSION: "0.11.0",
|
|
1319
1388
|
T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
|
|
1320
1389
|
background, chrome, footer, title, subtitle, kicker, contentTop,
|
|
1321
1390
|
card, iconTile, pill,
|
|
1322
1391
|
cover, divider, closing,
|
|
1323
|
-
agenda, team, fourCards, deliverColumns, coDelivery, flowCompare, timeline, notesText, context,
|
|
1392
|
+
agenda, team, fourCards, deliverColumns, coDelivery, flowCompare, timeline, chart, notesText, context,
|
|
1324
1393
|
threeUp, fourUpIcon, sixUpIcon, rows, parity, statQuad, statGrid, band,
|
|
1325
1394
|
colCards, featureStats, barRank, bridge, heroQuad, productMap,
|
|
1326
1395
|
|