wand-kit 0.0.0-stage → 0.2.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 (262) hide show
  1. package/.gitkeep +0 -0
  2. package/PROMPT.md +255 -0
  3. package/SKILL.md +117 -0
  4. package/START-HERE.md +16 -0
  5. package/brands/current/assets/bg-cover-dark.png +0 -0
  6. package/brands/current/assets/bg-overlay-dark.png +0 -0
  7. package/brands/current/assets/bg-statement-dark.png +0 -0
  8. package/brands/current/assets/closing-art.png +0 -0
  9. package/brands/current/assets/cover-art.png +0 -0
  10. package/brands/current/assets/divider-art.png +0 -0
  11. package/brands/current/assets/glyphs/README.md +29 -0
  12. package/brands/current/assets/glyphs/activity.svg +15 -0
  13. package/brands/current/assets/glyphs/atom.svg +17 -0
  14. package/brands/current/assets/glyphs/badge-check.svg +16 -0
  15. package/brands/current/assets/glyphs/bell.svg +16 -0
  16. package/brands/current/assets/glyphs/blocks.svg +16 -0
  17. package/brands/current/assets/glyphs/book-open.svg +16 -0
  18. package/brands/current/assets/glyphs/bot.svg +20 -0
  19. package/brands/current/assets/glyphs/boxes.svg +26 -0
  20. package/brands/current/assets/glyphs/brain.svg +23 -0
  21. package/brands/current/assets/glyphs/briefcase.svg +16 -0
  22. package/brands/current/assets/glyphs/building-2.svg +21 -0
  23. package/brands/current/assets/glyphs/calendar.svg +18 -0
  24. package/brands/current/assets/glyphs/chart-column.svg +18 -0
  25. package/brands/current/assets/glyphs/chart-line.svg +16 -0
  26. package/brands/current/assets/glyphs/chart-pie.svg +16 -0
  27. package/brands/current/assets/glyphs/circle-check.svg +16 -0
  28. package/brands/current/assets/glyphs/clipboard-check.svg +17 -0
  29. package/brands/current/assets/glyphs/clock.svg +16 -0
  30. package/brands/current/assets/glyphs/cloud.svg +15 -0
  31. package/brands/current/assets/glyphs/code.svg +15 -0
  32. package/brands/current/assets/glyphs/coins.svg +18 -0
  33. package/brands/current/assets/glyphs/compass.svg +16 -0
  34. package/brands/current/assets/glyphs/cpu.svg +24 -0
  35. package/brands/current/assets/glyphs/database.svg +17 -0
  36. package/brands/current/assets/glyphs/eye.svg +16 -0
  37. package/brands/current/assets/glyphs/file-check.svg +17 -0
  38. package/brands/current/assets/glyphs/filter.svg +15 -0
  39. package/brands/current/assets/glyphs/fingerprint.svg +23 -0
  40. package/brands/current/assets/glyphs/flag.svg +16 -0
  41. package/brands/current/assets/glyphs/gauge.svg +16 -0
  42. package/brands/current/assets/glyphs/gavel.svg +19 -0
  43. package/brands/current/assets/glyphs/git-branch.svg +18 -0
  44. package/brands/current/assets/glyphs/globe.svg +17 -0
  45. package/brands/current/assets/glyphs/graduation-cap.svg +17 -0
  46. package/brands/current/assets/glyphs/handshake.svg +19 -0
  47. package/brands/current/assets/glyphs/hexagon.svg +15 -0
  48. package/brands/current/assets/glyphs/index.json +177 -0
  49. package/brands/current/assets/glyphs/infinity.svg +15 -0
  50. package/brands/current/assets/glyphs/key.svg +17 -0
  51. package/brands/current/assets/glyphs/landmark.svg +20 -0
  52. package/brands/current/assets/glyphs/layers.svg +17 -0
  53. package/brands/current/assets/glyphs/lightbulb.svg +17 -0
  54. package/brands/current/assets/glyphs/link.svg +16 -0
  55. package/brands/current/assets/glyphs/list-checks.svg +19 -0
  56. package/brands/current/assets/glyphs/lock.svg +16 -0
  57. package/brands/current/assets/glyphs/map.svg +17 -0
  58. package/brands/current/assets/glyphs/megaphone.svg +16 -0
  59. package/brands/current/assets/glyphs/message-square.svg +15 -0
  60. package/brands/current/assets/glyphs/milestone.svg +17 -0
  61. package/brands/current/assets/glyphs/network.svg +19 -0
  62. package/brands/current/assets/glyphs/orbit.svg +19 -0
  63. package/brands/current/assets/glyphs/package.svg +18 -0
  64. package/brands/current/assets/glyphs/plug.svg +18 -0
  65. package/brands/current/assets/glyphs/puzzle.svg +15 -0
  66. package/brands/current/assets/glyphs/radar.svg +22 -0
  67. package/brands/current/assets/glyphs/receipt.svg +17 -0
  68. package/brands/current/assets/glyphs/refresh-cw.svg +18 -0
  69. package/brands/current/assets/glyphs/rocket.svg +18 -0
  70. package/brands/current/assets/glyphs/route.svg +17 -0
  71. package/brands/current/assets/glyphs/scale.svg +19 -0
  72. package/brands/current/assets/glyphs/search.svg +16 -0
  73. package/brands/current/assets/glyphs/server.svg +18 -0
  74. package/brands/current/assets/glyphs/settings.svg +16 -0
  75. package/brands/current/assets/glyphs/share-2.svg +19 -0
  76. package/brands/current/assets/glyphs/shield-check.svg +16 -0
  77. package/brands/current/assets/glyphs/shuffle.svg +19 -0
  78. package/brands/current/assets/glyphs/sparkles.svg +19 -0
  79. package/brands/current/assets/glyphs/square-stack.svg +17 -0
  80. package/brands/current/assets/glyphs/target.svg +17 -0
  81. package/brands/current/assets/glyphs/trending-up.svg +16 -0
  82. package/brands/current/assets/glyphs/triangle-alert.svg +17 -0
  83. package/brands/current/assets/glyphs/trophy.svg +20 -0
  84. package/brands/current/assets/glyphs/users.svg +18 -0
  85. package/brands/current/assets/glyphs/wallet.svg +16 -0
  86. package/brands/current/assets/glyphs/wand-sparkles.svg +22 -0
  87. package/brands/current/assets/glyphs/workflow.svg +17 -0
  88. package/brands/current/assets/glyphs/zap.svg +15 -0
  89. package/brands/current/assets/mark-dark.png +0 -0
  90. package/brands/current/assets/mark-white.png +0 -0
  91. package/brands/current/assets/orb-cover.png +0 -0
  92. package/brands/current/assets/orb-glow.png +0 -0
  93. package/brands/current/assets/wordmark-dark.png +0 -0
  94. package/brands/current/assets/wordmark-white.png +0 -0
  95. package/brands/current/fonts/Geist-Bold.ttf +0 -0
  96. package/brands/current/fonts/Geist-BoldItalic.ttf +0 -0
  97. package/brands/current/fonts/Geist-ExtraBold.ttf +0 -0
  98. package/brands/current/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  99. package/brands/current/fonts/Geist-Italic.ttf +0 -0
  100. package/brands/current/fonts/Geist-Light.ttf +0 -0
  101. package/brands/current/fonts/Geist-LightItalic.ttf +0 -0
  102. package/brands/current/fonts/Geist-Medium.ttf +0 -0
  103. package/brands/current/fonts/Geist-MediumItalic.ttf +0 -0
  104. package/brands/current/fonts/Geist-Regular.ttf +0 -0
  105. package/brands/current/fonts/Geist-SemiBold.ttf +0 -0
  106. package/brands/current/fonts/Geist-SemiBoldItalic.ttf +0 -0
  107. package/brands/current/lib/glyphs.js +87 -0
  108. package/brands/current/lib/measure.js +115 -0
  109. package/brands/current/lib/pdf.js +603 -0
  110. package/brands/current/lib/ttf.py +167 -0
  111. package/brands/current/lib/wandxml.py +37 -0
  112. package/brands/current/tokens.json +206 -0
  113. package/brands/next/assets/closing-art.png +0 -0
  114. package/brands/next/assets/cover-art.png +0 -0
  115. package/brands/next/assets/divider-art.png +0 -0
  116. package/brands/next/assets/glyphs/README.md +29 -0
  117. package/brands/next/assets/glyphs/activity.svg +15 -0
  118. package/brands/next/assets/glyphs/atom.svg +17 -0
  119. package/brands/next/assets/glyphs/badge-check.svg +16 -0
  120. package/brands/next/assets/glyphs/bell.svg +16 -0
  121. package/brands/next/assets/glyphs/blocks.svg +16 -0
  122. package/brands/next/assets/glyphs/book-open.svg +16 -0
  123. package/brands/next/assets/glyphs/bot.svg +20 -0
  124. package/brands/next/assets/glyphs/boxes.svg +26 -0
  125. package/brands/next/assets/glyphs/brain.svg +23 -0
  126. package/brands/next/assets/glyphs/briefcase.svg +16 -0
  127. package/brands/next/assets/glyphs/building-2.svg +21 -0
  128. package/brands/next/assets/glyphs/calendar.svg +18 -0
  129. package/brands/next/assets/glyphs/chart-column.svg +18 -0
  130. package/brands/next/assets/glyphs/chart-line.svg +16 -0
  131. package/brands/next/assets/glyphs/chart-pie.svg +16 -0
  132. package/brands/next/assets/glyphs/circle-check.svg +16 -0
  133. package/brands/next/assets/glyphs/clipboard-check.svg +17 -0
  134. package/brands/next/assets/glyphs/clock.svg +16 -0
  135. package/brands/next/assets/glyphs/cloud.svg +15 -0
  136. package/brands/next/assets/glyphs/code.svg +15 -0
  137. package/brands/next/assets/glyphs/coins.svg +18 -0
  138. package/brands/next/assets/glyphs/compass.svg +16 -0
  139. package/brands/next/assets/glyphs/cpu.svg +24 -0
  140. package/brands/next/assets/glyphs/database.svg +17 -0
  141. package/brands/next/assets/glyphs/eye.svg +16 -0
  142. package/brands/next/assets/glyphs/file-check.svg +17 -0
  143. package/brands/next/assets/glyphs/filter.svg +15 -0
  144. package/brands/next/assets/glyphs/fingerprint.svg +23 -0
  145. package/brands/next/assets/glyphs/flag.svg +16 -0
  146. package/brands/next/assets/glyphs/gauge.svg +16 -0
  147. package/brands/next/assets/glyphs/gavel.svg +19 -0
  148. package/brands/next/assets/glyphs/git-branch.svg +18 -0
  149. package/brands/next/assets/glyphs/globe.svg +17 -0
  150. package/brands/next/assets/glyphs/graduation-cap.svg +17 -0
  151. package/brands/next/assets/glyphs/handshake.svg +19 -0
  152. package/brands/next/assets/glyphs/hexagon.svg +15 -0
  153. package/brands/next/assets/glyphs/index.json +177 -0
  154. package/brands/next/assets/glyphs/infinity.svg +15 -0
  155. package/brands/next/assets/glyphs/key.svg +17 -0
  156. package/brands/next/assets/glyphs/landmark.svg +20 -0
  157. package/brands/next/assets/glyphs/layers.svg +17 -0
  158. package/brands/next/assets/glyphs/lightbulb.svg +17 -0
  159. package/brands/next/assets/glyphs/link.svg +16 -0
  160. package/brands/next/assets/glyphs/list-checks.svg +19 -0
  161. package/brands/next/assets/glyphs/lock.svg +16 -0
  162. package/brands/next/assets/glyphs/map.svg +17 -0
  163. package/brands/next/assets/glyphs/megaphone.svg +16 -0
  164. package/brands/next/assets/glyphs/message-square.svg +15 -0
  165. package/brands/next/assets/glyphs/milestone.svg +17 -0
  166. package/brands/next/assets/glyphs/network.svg +19 -0
  167. package/brands/next/assets/glyphs/orbit.svg +19 -0
  168. package/brands/next/assets/glyphs/package.svg +18 -0
  169. package/brands/next/assets/glyphs/plug.svg +18 -0
  170. package/brands/next/assets/glyphs/puzzle.svg +15 -0
  171. package/brands/next/assets/glyphs/radar.svg +22 -0
  172. package/brands/next/assets/glyphs/receipt.svg +17 -0
  173. package/brands/next/assets/glyphs/refresh-cw.svg +18 -0
  174. package/brands/next/assets/glyphs/rocket.svg +18 -0
  175. package/brands/next/assets/glyphs/route.svg +17 -0
  176. package/brands/next/assets/glyphs/scale.svg +19 -0
  177. package/brands/next/assets/glyphs/search.svg +16 -0
  178. package/brands/next/assets/glyphs/server.svg +18 -0
  179. package/brands/next/assets/glyphs/settings.svg +16 -0
  180. package/brands/next/assets/glyphs/share-2.svg +19 -0
  181. package/brands/next/assets/glyphs/shield-check.svg +16 -0
  182. package/brands/next/assets/glyphs/shuffle.svg +19 -0
  183. package/brands/next/assets/glyphs/sparkles.svg +19 -0
  184. package/brands/next/assets/glyphs/square-stack.svg +17 -0
  185. package/brands/next/assets/glyphs/target.svg +17 -0
  186. package/brands/next/assets/glyphs/trending-up.svg +16 -0
  187. package/brands/next/assets/glyphs/triangle-alert.svg +17 -0
  188. package/brands/next/assets/glyphs/trophy.svg +20 -0
  189. package/brands/next/assets/glyphs/users.svg +18 -0
  190. package/brands/next/assets/glyphs/wallet.svg +16 -0
  191. package/brands/next/assets/glyphs/wand-sparkles.svg +22 -0
  192. package/brands/next/assets/glyphs/workflow.svg +17 -0
  193. package/brands/next/assets/glyphs/zap.svg +15 -0
  194. package/brands/next/assets/layers-dark.png +0 -0
  195. package/brands/next/assets/layers-light.png +0 -0
  196. package/brands/next/assets/ornaments/burst-dark.png +0 -0
  197. package/brands/next/assets/ornaments/burst-light.png +0 -0
  198. package/brands/next/assets/ornaments/chain-dark.png +0 -0
  199. package/brands/next/assets/ornaments/chain-light.png +0 -0
  200. package/brands/next/assets/ornaments/coil-dark.png +0 -0
  201. package/brands/next/assets/ornaments/coil-light.png +0 -0
  202. package/brands/next/assets/ornaments/cone-dark.png +0 -0
  203. package/brands/next/assets/ornaments/cone-light.png +0 -0
  204. package/brands/next/assets/ornaments/diamond-dark.png +0 -0
  205. package/brands/next/assets/ornaments/diamond-light.png +0 -0
  206. package/brands/next/assets/ornaments/fan-dark.png +0 -0
  207. package/brands/next/assets/ornaments/fan-light.png +0 -0
  208. package/brands/next/assets/ornaments/globe-dark.png +0 -0
  209. package/brands/next/assets/ornaments/globe-light.png +0 -0
  210. package/brands/next/assets/ornaments/orbit-dark.png +0 -0
  211. package/brands/next/assets/ornaments/orbit-light.png +0 -0
  212. package/brands/next/assets/ornaments/rings-dark.png +0 -0
  213. package/brands/next/assets/ornaments/rings-light.png +0 -0
  214. package/brands/next/assets/ornaments/sphere-dark.png +0 -0
  215. package/brands/next/assets/ornaments/sphere-light.png +0 -0
  216. package/brands/next/assets/ornaments/sun-dark.png +0 -0
  217. package/brands/next/assets/ornaments/sun-light.png +0 -0
  218. package/brands/next/assets/photos/atrium.jpg +0 -0
  219. package/brands/next/assets/photos/cantilever.jpg +0 -0
  220. package/brands/next/assets/photos/corridor.jpg +0 -0
  221. package/brands/next/assets/photos/dome.jpg +0 -0
  222. package/brands/next/assets/photos/facade.jpg +0 -0
  223. package/brands/next/assets/photos/glass.jpg +0 -0
  224. package/brands/next/assets/photos/stairs.jpg +0 -0
  225. package/brands/next/assets/photos/tower.jpg +0 -0
  226. package/brands/next/assets/photos/walk.jpg +0 -0
  227. package/brands/next/assets/spark-gold.png +0 -0
  228. package/brands/next/assets/statement-art.png +0 -0
  229. package/brands/next/assets/wordmark-dark.png +0 -0
  230. package/brands/next/assets/wordmark-light.png +0 -0
  231. package/brands/next/assets/wordmark.svg +7 -0
  232. package/brands/next/fonts/FKRomanStandard-Light.ttf +0 -0
  233. package/brands/next/fonts/OFL-OverusedGrotesk.txt +93 -0
  234. package/brands/next/fonts/OverusedGrotesk-Italic.ttf +0 -0
  235. package/brands/next/fonts/OverusedGrotesk-Medium.ttf +0 -0
  236. package/brands/next/fonts/OverusedGrotesk-MediumItalic.ttf +0 -0
  237. package/brands/next/fonts/OverusedGrotesk-Roman.ttf +0 -0
  238. package/brands/next/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
  239. package/brands/next/fonts/OverusedGrotesk-SemiBoldItalic.ttf +0 -0
  240. package/brands/next/fonts/faces.json +27 -0
  241. package/brands/next/lib/glyphs.js +89 -0
  242. package/brands/next/lib/measure.js +121 -0
  243. package/brands/next/lib/pdf.js +597 -0
  244. package/brands/next/lib/ttf.py +169 -0
  245. package/brands/next/lib/wandxml.py +37 -0
  246. package/brands/next/tokens.json +174 -0
  247. package/examples/gallery/job.mjs +83 -0
  248. package/examples/overview/job.mjs +99 -0
  249. package/lib/brand.js +135 -0
  250. package/lib/components.js +313 -0
  251. package/lib/content.js +66 -0
  252. package/lib/engine.js +280 -0
  253. package/lib/html_export.js +71 -0
  254. package/lib/pptx.js +446 -0
  255. package/lib/surfaces.js +62 -0
  256. package/package.json +17 -4
  257. package/templates/card.mjs +30 -0
  258. package/templates/page.mjs +52 -0
  259. package/templates/slide.mjs +46 -0
  260. package/tools/embed_fonts.py +154 -0
  261. package/wand.js +179 -0
  262. package/README.md +0 -3
