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.
- package/.gitkeep +0 -0
- package/PROMPT.md +255 -0
- package/SKILL.md +117 -0
- package/START-HERE.md +16 -0
- package/brands/current/assets/bg-cover-dark.png +0 -0
- package/brands/current/assets/bg-overlay-dark.png +0 -0
- package/brands/current/assets/bg-statement-dark.png +0 -0
- package/brands/current/assets/closing-art.png +0 -0
- package/brands/current/assets/cover-art.png +0 -0
- package/brands/current/assets/divider-art.png +0 -0
- package/brands/current/assets/glyphs/README.md +29 -0
- package/brands/current/assets/glyphs/activity.svg +15 -0
- package/brands/current/assets/glyphs/atom.svg +17 -0
- package/brands/current/assets/glyphs/badge-check.svg +16 -0
- package/brands/current/assets/glyphs/bell.svg +16 -0
- package/brands/current/assets/glyphs/blocks.svg +16 -0
- package/brands/current/assets/glyphs/book-open.svg +16 -0
- package/brands/current/assets/glyphs/bot.svg +20 -0
- package/brands/current/assets/glyphs/boxes.svg +26 -0
- package/brands/current/assets/glyphs/brain.svg +23 -0
- package/brands/current/assets/glyphs/briefcase.svg +16 -0
- package/brands/current/assets/glyphs/building-2.svg +21 -0
- package/brands/current/assets/glyphs/calendar.svg +18 -0
- package/brands/current/assets/glyphs/chart-column.svg +18 -0
- package/brands/current/assets/glyphs/chart-line.svg +16 -0
- package/brands/current/assets/glyphs/chart-pie.svg +16 -0
- package/brands/current/assets/glyphs/circle-check.svg +16 -0
- package/brands/current/assets/glyphs/clipboard-check.svg +17 -0
- package/brands/current/assets/glyphs/clock.svg +16 -0
- package/brands/current/assets/glyphs/cloud.svg +15 -0
- package/brands/current/assets/glyphs/code.svg +15 -0
- package/brands/current/assets/glyphs/coins.svg +18 -0
- package/brands/current/assets/glyphs/compass.svg +16 -0
- package/brands/current/assets/glyphs/cpu.svg +24 -0
- package/brands/current/assets/glyphs/database.svg +17 -0
- package/brands/current/assets/glyphs/eye.svg +16 -0
- package/brands/current/assets/glyphs/file-check.svg +17 -0
- package/brands/current/assets/glyphs/filter.svg +15 -0
- package/brands/current/assets/glyphs/fingerprint.svg +23 -0
- package/brands/current/assets/glyphs/flag.svg +16 -0
- package/brands/current/assets/glyphs/gauge.svg +16 -0
- package/brands/current/assets/glyphs/gavel.svg +19 -0
- package/brands/current/assets/glyphs/git-branch.svg +18 -0
- package/brands/current/assets/glyphs/globe.svg +17 -0
- package/brands/current/assets/glyphs/graduation-cap.svg +17 -0
- package/brands/current/assets/glyphs/handshake.svg +19 -0
- package/brands/current/assets/glyphs/hexagon.svg +15 -0
- package/brands/current/assets/glyphs/index.json +177 -0
- package/brands/current/assets/glyphs/infinity.svg +15 -0
- package/brands/current/assets/glyphs/key.svg +17 -0
- package/brands/current/assets/glyphs/landmark.svg +20 -0
- package/brands/current/assets/glyphs/layers.svg +17 -0
- package/brands/current/assets/glyphs/lightbulb.svg +17 -0
- package/brands/current/assets/glyphs/link.svg +16 -0
- package/brands/current/assets/glyphs/list-checks.svg +19 -0
- package/brands/current/assets/glyphs/lock.svg +16 -0
- package/brands/current/assets/glyphs/map.svg +17 -0
- package/brands/current/assets/glyphs/megaphone.svg +16 -0
- package/brands/current/assets/glyphs/message-square.svg +15 -0
- package/brands/current/assets/glyphs/milestone.svg +17 -0
- package/brands/current/assets/glyphs/network.svg +19 -0
- package/brands/current/assets/glyphs/orbit.svg +19 -0
- package/brands/current/assets/glyphs/package.svg +18 -0
- package/brands/current/assets/glyphs/plug.svg +18 -0
- package/brands/current/assets/glyphs/puzzle.svg +15 -0
- package/brands/current/assets/glyphs/radar.svg +22 -0
- package/brands/current/assets/glyphs/receipt.svg +17 -0
- package/brands/current/assets/glyphs/refresh-cw.svg +18 -0
- package/brands/current/assets/glyphs/rocket.svg +18 -0
- package/brands/current/assets/glyphs/route.svg +17 -0
- package/brands/current/assets/glyphs/scale.svg +19 -0
- package/brands/current/assets/glyphs/search.svg +16 -0
- package/brands/current/assets/glyphs/server.svg +18 -0
- package/brands/current/assets/glyphs/settings.svg +16 -0
- package/brands/current/assets/glyphs/share-2.svg +19 -0
- package/brands/current/assets/glyphs/shield-check.svg +16 -0
- package/brands/current/assets/glyphs/shuffle.svg +19 -0
- package/brands/current/assets/glyphs/sparkles.svg +19 -0
- package/brands/current/assets/glyphs/square-stack.svg +17 -0
- package/brands/current/assets/glyphs/target.svg +17 -0
- package/brands/current/assets/glyphs/trending-up.svg +16 -0
- package/brands/current/assets/glyphs/triangle-alert.svg +17 -0
- package/brands/current/assets/glyphs/trophy.svg +20 -0
- package/brands/current/assets/glyphs/users.svg +18 -0
- package/brands/current/assets/glyphs/wallet.svg +16 -0
- package/brands/current/assets/glyphs/wand-sparkles.svg +22 -0
- package/brands/current/assets/glyphs/workflow.svg +17 -0
- package/brands/current/assets/glyphs/zap.svg +15 -0
- package/brands/current/assets/mark-dark.png +0 -0
- package/brands/current/assets/mark-white.png +0 -0
- package/brands/current/assets/orb-cover.png +0 -0
- package/brands/current/assets/orb-glow.png +0 -0
- package/brands/current/assets/wordmark-dark.png +0 -0
- package/brands/current/assets/wordmark-white.png +0 -0
- package/brands/current/fonts/Geist-Bold.ttf +0 -0
- package/brands/current/fonts/Geist-BoldItalic.ttf +0 -0
- package/brands/current/fonts/Geist-ExtraBold.ttf +0 -0
- package/brands/current/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Italic.ttf +0 -0
- package/brands/current/fonts/Geist-Light.ttf +0 -0
- package/brands/current/fonts/Geist-LightItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Medium.ttf +0 -0
- package/brands/current/fonts/Geist-MediumItalic.ttf +0 -0
- package/brands/current/fonts/Geist-Regular.ttf +0 -0
- package/brands/current/fonts/Geist-SemiBold.ttf +0 -0
- package/brands/current/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/brands/current/lib/glyphs.js +87 -0
- package/brands/current/lib/measure.js +115 -0
- package/brands/current/lib/pdf.js +603 -0
- package/brands/current/lib/ttf.py +167 -0
- package/brands/current/lib/wandxml.py +37 -0
- package/brands/current/tokens.json +206 -0
- package/brands/next/assets/closing-art.png +0 -0
- package/brands/next/assets/cover-art.png +0 -0
- package/brands/next/assets/divider-art.png +0 -0
- package/brands/next/assets/glyphs/README.md +29 -0
- package/brands/next/assets/glyphs/activity.svg +15 -0
- package/brands/next/assets/glyphs/atom.svg +17 -0
- package/brands/next/assets/glyphs/badge-check.svg +16 -0
- package/brands/next/assets/glyphs/bell.svg +16 -0
- package/brands/next/assets/glyphs/blocks.svg +16 -0
- package/brands/next/assets/glyphs/book-open.svg +16 -0
- package/brands/next/assets/glyphs/bot.svg +20 -0
- package/brands/next/assets/glyphs/boxes.svg +26 -0
- package/brands/next/assets/glyphs/brain.svg +23 -0
- package/brands/next/assets/glyphs/briefcase.svg +16 -0
- package/brands/next/assets/glyphs/building-2.svg +21 -0
- package/brands/next/assets/glyphs/calendar.svg +18 -0
- package/brands/next/assets/glyphs/chart-column.svg +18 -0
- package/brands/next/assets/glyphs/chart-line.svg +16 -0
- package/brands/next/assets/glyphs/chart-pie.svg +16 -0
- package/brands/next/assets/glyphs/circle-check.svg +16 -0
- package/brands/next/assets/glyphs/clipboard-check.svg +17 -0
- package/brands/next/assets/glyphs/clock.svg +16 -0
- package/brands/next/assets/glyphs/cloud.svg +15 -0
- package/brands/next/assets/glyphs/code.svg +15 -0
- package/brands/next/assets/glyphs/coins.svg +18 -0
- package/brands/next/assets/glyphs/compass.svg +16 -0
- package/brands/next/assets/glyphs/cpu.svg +24 -0
- package/brands/next/assets/glyphs/database.svg +17 -0
- package/brands/next/assets/glyphs/eye.svg +16 -0
- package/brands/next/assets/glyphs/file-check.svg +17 -0
- package/brands/next/assets/glyphs/filter.svg +15 -0
- package/brands/next/assets/glyphs/fingerprint.svg +23 -0
- package/brands/next/assets/glyphs/flag.svg +16 -0
- package/brands/next/assets/glyphs/gauge.svg +16 -0
- package/brands/next/assets/glyphs/gavel.svg +19 -0
- package/brands/next/assets/glyphs/git-branch.svg +18 -0
- package/brands/next/assets/glyphs/globe.svg +17 -0
- package/brands/next/assets/glyphs/graduation-cap.svg +17 -0
- package/brands/next/assets/glyphs/handshake.svg +19 -0
- package/brands/next/assets/glyphs/hexagon.svg +15 -0
- package/brands/next/assets/glyphs/index.json +177 -0
- package/brands/next/assets/glyphs/infinity.svg +15 -0
- package/brands/next/assets/glyphs/key.svg +17 -0
- package/brands/next/assets/glyphs/landmark.svg +20 -0
- package/brands/next/assets/glyphs/layers.svg +17 -0
- package/brands/next/assets/glyphs/lightbulb.svg +17 -0
- package/brands/next/assets/glyphs/link.svg +16 -0
- package/brands/next/assets/glyphs/list-checks.svg +19 -0
- package/brands/next/assets/glyphs/lock.svg +16 -0
- package/brands/next/assets/glyphs/map.svg +17 -0
- package/brands/next/assets/glyphs/megaphone.svg +16 -0
- package/brands/next/assets/glyphs/message-square.svg +15 -0
- package/brands/next/assets/glyphs/milestone.svg +17 -0
- package/brands/next/assets/glyphs/network.svg +19 -0
- package/brands/next/assets/glyphs/orbit.svg +19 -0
- package/brands/next/assets/glyphs/package.svg +18 -0
- package/brands/next/assets/glyphs/plug.svg +18 -0
- package/brands/next/assets/glyphs/puzzle.svg +15 -0
- package/brands/next/assets/glyphs/radar.svg +22 -0
- package/brands/next/assets/glyphs/receipt.svg +17 -0
- package/brands/next/assets/glyphs/refresh-cw.svg +18 -0
- package/brands/next/assets/glyphs/rocket.svg +18 -0
- package/brands/next/assets/glyphs/route.svg +17 -0
- package/brands/next/assets/glyphs/scale.svg +19 -0
- package/brands/next/assets/glyphs/search.svg +16 -0
- package/brands/next/assets/glyphs/server.svg +18 -0
- package/brands/next/assets/glyphs/settings.svg +16 -0
- package/brands/next/assets/glyphs/share-2.svg +19 -0
- package/brands/next/assets/glyphs/shield-check.svg +16 -0
- package/brands/next/assets/glyphs/shuffle.svg +19 -0
- package/brands/next/assets/glyphs/sparkles.svg +19 -0
- package/brands/next/assets/glyphs/square-stack.svg +17 -0
- package/brands/next/assets/glyphs/target.svg +17 -0
- package/brands/next/assets/glyphs/trending-up.svg +16 -0
- package/brands/next/assets/glyphs/triangle-alert.svg +17 -0
- package/brands/next/assets/glyphs/trophy.svg +20 -0
- package/brands/next/assets/glyphs/users.svg +18 -0
- package/brands/next/assets/glyphs/wallet.svg +16 -0
- package/brands/next/assets/glyphs/wand-sparkles.svg +22 -0
- package/brands/next/assets/glyphs/workflow.svg +17 -0
- package/brands/next/assets/glyphs/zap.svg +15 -0
- package/brands/next/assets/layers-dark.png +0 -0
- package/brands/next/assets/layers-light.png +0 -0
- package/brands/next/assets/ornaments/burst-dark.png +0 -0
- package/brands/next/assets/ornaments/burst-light.png +0 -0
- package/brands/next/assets/ornaments/chain-dark.png +0 -0
- package/brands/next/assets/ornaments/chain-light.png +0 -0
- package/brands/next/assets/ornaments/coil-dark.png +0 -0
- package/brands/next/assets/ornaments/coil-light.png +0 -0
- package/brands/next/assets/ornaments/cone-dark.png +0 -0
- package/brands/next/assets/ornaments/cone-light.png +0 -0
- package/brands/next/assets/ornaments/diamond-dark.png +0 -0
- package/brands/next/assets/ornaments/diamond-light.png +0 -0
- package/brands/next/assets/ornaments/fan-dark.png +0 -0
- package/brands/next/assets/ornaments/fan-light.png +0 -0
- package/brands/next/assets/ornaments/globe-dark.png +0 -0
- package/brands/next/assets/ornaments/globe-light.png +0 -0
- package/brands/next/assets/ornaments/orbit-dark.png +0 -0
- package/brands/next/assets/ornaments/orbit-light.png +0 -0
- package/brands/next/assets/ornaments/rings-dark.png +0 -0
- package/brands/next/assets/ornaments/rings-light.png +0 -0
- package/brands/next/assets/ornaments/sphere-dark.png +0 -0
- package/brands/next/assets/ornaments/sphere-light.png +0 -0
- package/brands/next/assets/ornaments/sun-dark.png +0 -0
- package/brands/next/assets/ornaments/sun-light.png +0 -0
- package/brands/next/assets/photos/atrium.jpg +0 -0
- package/brands/next/assets/photos/cantilever.jpg +0 -0
- package/brands/next/assets/photos/corridor.jpg +0 -0
- package/brands/next/assets/photos/dome.jpg +0 -0
- package/brands/next/assets/photos/facade.jpg +0 -0
- package/brands/next/assets/photos/glass.jpg +0 -0
- package/brands/next/assets/photos/stairs.jpg +0 -0
- package/brands/next/assets/photos/tower.jpg +0 -0
- package/brands/next/assets/photos/walk.jpg +0 -0
- package/brands/next/assets/spark-gold.png +0 -0
- package/brands/next/assets/statement-art.png +0 -0
- package/brands/next/assets/wordmark-dark.png +0 -0
- package/brands/next/assets/wordmark-light.png +0 -0
- package/brands/next/assets/wordmark.svg +7 -0
- package/brands/next/fonts/FKRomanStandard-Light.ttf +0 -0
- package/brands/next/fonts/OFL-OverusedGrotesk.txt +93 -0
- package/brands/next/fonts/OverusedGrotesk-Italic.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-Medium.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-MediumItalic.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-Roman.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/brands/next/fonts/OverusedGrotesk-SemiBoldItalic.ttf +0 -0
- package/brands/next/fonts/faces.json +27 -0
- package/brands/next/lib/glyphs.js +89 -0
- package/brands/next/lib/measure.js +121 -0
- package/brands/next/lib/pdf.js +597 -0
- package/brands/next/lib/ttf.py +169 -0
- package/brands/next/lib/wandxml.py +37 -0
- package/brands/next/tokens.json +174 -0
- package/examples/gallery/job.mjs +83 -0
- package/examples/overview/job.mjs +99 -0
- package/lib/brand.js +135 -0
- package/lib/components.js +313 -0
- package/lib/content.js +66 -0
- package/lib/engine.js +280 -0
- package/lib/html_export.js +71 -0
- package/lib/pptx.js +446 -0
- package/lib/surfaces.js +62 -0
- package/package.json +17 -4
- package/templates/card.mjs +30 -0
- package/templates/page.mjs +52 -0
- package/templates/slide.mjs +46 -0
- package/tools/embed_fonts.py +154 -0
- package/wand.js +179 -0
- 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