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.
Files changed (170) hide show
  1. package/PROMPT.md +461 -0
  2. package/README.md +137 -0
  3. package/START-HERE.md +98 -0
  4. package/assets/README.md +40 -0
  5. package/assets/_icon-contact-sheet.png +0 -0
  6. package/assets/bg-cover-dark.png +0 -0
  7. package/assets/bg-overlay-dark.png +0 -0
  8. package/assets/bg-statement-dark.png +0 -0
  9. package/assets/glyphs/README.md +29 -0
  10. package/assets/glyphs/activity.svg +15 -0
  11. package/assets/glyphs/atom.svg +17 -0
  12. package/assets/glyphs/badge-check.svg +16 -0
  13. package/assets/glyphs/bell.svg +16 -0
  14. package/assets/glyphs/blocks.svg +16 -0
  15. package/assets/glyphs/book-open.svg +16 -0
  16. package/assets/glyphs/bot.svg +20 -0
  17. package/assets/glyphs/boxes.svg +26 -0
  18. package/assets/glyphs/brain.svg +23 -0
  19. package/assets/glyphs/briefcase.svg +16 -0
  20. package/assets/glyphs/building-2.svg +21 -0
  21. package/assets/glyphs/calendar.svg +18 -0
  22. package/assets/glyphs/chart-column.svg +18 -0
  23. package/assets/glyphs/chart-line.svg +16 -0
  24. package/assets/glyphs/chart-pie.svg +16 -0
  25. package/assets/glyphs/circle-check.svg +16 -0
  26. package/assets/glyphs/clipboard-check.svg +17 -0
  27. package/assets/glyphs/clock.svg +16 -0
  28. package/assets/glyphs/cloud.svg +15 -0
  29. package/assets/glyphs/coins.svg +18 -0
  30. package/assets/glyphs/compass.svg +16 -0
  31. package/assets/glyphs/cpu.svg +24 -0
  32. package/assets/glyphs/database.svg +17 -0
  33. package/assets/glyphs/eye.svg +16 -0
  34. package/assets/glyphs/file-check.svg +17 -0
  35. package/assets/glyphs/filter.svg +15 -0
  36. package/assets/glyphs/fingerprint.svg +23 -0
  37. package/assets/glyphs/flag.svg +16 -0
  38. package/assets/glyphs/gauge.svg +16 -0
  39. package/assets/glyphs/gavel.svg +19 -0
  40. package/assets/glyphs/git-branch.svg +18 -0
  41. package/assets/glyphs/globe.svg +17 -0
  42. package/assets/glyphs/graduation-cap.svg +17 -0
  43. package/assets/glyphs/handshake.svg +19 -0
  44. package/assets/glyphs/hexagon.svg +15 -0
  45. package/assets/glyphs/index.json +175 -0
  46. package/assets/glyphs/infinity.svg +15 -0
  47. package/assets/glyphs/key.svg +17 -0
  48. package/assets/glyphs/landmark.svg +20 -0
  49. package/assets/glyphs/layers.svg +17 -0
  50. package/assets/glyphs/lightbulb.svg +17 -0
  51. package/assets/glyphs/link.svg +16 -0
  52. package/assets/glyphs/list-checks.svg +19 -0
  53. package/assets/glyphs/lock.svg +16 -0
  54. package/assets/glyphs/map.svg +17 -0
  55. package/assets/glyphs/megaphone.svg +16 -0
  56. package/assets/glyphs/message-square.svg +15 -0
  57. package/assets/glyphs/milestone.svg +17 -0
  58. package/assets/glyphs/network.svg +19 -0
  59. package/assets/glyphs/orbit.svg +19 -0
  60. package/assets/glyphs/package.svg +18 -0
  61. package/assets/glyphs/plug.svg +18 -0
  62. package/assets/glyphs/puzzle.svg +15 -0
  63. package/assets/glyphs/radar.svg +22 -0
  64. package/assets/glyphs/receipt.svg +17 -0
  65. package/assets/glyphs/refresh-cw.svg +18 -0
  66. package/assets/glyphs/rocket.svg +18 -0
  67. package/assets/glyphs/route.svg +17 -0
  68. package/assets/glyphs/scale.svg +19 -0
  69. package/assets/glyphs/search.svg +16 -0
  70. package/assets/glyphs/server.svg +18 -0
  71. package/assets/glyphs/settings.svg +16 -0
  72. package/assets/glyphs/share-2.svg +19 -0
  73. package/assets/glyphs/shield-check.svg +16 -0
  74. package/assets/glyphs/shuffle.svg +19 -0
  75. package/assets/glyphs/sparkles.svg +19 -0
  76. package/assets/glyphs/square-stack.svg +17 -0
  77. package/assets/glyphs/target.svg +17 -0
  78. package/assets/glyphs/trending-up.svg +16 -0
  79. package/assets/glyphs/triangle-alert.svg +17 -0
  80. package/assets/glyphs/trophy.svg +20 -0
  81. package/assets/glyphs/users.svg +18 -0
  82. package/assets/glyphs/wallet.svg +16 -0
  83. package/assets/glyphs/wand-sparkles.svg +22 -0
  84. package/assets/glyphs/workflow.svg +17 -0
  85. package/assets/glyphs/zap.svg +15 -0
  86. package/assets/icons/icon-11.png +0 -0
  87. package/assets/icons/icon-12.png +0 -0
  88. package/assets/icons/icon-13.png +0 -0
  89. package/assets/icons/icon-14.png +0 -0
  90. package/assets/icons/icon-15.png +0 -0
  91. package/assets/icons/icon-16.png +0 -0
  92. package/assets/icons/icon-17.png +0 -0
  93. package/assets/icons/icon-18.png +0 -0
  94. package/assets/icons/icon-20.png +0 -0
  95. package/assets/icons/icon-23.png +0 -0
  96. package/assets/icons/icon-24.png +0 -0
  97. package/assets/icons/icon-25.png +0 -0
  98. package/assets/icons/icon-27.png +0 -0
  99. package/assets/icons/icon-28.png +0 -0
  100. package/assets/icons/icon-30.png +0 -0
  101. package/assets/icons/icon-31.png +0 -0
  102. package/assets/icons/icon-32.png +0 -0
  103. package/assets/icons/icon-33.png +0 -0
  104. package/assets/icons/icon-34.png +0 -0
  105. package/assets/icons/icon-35.png +0 -0
  106. package/assets/icons/icon-36.png +0 -0
  107. package/assets/icons/icon-37.png +0 -0
  108. package/assets/icons/icon-39.png +0 -0
  109. package/assets/icons/icon-40.png +0 -0
  110. package/assets/icons/icon-41.png +0 -0
  111. package/assets/icons/icon-43.png +0 -0
  112. package/assets/icons/icon-44.png +0 -0
  113. package/assets/icons/icon-45.png +0 -0
  114. package/assets/icons/icon-46.png +0 -0
  115. package/assets/icons/icon-48.png +0 -0
  116. package/assets/icons/icon-49.png +0 -0
  117. package/assets/icons/icon-50.png +0 -0
  118. package/assets/icons/icon-51.png +0 -0
  119. package/assets/icons/icon-52.png +0 -0
  120. package/assets/icons/icon-53.png +0 -0
  121. package/assets/icons/icon-9.png +0 -0
  122. package/assets/mark-dark.png +0 -0
  123. package/assets/mark-white.png +0 -0
  124. package/assets/orb-cover.png +0 -0
  125. package/assets/wordmark-dark.png +0 -0
  126. package/assets/wordmark-white.png +0 -0
  127. package/build.js +192 -0
  128. package/build_example.js +91 -0
  129. package/build_infinite.js +283 -0
  130. package/docs/LAYOUTS.md +86 -0
  131. package/docs/PIPELINE.md +116 -0
  132. package/docs/SPEC.md +89 -0
  133. package/embed_fonts.py +193 -0
  134. package/examples/template.json +164 -0
  135. package/fonts/Geist-Bold.ttf +0 -0
  136. package/fonts/Geist-BoldItalic.ttf +0 -0
  137. package/fonts/Geist-ExtraBold.ttf +0 -0
  138. package/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  139. package/fonts/Geist-Italic.ttf +0 -0
  140. package/fonts/Geist-Light.ttf +0 -0
  141. package/fonts/Geist-LightItalic.ttf +0 -0
  142. package/fonts/Geist-Medium.ttf +0 -0
  143. package/fonts/Geist-MediumItalic.ttf +0 -0
  144. package/fonts/Geist-Regular.ttf +0 -0
  145. package/fonts/Geist-SemiBold.ttf +0 -0
  146. package/fonts/Geist-SemiBoldItalic.ttf +0 -0
  147. package/lib/README.md +23 -0
  148. package/lib/brand.js +51 -0
  149. package/lib/glyphs.js +87 -0
  150. package/lib/layouts_client.js +212 -0
  151. package/lib/layouts_consult.js +301 -0
  152. package/lib/layouts_dense.js +361 -0
  153. package/lib/measure.js +109 -0
  154. package/lib/pdf.js +517 -0
  155. package/lib/plan.js +305 -0
  156. package/lib/render_pdf.js +203 -0
  157. package/lib/schema.js +399 -0
  158. package/lib/shared.js +15 -0
  159. package/lib/trace.js +88 -0
  160. package/lib/ttf.py +167 -0
  161. package/lib/wandxml.py +37 -0
  162. package/package.json +17 -0
  163. package/preview/preview.css +112 -0
  164. package/preview/preview.html +24 -0
  165. package/preview/preview.js +169 -0
  166. package/qa_coverage.py +120 -0
  167. package/requirements.txt +9 -0
  168. package/tools/extract.py +199 -0
  169. package/wand.js +397 -0
  170. package/wand_kit.js +1016 -0
