wand-decks-kit 0.8.7 → 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 +31 -3
- 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 +12 -2
- 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
|
@@ -304,13 +304,23 @@ function audit(file, dump) {
|
|
|
304
304
|
console.log(soffice ? "pdftoppm absent — no renders" : "no LibreOffice — no renders; open the deck and look at every slide");
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
console.log(rule("3
|
|
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"));
|
|
308
317
|
let cov = null;
|
|
309
318
|
if (dump && fs.existsSync(dump)) cov = runPython([path.join(KIT, "qa_coverage.py"), dump, file]);
|
|
310
319
|
else console.log("no dump given — skipped. Pass --dump work/visible_dump.txt from `rebuild` to check\n" +
|
|
311
320
|
"every source string survived the edit.");
|
|
312
321
|
|
|
313
|
-
const gates = [["file", fileErrs === 0], ["
|
|
322
|
+
const gates = [["file", fileErrs === 0], ["brand", brand === 0],
|
|
323
|
+
["pdf text", garbled === null ? null : garbled === 0],
|
|
314
324
|
["renders", shots > 0 ? true : null], ["coverage", cov === null ? null : cov === 0]];
|
|
315
325
|
console.log(rule("gates"));
|
|
316
326
|
for (const [n, ok] of gates) console.log(` ${ok === null ? "skip" : ok ? " ok " : "FAIL"} ${n}`);
|
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
|
|