wand-decks-kit 0.8.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 +461 -0
- package/README.md +137 -0
- package/START-HERE.md +98 -0
- package/assets/README.md +40 -0
- package/assets/_icon-contact-sheet.png +0 -0
- package/assets/bg-cover-dark.png +0 -0
- package/assets/bg-overlay-dark.png +0 -0
- package/assets/bg-statement-dark.png +0 -0
- package/assets/glyphs/README.md +29 -0
- package/assets/glyphs/activity.svg +15 -0
- package/assets/glyphs/atom.svg +17 -0
- package/assets/glyphs/badge-check.svg +16 -0
- package/assets/glyphs/bell.svg +16 -0
- package/assets/glyphs/blocks.svg +16 -0
- package/assets/glyphs/book-open.svg +16 -0
- package/assets/glyphs/bot.svg +20 -0
- package/assets/glyphs/boxes.svg +26 -0
- package/assets/glyphs/brain.svg +23 -0
- package/assets/glyphs/briefcase.svg +16 -0
- package/assets/glyphs/building-2.svg +21 -0
- package/assets/glyphs/calendar.svg +18 -0
- package/assets/glyphs/chart-column.svg +18 -0
- package/assets/glyphs/chart-line.svg +16 -0
- package/assets/glyphs/chart-pie.svg +16 -0
- package/assets/glyphs/circle-check.svg +16 -0
- package/assets/glyphs/clipboard-check.svg +17 -0
- package/assets/glyphs/clock.svg +16 -0
- package/assets/glyphs/cloud.svg +15 -0
- package/assets/glyphs/coins.svg +18 -0
- package/assets/glyphs/compass.svg +16 -0
- package/assets/glyphs/cpu.svg +24 -0
- package/assets/glyphs/database.svg +17 -0
- package/assets/glyphs/eye.svg +16 -0
- package/assets/glyphs/file-check.svg +17 -0
- package/assets/glyphs/filter.svg +15 -0
- package/assets/glyphs/fingerprint.svg +23 -0
- package/assets/glyphs/flag.svg +16 -0
- package/assets/glyphs/gauge.svg +16 -0
- package/assets/glyphs/gavel.svg +19 -0
- package/assets/glyphs/git-branch.svg +18 -0
- package/assets/glyphs/globe.svg +17 -0
- package/assets/glyphs/graduation-cap.svg +17 -0
- package/assets/glyphs/handshake.svg +19 -0
- package/assets/glyphs/hexagon.svg +15 -0
- package/assets/glyphs/index.json +175 -0
- package/assets/glyphs/infinity.svg +15 -0
- package/assets/glyphs/key.svg +17 -0
- package/assets/glyphs/landmark.svg +20 -0
- package/assets/glyphs/layers.svg +17 -0
- package/assets/glyphs/lightbulb.svg +17 -0
- package/assets/glyphs/link.svg +16 -0
- package/assets/glyphs/list-checks.svg +19 -0
- package/assets/glyphs/lock.svg +16 -0
- package/assets/glyphs/map.svg +17 -0
- package/assets/glyphs/megaphone.svg +16 -0
- package/assets/glyphs/message-square.svg +15 -0
- package/assets/glyphs/milestone.svg +17 -0
- package/assets/glyphs/network.svg +19 -0
- package/assets/glyphs/orbit.svg +19 -0
- package/assets/glyphs/package.svg +18 -0
- package/assets/glyphs/plug.svg +18 -0
- package/assets/glyphs/puzzle.svg +15 -0
- package/assets/glyphs/radar.svg +22 -0
- package/assets/glyphs/receipt.svg +17 -0
- package/assets/glyphs/refresh-cw.svg +18 -0
- package/assets/glyphs/rocket.svg +18 -0
- package/assets/glyphs/route.svg +17 -0
- package/assets/glyphs/scale.svg +19 -0
- package/assets/glyphs/search.svg +16 -0
- package/assets/glyphs/server.svg +18 -0
- package/assets/glyphs/settings.svg +16 -0
- package/assets/glyphs/share-2.svg +19 -0
- package/assets/glyphs/shield-check.svg +16 -0
- package/assets/glyphs/shuffle.svg +19 -0
- package/assets/glyphs/sparkles.svg +19 -0
- package/assets/glyphs/square-stack.svg +17 -0
- package/assets/glyphs/target.svg +17 -0
- package/assets/glyphs/trending-up.svg +16 -0
- package/assets/glyphs/triangle-alert.svg +17 -0
- package/assets/glyphs/trophy.svg +20 -0
- package/assets/glyphs/users.svg +18 -0
- package/assets/glyphs/wallet.svg +16 -0
- package/assets/glyphs/wand-sparkles.svg +22 -0
- package/assets/glyphs/workflow.svg +17 -0
- package/assets/glyphs/zap.svg +15 -0
- package/assets/icons/icon-11.png +0 -0
- package/assets/icons/icon-12.png +0 -0
- package/assets/icons/icon-13.png +0 -0
- package/assets/icons/icon-14.png +0 -0
- package/assets/icons/icon-15.png +0 -0
- package/assets/icons/icon-16.png +0 -0
- package/assets/icons/icon-17.png +0 -0
- package/assets/icons/icon-18.png +0 -0
- package/assets/icons/icon-20.png +0 -0
- package/assets/icons/icon-23.png +0 -0
- package/assets/icons/icon-24.png +0 -0
- package/assets/icons/icon-25.png +0 -0
- package/assets/icons/icon-27.png +0 -0
- package/assets/icons/icon-28.png +0 -0
- package/assets/icons/icon-30.png +0 -0
- package/assets/icons/icon-31.png +0 -0
- package/assets/icons/icon-32.png +0 -0
- package/assets/icons/icon-33.png +0 -0
- package/assets/icons/icon-34.png +0 -0
- package/assets/icons/icon-35.png +0 -0
- package/assets/icons/icon-36.png +0 -0
- package/assets/icons/icon-37.png +0 -0
- package/assets/icons/icon-39.png +0 -0
- package/assets/icons/icon-40.png +0 -0
- package/assets/icons/icon-41.png +0 -0
- package/assets/icons/icon-43.png +0 -0
- package/assets/icons/icon-44.png +0 -0
- package/assets/icons/icon-45.png +0 -0
- package/assets/icons/icon-46.png +0 -0
- package/assets/icons/icon-48.png +0 -0
- package/assets/icons/icon-49.png +0 -0
- package/assets/icons/icon-50.png +0 -0
- package/assets/icons/icon-51.png +0 -0
- package/assets/icons/icon-52.png +0 -0
- package/assets/icons/icon-53.png +0 -0
- package/assets/icons/icon-9.png +0 -0
- package/assets/mark-dark.png +0 -0
- package/assets/mark-white.png +0 -0
- package/assets/orb-cover.png +0 -0
- package/assets/wordmark-dark.png +0 -0
- package/assets/wordmark-white.png +0 -0
- package/build.js +192 -0
- package/build_example.js +91 -0
- package/build_infinite.js +283 -0
- package/docs/LAYOUTS.md +86 -0
- package/docs/PIPELINE.md +116 -0
- package/docs/SPEC.md +89 -0
- package/embed_fonts.py +193 -0
- package/examples/template.json +164 -0
- package/fonts/Geist-Bold.ttf +0 -0
- package/fonts/Geist-BoldItalic.ttf +0 -0
- package/fonts/Geist-ExtraBold.ttf +0 -0
- package/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/fonts/Geist-Italic.ttf +0 -0
- package/fonts/Geist-Light.ttf +0 -0
- package/fonts/Geist-LightItalic.ttf +0 -0
- package/fonts/Geist-Medium.ttf +0 -0
- package/fonts/Geist-MediumItalic.ttf +0 -0
- package/fonts/Geist-Regular.ttf +0 -0
- package/fonts/Geist-SemiBold.ttf +0 -0
- package/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/lib/README.md +23 -0
- package/lib/brand.js +51 -0
- package/lib/glyphs.js +87 -0
- package/lib/layouts_client.js +212 -0
- package/lib/layouts_consult.js +301 -0
- package/lib/layouts_dense.js +361 -0
- package/lib/measure.js +109 -0
- package/lib/pdf.js +517 -0
- package/lib/plan.js +305 -0
- package/lib/render_pdf.js +203 -0
- package/lib/schema.js +399 -0
- package/lib/shared.js +15 -0
- package/lib/trace.js +88 -0
- package/lib/ttf.py +167 -0
- package/lib/wandxml.py +37 -0
- package/package.json +17 -0
- package/preview/preview.css +112 -0
- package/preview/preview.html +24 -0
- package/preview/preview.js +169 -0
- package/qa_coverage.py +120 -0
- package/requirements.txt +9 -0
- package/tools/extract.py +199 -0
- package/wand.js +397 -0
- package/wand_kit.js +1016 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/layouts_dense.js — layouts for dense enterprise decks.
|
|
3
|
+
*
|
|
4
|
+
* The base kit was measured off Infinite Labor, a narrative investor deck.
|
|
5
|
+
* Dense client decks carry 30-50 shapes a slide: roadmaps, layered
|
|
6
|
+
* architecture, categorised matrices, proof-point walls. These are those.
|
|
7
|
+
*
|
|
8
|
+
* Loaded and re-exported by wand_kit.js — call them as W.phaseCards(...) etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = function attach(W) {
|
|
12
|
+
const { T, F, COLS, ASSETS, radius, theme, autoSize, contentTop,
|
|
13
|
+
background, chrome, footer, title, subtitle, kicker,
|
|
14
|
+
card, iconTile, pill } = W;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* L14 phaseCards — a 4-step roadmap. Each card carries a step number, a
|
|
18
|
+
* period, a title and a bullet list. Card 1 is the emphasis card.
|
|
19
|
+
* phases: [{ period, title, bullets: [] }]
|
|
20
|
+
*/
|
|
21
|
+
function phaseCards(pres, { tag, titleText, subtitleText, phases, dark = true, deck, n, total }) {
|
|
22
|
+
const s = pres.addSlide();
|
|
23
|
+
background(s, dark);
|
|
24
|
+
chrome(s, { tag, dark });
|
|
25
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
26
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.40 });
|
|
27
|
+
const th = theme(dark);
|
|
28
|
+
const xs = [0.41, 2.71, 5.01, 7.31], w = 2.20;
|
|
29
|
+
const top = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.30 });
|
|
30
|
+
phases.slice(0, 4).forEach((ph, i) => {
|
|
31
|
+
const x = xs[i], y = top, h = 4.96 - top;
|
|
32
|
+
card(s, { x, y, w, h, dark, variant: i === 0 ? "emph" : "glass" });
|
|
33
|
+
const ink = i === 0 ? T.white : th.ink;
|
|
34
|
+
s.addText(String(i + 1), {
|
|
35
|
+
x: x + 0.20, y: y + 0.16, w: 0.60, h: 0.36, margin: 0, isTextBox: true,
|
|
36
|
+
fontFace: F.semi, fontSize: 20, color: i === 0 ? T.accentSoft : T.accent, valign: "middle",
|
|
37
|
+
});
|
|
38
|
+
s.addText(ph.period, {
|
|
39
|
+
x: x + 0.20, y: y + 0.56, w: w - 0.40, h: 0.22, margin: 0, isTextBox: true,
|
|
40
|
+
fontFace: F.med, fontSize: 9, color: i === 0 ? T.accentPale : T.accentDeep, valign: "middle",
|
|
41
|
+
});
|
|
42
|
+
s.addText(ph.title, {
|
|
43
|
+
x: x + 0.20, y: y + 0.80, w: w - 0.40, h: 0.30, margin: 0, isTextBox: true,
|
|
44
|
+
fontFace: F.semi,
|
|
45
|
+
fontSize: autoSize(ph.title, w - 0.40, 0.30, { family: F.semi, fontSize: 11, min: 9 }),
|
|
46
|
+
color: ink, valign: "middle",
|
|
47
|
+
});
|
|
48
|
+
s.addText(
|
|
49
|
+
ph.bullets.slice(0, 5).map((b, j, arr) => ({
|
|
50
|
+
text: b, options: { bullet: { code: "2022" }, breakLine: j !== arr.length - 1 },
|
|
51
|
+
})),
|
|
52
|
+
{ x: x + 0.22, y: y + 1.18, w: w - 0.42, h: h - 1.32, margin: 0, isTextBox: true,
|
|
53
|
+
fontFace: F.reg, fontSize: 8, color: i === 0 ? T.bodyDarkAlt : th.body,
|
|
54
|
+
valign: "top", paraSpaceAfter: 3, lineSpacingMultiple: 1.15 }
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* numberedCards — 2-4 cards each led by a big ordinal. For "three strategic
|
|
63
|
+
* priorities" style slides where the number is the organising device.
|
|
64
|
+
* items: [{ eyebrow, number, title, badge, body }]
|
|
65
|
+
*/
|
|
66
|
+
function numberedCards(pres, { tag, titleText, subtitleText, items, dark = true, deck, n, total }) {
|
|
67
|
+
const s = pres.addSlide();
|
|
68
|
+
background(s, dark);
|
|
69
|
+
chrome(s, { tag, dark });
|
|
70
|
+
title(s, titleText, { dark, y: 0.62, h: 0.5 });
|
|
71
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.46 });
|
|
72
|
+
const th = theme(dark);
|
|
73
|
+
const three = items.length >= 3;
|
|
74
|
+
const w = three ? 2.90 : 4.25;
|
|
75
|
+
const xs = three ? COLS["3wide"].x : COLS[2].x;
|
|
76
|
+
const top = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.32 });
|
|
77
|
+
items.slice(0, 3).forEach((it, i) => {
|
|
78
|
+
const x = xs[i], y = top, h = 4.96 - top;
|
|
79
|
+
card(s, { x, y, w, h, dark, variant: it.emph ? "emph" : "glass" });
|
|
80
|
+
if (it.eyebrow) s.addText(it.eyebrow.toUpperCase(), {
|
|
81
|
+
x: x + 0.24, y: y + 0.22, w: w - 0.48, h: 0.18, margin: 0, isTextBox: true,
|
|
82
|
+
fontFace: F.med, fontSize: 7.5, color: th.eyebrow, valign: "middle",
|
|
83
|
+
});
|
|
84
|
+
s.addText(String(it.number), {
|
|
85
|
+
x: x + 0.24, y: y + 0.42, w: w - 0.48, h: 0.52, margin: 0, isTextBox: true,
|
|
86
|
+
fontFace: F.xbold,
|
|
87
|
+
fontSize: autoSize(String(it.number), w - 0.48, 0.52, { family: F.xbold, fontSize: 30, min: 14 }),
|
|
88
|
+
color: T.accent, valign: "middle",
|
|
89
|
+
});
|
|
90
|
+
s.addText(it.title, {
|
|
91
|
+
x: x + 0.24, y: y + 1.02, w: w - 0.48, h: 0.52, margin: 0, isTextBox: true,
|
|
92
|
+
fontFace: F.semi,
|
|
93
|
+
fontSize: autoSize(it.title, w - 0.48, 0.52, { family: F.semi, fontSize: 14, min: 11, lineSpacingMultiple: 1.15 }),
|
|
94
|
+
color: th.ink, valign: "top", lineSpacingMultiple: 1.15,
|
|
95
|
+
});
|
|
96
|
+
if (it.badge) s.addText(it.badge, {
|
|
97
|
+
x: x + 0.24, y: y + 1.58, w: w - 0.48, h: 0.20, margin: 0, isTextBox: true,
|
|
98
|
+
fontFace: F.med, fontSize: 8.5, italic: true, color: T.accentDeep, valign: "middle",
|
|
99
|
+
});
|
|
100
|
+
s.addText(it.body, {
|
|
101
|
+
x: x + 0.24, y: y + 1.86, w: w - 0.48, h: h - 2.00, margin: 0, isTextBox: true,
|
|
102
|
+
fontFace: F.reg, fontSize: 8.5, color: th.body, valign: "top", lineSpacingMultiple: 1.25,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* archStack — a layered architecture diagram: a top actor band, a governed
|
|
111
|
+
* middle layer holding N modules, a row of source cards, and an outcome band.
|
|
112
|
+
* This is the shape every "we sit between your people and your vendors" slide
|
|
113
|
+
* wants, and nothing in the catalog covers it.
|
|
114
|
+
*
|
|
115
|
+
* top: { label, body }
|
|
116
|
+
* middle: { label, modules: [{ title, body }] }
|
|
117
|
+
* sources: { eyebrow, cards: [{ title, body, ours }] }
|
|
118
|
+
* outcome: string
|
|
119
|
+
*/
|
|
120
|
+
function archStack(pres, { tag, titleText, subtitleText, top, middle, note, sources, outcome, dark = true, deck, n, total }) {
|
|
121
|
+
const s = pres.addSlide();
|
|
122
|
+
background(s, dark);
|
|
123
|
+
chrome(s, { tag, dark });
|
|
124
|
+
title(s, titleText, { dark, y: 0.58, h: 0.44 });
|
|
125
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.04, h: 0.28 });
|
|
126
|
+
const th = theme(dark);
|
|
127
|
+
const L = 0.37, WIDTH = 9.25;
|
|
128
|
+
|
|
129
|
+
// The stack is laid out from the measured content top down to the y5.05
|
|
130
|
+
// hard limit, so a two-line title can never push the outcome band into the
|
|
131
|
+
// footer. Heights are proportional; only the start moves.
|
|
132
|
+
const topY = contentTop(titleText, subtitleText, { titleY: 0.58, gap: 0.20 });
|
|
133
|
+
const BOTTOM = 5.02;
|
|
134
|
+
const outcomeH = outcome ? 0.40 : 0;
|
|
135
|
+
const modules = (middle.modules || []).slice(0, 6);
|
|
136
|
+
const hasMods = modules.length > 0;
|
|
137
|
+
const pool = BOTTOM - topY - outcomeH - 0.42; // 3 inter-block gaps
|
|
138
|
+
const bandH = Math.max(0.42, pool * 0.16);
|
|
139
|
+
// With no modules the middle band is just a connector — don't let it balloon.
|
|
140
|
+
let midH = hasMods ? Math.max(1.05, pool * 0.42) : 0.56;
|
|
141
|
+
// One long module body used to drag the whole row down to 5.5pt, which is
|
|
142
|
+
// unreadable rather than merely tight. Grow the band toward what the longest
|
|
143
|
+
// body actually needs, borrowing the height from the source row below.
|
|
144
|
+
if (hasMods) {
|
|
145
|
+
const mwT = (WIDTH - 0.48 - 0.16 * (modules.length - 1)) / modules.length;
|
|
146
|
+
const worst = modules.reduce((a, m) => ((m.body || "").length > (a.body || "").length ? m : a), modules[0]);
|
|
147
|
+
const cap = pool * 0.56;
|
|
148
|
+
while (midH < cap &&
|
|
149
|
+
autoSize(worst.body || "", mwT - 0.20, midH - 0.88,
|
|
150
|
+
{ family: F.reg, fontSize: 7.5, min: 5.5, lineSpacingMultiple: 1.15 }) < 7) {
|
|
151
|
+
midH += 0.05;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const srcH = Math.max(0.92, pool * (hasMods ? 0.42 : 0.62));
|
|
155
|
+
const my = topY + bandH + 0.14;
|
|
156
|
+
const sy = my + midH + 0.36;
|
|
157
|
+
// Clamp: the outcome band must land inside the content limit, so the source
|
|
158
|
+
// row gives up whatever height is needed rather than pushing past it.
|
|
159
|
+
const oy = Math.min(sy + srcH + 0.14, BOTTOM - outcomeH);
|
|
160
|
+
const srcHc = outcome ? oy - 0.14 - sy : srcH;
|
|
161
|
+
|
|
162
|
+
// actor band
|
|
163
|
+
s.addShape("roundRect", {
|
|
164
|
+
x: L, y: topY, w: WIDTH, h: bandH, rectRadius: radius(WIDTH, bandH),
|
|
165
|
+
fill: { color: dark ? T.cardMuted : T.surface },
|
|
166
|
+
line: { color: dark ? T.cardMutedStroke : T.divider, width: 1 },
|
|
167
|
+
});
|
|
168
|
+
s.addText(top.label.toUpperCase(), {
|
|
169
|
+
x: L + 0.24, y: topY, w: 2.6, h: bandH, margin: 0, isTextBox: true,
|
|
170
|
+
fontFace: F.semi, fontSize: 9, color: T.accent, valign: "middle",
|
|
171
|
+
});
|
|
172
|
+
s.addText(top.body, {
|
|
173
|
+
x: L + 2.90, y: topY, w: WIDTH - 3.20, h: bandH, margin: 0, isTextBox: true,
|
|
174
|
+
fontFace: F.reg, fontSize: 8.5, color: th.body, valign: "middle",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// governed middle layer
|
|
178
|
+
const mh = midH;
|
|
179
|
+
s.addShape("roundRect", {
|
|
180
|
+
x: L, y: my, w: WIDTH, h: mh, rectRadius: radius(WIDTH, mh),
|
|
181
|
+
fill: { color: dark ? T.cardEmphFlat : T.cardLight },
|
|
182
|
+
line: { color: T.accent, width: 1.25 },
|
|
183
|
+
});
|
|
184
|
+
const mods = modules;
|
|
185
|
+
const mw = mods.length ? (WIDTH - 0.48 - 0.16 * (mods.length - 1)) / mods.length : 0;
|
|
186
|
+
s.addText(middle.label, {
|
|
187
|
+
x: L + 0.24, y: my + (mods.length ? 0.12 : 0), w: WIDTH - 0.48,
|
|
188
|
+
h: mods.length ? 0.24 : midH, margin: 0, isTextBox: true,
|
|
189
|
+
fontFace: F.semi, fontSize: mods.length ? 9 : 11,
|
|
190
|
+
color: dark ? T.white : T.accentDeep,
|
|
191
|
+
align: mods.length ? "left" : "center", valign: "middle",
|
|
192
|
+
});
|
|
193
|
+
mods.forEach((m, i) => {
|
|
194
|
+
const mx = L + 0.24 + i * (mw + 0.16);
|
|
195
|
+
const modH = mh - 0.52;
|
|
196
|
+
s.addShape("roundRect", {
|
|
197
|
+
x: mx, y: my + 0.44, w: mw, h: modH, rectRadius: radius(mw, modH),
|
|
198
|
+
fill: { color: dark ? "2A2160" : T.surface },
|
|
199
|
+
line: { color: dark ? T.cardMutedStroke : T.divider, width: 0.75 },
|
|
200
|
+
});
|
|
201
|
+
s.addText(m.title, {
|
|
202
|
+
x: mx + 0.12, y: my + 0.54, w: mw - 0.24, h: 0.24, margin: 0, isTextBox: true,
|
|
203
|
+
fontFace: F.semi,
|
|
204
|
+
fontSize: autoSize(m.title, mw - 0.24, 0.24, { family: F.semi, fontSize: 9.5, min: 7.5 }),
|
|
205
|
+
color: dark ? T.white : T.inkLight, align: "center", valign: "middle",
|
|
206
|
+
});
|
|
207
|
+
s.addText(m.body, {
|
|
208
|
+
x: mx + 0.10, y: my + 0.80, w: mw - 0.20, h: mh - 0.88, margin: 0, isTextBox: true,
|
|
209
|
+
fontFace: F.reg,
|
|
210
|
+
fontSize: autoSize(m.body, mw - 0.20, mh - 0.88, { family: F.reg, fontSize: 7.5, min: 5.5, lineSpacingMultiple: 1.15 }),
|
|
211
|
+
color: th.body,
|
|
212
|
+
align: "center", valign: "top", lineSpacingMultiple: 1.15,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// source cards
|
|
217
|
+
const sh = srcHc;
|
|
218
|
+
if (note) s.addText(note, {
|
|
219
|
+
x: L, y: my + midH + 0.04, w: WIDTH, h: 0.28, margin: 0, isTextBox: true,
|
|
220
|
+
fontFace: F.reg,
|
|
221
|
+
fontSize: autoSize(note, WIDTH, 0.28, { family: F.reg, fontSize: 8, min: 6 }),
|
|
222
|
+
color: th.body, align: "center", valign: "middle",
|
|
223
|
+
});
|
|
224
|
+
const ebW = sources && sources.eyebrow && sources.eyebrow.length > 34 ? WIDTH : 2.0;
|
|
225
|
+
if (sources && sources.eyebrow) s.addText(sources.eyebrow.toUpperCase(), {
|
|
226
|
+
x: L, y: sy - 0.24, w: ebW, h: 0.18, margin: 0, isTextBox: true,
|
|
227
|
+
fontFace: F.med,
|
|
228
|
+
fontSize: autoSize(sources.eyebrow.toUpperCase(), ebW, 0.18, { family: F.med, fontSize: 7.5, min: 5.5 }),
|
|
229
|
+
color: th.eyebrow, valign: "middle",
|
|
230
|
+
});
|
|
231
|
+
const sc = ((sources && sources.cards) || []).slice(0, 4);
|
|
232
|
+
const sw = (WIDTH - 0.22 * (sc.length - 1)) / sc.length;
|
|
233
|
+
sc.forEach((c, i) => {
|
|
234
|
+
const cx = L + i * (sw + 0.22);
|
|
235
|
+
card(s, { x: cx, y: sy, w: sw, h: sh, dark, variant: c.ours === false ? "muted" : "glass" });
|
|
236
|
+
s.addText(c.title, {
|
|
237
|
+
x: cx + 0.18, y: sy + 0.14, w: sw - 0.36, h: 0.26, margin: 0, isTextBox: true,
|
|
238
|
+
fontFace: F.semi,
|
|
239
|
+
fontSize: autoSize(c.title, sw - 0.36, 0.26, { family: F.semi, fontSize: 10.5, min: 8.5 }),
|
|
240
|
+
color: th.ink, valign: "middle",
|
|
241
|
+
});
|
|
242
|
+
s.addText(c.body, {
|
|
243
|
+
x: cx + 0.18, y: sy + 0.44, w: sw - 0.36, h: 0.56, margin: 0, isTextBox: true,
|
|
244
|
+
fontFace: F.reg, fontSize: 8, color: th.body, valign: "top", lineSpacingMultiple: 1.2,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// outcome band
|
|
249
|
+
if (outcome) {
|
|
250
|
+
s.addShape("roundRect", {
|
|
251
|
+
x: L, y: oy, w: WIDTH, h: outcomeH, rectRadius: radius(WIDTH, outcomeH),
|
|
252
|
+
fill: { color: dark ? T.cardMuted : T.surface },
|
|
253
|
+
line: { color: dark ? T.cardMutedStroke : T.divider, width: 1 },
|
|
254
|
+
});
|
|
255
|
+
s.addText(outcome, {
|
|
256
|
+
x: L + 0.24, y: oy, w: WIDTH - 0.48, h: outcomeH, margin: 0, isTextBox: true,
|
|
257
|
+
fontFace: F.med, fontSize: 8.5, color: dark ? T.bodyDarkAlt : T.accentDeep, valign: "middle",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
261
|
+
return s;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* matrixGrid — categorised cells: N category columns, each holding M items.
|
|
266
|
+
* For capability maps and use-case inventories. Handles 3-4 columns x 3 rows.
|
|
267
|
+
* groups: [{ category, items: [{ title, body }] }]
|
|
268
|
+
*/
|
|
269
|
+
function matrixGrid(pres, { tag, titleText, subtitleText, groups, kickerText, dark = false, deck, n, total }) {
|
|
270
|
+
const s = pres.addSlide();
|
|
271
|
+
background(s, dark);
|
|
272
|
+
chrome(s, { tag, dark });
|
|
273
|
+
title(s, titleText, { dark, y: 0.58, h: 0.44 });
|
|
274
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.04, h: 0.30 });
|
|
275
|
+
const th = theme(dark);
|
|
276
|
+
const g = groups.slice(0, 4);
|
|
277
|
+
const L = 0.37, WIDTH = 9.25, GAP = 0.20;
|
|
278
|
+
const cw = (WIDTH - GAP * (g.length - 1)) / g.length;
|
|
279
|
+
const rows = Math.max(...g.map((x) => x.items.length));
|
|
280
|
+
const top = contentTop(titleText, subtitleText, { titleY: 0.58, gap: 0.42 });
|
|
281
|
+
const avail = 4.94 - top;
|
|
282
|
+
const step = avail / rows, rh = Math.min(0.92, step - 0.10);
|
|
283
|
+
g.forEach((grp, i) => {
|
|
284
|
+
const x = L + i * (cw + GAP);
|
|
285
|
+
s.addText(grp.category.toUpperCase(), {
|
|
286
|
+
x, y: top - 0.30, w: cw, h: 0.22, margin: 0, isTextBox: true,
|
|
287
|
+
fontFace: F.semi, fontSize: 8.5, color: T.accent, valign: "middle",
|
|
288
|
+
});
|
|
289
|
+
grp.items.slice(0, rows).forEach((it, j) => {
|
|
290
|
+
const y = top + j * step;
|
|
291
|
+
card(s, { x, y, w: cw, h: rh, dark });
|
|
292
|
+
s.addText(it.title, {
|
|
293
|
+
x: x + 0.16, y: y + 0.12, w: cw - 0.32, h: 0.24, margin: 0, isTextBox: true,
|
|
294
|
+
fontFace: F.semi,
|
|
295
|
+
fontSize: autoSize(it.title, cw - 0.32, 0.24, { family: F.semi, fontSize: 10, min: 8 }),
|
|
296
|
+
color: th.ink, valign: "middle",
|
|
297
|
+
});
|
|
298
|
+
s.addText(it.body, {
|
|
299
|
+
x: x + 0.16, y: y + 0.38, w: cw - 0.32, h: rh - 0.48, margin: 0, isTextBox: true,
|
|
300
|
+
fontFace: F.reg, fontSize: 7.5, color: th.body, valign: "top", lineSpacingMultiple: 1.15,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
if (kickerText) kicker(s, kickerText, { dark, y: 4.94 });
|
|
305
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
306
|
+
return s;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* logoBands — stat cards over one or two labelled name rows. For proof-point
|
|
311
|
+
* slides: customers, investors, partners. Names are text, not logo images,
|
|
312
|
+
* because the source rarely ships usable transparent logos.
|
|
313
|
+
* stats: [{ value, caption }] bands: [{ label, names, note }]
|
|
314
|
+
*/
|
|
315
|
+
function logoBands(pres, { tag, titleText, subtitleText, stats, bands, dark = true, deck, n, total }) {
|
|
316
|
+
const s = pres.addSlide();
|
|
317
|
+
background(s, dark);
|
|
318
|
+
chrome(s, { tag, dark });
|
|
319
|
+
title(s, titleText, { dark, y: 0.62, h: 0.44 });
|
|
320
|
+
if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.08, h: 0.30 });
|
|
321
|
+
const th = theme(dark);
|
|
322
|
+
const { w, x } = COLS["4stat"];
|
|
323
|
+
stats.slice(0, 4).forEach((it, i) => {
|
|
324
|
+
const cx = x[i], cy = 1.60;
|
|
325
|
+
card(s, { x: cx, y: cy, w, h: 1.34, dark });
|
|
326
|
+
s.addText(it.value, {
|
|
327
|
+
x: cx + 0.18, y: cy + 0.14, w: w - 0.36, h: 0.42, margin: 0, isTextBox: true,
|
|
328
|
+
fontFace: F.semi,
|
|
329
|
+
fontSize: autoSize(it.value, w - 0.36, 0.42, { family: F.semi, fontSize: 21, min: 14 }),
|
|
330
|
+
color: T.accent, valign: "middle",
|
|
331
|
+
});
|
|
332
|
+
s.addText(it.caption, {
|
|
333
|
+
x: cx + 0.18, y: cy + 0.62, w: w - 0.36, h: 0.58, margin: 0, isTextBox: true,
|
|
334
|
+
fontFace: F.reg, fontSize: 8.5, color: th.body, valign: "top", lineSpacingMultiple: 1.2,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
bands.slice(0, 2).forEach((b, i) => {
|
|
338
|
+
const y = 3.20 + i * 0.96, h = b.note ? 0.84 : 0.62;
|
|
339
|
+
card(s, { x: 0.37, y, w: 9.25, h, dark, variant: i === 0 ? "glass" : "muted" });
|
|
340
|
+
s.addText(b.label.toUpperCase(), {
|
|
341
|
+
x: 0.61, y: y + 0.10, w: 3.0, h: 0.20, margin: 0, isTextBox: true,
|
|
342
|
+
fontFace: F.med, fontSize: 7.5, color: th.eyebrow, valign: "middle",
|
|
343
|
+
});
|
|
344
|
+
s.addText(b.names, {
|
|
345
|
+
x: 0.61, y: y + 0.28, w: 8.77, h: 0.26, margin: 0, isTextBox: true,
|
|
346
|
+
fontFace: F.semi,
|
|
347
|
+
fontSize: autoSize(b.names, 8.77, 0.26, { family: F.semi, fontSize: 11, min: 8 }),
|
|
348
|
+
color: th.ink, valign: "middle",
|
|
349
|
+
});
|
|
350
|
+
if (b.note) s.addText(b.note, {
|
|
351
|
+
x: 0.61, y: y + 0.54, w: 8.77, h: 0.24, margin: 0, isTextBox: true,
|
|
352
|
+
fontFace: F.reg, fontSize: 8, italic: true, color: th.body, valign: "middle",
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
if (deck) footer(s, { deck, n, total, dark });
|
|
356
|
+
return s;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Object.assign(W, { phaseCards, numberedCards, archStack, matrixGrid, logoBands });
|
|
360
|
+
return W;
|
|
361
|
+
};
|
package/lib/measure.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/measure.js — real text metrics against the actual Geist faces.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: §9 says LibreOffice ignores embedded fonts and substitutes
|
|
5
|
+
* DejaVu, so QA renders over-report overflow and lose all weight information.
|
|
6
|
+
* That makes visual QA unreliable for exactly the thing it looks like it's
|
|
7
|
+
* checking. This module measures the real font, so overflow is a build-time
|
|
8
|
+
* error rather than a judgement call about a screenshot.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const FONTS = path.join(__dirname, "..", "fonts");
|
|
13
|
+
|
|
14
|
+
/* family + style -> ttf stem, mirroring embed_fonts.py */
|
|
15
|
+
const FACE = {
|
|
16
|
+
"Geist": { r: "Geist-Regular", b: "Geist-Bold", i: "Geist-Italic", bi: "Geist-BoldItalic" },
|
|
17
|
+
"Geist Medium": { r: "Geist-Medium", b: "Geist-SemiBold", i: "Geist-MediumItalic", bi: "Geist-SemiBoldItalic" },
|
|
18
|
+
"Geist SemiBold": { r: "Geist-SemiBold", b: "Geist-ExtraBold", i: "Geist-SemiBoldItalic", bi: "Geist-ExtraBoldItalic" },
|
|
19
|
+
"Geist ExtraBold": { r: "Geist-ExtraBold", b: "Geist-ExtraBold", i: "Geist-ExtraBoldItalic", bi: "Geist-ExtraBoldItalic" },
|
|
20
|
+
"Geist Light": { r: "Geist-Light", b: "Geist-Medium", i: "Geist-LightItalic", bi: "Geist-MediumItalic" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let FONTKIT = null;
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
function face(family = "Geist", { bold = false, italic = false } = {}) {
|
|
26
|
+
if (!FONTKIT) throw new Error("measure.js was loaded without fontkit — require it through the kit's lib/shared.js");
|
|
27
|
+
const set = FACE[family] || FACE["Geist"];
|
|
28
|
+
const stem = set[bold && italic ? "bi" : bold ? "b" : italic ? "i" : "r"];
|
|
29
|
+
if (!cache.has(stem)) cache.set(stem, FONTKIT.openSync(path.join(FONTS, stem + ".ttf")));
|
|
30
|
+
return cache.get(stem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Width of a single line, in inches. */
|
|
34
|
+
function widthIn(text, { family, fontSize, bold, italic } = {}) {
|
|
35
|
+
if (!text) return 0;
|
|
36
|
+
const f = face(family, { bold, italic });
|
|
37
|
+
const run = f.layout(String(text));
|
|
38
|
+
return (run.advanceWidth / f.unitsPerEm) * (fontSize / 72);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Greedy word wrap at a given box width. Returns the lines it would occupy.
|
|
43
|
+
* Honours explicit \n. Matches how PowerPoint breaks closely enough to catch
|
|
44
|
+
* the failures that matter (a line too many, a word that can't fit at all).
|
|
45
|
+
*/
|
|
46
|
+
function wrap(text, boxWidthIn, opts = {}) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const para of String(text).split("\n")) {
|
|
49
|
+
const words = para.split(/\s+/).filter(Boolean);
|
|
50
|
+
if (!words.length) { out.push(""); continue; }
|
|
51
|
+
let line = words[0];
|
|
52
|
+
for (let i = 1; i < words.length; i++) {
|
|
53
|
+
const test = line + " " + words[i];
|
|
54
|
+
if (widthIn(test, opts) <= boxWidthIn) line = test;
|
|
55
|
+
else { out.push(line); line = words[i]; }
|
|
56
|
+
}
|
|
57
|
+
out.push(line);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Rendered height in inches for text wrapped into a box. */
|
|
63
|
+
function heightIn(text, boxWidthIn, opts = {}) {
|
|
64
|
+
const leading = opts.lineSpacingMultiple || 1.2;
|
|
65
|
+
return wrap(text, boxWidthIn, opts).length * (opts.fontSize / 72) * leading;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Does this text fit the box? Returns a verdict object rather than a boolean so
|
|
70
|
+
* callers can report *why*.
|
|
71
|
+
*/
|
|
72
|
+
function fits(text, boxWidthIn, boxHeightIn, opts = {}) {
|
|
73
|
+
const lines = wrap(text, boxWidthIn, opts);
|
|
74
|
+
const h = heightIn(text, boxWidthIn, opts);
|
|
75
|
+
const widest = Math.max(...lines.map((l) => widthIn(l, opts)));
|
|
76
|
+
return {
|
|
77
|
+
ok: h <= boxHeightIn + 1e-6 && widest <= boxWidthIn + 1e-6,
|
|
78
|
+
lines: lines.length,
|
|
79
|
+
heightIn: +h.toFixed(3),
|
|
80
|
+
widestIn: +widest.toFixed(3),
|
|
81
|
+
overflowIn: +Math.max(0, h - boxHeightIn).toFixed(3),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Largest font size at or below `fontSize` that fits the box, stepping down in
|
|
87
|
+
* `step` pt increments but never below `min`. Use this instead of hardcoding a
|
|
88
|
+
* length threshold — it works for any string, in any language.
|
|
89
|
+
*/
|
|
90
|
+
function fitSize(text, boxWidthIn, boxHeightIn, opts = {}) {
|
|
91
|
+
const { fontSize, min = 7, step = 0.5 } = opts;
|
|
92
|
+
for (let s = fontSize; s >= min; s -= step) {
|
|
93
|
+
if (fits(text, boxWidthIn, boxHeightIn, { ...opts, fontSize: s }).ok) return s;
|
|
94
|
+
}
|
|
95
|
+
return min;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Line count only — the common case for "will this title be 2 lines or 3?". */
|
|
99
|
+
const lineCount = (text, boxWidthIn, opts = {}) => wrap(text, boxWidthIn, opts).length;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* This module lives in brand/, outside any kit's `node_modules`, so Node cannot
|
|
103
|
+
* resolve `fontkit` from here. The kit that loads it hands its own copy in.
|
|
104
|
+
* Explicit beats a resolution trick that works until someone moves a directory.
|
|
105
|
+
*/
|
|
106
|
+
module.exports = function create(fontkit) {
|
|
107
|
+
FONTKIT = fontkit;
|
|
108
|
+
return { widthIn, wrap, heightIn, fits, fitSize, lineCount, face };
|
|
109
|
+
};
|