@@ -0,0 +1,46 @@
1
+ /**
2
+ * __NAME__ — a 16:9 deck on the Wand studio.
3
+ *
4
+ * One `c.page()` per slide. `notes` on a page become speaker notes in the
5
+ * .pptx. `pages(c)` composes with the components (node wand.js components)
6
+ * and may write raw markup where none fits; `css` may only use the brand's
7
+ * custom properties (--ink, --accent, --t-h1 …), never a hex.
8
+ */
9
+ export default {
10
+ name: "__NAME__",
11
+ brand: "__BRAND__", // current | next
12
+ surface: "slide",
13
+ tag: "Wand × Client",
14
+ footer: "Wand AI · Strictly confidential",
15
+ content: "content.txt", // delete this line when authoring from a brief
16
+ edits: {},
17
+ cut: [],
18
+ denylist: [],
19
+ css: ``,
20
+ pages: (c) => [
21
+ c.cover({
22
+ label: "Wand × Client",
23
+ title: `Building the <em>Dual Workforce</em>`,
24
+ lede: "One sentence that says what this deck argues.",
25
+ meta: "Prepared for … · October 2026",
26
+ notes: "Open on the claim; the next slide carries the evidence.",
27
+ }),
28
+
29
+ c.page({ eyebrow: "The shift", cls: "roomy", notes: "Left is where every airline is today; right is what we deploy.", body: `
30
+ ${c.h1(`Two <em>Models</em>`)}
31
+ ${c.cols(2, [
32
+ c.card({ tag: "Today · every organisation", title: "Human-centric", level: "h3", body: `<div class="sub">100% human</div><p class="body">Humans think, decide and execute. AI shows up in pockets, as tools.</p>` }),
33
+ c.card({ tag: "The target model", title: "Dual workforce", level: "h3", emph: true, body: `<div class="sub">Humans + AI agents</div><p class="body">One workforce, two kinds of colleagues; agent workers carry the volume growth.</p>` }),
34
+ ])}
35
+ ` }),
36
+
37
+ c.page({ eyebrow: "The journey", body: `
38
+ ${c.strip(["Foundation", "Beachhead", "Controlled expansion", "Enterprise scale"], { on: 0 })}
39
+ ${c.h1("One registry, one identity", "md")}
40
+ ${c.cols(2, [c.body("The first move was structural: one workforce registry for humans and agents."), c.body("Every agent is provisioned through the firm's standard identity infrastructure.")], { stretch: false })}
41
+ ${c.callout({ tag: "Why identity-first matters", text: "Treating agents as governed identities is what lets a regulated firm scale AI safely." })}
42
+ ` }),
43
+
44
+ c.closing({ pair: "Wand × Client", title: "Building the Dual Workforce", url: "www.wand.ai", eyebrow: "The road ahead" }),
45
+ ],
46
+ };
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ tools/embed_fonts.py: embed the faces a studio .pptx uses, and only those.
4
+
5
+ python3 embed_fonts.py --faces faces.json --fonts <brand fonts dir> --lib <brand lib dir> in.pptx out.pptx
6
+
7
+ faces.json is { "<typeface>": { "regular": "<file.ttf>", "italic": "<file.ttf>" } },
8
+ written by lib/pptx.js: one PowerPoint family per face the slides set. Weight
9
+ rides on the family name in this system ("Geist SemiBold" regular, not
10
+ "Geist" bold), so each family gets its regular slot and, when an italic file
11
+ was used, its italic slot, and never a bold slot. A renderer that matches by
12
+ name rather than by slot (LibreOffice does) then has one face per name and
13
+ cannot draw one weight for another, which the decks kit's all-slots embedding
14
+ let it do.
15
+
16
+ Why embed at all: a deck without its faces renders in Arial and Times on the
17
+ client's machine. The brand's ttf.py rewrites each file's name table so the
18
+ face reports the family the slides address it by; eot() wraps it the way
19
+ PowerPoint wants it, an uncompressed EOT, unsubsetted, with
20
+ saveSubsetFonts="0", so digits do not garble in exported PDFs. Standard
21
+ library only, plus ttf.py.
22
+ """
23
+
24
+ import argparse, json, os, re, shutil, struct, sys, tempfile, zipfile
25
+
26
+ REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
27
+ SLOTS = ("regular", "italic")
28
+ _ttf = None
29
+
30
+
31
+ def eot(ttf_bytes, font, family, italic):
32
+ """Wrap raw TTF bytes in an uncompressed EOT v2.1 container."""
33
+ os2 = font.os2
34
+ panose = os2["panose"]
35
+
36
+ def lpstr(s):
37
+ b = s.encode("utf-16-le")
38
+ return struct.pack("<HH", 0, len(b)) + b # padding + size + string
39
+
40
+ body = b""
41
+ body += panose # FontPANOSE[10]
42
+ body += struct.pack("<BB", 1, 1 if italic else 0) # Charset(DEFAULT), Italic
43
+ body += struct.pack("<I", os2["usWeightClass"]) # Weight
44
+ body += struct.pack("<HH", os2.get("fsType", 0), 0x504C) # the font's own embedding flags, MagicNumber
45
+ body += struct.pack("<IIII", *os2["ulUnicodeRange"])
46
+ body += struct.pack("<II", *os2["ulCodePageRange"])
47
+ body += struct.pack("<I", font.check_sum_adjustment)
48
+ body += struct.pack("<IIII", 0, 0, 0, 0) # Reserved1-4
49
+ body += lpstr(family) # FamilyName
50
+ body += lpstr("Italic" if italic else "Regular") # StyleName
51
+ body += lpstr("Version 1.0") # VersionName
52
+ body += lpstr(f"{family} Italic" if italic else family) # FullName
53
+ body += struct.pack("<HH", 0, 0) # v2.1: RootString (empty)
54
+
55
+ # EOTSize, FontDataSize, Version, Flags(0 = uncompressed, not subsetted)
56
+ prefix = struct.pack("<IIII", 16 + len(body) + len(ttf_bytes),
57
+ len(ttf_bytes), 0x00020001, 0x00000000)
58
+ return prefix + body + ttf_bytes
59
+
60
+
61
+ def retag(ttf_path, family, italic):
62
+ """The file with its name table rewritten to the family the slides use.
63
+
64
+ Without this PowerPoint matches the embedded face to nothing and falls
65
+ back; with it, every family reports exactly one regular and one italic.
66
+ """
67
+ f = _ttf.load(ttf_path)
68
+ sub = "Italic" if italic else "Regular"
69
+ full = f"{family} Italic" if italic else family
70
+ ps = family.replace(" ", "") + ("-Italic" if italic else "")
71
+ data = f.retagged(family, sub, full, ps)
72
+ return data, _ttf.TTF(data)
73
+
74
+
75
+ def main():
76
+ global _ttf
77
+ ap = argparse.ArgumentParser()
78
+ ap.add_argument("--faces", required=True)
79
+ ap.add_argument("--fonts", required=True)
80
+ ap.add_argument("--lib", required=True)
81
+ ap.add_argument("src")
82
+ ap.add_argument("out")
83
+ a = ap.parse_args()
84
+ if not os.path.isfile(os.path.join(a.lib, "ttf.py")):
85
+ sys.exit(f"no ttf.py in {a.lib}")
86
+ sys.path.insert(0, a.lib)
87
+ import ttf
88
+ _ttf = ttf
89
+ faces = json.load(open(a.faces, encoding="utf-8"))
90
+
91
+ work = tempfile.mkdtemp()
92
+ with zipfile.ZipFile(a.src) as z:
93
+ z.extractall(work)
94
+ fdir = os.path.join(work, "ppt", "fonts")
95
+ os.makedirs(fdir, exist_ok=True)
96
+ rels_path = os.path.join(work, "ppt", "_rels", "presentation.xml.rels")
97
+ rels = open(rels_path, encoding="utf-8").read()
98
+ next_id = max([int(m) for m in re.findall(r'Id="rId(\d+)"', rels)] or [0]) + 1
99
+
100
+ new_rels, blocks, made = [], [], 0
101
+ for family, slots in faces.items():
102
+ entries = []
103
+ for slot in SLOTS:
104
+ fn = slots.get(slot)
105
+ if not fn:
106
+ continue
107
+ src = os.path.join(a.fonts, fn)
108
+ if not os.path.isfile(src):
109
+ sys.exit(f"{family}/{slot}: {src} not found")
110
+ italic = slot == "italic"
111
+ data, font = retag(src, family, italic)
112
+ name = f"{family.replace(' ', '')}-{slot}.fntdata"
113
+ open(os.path.join(fdir, name), "wb").write(eot(data, font, family, italic))
114
+ rid = f"rId{next_id}"
115
+ next_id += 1
116
+ new_rels.append(f'<Relationship Id="{rid}" Type="{REL_FONT}" Target="fonts/{name}"/>')
117
+ entries.append(f'<p:{slot} r:id="{rid}"/>')
118
+ made += 1
119
+ if entries:
120
+ blocks.append(f'<p:embeddedFont><p:font typeface="{family}" pitchFamily="34" charset="0"/>'
121
+ + "".join(entries) + "</p:embeddedFont>")
122
+
123
+ open(rels_path, "w", encoding="utf-8").write(rels.replace("</Relationships>", "".join(new_rels) + "</Relationships>"))
124
+ ct_path = os.path.join(work, "[Content_Types].xml")
125
+ ct = open(ct_path, encoding="utf-8").read()
126
+ if "fntdata" not in ct:
127
+ ct = ct.replace("<Default", '<Default Extension="fntdata" ContentType="application/x-fontdata"/><Default', 1)
128
+ open(ct_path, "w", encoding="utf-8").write(ct)
129
+ p_path = os.path.join(work, "ppt", "presentation.xml")
130
+ p = open(p_path, encoding="utf-8").read()
131
+ p = re.sub(r'\s(embedTrueTypeFonts|saveSubsetFonts)="[^"]*"', "", p)
132
+ p = re.sub(r"(<p:presentation\b)", r'\1 embedTrueTypeFonts="1" saveSubsetFonts="0"', p, count=1)
133
+ p = re.sub(r"<p:embeddedFontLst>.*?</p:embeddedFontLst>", "", p, flags=re.S)
134
+ # schema-fixed position: immediately after <p:notesSz/>
135
+ anchor = re.search(r"<p:notesSz[^/]*/>", p) or re.search(r"<p:sldSz[^/]*/>", p)
136
+ if not anchor:
137
+ sys.exit("no <p:notesSz>/<p:sldSz> anchor in presentation.xml")
138
+ p = p[:anchor.end()] + "<p:embeddedFontLst>" + "".join(blocks) + "</p:embeddedFontLst>" + p[anchor.end():]
139
+ open(p_path, "w", encoding="utf-8").write(p)
140
+
141
+ if os.path.exists(a.out):
142
+ os.remove(a.out)
143
+ with zipfile.ZipFile(a.out, "w", zipfile.ZIP_DEFLATED) as zf:
144
+ names = sorted((os.path.relpath(os.path.join(r, f), work) for r, _, fs in os.walk(work) for f in fs),
145
+ key=lambda n: (n != "[Content_Types].xml", n))
146
+ for n in names:
147
+ zf.write(os.path.join(work, n), n)
148
+ shutil.rmtree(work)
149
+ print(f"embedded {made} faces in {len(blocks)} families: "
150
+ + "; ".join(f"{fam} ({', '.join(s for s in SLOTS if s in sl)})" for fam, sl in faces.items()))
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
package/wand.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wand.js — the studio's front door.
4
+ *
5
+ * node wand.js doctor what works here, and the brand rule
6
+ * node wand.js new <dir> [--surface letter] [--brand current|next] [--name "Title"]
7
+ * a job folder to compose in, on the default brand
8
+ * unless --brand names the other
9
+ * node wand.js build <job.mjs> gates, then the PDF, a PNG per page, the sheet
10
+ * node wand.js check <job.mjs> gates only
11
+ * node wand.js ship <job.mjs> [--pptx] [--no-html] everything: build, one-file HTML, PowerPoint
12
+ * node wand.js watch <job.mjs> rebuild on save, the browser kept warm
13
+ * node wand.js gallery [--brand current|next] every component, light and dark, on one surface each
14
+ * node wand.js components the vocabulary
15
+ * node wand.js brands the brands this kit can draw with
16
+ *
17
+ * A job is a folder with `job.mjs` (the composition) and, for a rebuild,
18
+ * `content.txt` (the words, one per line). Outputs land in `<job>/out/`.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { createRequire } from "node:module";
24
+ import { execFileSync, spawnSync } from "node:child_process";
25
+ import { BRANDS, DEFAULT_BRAND, ASKED_AS, brandRule } from "./lib/brand.js";
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const KIT = path.dirname(fileURLToPath(import.meta.url));
29
+ const args = process.argv.slice(2);
30
+ const cmd = args[0];
31
+ const flag = (k, d = null) => { const i = args.indexOf(k); return i < 0 ? d : args[i + 1] ?? d; };
32
+ const on = (k) => args.includes(k);
33
+ const die = (m, c = 2) => { console.error(`\n${m}\n`); process.exit(c); };
34
+ const rel = (p) => path.relative(process.cwd(), p) || ".";
35
+
36
+ async function doctor() {
37
+ const { findChrome, has } = await import("./lib/engine.js");
38
+ const { loadBrand } = await import("./lib/brand.js");
39
+ const rows = [];
40
+ rows.push(["node", process.version, +process.version.slice(1).split(".")[0] >= 18 ? "ok" : "MISSING - Node 18 or newer"]);
41
+ for (const d of ["playwright-core", "pptxgenjs", "jszip", "fontkit"]) { let s = "ok"; try { require.resolve(d); } catch { s = `MISSING - npm install in ${rel(KIT)}`; } rows.push([d, s === "ok" ? "bundled" : "-", s]); }
42
+ const chrome = findChrome();
43
+ rows.push(["chromium", chrome ? chrome.replace(process.env.HOME || "~", "~") : "MISSING", chrome ? "ok" : "no renderer - set WAND_CHROME, or `npx playwright-core install chromium`"]);
44
+ for (const b of BRANDS) { try { const br = loadBrand(b); rows.push([`brand ${b}`, rel(br.dir), "ok"]); } catch (e) { rows.push([`brand ${b}`, "MISSING", e.message.split("\n")[0]]); } }
45
+ const py = ["python3", "python"].find((c) => spawnSync(c, ["-c", "import sys;print(sys.version_info[0])"], { encoding: "utf8" }).stdout?.trim() === "3");
46
+ rows.push(["python3", py ? execFileSync(py, ["-V"], { encoding: "utf8" }).trim() : "absent", py ? "ok" : "the .pptx ships without embedded fonts"]);
47
+ rows.push(["pdftotext", has("pdftotext") ? "found" : "absent", has("pdftotext") ? "ok" : "coverage is read from the DOM only (poppler adds the PDF read-back)"]);
48
+ const w = Math.max(...rows.map((r) => r[0].length));
49
+ console.log("");
50
+ for (const [k, v, s] of rows) console.log(`${s === "ok" ? " " : "! "}${k.padEnd(w)} ${String(v).padEnd(36)}${s === "ok" ? "" : s}`);
51
+ const blocked = rows.some((r) => r[2].startsWith("MISSING") || r[2].startsWith("no renderer"));
52
+ console.log(`\n brand rule ${brandRule()}`);
53
+ console.log(blocked ? "\nBLOCKED - fix the rows marked !\n" : "\nready - node wand.js new <dir>, then build <dir>/job.mjs\n");
54
+ return blocked ? 1 : 0;
55
+ }
56
+
57
+ function newJob() {
58
+ const dir = args[1]; if (!dir) die("usage: node wand.js new <dir> [--surface letter|a4|slide|square|og] [--brand current|next] [--name \"Title\"]");
59
+ const surface = flag("--surface", "letter"), brand = flag("--brand", DEFAULT_BRAND), name = flag("--name", path.basename(dir));
60
+ if (!BRANDS.includes(brand)) die(`unknown brand "${brand}" - one of ${BRANDS.join(", ")}`);
61
+ const tpl = path.join(KIT, "templates", `${["slide", "square", "og"].includes(surface) ? (surface === "slide" ? "slide" : "card") : "page"}.mjs`);
62
+ if (!fs.existsSync(tpl)) die(`no template for surface "${surface}"`);
63
+ if (fs.existsSync(path.join(dir, "job.mjs"))) die(`${dir}/job.mjs already exists`);
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ const src = fs.readFileSync(tpl, "utf8").replace(/__NAME__/g, name.replace(/"/g, '\\"')).replace(/__SURFACE__/g, surface).replace(/__BRAND__/g, brand);
66
+ fs.writeFileSync(path.join(dir, "job.mjs"), src);
67
+ if (!fs.existsSync(path.join(dir, "content.txt"))) fs.writeFileSync(path.join(dir, "content.txt"), "");
68
+ console.log(`${dir}/\n job.mjs the composition — pages(c) and css\n content.txt the words, one source string per line (leave empty when authoring)\n\nnext: node ${rel(path.join(KIT, "wand.js"))} build ${path.join(dir, "job.mjs")}`);
69
+ }
70
+
71
+ async function build(jobPath, opts) {
72
+ if (!jobPath) die("usage: node wand.js build <job.mjs>");
73
+ const { build } = await import("./lib/engine.js");
74
+ return build(jobPath, opts);
75
+ }
76
+
77
+ async function ship(jobPath) {
78
+ if (!jobPath) die("usage: node wand.js ship <job.mjs> [--pptx] [--no-html]");
79
+ const { build, closeBrowser } = await import("./lib/engine.js");
80
+ const r = await build(jobPath, { quiet: true });
81
+ const { surface } = await import("./lib/surfaces.js");
82
+ const s = surface(r.surface);
83
+ if (!on("--no-html")) {
84
+ const { singleFile } = await import("./lib/html_export.js");
85
+ const out = path.join(path.dirname(r.outputs.pdf), `${r.name}.html`);
86
+ const sf = singleFile(r.outputs.html, out, { kind: s.kind, w: s.w, h: s.h });
87
+ r.outputs.single = sf.out;
88
+ if (sf.missing.length) r.rows.push({ ok: false, gate: "html", msg: `missing files: ${sf.missing.join(", ")}` });
89
+ }
90
+ if (on("--pptx")) {
91
+ try {
92
+ const { exportPptx } = await import("./lib/pptx.js");
93
+ const out = path.join(path.dirname(r.outputs.pdf), `${r.name}.pptx`);
94
+ const px = await exportPptx({ htmlPath: r.outputs.html, outPath: out, brand: r.brand, surface: s, notes: r.notes, name: r.name });
95
+ r.outputs.pptx = out;
96
+ r.rows.push({ ok: px.ok, gate: "pptx", msg: px.msg, warn: px.warn });
97
+ } catch (e) { r.rows.push({ ok: false, gate: "pptx", msg: e.message.split("\n")[0] }); }
98
+ }
99
+ r.ok = r.rows.every((x) => x.ok || x.warn);
100
+ const { printReport } = await import("./lib/engine.js");
101
+ printReport(r);
102
+ await closeBrowser();
103
+ return r.ok ? 0 : 1;
104
+ }
105
+
106
+ async function watch(jobPath) {
107
+ if (!jobPath) die("usage: node wand.js watch <job.mjs>");
108
+ const { build, closeBrowser } = await import("./lib/engine.js");
109
+ const dir = path.dirname(path.resolve(jobPath));
110
+ let busy = false, again = false, timer = null;
111
+ const run = async () => {
112
+ if (busy) { again = true; return; }
113
+ busy = true;
114
+ try { console.clear(); console.log(`${new Date().toLocaleTimeString()} ${rel(jobPath)}`); await build(jobPath, {}); }
115
+ catch (e) { console.error(`\n FAIL ${e.message}\n`); }
116
+ busy = false;
117
+ if (again) { again = false; run(); }
118
+ };
119
+ fs.watch(dir, { recursive: false }, (ev, f) => { if (f && !/^out\b/.test(f)) { clearTimeout(timer); timer = setTimeout(run, 150); } });
120
+ for (const d of ["lib", "templates"]) fs.watch(path.join(KIT, d), () => { clearTimeout(timer); timer = setTimeout(run, 150); });
121
+ await run();
122
+ console.log("\n watching — save to rebuild, Ctrl-C to stop");
123
+ process.on("SIGINT", async () => { await closeBrowser(); process.exit(0); });
124
+ }
125
+
126
+ async function gallery() {
127
+ const brand = flag("--brand", DEFAULT_BRAND);
128
+ const { build, closeBrowser } = await import("./lib/engine.js");
129
+ process.env.WAND_GALLERY_BRAND = brand;
130
+ const r = await build(path.join(KIT, "examples", "gallery", "job.mjs"), {});
131
+ await closeBrowser();
132
+ return r.ok ? 0 : 1;
133
+ }
134
+
135
+ async function components() {
136
+ const { bind } = await import("./lib/components.js");
137
+ const { loadBrand } = await import("./lib/brand.js");
138
+ const { surface } = await import("./lib/surfaces.js");
139
+ const c = bind({ brand: loadBrand("current"), surface: surface("letter"), job: {}, content: { L: () => "", lines: [] } });
140
+ const doc = {
141
+ page: "page({ eyebrow, dark, art, cls, body, notes }) a page with the chrome", cover: "cover({ label, title, lede, body, meta, extra, art })", closing: "closing({ pair, title, url, lede })",
142
+ sec: "sec(num, titleHtml, { stack }) numbered section heading", h1: "h1(html, cls) … h5", lead: "lead(html) the standfirst", body: "body(html) running text", small: "small(html)", note: "note(html) small print",
143
+ tag: "tag(text) a caps label", cols: "cols(n, [items], { stretch }) a grid", card: "card({ tag, title, level, body, out, emph, sm })", callout: "callout({ tag, text, emph })",
144
+ strip: "strip([labels], { on }) numbered chips with arrows", chip: "chip(i, label, state)", phase: "phase(i, label, line) chip then heading", flow: "flow({ label, nodes: [[who, verb, 'a'|'p']], note })",
145
+ table: "table({ head, rows, emph }) cells: string or { v, meter, cls }", quote: "quote({ text, by, emph })", who: "who(name, role)", stats: "stats([{ v, k, hero }])", delta: "delta(from, to)",
146
+ steps: "steps([{ title, when, body, emph }])", list: "list([items])", chapter: "chapter(num) the deck's numeral panel", img: "img(src, cls, alt)", icon: "icon(name) a brand glyph", tile: "tile(name) a glyph on a tile",
147
+ accent: "accent(text, phrase) the phrase in the accent colour", split: "split(line, sep)", L: "L(n) line n of content.txt", esc: "esc(s)", art: "art(role) a brand art url", asset: "asset(file)",
148
+ };
149
+ console.log("");
150
+ for (const k of Object.keys(doc)) console.log(` ${k.padEnd(9)} ${doc[k]}`);
151
+ console.log(`\n ${Object.keys(c).length} helpers bound per job; see PROMPT.md §5 for when each is right.\n`);
152
+ }
153
+
154
+ async function brands() {
155
+ const { loadBrand } = await import("./lib/brand.js");
156
+ for (const b of BRANDS) { const br = loadBrand(b); console.log(`\n ${b.padEnd(8)} ${br.title}${b === DEFAULT_BRAND ? " (default)" : ""}\n ${"".padEnd(8)} ${rel(br.dir)} · faces: ${br.families.join(", ")} · ${br.faceFiles.length} files · ${Object.keys(br.tokens.art).length} art roles\n ${"".padEnd(8)} asked for as: ${ASKED_AS[b].join(", ")}`); }
157
+ console.log(`\n rule: ${brandRule()}\n`);
158
+ }
159
+
160
+ try {
161
+ switch (cmd) {
162
+ case "doctor": process.exit(await doctor());
163
+ case "new": newJob(); break;
164
+ case "build": { const r = await build(args[1], {}); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
165
+ case "check": { const r = await build(args[1], { outputs: false }); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
166
+ case "ship": process.exit(await ship(args[1]));
167
+ case "watch": await watch(args[1]); break;
168
+ case "gallery": process.exit(await gallery());
169
+ case "components": await components(); break;
170
+ case "brands": await brands(); break;
171
+ default:
172
+ console.log(fs.readFileSync(fileURLToPath(import.meta.url), "utf8").split("*/")[0].split("\n").slice(2).map((l) => l.replace(/^ \* ?/, "")).join("\n"));
173
+ process.exit(cmd ? 2 : 0);
174
+ }
175
+ } catch (e) {
176
+ const { closeBrowser } = await import("./lib/engine.js").catch(() => ({ closeBrowser: async () => {} }));
177
+ await closeBrowser();
178
+ die(`${e.message}${process.env.WAND_DEBUG ? "\n" + e.stack : ""}`);
179
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.