package/lib/brand.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * brand.js — locate the shared brand payload (fonts, assets).
3
+ *
4
+ * The kit runs in two shapes and must work in both:
5
+ *
6
+ * repo power-prompts/decks/kit/ -> brand/ at the repo root, shared by
7
+ * every power prompt, so there is one copy of Geist and one copy
8
+ * of the gradients.
9
+ * packaged tools/package.mjs copies brand/ into the zip root, so an
10
+ * unzipped kit is self-contained with no repo around it.
11
+ *
12
+ * `WAND_BRAND_DIR` overrides both.
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ const KIT = path.join(__dirname, "..");
19
+
20
+ /**
21
+ * A file that proves the directory is the brand payload and not something that
22
+ * merely shares its name. Without this, `resolve("lib")` matched the kit's own
23
+ * `lib/` — the first candidate, and always present.
24
+ */
25
+ const MARKER = {
26
+ fonts: "Geist-Regular.ttf",
27
+ assets: "glyphs/index.json",
28
+ lib: "measure.js",
29
+ };
30
+
31
+ /** Absolute path to a brand subdirectory (`"fonts"`, `"assets"`, `"lib"`). */
32
+ function resolve(name) {
33
+ const marker = MARKER[name];
34
+ const tried = [];
35
+ for (const dir of candidates()) {
36
+ const p = path.join(dir, name);
37
+ tried.push(p);
38
+ if (marker ? fs.existsSync(path.join(p, marker)) : fs.existsSync(p)) return p;
39
+ }
40
+ throw new Error(`brand/${name} not found. Looked in:\n ${tried.join("\n ")}`);
41
+ }
42
+
43
+ function candidates() {
44
+ const out = [];
45
+ if (process.env.WAND_BRAND_DIR) out.push(path.resolve(process.env.WAND_BRAND_DIR));
46
+ out.push(KIT); // packaged: bundled alongside the kit
47
+ out.push(path.join(KIT, "..", "..", "..", "brand")); // repo: power-prompts/<x>/kit -> brand
48
+ return out;
49
+ }
50
+
51
+ module.exports = { resolve };
package/lib/glyphs.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * lib/glyphs.js — the named icon set.
3
+ *
4
+ * The old library was 36 PNGs named after their extraction order, half baked
5
+ * white and half baked accent. That produced two standing bugs: a white glyph on
6
+ * a light tile is invisible, and nobody could pick an icon by meaning because
7
+ * the names carried none.
8
+ *
9
+ * These are Lucide's single-colour stroke SVGs. `stroke="currentColor"` means
10
+ * the kit substitutes the theme's colour at build time, so polarity cannot be
11
+ * wrong. They go into the deck as vector — pptxgenjs writes the OOXML
12
+ * `asvg:svgBlip` extension with a raster fallback — so nothing is rasterised
13
+ * and the kit keeps needing no native dependency.
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const DIR = path.join(__dirname, "..", "assets", "glyphs");
20
+ let INDEX = null, BUNDLE = null;
21
+
22
+ function index() {
23
+ if (!INDEX) INDEX = JSON.parse(fs.readFileSync(path.join(DIR, "index.json"), "utf8"));
24
+ return INDEX;
25
+ }
26
+
27
+ /**
28
+ * 75 SVGs is 75 files, and a packaged skill is capped at 200 of them. The
29
+ * packager collapses them into one `glyphs.json`; loose files still win when
30
+ * they are there, so the repo keeps its readable one-file-per-icon layout.
31
+ */
32
+ function bundle() {
33
+ if (BUNDLE === null) {
34
+ const f = path.join(DIR, "glyphs.json");
35
+ BUNDLE = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, "utf8")) : false;
36
+ }
37
+ return BUNDLE;
38
+ }
39
+
40
+ /** The raw SVG source for a glyph. */
41
+ function svg(name) {
42
+ const b = bundle();
43
+ if (b && b[name]) return b[name];
44
+ return fs.readFileSync(path.join(DIR, name + ".svg"), "utf8");
45
+ }
46
+
47
+ /** Every glyph name, for validation and for `wand.js glyphs`. */
48
+ const names = () => index().icons;
49
+ const has = (name) => typeof name === "string" && index().icons.includes(name);
50
+
51
+ const cache = new Map();
52
+
53
+ /**
54
+ * A recoloured glyph as a data URI, ready for `addImage({ data })`.
55
+ * Returns null for anything that isn't a known glyph name, so callers can fall
56
+ * back to the legacy PNG path.
57
+ */
58
+ function dataUri(name, color) {
59
+ if (!has(name)) return null;
60
+ const key = `${name}|${color}`;
61
+ if (cache.has(key)) return cache.get(key);
62
+ const src = svg(name).replace(/currentColor/g, "#" + String(color).replace(/^#/, ""));
63
+ const uri = "image/svg+xml;base64," + Buffer.from(src, "utf8").toString("base64");
64
+ cache.set(key, uri);
65
+ return uri;
66
+ }
67
+
68
+ /** Nearest names, for a useful error when someone mistypes one. */
69
+ function suggest(name, n = 4) {
70
+ const q = String(name || "").toLowerCase();
71
+ return index().icons
72
+ .map((g) => [g, g.includes(q) || q.includes(g) ? 0 : distance(q, g)])
73
+ .sort((a, b) => a[1] - b[1]).slice(0, n).map((r) => r[0]);
74
+ }
75
+
76
+ function distance(a, b) {
77
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
78
+ for (let j = 0; j <= b.length; j++) d[0][j] = j;
79
+ for (let i = 1; i <= a.length; i++) {
80
+ for (let j = 1; j <= b.length; j++) {
81
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
82
+ }
83
+ }
84
+ return d[a.length][b.length];
85
+ }
86
+
87
+ module.exports = { names, has, svg, dataUri, suggest, index, DIR };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * lib/layouts_client.js — layouts a dense client deck needs and the earlier
3
+ * catalog had no answer to. Added for a dense banking client rebuild.
4
+ *
5
+ * Loaded and re-exported by wand_kit.js.
6
+ */
7
+
8
+ module.exports = function attach(W) {
9
+ const { T, F, radius, theme, autoSize, contentTop,
10
+ background, chrome, footer, title, subtitle, kicker, card } = W;
11
+
12
+ const L = 0.37, WIDTH = 9.25, FLOOR = 4.96;
13
+
14
+ /** A small caps label. Returns the y to carry on from. */
15
+ function label(s, text, { x = L, y, w = WIDTH, dark, color }) {
16
+ if (!text) return y;
17
+ s.addText(String(text).toUpperCase(), {
18
+ x, y, w, h: 0.20, margin: 0, isTextBox: true,
19
+ fontFace: F.med, fontSize: 8, color: color || theme(dark).eyebrow, valign: "middle",
20
+ });
21
+ return y + 0.26;
22
+ }
23
+
24
+ /**
25
+ * stripGrid — a strip of figures, then a labelled column grid, then a closing
26
+ * prose band. Every part is optional except the geometry that ties them
27
+ * together, which is the point: a year page is stats over three columns, a
28
+ * results page is stats over one wide column, a memory page is four columns
29
+ * over a closing paragraph. One shape, not three layouts.
30
+ *
31
+ * stats: [{ value, label, note }] up to 6, across the top
32
+ * lead: { label, text } a full-width statement
33
+ * columns: [{ label, items: [{ title, body }] }] up to 4
34
+ * close: { label, body } a full-width closing band
35
+ */
36
+ function stripGrid(pres, { tag, titleText, subtitleText, stats, lead, columns, close,
37
+ dark = true, deck, n, total }) {
38
+ const s = pres.addSlide();
39
+ background(s, dark);
40
+ chrome(s, { tag, dark });
41
+ title(s, titleText, { dark, y: 0.62, h: 0.5 });
42
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.44 });
43
+ const th = theme(dark);
44
+ let y = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.26 });
45
+
46
+ // reserve from the bottom up, so the grid takes whatever is left
47
+ const closeH = close ? (close.label ? 0.26 : 0) + 0.72 : 0;
48
+ const closeY = FLOOR - closeH;
49
+
50
+ const st = (stats || []).slice(0, 6);
51
+ if (st.length) {
52
+ const noted = st.some((v) => v.note);
53
+ const h = noted ? 1.06 : 0.74;
54
+ const cw = (WIDTH - 0.18 * (st.length - 1)) / st.length;
55
+ st.forEach((v, i) => {
56
+ const x = L + i * (cw + 0.18);
57
+ card(s, { x, y, w: cw, h, dark });
58
+ // a "value" is usually a figure but sometimes a phrase — "Weeks, not
59
+ // quarters" has to be allowed to wrap and shrink like any other text
60
+ const vh = v.note ? 0.38 : 0.42;
61
+ s.addText(String(v.value), {
62
+ x: x + 0.16, y: y + 0.10, w: cw - 0.32, h: vh, margin: 0, isTextBox: true,
63
+ fontFace: F.semi,
64
+ fontSize: autoSize(String(v.value), cw - 0.32, vh, { family: F.semi, fontSize: 21, min: 8, lineSpacingMultiple: 1.15 }),
65
+ color: T.accent, valign: "middle", lineSpacingMultiple: 1.15,
66
+ });
67
+ const labelH = noted ? 0.22 : 0.26;
68
+ if (v.label) s.addText(v.label, {
69
+ x: x + 0.16, y: y + (noted ? 0.50 : 0.56), w: cw - 0.32, h: labelH, margin: 0, isTextBox: true,
70
+ fontFace: F.med,
71
+ fontSize: autoSize(v.label, cw - 0.32, labelH, { family: F.med, fontSize: 7.5, min: 6, lineSpacingMultiple: 1.15 }),
72
+ color: th.body, valign: "top", lineSpacingMultiple: 1.15,
73
+ });
74
+ if (v.note) s.addText(v.note, {
75
+ x: x + 0.16, y: y + 0.72, w: cw - 0.32, h: 0.30, margin: 0, isTextBox: true,
76
+ fontFace: F.reg,
77
+ fontSize: autoSize(v.note, cw - 0.32, 0.30, { family: F.reg, fontSize: 7, min: 5.5, lineSpacingMultiple: 1.2 }),
78
+ color: th.body, valign: "top", lineSpacingMultiple: 1.2,
79
+ });
80
+ });
81
+ y += h + 0.24;
82
+ }
83
+
84
+ if (lead) {
85
+ y = label(s, lead.label, { y, dark });
86
+ if (lead.text) {
87
+ const h = 0.42;
88
+ s.addText(lead.text, {
89
+ x: L, y, w: WIDTH, h, margin: 0, isTextBox: true,
90
+ fontFace: F.semi,
91
+ fontSize: autoSize(lead.text, WIDTH, h, { family: F.semi, fontSize: 14, min: 11, lineSpacingMultiple: 1.2 }),
92
+ color: th.ink, valign: "middle", lineSpacingMultiple: 1.2,
93
+ });
94
+ y += h + 0.18;
95
+ }
96
+ }
97
+
98
+ const cols = (columns || []).slice(0, 4);
99
+ if (cols.length) {
100
+ const cw = (WIDTH - 0.24 * (cols.length - 1)) / cols.length;
101
+ const bottom = closeY - (close ? 0.22 : 0);
102
+ cols.forEach((c, i) => {
103
+ const x = L + i * (cw + 0.24);
104
+ const cy = label(s, c.label, { x, y, w: cw, dark, color: T.accent });
105
+ const items = (c.items || []).slice(0, 6);
106
+ if (!items.length) return;
107
+ const ih = (bottom - cy - 0.12 * (items.length - 1)) / items.length;
108
+ items.forEach((it, j) => {
109
+ const iy = cy + j * (ih + 0.12);
110
+ card(s, { x, y: iy, w: cw, h: ih, dark });
111
+ // one measurement for the whole row: a title and its body share the
112
+ // box, so sizing them independently is how a card silently overflows
113
+ const whole = (it.title ? it.title + " " : "") + (it.body || "");
114
+ // measure in the widest face the row actually uses: a title run is
115
+ // SemiBold, and sizing it against Regular is how a card overflows by 0.08"
116
+ const pt = autoSize(whole, cw - 0.36, ih,
117
+ { family: it.title ? F.semi : F.reg, fontSize: 8.5, min: 6, lineSpacingMultiple: 1.25 });
118
+ const runs = it.title
119
+ ? [{ text: it.title + (it.body ? " " : ""), options: { fontFace: F.semi, fontSize: pt, color: th.ink } },
120
+ { text: it.body || "", options: { fontFace: F.reg, fontSize: pt, color: th.body } }]
121
+ : [{ text: it.body || "", options: { fontFace: F.reg, fontSize: pt, color: th.body } }];
122
+ s.addText(runs, {
123
+ x: x + 0.18, y: iy, w: cw - 0.36, h: ih, margin: 0, isTextBox: true,
124
+ valign: "middle", lineSpacingMultiple: 1.25,
125
+ });
126
+ });
127
+ });
128
+ }
129
+
130
+ if (close) {
131
+ let cy = label(s, close.label, { y: closeY, dark });
132
+ s.addText(close.body, {
133
+ x: L, y: cy, w: WIDTH, h: FLOOR - cy, margin: 0, isTextBox: true,
134
+ fontFace: F.reg,
135
+ fontSize: autoSize(close.body, WIDTH, FLOOR - cy, { family: F.reg, fontSize: 10.5, min: 8, lineSpacingMultiple: 1.35 }),
136
+ color: th.body, valign: "top", lineSpacingMultiple: 1.35,
137
+ });
138
+ }
139
+
140
+ if (deck) footer(s, { deck, n, total, dark });
141
+ return s;
142
+ }
143
+
144
+ /**
145
+ * compareGrid — a comparison wider than `compareTable` can carry: up to three
146
+ * headed columns over up to six labelled rows, with an optional second line
147
+ * per cell. The last column is ours and is tinted.
148
+ *
149
+ * heads: ["BEFORE", "AFTER", "WHY ONLY WAND"] optional
150
+ * rows: [{ label, cells: [{ lead, body }] }]
151
+ */
152
+ function compareGrid(pres, { tag, titleText, subtitleText, heads, rows,
153
+ dark = false, deck, n, total }) {
154
+ const s = pres.addSlide();
155
+ background(s, dark);
156
+ chrome(s, { tag, dark });
157
+ title(s, titleText, { dark, y: 0.62, h: 0.5 });
158
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.44 });
159
+ const th = theme(dark);
160
+ let y = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.26 });
161
+
162
+ const rs = rows.slice(0, 6);
163
+ const cols = Math.min(3, Math.max(...rs.map((r) => r.cells.length)));
164
+ const labelW = rs.some((r) => r.label) ? 1.05 : 0;
165
+ const gap = 0.16;
166
+ const cw = (WIDTH - labelW - gap * cols) / cols;
167
+
168
+ const heading = (heads || []).slice(0, cols);
169
+ if (heading.length) {
170
+ heading.forEach((h, i) => {
171
+ s.addText(String(h).toUpperCase(), {
172
+ x: L + labelW + i * (cw + gap), y, w: cw, h: 0.20, margin: 0, isTextBox: true,
173
+ fontFace: F.med, fontSize: 8,
174
+ color: i === cols - 1 ? T.accent : th.eyebrow, valign: "middle",
175
+ });
176
+ });
177
+ y += 0.28;
178
+ }
179
+
180
+ const rh = (FLOOR - y - 0.10 * (rs.length - 1)) / rs.length;
181
+ rs.forEach((r, i) => {
182
+ const ry = y + i * (rh + 0.10);
183
+ if (labelW) s.addText(r.label, {
184
+ x: L, y: ry, w: labelW - 0.12, h: rh, margin: 0, isTextBox: true,
185
+ fontFace: F.semi, fontSize: 8, color: th.ink, valign: "middle", lineSpacingMultiple: 1.2,
186
+ });
187
+ r.cells.slice(0, cols).forEach((c, j) => {
188
+ const x = L + labelW + j * (cw + gap);
189
+ const ours = j === cols - 1;
190
+ card(s, { x, y: ry, w: cw, h: rh, dark, variant: ours ? "emph" : "glass" });
191
+ const ink = ours ? T.white : th.ink;
192
+ const body = ours ? T.accentPale : th.body;
193
+ const whole = (c.lead ? c.lead + " " : "") + (c.body || "");
194
+ const pt = autoSize(whole, cw - 0.32, rh,
195
+ { family: c.lead ? F.semi : F.reg, fontSize: 8, min: 6, lineSpacingMultiple: 1.25 });
196
+ const runs = [];
197
+ if (c.lead) runs.push({ text: c.lead + (c.body ? " " : ""), options: { fontFace: F.semi, fontSize: pt, color: ink } });
198
+ if (c.body) runs.push({ text: c.body, options: { fontFace: F.reg, fontSize: pt, color: c.lead ? body : (ours ? T.white : th.body) } });
199
+ s.addText(runs.length ? runs : " ", {
200
+ x: x + 0.16, y: ry, w: cw - 0.32, h: rh, margin: 0, isTextBox: true,
201
+ valign: "middle", lineSpacingMultiple: 1.25,
202
+ });
203
+ });
204
+ });
205
+
206
+ if (deck) footer(s, { deck, n, total, dark });
207
+ return s;
208
+ }
209
+
210
+ Object.assign(W, { stripGrid, compareGrid });
211
+ return W;
212
+ };
@@ -0,0 +1,301 @@
1
+ /**
2
+ * lib/layouts_consult.js — layouts that consulting and client decks reach for
3
+ * and the investor-deck catalog has no answer to.
4
+ *
5
+ * Loaded and re-exported by wand_kit.js.
6
+ */
7
+
8
+ module.exports = function attach(W) {
9
+ const { T, F, COLS, radius, theme, autoSize, contentTop,
10
+ background, chrome, footer, title, subtitle, kicker,
11
+ card, iconTile, pill } = W;
12
+
13
+ /**
14
+ * problemGrid — a strip of "today's reality" chips, then a grid of the
15
+ * problems that reality creates. The canonical challenge slide.
16
+ * strip: { label, chips: [{ title, body }] }
17
+ * problems: { label, items: [{ title, body }] }
18
+ */
19
+ function problemGrid(pres, { tag, titleText, subtitleText, strip, problems, dark = true, deck, n, total }) {
20
+ const s = pres.addSlide();
21
+ background(s, dark);
22
+ chrome(s, { tag, dark });
23
+ title(s, titleText, { dark, y: 0.62, h: 0.5 });
24
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.32 });
25
+ const th = theme(dark);
26
+ const L = 0.37, WIDTH = 9.25;
27
+ let y = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.28 });
28
+
29
+ if (strip.label) {
30
+ s.addText(strip.label.toUpperCase(), {
31
+ x: L, y, w: 3.0, h: 0.20, margin: 0, isTextBox: true,
32
+ fontFace: F.med, fontSize: 8, color: th.eyebrow, valign: "middle",
33
+ });
34
+ y += 0.26;
35
+ }
36
+ const chips = strip.chips.slice(0, 4);
37
+ const cw = (WIDTH - 0.22 * (chips.length - 1)) / chips.length;
38
+ chips.forEach((c, i) => {
39
+ const x = L + i * (cw + 0.22);
40
+ card(s, { x, y, w: cw, h: 0.78, dark, variant: "muted" });
41
+ s.addText(c.title, {
42
+ x: x + 0.18, y: y + 0.12, w: cw - 0.36, h: 0.26, margin: 0, isTextBox: true,
43
+ fontFace: F.semi,
44
+ fontSize: autoSize(c.title, cw - 0.36, 0.26, { family: F.semi, fontSize: 11, min: 9 }),
45
+ color: th.ink, valign: "middle",
46
+ });
47
+ s.addText(c.body, {
48
+ x: x + 0.18, y: y + 0.40, w: cw - 0.36, h: 0.26, margin: 0, isTextBox: true,
49
+ fontFace: F.reg, fontSize: 8.5, color: th.body, valign: "middle",
50
+ });
51
+ });
52
+ y += 1.04;
53
+
54
+ if (problems.label) {
55
+ s.addText(problems.label.toUpperCase(), {
56
+ x: L, y, w: 3.0, h: 0.20, margin: 0, isTextBox: true,
57
+ fontFace: F.med, fontSize: 8, color: T.accent, valign: "middle",
58
+ });
59
+ y += 0.26;
60
+ }
61
+ const items = problems.items.slice(0, 4);
62
+ const rows = Math.ceil(items.length / 2);
63
+ const ph = Math.min(0.92, (4.98 - y - 0.16 * (rows - 1)) / rows);
64
+ items.forEach((it, i) => {
65
+ const x = i % 2 ? 5.13 : L, py = y + Math.floor(i / 2) * (ph + 0.16);
66
+ card(s, { x, y: py, w: 4.49, h: ph, dark });
67
+ s.addText([
68
+ { text: it.title + " — ", options: { fontFace: F.semi, fontSize: 10.5, color: T.accent } },
69
+ { text: it.body, options: { fontFace: F.reg, fontSize: 9.5, color: th.body } },
70
+ ], { x: x + 0.20, y: py, w: 4.09, h: ph, margin: 0, isTextBox: true,
71
+ valign: "middle", lineSpacingMultiple: 1.3 });
72
+ });
73
+ if (deck) footer(s, { deck, n, total, dark });
74
+ return s;
75
+ }
76
+
77
+ /**
78
+ * splitPanel — a prose panel on one side, a list of labelled rows on the
79
+ * other. For "why this fits / what it gives you" pillar slides where one half
80
+ * is argument and the other is evidence.
81
+ * left: { eyebrow, heading, body, note }
82
+ * right: { eyebrow, heading, items: [{ label, body }] }
83
+ */
84
+ function splitPanel(pres, { tag, titleText, subtitleText, left, right, dark = true, deck, n, total }) {
85
+ const s = pres.addSlide();
86
+ background(s, dark);
87
+ chrome(s, { tag, dark });
88
+ title(s, titleText, { dark, y: 0.62, h: 0.5 });
89
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.14, h: 0.44 });
90
+ const th = theme(dark);
91
+ const top = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.28 });
92
+ const h = 4.96 - top;
93
+
94
+ // left: argument
95
+ card(s, { x: 0.37, y: top, w: 4.25, h, dark, variant: "emph" });
96
+ let ly = top + 0.24;
97
+ if (left.eyebrow) {
98
+ s.addText(left.eyebrow.toUpperCase(), {
99
+ x: 0.61, y: ly, w: 3.77, h: 0.20, margin: 0, isTextBox: true,
100
+ fontFace: F.med, fontSize: 8, color: T.accentPale, valign: "middle",
101
+ });
102
+ ly += 0.26;
103
+ }
104
+ s.addText(left.heading, {
105
+ x: 0.61, y: ly, w: 3.77, h: 0.56, margin: 0, isTextBox: true,
106
+ fontFace: F.semi,
107
+ fontSize: autoSize(left.heading, 3.77, 0.56, { family: F.semi, fontSize: 15, min: 12, lineSpacingMultiple: 1.15 }),
108
+ color: T.white, valign: "top", lineSpacingMultiple: 1.15,
109
+ });
110
+ ly += 0.66;
111
+ const noteH = left.note ? 0.62 : 0;
112
+ s.addText(left.body, {
113
+ x: 0.61, y: ly, w: 3.77, h: top + h - ly - noteH - 0.24, margin: 0, isTextBox: true,
114
+ fontFace: F.reg, fontSize: 9, color: T.bodyDarkAlt, valign: "top", lineSpacingMultiple: 1.3,
115
+ });
116
+ if (left.note) s.addText(left.note, {
117
+ x: 0.61, y: top + h - noteH - 0.14, w: 3.77, h: noteH, margin: 0, isTextBox: true,
118
+ fontFace: F.med, fontSize: 8.5, italic: true, color: T.accentSoft, valign: "top", lineSpacingMultiple: 1.25,
119
+ });
120
+
121
+ // right: evidence rows
122
+ let ry = top;
123
+ if (right.eyebrow) {
124
+ s.addText(right.eyebrow.toUpperCase(), {
125
+ x: 5.38, y: ry, w: 4.25, h: 0.20, margin: 0, isTextBox: true,
126
+ fontFace: F.med, fontSize: 8, color: T.accent, valign: "middle",
127
+ });
128
+ ry += 0.24;
129
+ }
130
+ if (right.heading) {
131
+ s.addText(right.heading, {
132
+ x: 5.38, y: ry, w: 4.25, h: 0.32, margin: 0, isTextBox: true,
133
+ fontFace: F.semi, fontSize: 13, color: th.ink, valign: "middle",
134
+ });
135
+ ry += 0.40;
136
+ }
137
+ const items = right.items.slice(0, 4);
138
+ const rh = (top + h - ry - 0.14 * (items.length - 1)) / items.length;
139
+ items.forEach((it, i) => {
140
+ const y = ry + i * (rh + 0.14);
141
+ card(s, { x: 5.38, y, w: 4.25, h: rh, dark });
142
+ if (it.label) {
143
+ s.addText([
144
+ { text: it.label + " — ", options: { fontFace: F.semi, fontSize: 9, color: th.ink } },
145
+ { text: it.body, options: { fontFace: F.reg, fontSize: 9, color: th.body } },
146
+ ], { x: 5.60, y, w: 3.81, h: rh, margin: 0, isTextBox: true,
147
+ valign: "middle", lineSpacingMultiple: 1.25 });
148
+ } else {
149
+ s.addText(it.body, {
150
+ x: 5.60, y, w: 3.81, h: rh, margin: 0, isTextBox: true,
151
+ fontFace: F.reg, fontSize: 9, color: th.body, valign: "middle", lineSpacingMultiple: 1.25,
152
+ });
153
+ }
154
+ });
155
+ if (deck) footer(s, { deck, n, total, dark });
156
+ return s;
157
+ }
158
+
159
+ /**
160
+ * compareTable — reason cards on top, then a labelled two-column comparison.
161
+ * The right column is ours and is tinted; the left is the status quo.
162
+ * reasons: [{ title, body }]
163
+ * table: { leftHead, rightHead, rows: [{ label, left, right }] }
164
+ */
165
+ function compareTable(pres, { tag, titleText, subtitleText, reasons, table, dark = false, deck, n, total }) {
166
+ const s = pres.addSlide();
167
+ background(s, dark);
168
+ chrome(s, { tag, dark });
169
+ title(s, titleText, { dark, y: 0.62, h: 0.46 });
170
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.12, h: 0.30 });
171
+ const th = theme(dark);
172
+ const L = 0.37, WIDTH = 9.25;
173
+ let y = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.26 });
174
+
175
+ const r = reasons.slice(0, 3);
176
+ const rw = (WIDTH - 0.22 * (r.length - 1)) / r.length;
177
+ const rh = 1.32;
178
+ r.forEach((it, i) => {
179
+ const x = L + i * (rw + 0.22);
180
+ card(s, { x, y, w: rw, h: rh, dark, variant: i === 0 ? "emph" : "glass" });
181
+ const ink = i === 0 ? T.white : th.ink;
182
+ s.addText(it.title, {
183
+ x: x + 0.20, y: y + 0.14, w: rw - 0.40, h: 0.28, margin: 0, isTextBox: true,
184
+ fontFace: F.semi,
185
+ fontSize: autoSize(it.title, rw - 0.40, 0.28, { family: F.semi, fontSize: 11.5, min: 9.5 }),
186
+ color: ink, valign: "middle",
187
+ });
188
+ s.addText(it.body, {
189
+ x: x + 0.20, y: y + 0.46, w: rw - 0.40, h: rh - 0.60, margin: 0, isTextBox: true,
190
+ fontFace: F.reg, fontSize: 8, color: i === 0 ? T.bodyDarkAlt : th.body,
191
+ valign: "top", lineSpacingMultiple: 1.25,
192
+ });
193
+ });
194
+ y += rh + 0.30;
195
+
196
+ // table header
197
+ const LAB = 1.55, COL = (WIDTH - LAB - 0.18) / 2;
198
+ s.addText(table.leftHead.toUpperCase(), {
199
+ x: L + LAB, y, w: COL, h: 0.24, margin: 0, isTextBox: true,
200
+ fontFace: F.med, fontSize: 8, color: th.body, valign: "middle",
201
+ });
202
+ s.addText(table.rightHead.toUpperCase(), {
203
+ x: L + LAB + COL + 0.18, y, w: COL, h: 0.24, margin: 0, isTextBox: true,
204
+ fontFace: F.semi, fontSize: 8, color: T.accent, valign: "middle",
205
+ });
206
+ y += 0.28;
207
+
208
+ const rows = table.rows.slice(0, 4);
209
+ const trh = Math.min(0.50, (4.96 - y - 0.10 * (rows.length - 1)) / rows.length);
210
+ rows.forEach((row, i) => {
211
+ const ry = y + i * (trh + 0.10);
212
+ s.addText(row.label, {
213
+ x: L, y: ry, w: LAB - 0.14, h: trh, margin: 0, isTextBox: true,
214
+ fontFace: F.semi, fontSize: 9, color: th.ink, valign: "middle",
215
+ });
216
+ s.addShape("roundRect", {
217
+ x: L + LAB, y: ry, w: COL, h: trh, rectRadius: radius(COL, trh),
218
+ fill: { color: dark ? T.cardMuted : T.surface }, line: { width: 0 },
219
+ });
220
+ s.addText(row.left, {
221
+ x: L + LAB + 0.18, y: ry, w: COL - 0.36, h: trh, margin: 0, isTextBox: true,
222
+ fontFace: F.reg, fontSize: 8.5, color: th.body, valign: "middle",
223
+ });
224
+ const rx = L + LAB + COL + 0.18;
225
+ s.addShape("roundRect", {
226
+ x: rx, y: ry, w: COL, h: trh, rectRadius: radius(COL, trh),
227
+ fill: dark ? { color: T.cardEmphFlat } : { color: "9481FF", transparency: 88 },
228
+ line: { color: T.accent, width: 0.75 },
229
+ });
230
+ s.addText(row.right, {
231
+ x: rx + 0.18, y: ry, w: COL - 0.36, h: trh, margin: 0, isTextBox: true,
232
+ fontFace: F.semi, fontSize: 8.5, color: dark ? T.white : T.accentDeep, valign: "middle",
233
+ });
234
+ });
235
+ if (deck) footer(s, { deck, n, total, dark });
236
+ return s;
237
+ }
238
+
239
+ /**
240
+ * L12 numberedList — numbered rows in one or two columns, with an optional
241
+ * closing statement band. For agendas, asks, and "what we'll cover" slides.
242
+ * items: [{ title, body }]
243
+ * closing: { line1, line2 }
244
+ */
245
+ function numberedList(pres, { tag, titleText, subtitleText, items, closing, signoff, columns = 1, dark = true, deck, n, total }) {
246
+ const s = pres.addSlide();
247
+ background(s, dark);
248
+ chrome(s, { tag, dark });
249
+ title(s, titleText, { dark, y: 0.62, h: 0.46 });
250
+ if (subtitleText) subtitle(s, subtitleText, { dark, y: 1.12, h: 0.36 });
251
+ const th = theme(dark);
252
+ const top = contentTop(titleText, subtitleText, { titleY: 0.62, gap: 0.26 });
253
+ const closeH = closing ? 0.62 : 0;
254
+ const signH = signoff ? 0.24 : 0;
255
+ const list = items.slice(0, 6);
256
+ const perCol = Math.ceil(list.length / columns);
257
+ const colW = columns === 2 ? 4.49 : 9.25;
258
+ const avail = 4.96 - top - closeH - signH - (closing ? 0.22 : 0);
259
+ const GAP = 0.10;
260
+ const rh = Math.max(0.32, Math.min(0.72, (avail - GAP * (perCol - 1)) / perCol));
261
+
262
+ list.forEach((it, i) => {
263
+ const c = Math.floor(i / perCol), r = i % perCol;
264
+ const x = 0.37 + c * (colW + 0.27);
265
+ const y = top + r * (rh + GAP);
266
+ card(s, { x, y, w: colW, h: rh, dark });
267
+ s.addText(String(i + 1), {
268
+ x: x + 0.20, y, w: 0.34, h: rh, margin: 0, isTextBox: true,
269
+ fontFace: F.xbold, fontSize: 14, color: T.accent, valign: "middle",
270
+ });
271
+ s.addText([
272
+ ...(it.title ? [{ text: it.title + " ", options: { fontFace: F.semi, fontSize: 10, color: th.ink } }] : []),
273
+ { text: it.body, options: { fontFace: F.reg, fontSize: 9, color: th.body } },
274
+ ], { x: x + 0.62, y, w: colW - 0.86, h: rh, margin: 0, isTextBox: true,
275
+ valign: "middle", lineSpacingMultiple: 1.25 });
276
+ });
277
+
278
+ if (closing) {
279
+ const cy = 4.96 - closeH - signH;
280
+ s.addShape("roundRect", {
281
+ x: 0.37, y: cy, w: 9.25, h: closeH, rectRadius: radius(9.25, closeH),
282
+ fill: { color: dark ? T.cardEmphFlat : T.surface },
283
+ line: { color: T.accent, width: 1 },
284
+ });
285
+ s.addText([
286
+ { text: closing.line1 + " ", options: { fontFace: F.semi, fontSize: 13, color: dark ? T.bodyDark : T.bodyLight } },
287
+ { text: closing.line2, options: { fontFace: F.semi, fontSize: 13, italic: true, color: T.accent } },
288
+ ], { x: 0.37, y: cy, w: 9.25, h: closeH, margin: 0, isTextBox: true, align: "center", valign: "middle" });
289
+ }
290
+ if (signoff) s.addText(signoff, {
291
+ x: 0.37, y: 4.96 - signH, w: 9.25, h: signH, margin: 0, isTextBox: true,
292
+ fontFace: F.reg, fontSize: 9.5, italic: true, color: theme(dark).body,
293
+ align: "center", valign: "middle",
294
+ });
295
+ if (deck) footer(s, { deck, n, total, dark });
296
+ return s;
297
+ }
298
+
299
+ Object.assign(W, { problemGrid, splitPanel, compareTable, numberedList });
300
+ return W;
301
+ };