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/wandxml.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ wandxml.py — XML parsing that works with nothing installed.
3
+
4
+ The kit has to run wherever someone unzips it. `defusedxml` is the better
5
+ parser and we use it when it's there, but requiring a pip install to get past
6
+ Phase 0 means the engine is unusable in exactly the situation it was built for:
7
+ a fresh container with a zip and a .pptx in it.
8
+
9
+ The stdlib fallback is guarded against the one attack that matters for a parser
10
+ pointed at an untrusted .pptx — entity expansion — by refusing any document with
11
+ a DOCTYPE. Office files never have one.
12
+ """
13
+
14
+ try:
15
+ from defusedxml import minidom as _md
16
+ HARDENED = True
17
+ except ImportError: # stdlib fallback
18
+ from xml.dom import minidom as _md
19
+ HARDENED = False
20
+
21
+
22
+ class UnsafeXML(ValueError):
23
+ pass
24
+
25
+
26
+ def parse(data):
27
+ """Parse .pptx part bytes (or str) into a minidom Document."""
28
+ probe = data[:8192]
29
+ if isinstance(probe, bytes):
30
+ probe = probe.decode("utf-8", "ignore")
31
+ if not HARDENED and ("<!DOCTYPE" in probe or "<!ENTITY" in probe):
32
+ raise UnsafeXML(
33
+ "document declares a DOCTYPE or ENTITY. Office parts never do. "
34
+ "Refusing to parse it with the stdlib parser — `pip install defusedxml` "
35
+ "if this file is genuinely trusted."
36
+ )
37
+ return _md.parseString(data)
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "wand-decks-kit",
3
+ "version": "0.8.0",
4
+ "description": "Rebuilds or authors a client-ready .pptx on the Wand design system: measured fit, embedded Geist, verbatim content.",
5
+ "license": "SEE LICENSE IN PROMPT.md",
6
+ "bin": {
7
+ "wand-decks-kit": "./wand.js"
8
+ },
9
+ "main": "./wand_kit.js",
10
+ "dependencies": {
11
+ "fontkit": "^2.0.0",
12
+ "pptxgenjs": "^3.12.0"
13
+ },
14
+ "scripts": {
15
+ "doctor": "node wand.js doctor"
16
+ }
17
+ }
@@ -0,0 +1,112 @@
1
+ /* preview.css — the Wand design system as CSS.
2
+ Tokens mirror wand_kit.js §2 exactly. If you change one, change both. */
3
+
4
+ /* @font-face is injected by preview.js - the path to brand/fonts depends on
5
+ whether the kit is running from the repo or from a packaged zip. */
6
+
7
+ :root {
8
+ --accent: #8B7CF6;
9
+ --accent-deep: #6C5CE7;
10
+ --accent-soft: #A99CF8;
11
+ --accent-pale: #C4B5F4;
12
+
13
+ --bg-dark: #0F0C26;
14
+ --ink-dark: #F3F1FA;
15
+ --body-dark: #B7B0DC;
16
+ --body-dark-alt: #CCC0F2;
17
+ --card-stroke: #2E2757;
18
+ --card-solid: #2C2460;
19
+ --card-muted: #241D4F;
20
+ --card-muted-stroke: #4A3F8C;
21
+ --card-emph-flat: #352B70;
22
+
23
+ --bg-light: #F8F6FB;
24
+ --ink-light: #1D1A33;
25
+ --body-light: #5C5779;
26
+ --muted-light: #555077;
27
+ --surface: #EBE8F8;
28
+ --track: #E9E6F5;
29
+ --divider: #E4E0F2;
30
+
31
+ /* 1 inch on the 10" canvas, in px. Set by preview.js from the zoom control. */
32
+ --in: 96px;
33
+ }
34
+
35
+ * { box-sizing: border-box; }
36
+
37
+ body {
38
+ margin: 0;
39
+ padding: 28px 24px 64px;
40
+ background: #14121f;
41
+ color: #cfc9e6;
42
+ font-family: "Geist", system-ui, sans-serif;
43
+ font-size: 13px;
44
+ }
45
+
46
+ header {
47
+ position: sticky; top: 0; z-index: 20;
48
+ display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
49
+ margin: -28px -24px 24px; padding: 12px 24px;
50
+ background: rgba(20, 18, 31, .92);
51
+ backdrop-filter: blur(8px);
52
+ border-bottom: 1px solid #2a2540;
53
+ }
54
+ header h1 { margin: 0; font-family: "Geist SemiBold"; font-size: 14px; color: #fff; letter-spacing: .01em; }
55
+ header .spacer { flex: 1; }
56
+ header label { display: flex; align-items: center; gap: 6px; color: #9b93bd; font-size: 12px; cursor: pointer; }
57
+ header input[type="range"] { width: 120px; accent-color: var(--accent); }
58
+
59
+ .errors {
60
+ margin: 0 0 20px; padding: 10px 14px;
61
+ background: #3a1d2c; border: 1px solid #7a3050; border-radius: 8px;
62
+ color: #ffb3c8; font-size: 12px; white-space: pre-wrap;
63
+ }
64
+ .errors:empty { display: none; }
65
+
66
+ .deck { display: flex; flex-direction: column; align-items: center; gap: 28px; }
67
+
68
+ .slide-wrap { position: relative; }
69
+
70
+ .slide {
71
+ position: relative;
72
+ width: calc(10 * var(--in));
73
+ height: calc(5.625 * var(--in));
74
+ overflow: hidden;
75
+ border-radius: 4px;
76
+ box-shadow: 0 8px 30px rgba(0,0,0,.45);
77
+ }
78
+ .slide.dark { background: var(--bg-dark); }
79
+ .slide.light { background: var(--bg-light); }
80
+
81
+ .slide-no {
82
+ position: absolute; top: 0; left: calc(-1 * var(--in) * .52);
83
+ font-family: "Geist SemiBold"; font-size: 12px; color: #5d5680;
84
+ }
85
+
86
+ /* every traced shape is absolutely positioned in inches */
87
+ .sh { position: absolute; }
88
+ .sh.text { display: flex; white-space: pre-wrap; overflow: visible; }
89
+ .sh.text > span { display: block; width: 100%; }
90
+
91
+ .sh.rect { border-radius: 0; }
92
+ .sh.roundRect { border-radius: calc(var(--in) * .10); }
93
+ .sh.ellipse { border-radius: 50%; }
94
+
95
+ .sh.image { object-fit: contain; }
96
+
97
+ /* overflow flagging, driven by the same measurement the build uses */
98
+ .slide.flag .sh.overflow { outline: 1.5px solid #ff5c8a; outline-offset: 1px; }
99
+ .slide.flag .sh.overflow::after {
100
+ content: "▲"; position: absolute; top: -9px; right: -9px;
101
+ font-size: 10px; color: #ff5c8a;
102
+ }
103
+
104
+ /* the §1 layout rails, toggled on to check alignment */
105
+ .grid-overlay { position: absolute; inset: 0; pointer-events: none; display: none; }
106
+ .slide.grid .grid-overlay { display: block; }
107
+ .grid-overlay i {
108
+ position: absolute; background: rgba(139,124,246,.38);
109
+ }
110
+ .grid-overlay i.v { top: 0; bottom: 0; width: 1px; }
111
+ .grid-overlay i.h { left: 0; right: 0; height: 1px; }
112
+ .grid-overlay i.hot { background: rgba(255,92,138,.55); }
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Wand deck preview</title>
6
+ <link rel="stylesheet" href="preview.css">
7
+ </head>
8
+ <body>
9
+
10
+ <header>
11
+ <h1 id="title">Wand deck preview</h1>
12
+ <div class="spacer"></div>
13
+ <label><input type="checkbox" id="flag" checked> flag overflow</label>
14
+ <label><input type="checkbox" id="grid"> §1 rails</label>
15
+ <label>zoom <input type="range" id="zoom" min="60" max="170" value="96"></label>
16
+ </header>
17
+
18
+ <pre class="errors" id="errors"></pre>
19
+ <div class="deck" id="deck"></div>
20
+
21
+ <script src="trace.js"></script>
22
+ <script src="preview.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,169 @@
1
+ /* preview.js — render a shape trace to the DOM at the 10" x 5.625" grid.
2
+ Knows nothing about layouts: it draws whatever wand_kit recorded, so the
3
+ preview and the .pptx cannot disagree about geometry. */
4
+
5
+ (function () {
6
+ const T = window.__WAND_TRACE__;
7
+ const deck = document.getElementById("deck");
8
+ const errBox = document.getElementById("errors");
9
+ const zoom = document.getElementById("zoom");
10
+ const flag = document.getElementById("flag");
11
+ const grid = document.getElementById("grid");
12
+
13
+ // @font-face lives here, not in preview.css: the path to the faces depends on
14
+ // where brand/ resolved, and CSS cannot be told. One source of truth.
15
+ const FACES = [
16
+ ["Geist", "Geist-Regular", 400, "normal"], ["Geist", "Geist-Bold", 700, "normal"],
17
+ ["Geist", "Geist-Italic", 400, "italic"], ["Geist", "Geist-BoldItalic", 700, "italic"],
18
+ ["Geist Medium", "Geist-Medium", 400, "normal"], ["Geist Medium", "Geist-MediumItalic", 400, "italic"],
19
+ ["Geist SemiBold", "Geist-SemiBold", 400, "normal"], ["Geist SemiBold", "Geist-SemiBoldItalic", 400, "italic"],
20
+ ["Geist ExtraBold", "Geist-ExtraBold", 400, "normal"], ["Geist ExtraBold", "Geist-ExtraBoldItalic", 400, "italic"],
21
+ ["Geist Light", "Geist-Light", 400, "normal"], ["Geist Light", "Geist-LightItalic", 400, "italic"],
22
+ ];
23
+ const base = T.fontsBase || "../fonts/";
24
+ document.head.appendChild(Object.assign(document.createElement("style"), {
25
+ textContent: FACES.map(([fam, file, w, st]) =>
26
+ `@font-face{font-family:"${fam}";src:url("${base}${file}.ttf");font-weight:${w};font-style:${st};}`).join("\n"),
27
+ }));
28
+
29
+ document.getElementById("title").textContent = T.title || "Wand deck preview";
30
+ if (T.errors && T.errors.length) errBox.textContent = T.errors.join("\n");
31
+
32
+ const inch = (v) => `calc(${v} * var(--in))`;
33
+ const hex = (c) => (typeof c === "string" ? "#" + c.replace(/^#/, "") : null);
34
+
35
+ /** pptxgenjs fill/line objects -> CSS */
36
+ function paint(el, o) {
37
+ if (o.fill && o.fill.color) {
38
+ const a = o.fill.transparency ? 1 - o.fill.transparency / 100 : 1;
39
+ el.style.background = a < 1 ? withAlpha(o.fill.color, a) : hex(o.fill.color);
40
+ }
41
+ if (o.line && o.line.width) {
42
+ const style = o.line.dashType === "dash" ? "dashed" : "solid";
43
+ el.style.border = `${o.line.width}px ${style} ${hex(o.line.color || "000000")}`;
44
+ }
45
+ if (o.rectRadius != null) el.style.borderRadius = inch(o.rectRadius);
46
+ }
47
+
48
+ function withAlpha(c, a) {
49
+ const h = c.replace(/^#/, "");
50
+ const n = parseInt(h, 16);
51
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a.toFixed(3)})`;
52
+ }
53
+
54
+ const ALIGN = { left: "flex-start", center: "center", right: "flex-end" };
55
+ const VALIGN = { top: "flex-start", middle: "center", bottom: "flex-end" };
56
+
57
+ function textEl(sh) {
58
+ const el = document.createElement("div");
59
+ el.className = "sh text";
60
+ const first = (sh.runs[0] && sh.runs[0].options) || {};
61
+ el.style.justifyContent = ALIGN[sh.align || "left"];
62
+ el.style.alignItems = VALIGN[sh.valign || "top"];
63
+ el.style.lineHeight = String(sh.lineSpacingMultiple || 1.2);
64
+ const span = document.createElement("span");
65
+ span.style.textAlign = sh.align || "left";
66
+
67
+ sh.runs.forEach((r) => {
68
+ const o = r.options || {};
69
+ const s = document.createElement("span");
70
+ s.textContent = r.text;
71
+ s.style.fontFamily = `"${o.fontFace || sh.fontFace || "Geist"}"`;
72
+ s.style.fontSize = inch((o.fontSize || sh.fontSize || 12) / 72);
73
+ s.style.color = hex(o.color || sh.color || "000000");
74
+ if (o.bold ?? sh.bold) s.style.fontWeight = "700";
75
+ if (o.italic ?? sh.italic) s.style.fontStyle = "italic";
76
+ span.appendChild(s);
77
+ if (o.breakLine) span.appendChild(document.createElement("br"));
78
+ if (o.bullet) { s.textContent = "▪ " + r.text; s.style.display = "block"; }
79
+ });
80
+ el.appendChild(span);
81
+ el.dataset.size = String(first.fontSize || sh.fontSize || 12);
82
+ return el;
83
+ }
84
+
85
+ function render() {
86
+ deck.innerHTML = "";
87
+ T.slides.forEach((sl) => {
88
+ const wrap = document.createElement("div");
89
+ wrap.className = "slide-wrap";
90
+
91
+ const no = document.createElement("div");
92
+ no.className = "slide-no";
93
+ no.textContent = String(sl.index).padStart(2, "0");
94
+ wrap.appendChild(no);
95
+
96
+ const s = document.createElement("div");
97
+ const bg = sl.background && sl.background.color;
98
+ s.className = "slide " + (bg && bg.toUpperCase() === "0F0C26" ? "dark" : "light");
99
+ if (flag.checked) s.classList.add("flag");
100
+ if (grid.checked) s.classList.add("grid");
101
+ s.title = sl.layout;
102
+
103
+ sl.shapes.forEach((sh) => {
104
+ let el;
105
+ if (sh.kind === "image") {
106
+ el = document.createElement("img");
107
+ el.className = "sh image";
108
+ // a named glyph arrives as an inline data URI, a legacy PNG as a path
109
+ el.src = sh.data ? "data:" + sh.data
110
+ : sh.path ? (T.assetsBase || "../assets/") + sh.path.split(/[\\/]assets[\\/]/).pop()
111
+ : "";
112
+ } else if (sh.kind === "text") {
113
+ el = textEl(sh);
114
+ } else {
115
+ el = document.createElement("div");
116
+ el.className = "sh " + (sh.type || "rect");
117
+ paint(el, sh);
118
+ }
119
+ el.style.left = inch(sh.x || 0);
120
+ el.style.top = inch(sh.y || 0);
121
+ el.style.width = inch(sh.w || 0);
122
+ el.style.height = inch(sh.h || 0);
123
+ if (sh.kind === "text" && sh.margin === 0) el.style.padding = "0";
124
+ s.appendChild(el);
125
+ });
126
+
127
+ // §1 rails
128
+ const g = document.createElement("div");
129
+ g.className = "grid-overlay";
130
+ [[0.37, 1], [9.62, 1], [3.58, 0], [6.78, 0]].forEach(([x, hot]) => {
131
+ const i = document.createElement("i");
132
+ i.className = "v" + (hot ? " hot" : "");
133
+ i.style.left = inch(x); g.appendChild(i);
134
+ });
135
+ [[0.62, 1], [1.60, 0], [5.05, 1], [5.21, 0]].forEach(([y, hot]) => {
136
+ const i = document.createElement("i");
137
+ i.className = "h" + (hot ? " hot" : "");
138
+ i.style.top = inch(y); g.appendChild(i);
139
+ });
140
+ s.appendChild(g);
141
+
142
+ wrap.appendChild(s);
143
+ deck.appendChild(wrap);
144
+ if (flag.checked) markOverflow(s);
145
+ });
146
+ }
147
+
148
+ /** Browser-side overflow detection: does the laid-out text exceed its box? */
149
+ function markOverflow(slideEl) {
150
+ slideEl.querySelectorAll(".sh.text").forEach((el) => {
151
+ const span = el.firstElementChild;
152
+ if (!span) return;
153
+ if (span.scrollHeight - el.clientHeight > 1.5 || span.scrollWidth - el.clientWidth > 1.5) {
154
+ el.classList.add("overflow");
155
+ }
156
+ });
157
+ }
158
+
159
+ function setZoom() {
160
+ document.documentElement.style.setProperty("--in", zoom.value + "px");
161
+ if (flag.checked) document.querySelectorAll(".slide").forEach(markOverflow);
162
+ }
163
+
164
+ zoom.addEventListener("input", setZoom);
165
+ flag.addEventListener("change", render);
166
+ grid.addEventListener("change", render);
167
+ setZoom();
168
+ render();
169
+ })();
package/qa_coverage.py ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ qa_coverage.py — makes "faithful = verbatim" enforceable instead of aspirational.
4
+
5
+ python qa_coverage.py visible_dump.txt output.pptx [--min 4] [--json report.json]
6
+
7
+ Reads every string from the Phase 1 verbatim dump, reads every string out of the
8
+ built deck, and reports what didn't make it. Exit 1 if anything is missing, so it
9
+ can gate a build.
10
+
11
+ Matching is whitespace- and case-normalised, and normalises the typographic
12
+ characters that get swapped in transit (curly quotes, dashes, non-breaking
13
+ spaces). It is deliberately NOT fuzzy beyond that: a string that was reworded is
14
+ supposed to fail.
15
+ """
16
+
17
+ import argparse, json, os, re, sys, unicodedata, zipfile
18
+
19
+ def _brand_lib():
20
+ """brand/lib in the repo, or lib/ beside the kit once packaged."""
21
+ import os
22
+ here = os.path.dirname(os.path.abspath(__file__))
23
+ for d in filter(None, [os.environ.get("WAND_BRAND_DIR"),
24
+ os.path.join(here, "lib"),
25
+ os.path.join(here, "..", "lib"),
26
+ os.path.join(here, "..", "..", "..", "..", "brand", "lib"),
27
+ os.path.join(here, "..", "..", "..", "brand", "lib")]):
28
+ if os.path.isfile(os.path.join(d, "wandxml.py")):
29
+ return os.path.normpath(d)
30
+ raise SystemExit("brand/lib not found")
31
+
32
+ sys.path.insert(0, _brand_lib())
33
+ import wandxml
34
+
35
+ TRANS = str.maketrans({
36
+ "\u2018": "'", "\u2019": "'", "\u201c": '"', "\u201d": '"',
37
+ "\u2013": "-", "\u2014": "-", "\u2212": "-", "\u00a0": " ",
38
+ "\u2026": "...", "\u00b7": "·",
39
+ })
40
+
41
+
42
+ def norm(s):
43
+ s = unicodedata.normalize("NFKC", s).translate(TRANS)
44
+ return re.sub(r"\s+", " ", s).strip().lower()
45
+
46
+
47
+ def strings_from_pptx(path):
48
+ out = []
49
+ with zipfile.ZipFile(path) as z:
50
+ names = sorted((n for n in z.namelist()
51
+ if re.fullmatch(r"ppt/slides/slide\d+\.xml", n)),
52
+ key=lambda n: int(re.findall(r"\d+", n)[0]))
53
+ for i, n in enumerate(names, 1):
54
+ doc = wandxml.parse(z.read(n))
55
+ for sp in doc.getElementsByTagName("p:txBody"):
56
+ # join runs within a paragraph, paragraphs with a space — a line
57
+ # break in the deck is a space in the source string
58
+ paras = []
59
+ for p in sp.getElementsByTagName("a:p"):
60
+ runs = [t.firstChild.nodeValue for t in p.getElementsByTagName("a:t") if t.firstChild]
61
+ if runs:
62
+ paras.append("".join(runs))
63
+ if paras:
64
+ out.append((i, " ".join(paras)))
65
+ return out
66
+
67
+
68
+ def main():
69
+ ap = argparse.ArgumentParser()
70
+ ap.add_argument("dump")
71
+ ap.add_argument("pptx")
72
+ ap.add_argument("--min", type=int, default=4,
73
+ help="ignore source strings shorter than this (default 4)")
74
+ ap.add_argument("--json")
75
+ a = ap.parse_args()
76
+
77
+ source = [l.rstrip("\n") for l in open(a.dump, encoding="utf-8")]
78
+ short = [s for s in source if 0 < len(s.strip()) < a.min]
79
+ source = [s for s in source if len(s.strip()) >= a.min]
80
+
81
+ built = strings_from_pptx(a.pptx)
82
+ haystack = norm(" \u241e ".join(t for _, t in built))
83
+ per_slide = [(i, norm(t)) for i, t in built]
84
+
85
+ missing, found = [], []
86
+ for s in source:
87
+ n = norm(s)
88
+ if not n:
89
+ continue
90
+ if n in haystack:
91
+ where = next((i for i, t in per_slide if n in t), None)
92
+ found.append((s, where))
93
+ else:
94
+ missing.append(s)
95
+
96
+ total = len(found) + len(missing)
97
+ pct = (100.0 * len(found) / total) if total else 100.0
98
+ print(f"coverage: {len(found)}/{total} strings ({pct:.1f}%) across {len(set(i for i,_ in built))} slides")
99
+ if short:
100
+ # Say this out loud. Stat values and ordinals are often 1-3 characters,
101
+ # so a silent length filter is a coverage number that flatters itself.
102
+ missed = [s for s in short if norm(s) not in haystack]
103
+ print(f" {len(short)} string(s) under {a.min} chars not compared" +
104
+ (f" - {len(missed)} of them are absent from the deck: " +
105
+ ", ".join(repr(s.strip()) for s in missed[:8]) if missed else " - all present anyway"))
106
+
107
+ if missing:
108
+ print(f"\nMISSING ({len(missing)}) — each must land on a slide or be listed in the Review Note:")
109
+ for s in missing:
110
+ print(" ·", s[:110] + ("…" if len(s) > 110 else ""))
111
+
112
+ if a.json:
113
+ json.dump({"total": total, "found": len(found), "missing": missing},
114
+ open(a.json, "w"), indent=2, ensure_ascii=False)
115
+
116
+ sys.exit(1 if missing else 0)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,9 @@
1
+ # Optional. The kit runs with nothing installed.
2
+ #
3
+ # pylib/wandxml.py falls back to stdlib xml.dom.minidom, and pylib/ttf.py reads
4
+ # and rewrites the font tables embed_fonts.py needs without fontTools. Install
5
+ # these only if you want the hardened XML parser or are working on the font code.
6
+ #
7
+ # pip install -r requirements.txt
8
+ defusedxml>=0.7 # tools/extract.py, qa_coverage.py - entity-safe XML
9
+ fonttools>=4.50 # cross-checking pylib/ttf.py output