wand-decks-kit 0.8.6 → 0.9.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 +35 -0
- package/SKILL.md +66 -2
- package/assets/closing-art.png +0 -0
- package/assets/cover-art.png +0 -0
- package/assets/orb-glow.png +0 -0
- package/docs/LAYOUTS.md +5 -0
- package/examples/template.json +639 -126
- package/lib/schema.js +24 -1
- package/package.json +1 -1
- package/tools/brandlint.py +74 -0
- package/wand.js +81 -0
- package/wand_kit.js +241 -56
package/lib/schema.js
CHANGED
|
@@ -36,7 +36,8 @@ const L = {
|
|
|
36
36
|
bleed: true,
|
|
37
37
|
fields: {
|
|
38
38
|
eyebrow: str(), body: str(), kickerPlain: str(), kickerBold: str(),
|
|
39
|
-
titleLines: s("string[]", req({ max: 3, note: "one entry per line; auto-shrinks
|
|
39
|
+
titleLines: s("string[]", req({ max: 3, note: "one entry per line; auto-shrinks 39→30pt" })),
|
|
40
|
+
presentedTo: s("string[]", { max: 3, note: "the audience, bottom right — one name and title per entry" }),
|
|
40
41
|
},
|
|
41
42
|
},
|
|
42
43
|
divider: {
|
|
@@ -50,6 +51,28 @@ const L = {
|
|
|
50
51
|
fields: { eyebrow: str(), line1: str(req()), line2: str(), body: str(), url: str() },
|
|
51
52
|
},
|
|
52
53
|
|
|
54
|
+
/* ----------------------------------------------------- guideline (0.9) */
|
|
55
|
+
agenda: {
|
|
56
|
+
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() } })) },
|
|
58
|
+
},
|
|
59
|
+
team: {
|
|
60
|
+
use: "Who's in the room — 2–4 people, each with a headshot slot the user fills",
|
|
61
|
+
fields: { items: s("object[]", req({ min: 2, max: 4, of: { name: str(req()), role: str(), initials: str() } })) },
|
|
62
|
+
},
|
|
63
|
+
fourCards: {
|
|
64
|
+
use: "Four titled cards under a lede — demo objectives, pillars, principles",
|
|
65
|
+
fields: { items: s("object[]", req({ min: 3, max: 4, of: { title: str(req()), body: str(req()) } })) },
|
|
66
|
+
},
|
|
67
|
+
deliverColumns: {
|
|
68
|
+
use: "Three flat columns with an accent bar — how we deliver, phases with an owner line. Light by default",
|
|
69
|
+
fields: { columns: s("object[]", req({ min: 2, max: 3, of: { heading: str(req()), lead: str(), body: str(req()) } })) },
|
|
70
|
+
},
|
|
71
|
+
coDelivery: {
|
|
72
|
+
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()) } })) },
|
|
74
|
+
},
|
|
75
|
+
|
|
53
76
|
/* --------------------------------------------------------------- content */
|
|
54
77
|
threeUp: {
|
|
55
78
|
use: "3 comparable things. `arrows: true` for a progression",
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
brandlint.py — is this deck on the guideline?
|
|
4
|
+
|
|
5
|
+
python3 tools/brandlint.py deck.pptx palette.json
|
|
6
|
+
|
|
7
|
+
For a deck the kit did not build — one edited directly, or a client's file — the
|
|
8
|
+
fit gate cannot run, but the guideline still can: is every text run in Geist, is
|
|
9
|
+
every colour a token, is the canvas 10 x 5.625, does every content slide carry
|
|
10
|
+
the tag and the page footer. Reports per slide and exits 1 on any miss, so an
|
|
11
|
+
edit-only job ends with a list of inconsistencies to put to the user, not a
|
|
12
|
+
silent hand-back.
|
|
13
|
+
|
|
14
|
+
Only runs that carry text count. PowerPoint leaves Calibri on empty end-of-
|
|
15
|
+
paragraph runs; flagging those would make every deck fail.
|
|
16
|
+
"""
|
|
17
|
+
import json, os, re, sys, zipfile, collections
|
|
18
|
+
|
|
19
|
+
deck, palette = sys.argv[1], json.load(open(sys.argv[2]))
|
|
20
|
+
tokens = {v.upper() for v in palette.values()}
|
|
21
|
+
GEIST = re.compile(r"^Geist( |$)")
|
|
22
|
+
z = zipfile.ZipFile(deck)
|
|
23
|
+
names = sorted((n for n in z.namelist() if re.fullmatch(r"ppt/slides/slide\d+\.xml", n)),
|
|
24
|
+
key=lambda n: int(re.findall(r"\d+", n)[0]))
|
|
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())
|
|
27
|
+
canvas_ok = abs(cx / 914400 - 10) < 0.01 and abs(cy / 914400 - 5.625) < 0.01
|
|
28
|
+
|
|
29
|
+
bad = 0
|
|
30
|
+
print(f"canvas : {cx/914400:.3f} x {cy/914400:.3f} in — {'ok' if canvas_ok else 'NOT the 10 x 5.625 grid'}")
|
|
31
|
+
bad += not canvas_ok
|
|
32
|
+
font_hits = collections.Counter(); colour_hits = collections.Counter(); chrome_miss = []
|
|
33
|
+
for n in names:
|
|
34
|
+
i = int(re.findall(r"\d+", n)[0])
|
|
35
|
+
s = z.read(n).decode("utf8", "ignore")
|
|
36
|
+
# runs with text: <a:r><a:rPr ...>…</a:rPr><a:t>text</a:t></a:r>
|
|
37
|
+
for m in re.finditer(r"<a:r>(.*?)</a:r>", s, re.S):
|
|
38
|
+
run = m.group(1)
|
|
39
|
+
t = re.search(r"<a:t>([^<]*)</a:t>", run)
|
|
40
|
+
if not t or not t.group(1).strip():
|
|
41
|
+
continue
|
|
42
|
+
face = re.search(r'<a:latin typeface="([^"]+)"', run)
|
|
43
|
+
if face and not GEIST.match(face.group(1)):
|
|
44
|
+
font_hits[(i, face.group(1))] += 1
|
|
45
|
+
for c in re.findall(r'<a:srgbClr val="([0-9A-Fa-f]{6})"', run):
|
|
46
|
+
if c.upper() not in tokens:
|
|
47
|
+
colour_hits[(i, c.upper())] += 1
|
|
48
|
+
# Content slides carry the footer; cover, dividers and the closing drop it by
|
|
49
|
+
# design. Those three are the first slide, the last, and any slide with fewer
|
|
50
|
+
# than seven text frames (a divider is tag, eyebrow, title, body).
|
|
51
|
+
frames = [t for t in re.findall(r"<a:t>([^<]*)</a:t>", s) if t.strip()]
|
|
52
|
+
if i not in (1, len(names)) and len(frames) >= 7 and "WAND" not in " ".join(frames).upper():
|
|
53
|
+
chrome_miss.append(i)
|
|
54
|
+
|
|
55
|
+
if font_hits:
|
|
56
|
+
bad += 1
|
|
57
|
+
by = collections.defaultdict(list)
|
|
58
|
+
for (i, f), c in sorted(font_hits.items()): by[i].append(f"{f} x{c}")
|
|
59
|
+
print("fonts : NOT Geist on " + ", ".join(f"S{i:02} ({'; '.join(v)})" for i, v in by.items()))
|
|
60
|
+
else:
|
|
61
|
+
print("fonts : every text run is Geist")
|
|
62
|
+
if colour_hits:
|
|
63
|
+
top = colour_hits.most_common(8)
|
|
64
|
+
print("colours : " + str(len(colour_hits)) + " off-palette use(s) — " +
|
|
65
|
+
", ".join(f"S{i:02} {c} x{n}" for (i, c), n in top))
|
|
66
|
+
bad += 1
|
|
67
|
+
else:
|
|
68
|
+
print("colours : every text colour is a token")
|
|
69
|
+
if chrome_miss:
|
|
70
|
+
print("chrome : no WAND footer/tag text on " + ", ".join(f"S{i:02}" for i in chrome_miss))
|
|
71
|
+
bad += 1
|
|
72
|
+
else:
|
|
73
|
+
print("chrome : footer present on every content slide")
|
|
74
|
+
sys.exit(1 if bad else 0)
|
package/wand.js
CHANGED
|
@@ -56,6 +56,7 @@ function runPython(args) {
|
|
|
56
56
|
function doctor() {
|
|
57
57
|
const rows = [];
|
|
58
58
|
const py = python();
|
|
59
|
+
rows.push(["wand-decks-kit", require("./wand_kit").VERSION, "ok"]);
|
|
59
60
|
rows.push(["node", process.version, "ok"]);
|
|
60
61
|
rows.push(["python3", py ? execFileSync(py, ["-V"], { encoding: "utf8" }).trim() : "MISSING",
|
|
61
62
|
py ? "ok" : "extract and font embedding unavailable"]);
|
|
@@ -253,6 +254,80 @@ function ship(specPath, outArg) {
|
|
|
253
254
|
return gates.some(([, ok]) => ok === false) ? 1 : 0;
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Every gate that does not need a spec, on any .pptx.
|
|
259
|
+
*
|
|
260
|
+
* A field run adapted a source deck by editing it directly — the right call,
|
|
261
|
+
* this kit cannot copy a source slide's shapes — and then delivered it with
|
|
262
|
+
* no gate but a generic validator, because `ship` wants a spec. Now the
|
|
263
|
+
* spec-free gates run on their own: file structure and fonts, the PDF text
|
|
264
|
+
* layer, a render of every slide with the contact sheet, and coverage when a
|
|
265
|
+
* verbatim dump is given. LibreOffice does the render, so fit is *not* judged
|
|
266
|
+
* here (§9 — DejaVu is wider than Geist); geometry, overlap and contrast are.
|
|
267
|
+
*/
|
|
268
|
+
function audit(file, dump) {
|
|
269
|
+
const work = path.join(path.dirname(file), "audit");
|
|
270
|
+
fs.mkdirSync(work, { recursive: true });
|
|
271
|
+
console.log(rule("1 file"));
|
|
272
|
+
const fileErrs = inspect(file);
|
|
273
|
+
|
|
274
|
+
console.log(rule("2 render"));
|
|
275
|
+
let shots = 0, garbled = null, pdf = null;
|
|
276
|
+
// on PATH in a sandbox; an app bundle on a Mac
|
|
277
|
+
const soffice = ["soffice", "libreoffice"].find((c) => has(c))
|
|
278
|
+
|| ["/Applications/LibreOffice.app/Contents/MacOS/soffice"].find((p) => fs.existsSync(p)) || null;
|
|
279
|
+
if (soffice) {
|
|
280
|
+
spawnSync(soffice, ["--headless", "--convert-to", "pdf", "--outdir", work, file], { stdio: "ignore" });
|
|
281
|
+
pdf = path.join(work, path.basename(file).replace(/\.pptx$/i, ".pdf"));
|
|
282
|
+
}
|
|
283
|
+
if (pdf && fs.existsSync(pdf) && has("pdftoppm")) {
|
|
284
|
+
const dir = path.join(work, "slides");
|
|
285
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
286
|
+
spawnSync("pdftoppm", ["-r", "96", "-png", pdf, path.join(dir, "slide")], { stdio: "ignore" });
|
|
287
|
+
shots = fs.readdirSync(dir).filter((f) => f.startsWith("slide")).length;
|
|
288
|
+
const small = fs.mkdtempSync(path.join(require("os").tmpdir(), "wand-sheet-"));
|
|
289
|
+
spawnSync("pdftoppm", ["-r", "26", "-png", pdf, path.join(small, "s")], { stdio: "ignore" });
|
|
290
|
+
const sheet = path.join(dir, "sheet.png");
|
|
291
|
+
runPython([path.join(KIT, "tools", "sheet.py"), small, sheet]);
|
|
292
|
+
fs.rmSync(small, { recursive: true, force: true });
|
|
293
|
+
console.log(`rendered : ${shots} slide(s) — open ${rel(sheet)} (LibreOffice: trust geometry,`);
|
|
294
|
+
console.log(` overlap and contrast; do NOT judge fit or weight from it)`);
|
|
295
|
+
if (has("pdftotext")) {
|
|
296
|
+
const r = spawnSync("pdftotext", [pdf, "-"], { encoding: "utf8" });
|
|
297
|
+
if (r.status === 0) {
|
|
298
|
+
garbled = 0;
|
|
299
|
+
for (const c of r.stdout || "") { const cp = c.codePointAt(0); if (cp >= 0x01c9 && cp <= 0x024f) garbled++; }
|
|
300
|
+
console.log(garbled ? `text layer : ${garbled} garbled glyph(s) — fonts are subsetted` : "text layer : digits decode clean");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
console.log(soffice ? "pdftoppm absent — no renders" : "no LibreOffice — no renders; open the deck and look at every slide");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(rule("3 brand"));
|
|
308
|
+
// The guideline, as a gate: fonts, colours, canvas, chrome. This is what an
|
|
309
|
+
// edit-only job reports back as "design inconsistencies" before touching
|
|
310
|
+
// anything the user did not ask for.
|
|
311
|
+
const pal = path.join(require("os").tmpdir(), `wand-palette-${process.pid}.json`);
|
|
312
|
+
fs.writeFileSync(pal, JSON.stringify(require("./wand_kit").T));
|
|
313
|
+
const brand = runPython([path.join(KIT, "tools", "brandlint.py"), file, pal]);
|
|
314
|
+
fs.rmSync(pal, { force: true });
|
|
315
|
+
|
|
316
|
+
console.log(rule("4 coverage"));
|
|
317
|
+
let cov = null;
|
|
318
|
+
if (dump && fs.existsSync(dump)) cov = runPython([path.join(KIT, "qa_coverage.py"), dump, file]);
|
|
319
|
+
else console.log("no dump given — skipped. Pass --dump work/visible_dump.txt from `rebuild` to check\n" +
|
|
320
|
+
"every source string survived the edit.");
|
|
321
|
+
|
|
322
|
+
const gates = [["file", fileErrs === 0], ["brand", brand === 0],
|
|
323
|
+
["pdf text", garbled === null ? null : garbled === 0],
|
|
324
|
+
["renders", shots > 0 ? true : null], ["coverage", cov === null ? null : cov === 0]];
|
|
325
|
+
console.log(rule("gates"));
|
|
326
|
+
for (const [n, ok] of gates) console.log(` ${ok === null ? "skip" : ok ? " ok " : "FAIL"} ${n}`);
|
|
327
|
+
console.log(" (fit is not gated on a deck built outside the kit — say so in the review note)\n");
|
|
328
|
+
return gates.some(([, ok]) => ok === false) ? 1 : 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
256
331
|
/**
|
|
257
332
|
* File-level check, in-process. `validate.py` from the pptx skill is not in the
|
|
258
333
|
* zip and LibreOffice may not be installed, so the kit carries its own: the
|
|
@@ -415,6 +490,12 @@ to.
|
|
|
415
490
|
if (!positional[0]) die("usage: node wand.js inspect <deck.pptx>");
|
|
416
491
|
process.exit(inspect(path.resolve(positional[0])) ? 1 : 0);
|
|
417
492
|
|
|
493
|
+
case "audit": {
|
|
494
|
+
if (!positional[0]) die("usage: node wand.js audit <deck.pptx> [--dump visible_dump.txt]");
|
|
495
|
+
const di = rest.indexOf("--dump");
|
|
496
|
+
process.exit(audit(path.resolve(positional[0]), di >= 0 ? path.resolve(rest[di + 1]) : null));
|
|
497
|
+
}
|
|
498
|
+
|
|
418
499
|
case "ship":
|
|
419
500
|
if (!positional[0]) die("usage: node wand.js ship <spec.json> [out.pptx]");
|
|
420
501
|
process.exit(ship(positional[0], positional[1]));
|
package/wand_kit.js
CHANGED
|
@@ -25,23 +25,27 @@ function autoSize(text, w, h, opts) {
|
|
|
25
25
|
/* ------------------------------------------------------------------ tokens */
|
|
26
26
|
|
|
27
27
|
const T = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// Measured from the guideline deck (Vivicta_Session_Deck, 2026-09-23): the
|
|
29
|
+
// counts are how many shape fills / text runs use each value across its 16
|
|
30
|
+
// slides. A token here is a guideline value, not a taste.
|
|
31
|
+
accent: "8B7CF6", // tags, accent-italic, rules, pills (fills 18, text 14)
|
|
32
|
+
accentDeep: "6C5CE7", // accent text on light surfaces
|
|
33
|
+
accentSoft: "9481FF", // accent text on dark: eyebrows, links, pills (text 22)
|
|
34
|
+
accentPale: "DCD6F5", // quote marks, soft labels on dark (text 12)
|
|
32
35
|
|
|
33
|
-
bgDark: "
|
|
34
|
-
inkDark: "
|
|
36
|
+
bgDark: "240F48", // flat dark ground — the guideline paints no art on interior slides
|
|
37
|
+
inkDark: "FFFFFF", // titles (text 101)
|
|
35
38
|
white: "FFFFFF",
|
|
36
|
-
bodyDark: "
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
bodyDark: "F5F5F5", // body (text 54)
|
|
40
|
+
mutedDark: "BBBBBB", // captions, footer (text 32)
|
|
41
|
+
bodyDarkAlt: "C6BFE4", // lists inside cards (text 22)
|
|
42
|
+
cardStroke: "5B4BB0", // 1pt card stroke (lines 16)
|
|
43
|
+
cardSolid: "2A1F5C", // opaque card (fills 23)
|
|
44
|
+
cardMuted: "1B1738", // de-emphasised card (fills 16)
|
|
45
|
+
cardMutedStroke: "3A2E63",
|
|
46
|
+
cardEmphFlat: "251C63", // emphasis surface (fills 8)
|
|
47
|
+
band: "240F48",
|
|
48
|
+
footerDark: "BBBBBB",
|
|
45
49
|
|
|
46
50
|
bgLight: "F8F6FB",
|
|
47
51
|
inkLight: "1D1A33",
|
|
@@ -93,8 +97,9 @@ const theme = (dark) => dark
|
|
|
93
97
|
|
|
94
98
|
function background(slide, dark) {
|
|
95
99
|
if (dark) {
|
|
100
|
+
// The guideline's interior dark slides are a flat 240F48 — the gradient
|
|
101
|
+
// art belongs to the cover and closing only.
|
|
96
102
|
slide.background = { color: T.bgDark };
|
|
97
|
-
slide.addImage({ path: `${ASSETS}/bg-overlay-dark.png`, x: 0, y: 0, w: 10, h: 5.625 });
|
|
98
103
|
} else {
|
|
99
104
|
slide.background = { color: T.bgLight };
|
|
100
105
|
}
|
|
@@ -104,27 +109,28 @@ function background(slide, dark) {
|
|
|
104
109
|
function chrome(slide, { tag, dark = true }) {
|
|
105
110
|
if (tag) {
|
|
106
111
|
slide.addText(tag.toUpperCase(), {
|
|
107
|
-
x: 0.
|
|
112
|
+
x: 0.42, y: 0.24, w: 6.75, h: 0.17, margin: 0, isTextBox: true,
|
|
108
113
|
fontFace: F.reg, fontSize: 8, bold: true, color: T.accent, valign: "middle",
|
|
109
114
|
});
|
|
110
115
|
}
|
|
111
116
|
slide.addImage({
|
|
112
117
|
path: `${ASSETS}/${dark ? "mark-white.png" : "mark-dark.png"}`,
|
|
113
|
-
x: 9.
|
|
118
|
+
x: 9.40, y: 0.26, w: 0.22, h: 0.18,
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/** Footer band. `n` and `total` are literal — never a field. */
|
|
118
123
|
function footer(slide, { deck, n, total, dark = true, confidential = "STRICTLY CONFIDENTIAL" }) {
|
|
119
124
|
const c = dark ? T.footerDark : T.footerLight;
|
|
125
|
+
// guideline: a 0.34" band at y 5.29, 7pt, "WAND · CONFIDENTIAL" left, "03/13" right
|
|
120
126
|
slide.addText(`WAND · ${deck.toUpperCase()}`, {
|
|
121
|
-
x: 0.
|
|
122
|
-
fontFace: F.reg, fontSize:
|
|
127
|
+
x: 0.42, y: 5.29, w: 4.58, h: 0.34, margin: 0, isTextBox: true,
|
|
128
|
+
fontFace: F.reg, fontSize: 7, color: c, valign: "middle",
|
|
123
129
|
});
|
|
124
130
|
const pad = (v) => String(v).padStart(2, "0");
|
|
125
|
-
slide.addText(`${confidential} · ${pad(n)}
|
|
126
|
-
x: 5.23, y: 5.
|
|
127
|
-
fontFace: F.reg, fontSize:
|
|
131
|
+
slide.addText(`${confidential} · ${pad(n)}/${pad(total)}`, {
|
|
132
|
+
x: 5.23, y: 5.29, w: 4.38, h: 0.34, margin: 0, isTextBox: true,
|
|
133
|
+
fontFace: F.reg, fontSize: 7, color: c, align: "right", valign: "middle",
|
|
128
134
|
});
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -216,9 +222,17 @@ function kicker(slide, text, { dark = true, y = 4.82 } = {}) {
|
|
|
216
222
|
function card(slide, { x, y, w, h, dark = true, variant = "glass" }) {
|
|
217
223
|
const th = theme(dark);
|
|
218
224
|
let fill = th.cardFill, line = th.cardLine;
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
if (dark) {
|
|
226
|
+
if (variant === "solid") { fill = { color: T.cardSolid }; line = { color: T.cardMutedStroke, width: 1 }; }
|
|
227
|
+
if (variant === "muted") { fill = { color: T.cardMuted }; line = { color: T.cardMutedStroke, width: 1, dashType: "dash" }; }
|
|
228
|
+
if (variant === "emph") { fill = { color: T.cardEmphFlat }; line = { color: T.cardMutedStroke, width: 1 }; }
|
|
229
|
+
} else {
|
|
230
|
+
// The dark variants are dark purples; painted on a light slide they hide
|
|
231
|
+
// the light theme's ink. A guideline agenda rendered as four black boxes.
|
|
232
|
+
if (variant === "solid") { fill = { color: T.cardLight }; line = { color: T.divider, width: 1 }; }
|
|
233
|
+
if (variant === "muted") { fill = { color: T.surface }; line = { color: T.divider, width: 1, dashType: "dash" }; }
|
|
234
|
+
if (variant === "emph") { fill = { color: T.surface }; line = { color: T.accentPale, width: 1 }; }
|
|
235
|
+
}
|
|
222
236
|
slide.addShape("roundRect", {
|
|
223
237
|
x, y, w, h, fill: { ...fill }, line: { ...line }, rectRadius: radius(w, h),
|
|
224
238
|
});
|
|
@@ -267,37 +281,43 @@ function pill(slide, { x, y, w, h = 0.34, text, variant = "outline", dark = true
|
|
|
267
281
|
/* ------------------------------------------------------------------ layouts */
|
|
268
282
|
|
|
269
283
|
/** L1 cover — dark only, full bleed, no footer. */
|
|
270
|
-
function cover(pres, { eyebrow, titleLines, body, kickerPlain, kickerBold, tag }) {
|
|
284
|
+
function cover(pres, { eyebrow, titleLines, body, kickerPlain, kickerBold, tag, presentedTo }) {
|
|
271
285
|
const s = pres.addSlide();
|
|
272
286
|
s.background = { color: T.bgDark };
|
|
273
|
-
|
|
274
|
-
s.addImage({ path: `${ASSETS}/
|
|
287
|
+
// The guideline cover is one full-bleed composition with the orb baked in.
|
|
288
|
+
s.addImage({ path: `${ASSETS}/cover-art.png`, x: 0, y: 0, w: 10, h: 5.625 });
|
|
275
289
|
if (tag) chrome(s, { tag, dark: true });
|
|
276
290
|
if (eyebrow) s.addText(eyebrow.toUpperCase(), {
|
|
277
|
-
x: 0.
|
|
278
|
-
fontFace: F.med, fontSize:
|
|
291
|
+
x: 0.42, y: 1.36, w: 6.0, h: 0.3, margin: 0, isTextBox: true,
|
|
292
|
+
fontFace: F.med, fontSize: 11, color: T.accentSoft, valign: "middle",
|
|
279
293
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const tSize = autoSize(titleLines.join("\n"), 5.55, 1.55,
|
|
283
|
-
{ family: F.xbold, fontSize: 46, min: 32, lineSpacingMultiple: 1.05 });
|
|
294
|
+
const tSize = autoSize(titleLines.join("\n"), 6.29, 1.23,
|
|
295
|
+
{ family: F.xbold, fontSize: 39, min: 30, lineSpacingMultiple: 1.05 });
|
|
284
296
|
s.addText(titleLines.join("\n"), {
|
|
285
|
-
x: 0.
|
|
297
|
+
x: 0.42, y: 1.79, w: 6.29, h: 1.23, margin: 0, isTextBox: true,
|
|
286
298
|
fontFace: F.xbold, fontSize: tSize, color: T.white, valign: "top", lineSpacingMultiple: 1.05,
|
|
287
299
|
});
|
|
288
|
-
s.addText(body, {
|
|
289
|
-
x: 0.
|
|
290
|
-
fontFace: F.reg, fontSize: 12.5, color: T.
|
|
300
|
+
if (body) s.addText(body, {
|
|
301
|
+
x: 0.42, y: 3.15, w: 5.56, h: 0.6, margin: 0, isTextBox: true,
|
|
302
|
+
fontFace: F.reg, fontSize: 12.5, color: T.bodyDark, valign: "top", lineSpacingMultiple: 1.12,
|
|
291
303
|
});
|
|
292
|
-
s.addShape("rect", { x: 0.38, y: 3.95, w: 0.60, h: 0.04, fill: { color: T.accent }, line: { width: 0 } });
|
|
293
|
-
// Concatenating an absent kicker used to paint the word "undefined" on the
|
|
294
|
-
// cover — visible only in a render, past every machine gate.
|
|
295
304
|
const kickerRuns = [
|
|
296
|
-
kickerPlain && { text: kickerPlain + " ", options: { fontFace: F.reg, fontSize: 10, color: T.
|
|
305
|
+
kickerPlain && { text: kickerPlain + " ", options: { fontFace: F.reg, fontSize: 10, color: T.bodyDark } },
|
|
297
306
|
kickerBold && { text: kickerBold, options: { fontFace: F.reg, fontSize: 10, bold: true, color: T.white } },
|
|
298
307
|
].filter(Boolean);
|
|
299
308
|
if (kickerRuns.length) s.addText(kickerRuns,
|
|
300
|
-
{ x: 0.
|
|
309
|
+
{ x: 0.42, y: 4.52, w: 5.5, h: 0.4, margin: 0, isTextBox: true, valign: "middle" });
|
|
310
|
+
// "PRESENTED TO" block, bottom right — the guideline's audience line.
|
|
311
|
+
if (presentedTo && presentedTo.length) {
|
|
312
|
+
s.addText("PRESENTED TO", {
|
|
313
|
+
x: 6.67, y: 4.52, w: 3.21, h: 0.22, margin: 0, isTextBox: true,
|
|
314
|
+
fontFace: F.med, fontSize: 8, color: T.accentSoft, valign: "middle",
|
|
315
|
+
});
|
|
316
|
+
s.addText(presentedTo.join("\n"), {
|
|
317
|
+
x: 6.67, y: 4.74, w: 3.21, h: 0.66, margin: 0, isTextBox: true,
|
|
318
|
+
fontFace: F.reg, fontSize: 9.5, color: T.white, valign: "top", lineSpacingMultiple: 1.2,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
301
321
|
return s;
|
|
302
322
|
}
|
|
303
323
|
|
|
@@ -331,23 +351,30 @@ function divider(pres, { tag, eyebrow, titleText, body }) {
|
|
|
331
351
|
function closing(pres, { eyebrow, line1, line2, body, url }) {
|
|
332
352
|
const s = pres.addSlide();
|
|
333
353
|
s.background = { color: T.bgDark };
|
|
334
|
-
s.addImage({ path: `${ASSETS}/
|
|
335
|
-
s.addImage({ path: `${ASSETS}/
|
|
354
|
+
s.addImage({ path: `${ASSETS}/closing-art.png`, x: 0, y: 0, w: 10, h: 5.625 });
|
|
355
|
+
s.addImage({ path: `${ASSETS}/mark-white.png`, x: 9.40, y: 0.24, w: 0.22, h: 0.19 });
|
|
356
|
+
s.addImage({ path: `${ASSETS}/orb-glow.png`, x: 3.98, y: 0.69, w: 2.03, h: 2.03 });
|
|
357
|
+
// The orb owns the top; everything reads below it. A long line shrinks
|
|
358
|
+
// rather than wrapping into the eyebrow — the guideline's "Thank You" is
|
|
359
|
+
// 33pt, but a real ask is usually a sentence.
|
|
336
360
|
if (eyebrow) s.addText(eyebrow.toUpperCase(), {
|
|
337
|
-
x:
|
|
338
|
-
fontFace: F.med, fontSize:
|
|
361
|
+
x: 1.16, y: 2.82, w: 7.67, h: 0.24, margin: 0, isTextBox: true,
|
|
362
|
+
fontFace: F.med, fontSize: 10, color: T.accentSoft, align: "center", valign: "middle",
|
|
339
363
|
});
|
|
364
|
+
const joined = (line1 || "") + (line2 ? " " + line2 : "");
|
|
365
|
+
const size = autoSize(joined, 7.67, 0.72, { family: F.semi, fontSize: 33, min: 22, lineSpacingMultiple: 1.05 });
|
|
340
366
|
s.addText([
|
|
341
|
-
line1 && { text: line1 + (line2 ? "
|
|
342
|
-
line2 && { text: line2, options: { fontFace: F.
|
|
343
|
-
].filter(Boolean), { x:
|
|
367
|
+
line1 && { text: line1 + (line2 ? " " : ""), options: { fontFace: F.semi, fontSize: size, color: T.white } },
|
|
368
|
+
line2 && { text: line2, options: { fontFace: F.semi, fontSize: size, italic: true, color: T.accentSoft } },
|
|
369
|
+
].filter(Boolean), { x: 1.16, y: 3.08, w: 7.67, h: 0.72, margin: 0, isTextBox: true, align: "center", valign: "middle", lineSpacingMultiple: 1.05 });
|
|
344
370
|
if (body) s.addText(body, {
|
|
345
|
-
x: 1.
|
|
346
|
-
fontFace: F.reg, fontSize:
|
|
371
|
+
x: 1.16, y: 3.90, w: 7.67, h: 0.62, margin: 0, isTextBox: true,
|
|
372
|
+
fontFace: F.reg, fontSize: autoSize(body, 7.67, 0.62, { family: F.reg, fontSize: 16, min: 11, lineSpacingMultiple: 1.2 }),
|
|
373
|
+
color: T.bodyDark, align: "center", valign: "top", lineSpacingMultiple: 1.2,
|
|
347
374
|
});
|
|
348
375
|
if (url) s.addText(url, {
|
|
349
|
-
x: 1.
|
|
350
|
-
fontFace: F.reg, fontSize: 10,
|
|
376
|
+
x: 1.16, y: 4.62, w: 7.67, h: 0.24, margin: 0, isTextBox: true,
|
|
377
|
+
fontFace: F.reg, fontSize: 10, color: T.mutedDark, align: "center", valign: "middle",
|
|
351
378
|
});
|
|
352
379
|
return s;
|
|
353
380
|
}
|
|
@@ -998,12 +1025,170 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
|
|
|
998
1025
|
/* ------------------------------------------------------------------ export */
|
|
999
1026
|
|
|
1000
1027
|
|
|
1028
|
+
|
|
1029
|
+
/* ------------------------------------------------- guideline layouts (0.9) */
|
|
1030
|
+
/* Measured from Vivicta_Session_Deck: the slides a sales session adds to the
|
|
1031
|
+
standard deck — agenda, team, demo objectives, delivery columns, co-delivery. */
|
|
1032
|
+
|
|
1033
|
+
/** Agenda — 2–5 numbered cards in a row (S03). items: [{ title, body }] */
|
|
1034
|
+
function agenda(pres, { tag, titleText, subtitleText, items, dark = true, deck, n, total }) {
|
|
1035
|
+
const s = pres.addSlide();
|
|
1036
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1037
|
+
const th = theme(dark);
|
|
1038
|
+
const top = contentTop(titleText, subtitleText);
|
|
1039
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
1040
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.3 });
|
|
1041
|
+
const list = items.slice(0, 5);
|
|
1042
|
+
const gap = 0.22, w = (9.17 - gap * (list.length - 1)) / list.length, cy = 1.62, ch = 2.41;
|
|
1043
|
+
list.forEach((it, i) => {
|
|
1044
|
+
const cx = 0.42 + i * (w + gap);
|
|
1045
|
+
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,
|
|
1048
|
+
fontFace: F.semi, fontSize: 8, color: T.accentSoft, valign: "middle",
|
|
1049
|
+
});
|
|
1050
|
+
s.addText(it.title, {
|
|
1051
|
+
x: cx + 0.15, y: cy + 0.44, w: w - 0.3, h: 0.5, margin: 0, isTextBox: true,
|
|
1052
|
+
fontFace: F.semi, fontSize: 11, color: th.ink, valign: "top", lineSpacingMultiple: 1.15,
|
|
1053
|
+
});
|
|
1054
|
+
if (it.body) s.addText(it.body, {
|
|
1055
|
+
x: cx + 0.15, y: cy + 1.0, w: w - 0.3, h: ch - 1.15, margin: 0, isTextBox: true,
|
|
1056
|
+
fontFace: F.reg, fontSize: autoSize(it.body, w - 0.3, ch - 1.15, { family: F.reg, fontSize: 8.5, min: 7, lineSpacingMultiple: 1.3 }),
|
|
1057
|
+
color: th.body, valign: "top", lineSpacingMultiple: 1.3,
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1061
|
+
return s;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** Team — 2–4 people with a headshot slot (S02). items: [{ name, role, initials }] */
|
|
1065
|
+
function team(pres, { tag, titleText, items, dark = true, deck, n, total }) {
|
|
1066
|
+
const s = pres.addSlide();
|
|
1067
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1068
|
+
const th = theme(dark);
|
|
1069
|
+
title(s, titleText, { dark, y: 0.70, h: 0.5 });
|
|
1070
|
+
const list = items.slice(0, 4);
|
|
1071
|
+
const cw = 2.14, ch = 2.34, gap = 0.20;
|
|
1072
|
+
const x0 = (10 - (cw * list.length + gap * (list.length - 1))) / 2;
|
|
1073
|
+
list.forEach((it, i) => {
|
|
1074
|
+
const cx = x0 + i * (cw + gap), cy = 1.62;
|
|
1075
|
+
card(s, { x: cx, y: cy, w: cw, h: ch, dark, variant: "solid" });
|
|
1076
|
+
// headshot slot: a circle the user drops a picture onto; initials until then
|
|
1077
|
+
const d = 0.95;
|
|
1078
|
+
s.addShape("ellipse", { x: cx + (cw - d) / 2, y: cy + 0.22, w: d, h: d,
|
|
1079
|
+
fill: { color: dark ? T.cardMuted : T.surface }, line: { color: T.accentSoft, width: 1 } });
|
|
1080
|
+
s.addText(it.initials || it.name.split(/\s+/).map((p) => p[0]).join("").slice(0, 2).toUpperCase(), {
|
|
1081
|
+
x: cx + (cw - d) / 2, y: cy + 0.22, w: d, h: d, margin: 0, isTextBox: true,
|
|
1082
|
+
fontFace: F.semi, fontSize: 18, color: T.accentSoft, align: "center", valign: "middle",
|
|
1083
|
+
});
|
|
1084
|
+
s.addText(it.name, {
|
|
1085
|
+
x: cx + 0.15, y: cy + 1.32, w: cw - 0.3, h: 0.26, margin: 0, isTextBox: true,
|
|
1086
|
+
fontFace: F.semi, fontSize: 11, color: th.ink, align: "center", valign: "middle",
|
|
1087
|
+
});
|
|
1088
|
+
if (it.role) s.addText(it.role, {
|
|
1089
|
+
x: cx + 0.15, y: cy + 1.60, w: cw - 0.3, h: 0.5, margin: 0, isTextBox: true,
|
|
1090
|
+
fontFace: F.reg, fontSize: 8.5, color: T.accentSoft, align: "center", valign: "top", lineSpacingMultiple: 1.25,
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1094
|
+
return s;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Four cards under a title and lede — demo objectives, pillars (S12). items: [{ title, body }] */
|
|
1098
|
+
function fourCards(pres, { tag, titleText, subtitleText, items, dark = true, deck, n, total }) {
|
|
1099
|
+
const s = pres.addSlide();
|
|
1100
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1101
|
+
const th = theme(dark);
|
|
1102
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
1103
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.45 });
|
|
1104
|
+
const list = items.slice(0, 4);
|
|
1105
|
+
const gap = 0.20, w = (9.17 - gap * (list.length - 1)) / list.length, cy = 1.92, ch = 2.55;
|
|
1106
|
+
list.forEach((it, i) => {
|
|
1107
|
+
const cx = 0.42 + i * (w + gap);
|
|
1108
|
+
card(s, { x: cx, y: cy, w, h: ch, dark, variant: "solid" });
|
|
1109
|
+
s.addText(it.title, {
|
|
1110
|
+
x: cx + 0.17, y: cy + 0.22, w: w - 0.34, h: 0.5, margin: 0, isTextBox: true,
|
|
1111
|
+
fontFace: F.semi, fontSize: 11, color: th.ink, valign: "top", lineSpacingMultiple: 1.15,
|
|
1112
|
+
});
|
|
1113
|
+
s.addText(it.body, {
|
|
1114
|
+
x: cx + 0.17, y: cy + 0.82, w: w - 0.34, h: ch - 1.0, margin: 0, isTextBox: true,
|
|
1115
|
+
fontFace: F.reg, fontSize: autoSize(it.body, w - 0.34, ch - 1.0, { family: F.reg, fontSize: 8.5, min: 7, lineSpacingMultiple: 1.3 }),
|
|
1116
|
+
color: th.body, valign: "top", lineSpacingMultiple: 1.3,
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1120
|
+
return s;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Three flat columns with an accent bar — "how we deliver" (S14). columns: [{ heading, lead, body }] */
|
|
1124
|
+
function deliverColumns(pres, { tag, titleText, subtitleText, columns, dark = false, deck, n, total }) {
|
|
1125
|
+
const s = pres.addSlide();
|
|
1126
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1127
|
+
const th = theme(dark);
|
|
1128
|
+
title(s, titleText, { dark, y: 0.58, h: 0.5 });
|
|
1129
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.3 });
|
|
1130
|
+
const list = columns.slice(0, 3);
|
|
1131
|
+
const gap = 0.23, w = (9.17 - gap * (list.length - 1)) / list.length, cy = 1.62, ch = 3.02;
|
|
1132
|
+
list.forEach((c, i) => {
|
|
1133
|
+
const cx = 0.42 + i * (w + gap);
|
|
1134
|
+
s.addShape("rect", { x: cx, y: cy, w, h: ch, fill: { color: dark ? T.cardSolid : T.surface }, line: { width: 0 } });
|
|
1135
|
+
s.addShape("rect", { x: cx, y: cy, w, h: 0.04, fill: { color: T.accent }, line: { width: 0 } });
|
|
1136
|
+
s.addText(c.heading, {
|
|
1137
|
+
x: cx + 0.2, y: cy + 0.15, w: w - 0.4, h: 0.24, margin: 0, isTextBox: true,
|
|
1138
|
+
fontFace: F.semi, fontSize: 11, color: th.ink, valign: "middle",
|
|
1139
|
+
});
|
|
1140
|
+
if (c.lead) s.addText(c.lead, {
|
|
1141
|
+
x: cx + 0.2, y: cy + 0.40, w: w - 0.4, h: 0.32, margin: 0, isTextBox: true,
|
|
1142
|
+
fontFace: F.med, fontSize: 9.5, color: dark ? T.accentSoft : T.accentDeep, valign: "top", lineSpacingMultiple: 1.2,
|
|
1143
|
+
});
|
|
1144
|
+
s.addText(c.body, {
|
|
1145
|
+
x: cx + 0.2, y: cy + 0.80, w: w - 0.4, h: ch - 0.95, margin: 0, isTextBox: true,
|
|
1146
|
+
fontFace: F.reg, fontSize: autoSize(c.body, w - 0.4, ch - 0.95, { family: F.reg, fontSize: 8.5, min: 7, lineSpacingMultiple: 1.3 }),
|
|
1147
|
+
color: th.body, valign: "top", lineSpacingMultiple: 1.3,
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1151
|
+
return s;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Numbered rows in two columns — a co-delivery model, principles (S13). items: [{ title, body }] up to 6 */
|
|
1155
|
+
function coDelivery(pres, { tag, titleText, subtitleText, items, dark = true, deck, n, total }) {
|
|
1156
|
+
const s = pres.addSlide();
|
|
1157
|
+
background(s, dark); chrome(s, { tag, dark });
|
|
1158
|
+
const th = theme(dark);
|
|
1159
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
1160
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.3 });
|
|
1161
|
+
const list = items.slice(0, 6);
|
|
1162
|
+
const cw = 4.17, ch = 0.80, colX = [0.55, 5.04], rows = Math.ceil(list.length / 2), y0 = 1.65, gy = 0.26;
|
|
1163
|
+
list.forEach((it, i) => {
|
|
1164
|
+
const col = Math.floor(i / rows), row = i % rows;
|
|
1165
|
+
const cx = colX[col], cy = y0 + row * (ch + gy);
|
|
1166
|
+
card(s, { x: cx, y: cy, w: cw, h: ch, dark, variant: "solid" });
|
|
1167
|
+
s.addText(String(i + 1).padStart(2, "0"), {
|
|
1168
|
+
x: cx + 0.14, y: cy + 0.2, w: 0.35, h: 0.25, margin: 0, isTextBox: true,
|
|
1169
|
+
fontFace: F.semi, fontSize: 11, color: T.accentSoft, valign: "middle",
|
|
1170
|
+
});
|
|
1171
|
+
s.addText(it.title, {
|
|
1172
|
+
x: cx + 0.55, y: cy + 0.12, w: cw - 0.7, h: 0.19, margin: 0, isTextBox: true,
|
|
1173
|
+
fontFace: F.semi, fontSize: 10, color: th.ink, valign: "middle",
|
|
1174
|
+
});
|
|
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,
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
1182
|
+
return s;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1001
1185
|
module.exports = {
|
|
1002
|
-
VERSION: "0.
|
|
1186
|
+
VERSION: "0.9.0",
|
|
1003
1187
|
T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
|
|
1004
1188
|
background, chrome, footer, title, subtitle, kicker, contentTop,
|
|
1005
1189
|
card, iconTile, pill,
|
|
1006
1190
|
cover, divider, closing,
|
|
1191
|
+
agenda, team, fourCards, deliverColumns, coDelivery,
|
|
1007
1192
|
threeUp, fourUpIcon, sixUpIcon, rows, parity, statQuad, statGrid, band,
|
|
1008
1193
|
colCards, featureStats, barRank, bridge, heroQuad, productMap,
|
|
1009
1194
|
|