wand-decks-kit 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROMPT.md +461 -0
- package/README.md +137 -0
- package/START-HERE.md +98 -0
- package/assets/README.md +40 -0
- package/assets/_icon-contact-sheet.png +0 -0
- package/assets/bg-cover-dark.png +0 -0
- package/assets/bg-overlay-dark.png +0 -0
- package/assets/bg-statement-dark.png +0 -0
- package/assets/glyphs/README.md +29 -0
- package/assets/glyphs/activity.svg +15 -0
- package/assets/glyphs/atom.svg +17 -0
- package/assets/glyphs/badge-check.svg +16 -0
- package/assets/glyphs/bell.svg +16 -0
- package/assets/glyphs/blocks.svg +16 -0
- package/assets/glyphs/book-open.svg +16 -0
- package/assets/glyphs/bot.svg +20 -0
- package/assets/glyphs/boxes.svg +26 -0
- package/assets/glyphs/brain.svg +23 -0
- package/assets/glyphs/briefcase.svg +16 -0
- package/assets/glyphs/building-2.svg +21 -0
- package/assets/glyphs/calendar.svg +18 -0
- package/assets/glyphs/chart-column.svg +18 -0
- package/assets/glyphs/chart-line.svg +16 -0
- package/assets/glyphs/chart-pie.svg +16 -0
- package/assets/glyphs/circle-check.svg +16 -0
- package/assets/glyphs/clipboard-check.svg +17 -0
- package/assets/glyphs/clock.svg +16 -0
- package/assets/glyphs/cloud.svg +15 -0
- package/assets/glyphs/coins.svg +18 -0
- package/assets/glyphs/compass.svg +16 -0
- package/assets/glyphs/cpu.svg +24 -0
- package/assets/glyphs/database.svg +17 -0
- package/assets/glyphs/eye.svg +16 -0
- package/assets/glyphs/file-check.svg +17 -0
- package/assets/glyphs/filter.svg +15 -0
- package/assets/glyphs/fingerprint.svg +23 -0
- package/assets/glyphs/flag.svg +16 -0
- package/assets/glyphs/gauge.svg +16 -0
- package/assets/glyphs/gavel.svg +19 -0
- package/assets/glyphs/git-branch.svg +18 -0
- package/assets/glyphs/globe.svg +17 -0
- package/assets/glyphs/graduation-cap.svg +17 -0
- package/assets/glyphs/handshake.svg +19 -0
- package/assets/glyphs/hexagon.svg +15 -0
- package/assets/glyphs/index.json +175 -0
- package/assets/glyphs/infinity.svg +15 -0
- package/assets/glyphs/key.svg +17 -0
- package/assets/glyphs/landmark.svg +20 -0
- package/assets/glyphs/layers.svg +17 -0
- package/assets/glyphs/lightbulb.svg +17 -0
- package/assets/glyphs/link.svg +16 -0
- package/assets/glyphs/list-checks.svg +19 -0
- package/assets/glyphs/lock.svg +16 -0
- package/assets/glyphs/map.svg +17 -0
- package/assets/glyphs/megaphone.svg +16 -0
- package/assets/glyphs/message-square.svg +15 -0
- package/assets/glyphs/milestone.svg +17 -0
- package/assets/glyphs/network.svg +19 -0
- package/assets/glyphs/orbit.svg +19 -0
- package/assets/glyphs/package.svg +18 -0
- package/assets/glyphs/plug.svg +18 -0
- package/assets/glyphs/puzzle.svg +15 -0
- package/assets/glyphs/radar.svg +22 -0
- package/assets/glyphs/receipt.svg +17 -0
- package/assets/glyphs/refresh-cw.svg +18 -0
- package/assets/glyphs/rocket.svg +18 -0
- package/assets/glyphs/route.svg +17 -0
- package/assets/glyphs/scale.svg +19 -0
- package/assets/glyphs/search.svg +16 -0
- package/assets/glyphs/server.svg +18 -0
- package/assets/glyphs/settings.svg +16 -0
- package/assets/glyphs/share-2.svg +19 -0
- package/assets/glyphs/shield-check.svg +16 -0
- package/assets/glyphs/shuffle.svg +19 -0
- package/assets/glyphs/sparkles.svg +19 -0
- package/assets/glyphs/square-stack.svg +17 -0
- package/assets/glyphs/target.svg +17 -0
- package/assets/glyphs/trending-up.svg +16 -0
- package/assets/glyphs/triangle-alert.svg +17 -0
- package/assets/glyphs/trophy.svg +20 -0
- package/assets/glyphs/users.svg +18 -0
- package/assets/glyphs/wallet.svg +16 -0
- package/assets/glyphs/wand-sparkles.svg +22 -0
- package/assets/glyphs/workflow.svg +17 -0
- package/assets/glyphs/zap.svg +15 -0
- package/assets/icons/icon-11.png +0 -0
- package/assets/icons/icon-12.png +0 -0
- package/assets/icons/icon-13.png +0 -0
- package/assets/icons/icon-14.png +0 -0
- package/assets/icons/icon-15.png +0 -0
- package/assets/icons/icon-16.png +0 -0
- package/assets/icons/icon-17.png +0 -0
- package/assets/icons/icon-18.png +0 -0
- package/assets/icons/icon-20.png +0 -0
- package/assets/icons/icon-23.png +0 -0
- package/assets/icons/icon-24.png +0 -0
- package/assets/icons/icon-25.png +0 -0
- package/assets/icons/icon-27.png +0 -0
- package/assets/icons/icon-28.png +0 -0
- package/assets/icons/icon-30.png +0 -0
- package/assets/icons/icon-31.png +0 -0
- package/assets/icons/icon-32.png +0 -0
- package/assets/icons/icon-33.png +0 -0
- package/assets/icons/icon-34.png +0 -0
- package/assets/icons/icon-35.png +0 -0
- package/assets/icons/icon-36.png +0 -0
- package/assets/icons/icon-37.png +0 -0
- package/assets/icons/icon-39.png +0 -0
- package/assets/icons/icon-40.png +0 -0
- package/assets/icons/icon-41.png +0 -0
- package/assets/icons/icon-43.png +0 -0
- package/assets/icons/icon-44.png +0 -0
- package/assets/icons/icon-45.png +0 -0
- package/assets/icons/icon-46.png +0 -0
- package/assets/icons/icon-48.png +0 -0
- package/assets/icons/icon-49.png +0 -0
- package/assets/icons/icon-50.png +0 -0
- package/assets/icons/icon-51.png +0 -0
- package/assets/icons/icon-52.png +0 -0
- package/assets/icons/icon-53.png +0 -0
- package/assets/icons/icon-9.png +0 -0
- package/assets/mark-dark.png +0 -0
- package/assets/mark-white.png +0 -0
- package/assets/orb-cover.png +0 -0
- package/assets/wordmark-dark.png +0 -0
- package/assets/wordmark-white.png +0 -0
- package/build.js +192 -0
- package/build_example.js +91 -0
- package/build_infinite.js +283 -0
- package/docs/LAYOUTS.md +86 -0
- package/docs/PIPELINE.md +116 -0
- package/docs/SPEC.md +89 -0
- package/embed_fonts.py +193 -0
- package/examples/template.json +164 -0
- package/fonts/Geist-Bold.ttf +0 -0
- package/fonts/Geist-BoldItalic.ttf +0 -0
- package/fonts/Geist-ExtraBold.ttf +0 -0
- package/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/fonts/Geist-Italic.ttf +0 -0
- package/fonts/Geist-Light.ttf +0 -0
- package/fonts/Geist-LightItalic.ttf +0 -0
- package/fonts/Geist-Medium.ttf +0 -0
- package/fonts/Geist-MediumItalic.ttf +0 -0
- package/fonts/Geist-Regular.ttf +0 -0
- package/fonts/Geist-SemiBold.ttf +0 -0
- package/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/lib/README.md +23 -0
- package/lib/brand.js +51 -0
- package/lib/glyphs.js +87 -0
- package/lib/layouts_client.js +212 -0
- package/lib/layouts_consult.js +301 -0
- package/lib/layouts_dense.js +361 -0
- package/lib/measure.js +109 -0
- package/lib/pdf.js +517 -0
- package/lib/plan.js +305 -0
- package/lib/render_pdf.js +203 -0
- package/lib/schema.js +399 -0
- package/lib/shared.js +15 -0
- package/lib/trace.js +88 -0
- package/lib/ttf.py +167 -0
- package/lib/wandxml.py +37 -0
- package/package.json +17 -0
- package/preview/preview.css +112 -0
- package/preview/preview.html +24 -0
- package/preview/preview.js +169 -0
- package/qa_coverage.py +120 -0
- package/requirements.txt +9 -0
- package/tools/extract.py +199 -0
- package/wand.js +397 -0
- package/wand_kit.js +1016 -0
package/docs/PIPELINE.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Pipeline
|
|
2
|
+
|
|
3
|
+
The §6 phases, as commands. Two of them do everything; the rest exist so a
|
|
4
|
+
failed stage can be re-run on its own.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
node wand.js doctor # before anything: what works in this container
|
|
8
|
+
node wand.js rebuild source.pptx # phases 0-3
|
|
9
|
+
# ... you write work/deck.json ...
|
|
10
|
+
node wand.js ship work/deck.json # phases 4-6, every gate, one table at the end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 0–1 · Normalize and extract — `rebuild`, stage 1
|
|
14
|
+
|
|
15
|
+
Reports the source canvas and the scale factor onto the 10" grid; most decks are
|
|
16
|
+
13.333" × 7.5" and need ×0.75. Writes into `work/`:
|
|
17
|
+
|
|
18
|
+
| File | What |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `visible_dump.txt` | every verbatim string — the contract coverage checks against |
|
|
21
|
+
| `all_text.json` | per-slide, per-frame, per-paragraph structure |
|
|
22
|
+
| `geometry.txt` | every shape, normalised to the Wand grid |
|
|
23
|
+
| `analysis.json` | text frames with box, point size and weight, plus the slide background |
|
|
24
|
+
| `deck.skeleton.json` | one entry per visible slide, `layout: "TODO"` |
|
|
25
|
+
|
|
26
|
+
Standalone: `node wand.js extract source.pptx -o work/`
|
|
27
|
+
|
|
28
|
+
## 2–3 · Classify and map — `rebuild`, stage 2
|
|
29
|
+
|
|
30
|
+
Writes `work/PLAN.md`. Per slide: how many cards in how many columns over how
|
|
31
|
+
many bands, the tag / title / subtitle already identified by point size, a
|
|
32
|
+
shortlist of layouts with the reason for each, every verbatim string grouped by
|
|
33
|
+
frame, and a stub carrying the top candidate's required fields with the theme
|
|
34
|
+
read off the source background.
|
|
35
|
+
|
|
36
|
+
The shortlist is ranked by **shape**. It cannot see meaning, and §6 Phase 3
|
|
37
|
+
orders on meaning — structural first, then quotes, timelines, data, comparisons,
|
|
38
|
+
stacks, assertions, numbered asks, and only then item count. Overrule it freely.
|
|
39
|
+
|
|
40
|
+
Emit the mapping plan before writing any JSON:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
S07 (timeline, 6 phases + trajectory band) → phaseCards-D
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Standalone: `node wand.js plan work/`
|
|
47
|
+
|
|
48
|
+
## 4 · Build — `ship`, stages 1–4
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
1 spec fields, types, and item counts against the layout catalog
|
|
52
|
+
2 fit every text box measured in real Geist
|
|
53
|
+
bounds every shape inside the content band
|
|
54
|
+
asset every glyph and image the spec names actually exists
|
|
55
|
+
3 build .pptx
|
|
56
|
+
4 embed fonts required (§7). Without it the deck renders as Arial
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Iterate in the browser rather than round-tripping the file:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node wand.js preview work/deck.json # then open preview/preview.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The preview renders the same recorded geometry the `.pptx` gets, in real Geist,
|
|
66
|
+
with overflow flagging and the §1 rails as a toggle.
|
|
67
|
+
|
|
68
|
+
`node wand.js layouts <name>` prints any layout's fields when you are writing the
|
|
69
|
+
spec; `node wand.js validate work/deck.json` checks them before you build.
|
|
70
|
+
|
|
71
|
+
## 5 · Review note
|
|
72
|
+
|
|
73
|
+
Markdown, slide-numbered. Every split or merge, every fallback mapping, anything
|
|
74
|
+
tight after fitting, anything off-brand you couldn't rebrand, **everything
|
|
75
|
+
dropped from the verbatim dump**, and any place a reference deck and the spec
|
|
76
|
+
disagreed.
|
|
77
|
+
|
|
78
|
+
## 6 · QA — `ship`, stages 5–7
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
5 coverage every source string present, and how many were too short to compare
|
|
82
|
+
6 file parts, relationships, embedded and unsubsetted fonts
|
|
83
|
+
7 visual the preview — the only gate that sees a slide that is merely ugly
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`ship` prints a gate table and exits non-zero if any of them failed.
|
|
87
|
+
|
|
88
|
+
If LibreOffice is installed you can also render:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
soffice --headless --convert-to pdf work/deck.pptx
|
|
92
|
+
pdftoppm -jpeg -r 150 deck.pdf slide
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Plus the subset check — empty output is a pass:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 -c "import subprocess; t=subprocess.run(['pdftotext','deck.pdf','-'],capture_output=True,text=True).stdout; print([c for c in t if 0x01C9<=ord(c)<=0x024F] or 'clean')"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Deliver the `.pptx` first, then the review note.
|
|
102
|
+
|
|
103
|
+
## What QA can and can't tell you
|
|
104
|
+
|
|
105
|
+
LibreOffice ignores embedded fonts and substitutes DejaVu. So:
|
|
106
|
+
|
|
107
|
+
- **Don't trust it on fit.** DejaVu is wider than Geist, so renders over-report
|
|
108
|
+
overflow. Use the fit check, which measures the real face.
|
|
109
|
+
- **Don't trust it on weight.** `Geist ExtraBold` renders regular. Verify the
|
|
110
|
+
accent-italic device and any hierarchy question in the XML or the preview.
|
|
111
|
+
- **Do trust it on** geometry, overlap, alignment, gaps, contrast, leftover
|
|
112
|
+
placeholders, orphaned decoration, mispositioned art.
|
|
113
|
+
|
|
114
|
+
And when it isn't installed at all — which is the normal case in a fresh
|
|
115
|
+
container — the browser preview covers everything in that last list, in the real
|
|
116
|
+
faces, which LibreOffice cannot do.
|
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Deck spec format
|
|
2
|
+
|
|
3
|
+
A deck is data. `build.js` turns a spec into a `.pptx`; `--preview` turns the
|
|
4
|
+
same spec into a browser preview. Neither re-implements the other, so they can't
|
|
5
|
+
disagree.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node wand.js validate deck.json # fields, types, item counts. Run this first
|
|
9
|
+
node wand.js layouts numberedCards # what a layout takes, with the caps
|
|
10
|
+
node wand.js check deck.json # fit, bounds and assets. Exit 1 on any
|
|
11
|
+
node wand.js preview deck.json # write preview/trace.js, then open preview/preview.html
|
|
12
|
+
node wand.js ship deck.json # every gate, then the .pptx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`lib/schema.js` is the machine-readable version of this document: every layout's
|
|
16
|
+
fields, their types, and the number of items it can actually draw. `validate`,
|
|
17
|
+
`layouts` and the `plan` worksheet all read it, so they cannot drift from each
|
|
18
|
+
other. If a layout gains a field, it goes there.
|
|
19
|
+
|
|
20
|
+
## Shape
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"deck": "WAND × ACME",
|
|
25
|
+
"confidential": "CONFIDENTIAL",
|
|
26
|
+
"slides": [
|
|
27
|
+
{ "layout": "cover", "eyebrow": "...", "titleLines": ["...", "..."] },
|
|
28
|
+
{ "layout": "rows", "dark": true, "tag": "THE FULL STACK", "items": [] }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`deck` and `confidential` fill the footer and can be overridden per slide.
|
|
34
|
+
Page numbers are computed — never write them into a spec.
|
|
35
|
+
|
|
36
|
+
Full-bleed layouts (`cover`, `divider`, `closing`) get no footer automatically.
|
|
37
|
+
|
|
38
|
+
## Every slide takes
|
|
39
|
+
|
|
40
|
+
| Key | Meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `layout` | required — a function name exported by `wand_kit.js` |
|
|
43
|
+
| `tag` | the caps label top-left |
|
|
44
|
+
| `dark` | `true` dark theme, `false` light. Alternate across the deck |
|
|
45
|
+
| `titleText` | string, or `["before ", "accent phrase", " after"]` for the accent-italic device |
|
|
46
|
+
| `subtitleText` | the lede under the title |
|
|
47
|
+
|
|
48
|
+
Everything else is layout-specific — see `docs/LAYOUTS.md`, or run
|
|
49
|
+
`node wand.js layouts <name>`, which prints the same thing from the schema and
|
|
50
|
+
marks the required fields.
|
|
51
|
+
|
|
52
|
+
## Item caps are enforced
|
|
53
|
+
|
|
54
|
+
Every layout slices its items to what it can draw. A 7th item on a `sixUpIcon`
|
|
55
|
+
used to vanish silently and turn up as a coverage miss three steps later;
|
|
56
|
+
`validate` now fails on it, and says how many would have been dropped.
|
|
57
|
+
|
|
58
|
+
## The accent-italic device
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
"titleText": ["Two kinds of colleagues. ", "One set of drives", "."]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Element 1 renders accent + italic. The trailing period stays in ink. One per
|
|
65
|
+
title, never two. A plain string is fine when a title needs no emphasis.
|
|
66
|
+
|
|
67
|
+
## Line breaks
|
|
68
|
+
|
|
69
|
+
Use `\n` inside a title or caption where the source deck breaks a line. The kit
|
|
70
|
+
splits on it and carries the break explicitly — an embedded newline used to
|
|
71
|
+
push every following run onto its own line.
|
|
72
|
+
|
|
73
|
+
## What the spec deliberately cannot do
|
|
74
|
+
|
|
75
|
+
No coordinates, no colours, no font sizes. If a slide needs geometry the
|
|
76
|
+
layouts don't provide, add a layout — don't leak positioning into content.
|
|
77
|
+
That separation is what makes a rebuild reviewable: the spec diffs against the
|
|
78
|
+
source's verbatim text, and the layout diffs against the design system.
|
|
79
|
+
|
|
80
|
+
## Starting from a source deck
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node wand.js rebuild source.pptx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
writes `work/PLAN.md` — the worksheet you fill in. Per slide it carries the
|
|
87
|
+
source's shape, a layout shortlist with reasons, every verbatim string grouped
|
|
88
|
+
by frame, and a stub with the top candidate's required fields already present.
|
|
89
|
+
Choose the layout by §6 Phase 3, transcribe the text, `validate`, `ship`.
|
package/embed_fonts.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
embed_fonts.py — embed the five Geist families into a .pptx, unsubsetted.
|
|
4
|
+
|
|
5
|
+
python embed_fonts.py deck.pptx [-o out.pptx] [--fonts DIR]
|
|
6
|
+
|
|
7
|
+
Why this exists: a Geist deck without embedded fonts renders as Arial on the
|
|
8
|
+
client's machine. Spec §7.
|
|
9
|
+
|
|
10
|
+
Two things this does that the reference decks get wrong:
|
|
11
|
+
* writes UNCOMPRESSED EOT and sets saveSubsetFonts="0", so digits don't
|
|
12
|
+
garble in exported PDFs (spec §9)
|
|
13
|
+
* ships full faces, not subsets
|
|
14
|
+
|
|
15
|
+
The .fntdata part is an EOT (Embedded OpenType) container — magic 0x504C,
|
|
16
|
+
version 2.1, TTCOMPRESSED flag clear, raw TTF appended.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse, os, re, shutil, struct, sys, tempfile, zipfile
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _brand_lib():
|
|
23
|
+
"""brand/lib in the repo, or lib/ beside the kit once packaged."""
|
|
24
|
+
import os
|
|
25
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
for d in filter(None, [os.environ.get("WAND_BRAND_DIR"),
|
|
27
|
+
os.path.join(here, "lib"),
|
|
28
|
+
os.path.join(here, "..", "lib"),
|
|
29
|
+
os.path.join(here, "..", "..", "..", "..", "brand", "lib"),
|
|
30
|
+
os.path.join(here, "..", "..", "..", "brand", "lib")]):
|
|
31
|
+
if os.path.isfile(os.path.join(d, "wandxml.py")):
|
|
32
|
+
return os.path.normpath(d)
|
|
33
|
+
raise SystemExit("brand/lib not found")
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, _brand_lib())
|
|
36
|
+
import ttf as _ttf
|
|
37
|
+
|
|
38
|
+
# family -> (regular, bold, italic, boldItalic) source stems
|
|
39
|
+
FAMILIES = {
|
|
40
|
+
"Geist": ("Geist-Regular", "Geist-Bold", "Geist-Italic", "Geist-BoldItalic"),
|
|
41
|
+
"Geist Medium": ("Geist-Medium", "Geist-SemiBold", "Geist-MediumItalic", "Geist-SemiBoldItalic"),
|
|
42
|
+
"Geist SemiBold": ("Geist-SemiBold", "Geist-ExtraBold", "Geist-SemiBoldItalic", "Geist-ExtraBoldItalic"),
|
|
43
|
+
"Geist ExtraBold": ("Geist-ExtraBold", "Geist-ExtraBold", "Geist-ExtraBoldItalic","Geist-ExtraBoldItalic"),
|
|
44
|
+
"Geist Light": ("Geist-Light", "Geist-Medium", "Geist-LightItalic", "Geist-MediumItalic"),
|
|
45
|
+
}
|
|
46
|
+
FACES = ("regular", "bold", "italic", "boldItalic")
|
|
47
|
+
|
|
48
|
+
NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
49
|
+
REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def retag(ttf_path, family, italic):
|
|
53
|
+
"""Rewrite the TTF name table so its family matches the name used in the XML.
|
|
54
|
+
|
|
55
|
+
Weight is carried by the family name in this system, so all five families
|
|
56
|
+
must report distinct names or PowerPoint collapses them onto one face.
|
|
57
|
+
"""
|
|
58
|
+
f = _ttf.load(ttf_path)
|
|
59
|
+
sub = "Italic" if italic else "Regular"
|
|
60
|
+
full = f"{family} Italic" if italic else family
|
|
61
|
+
data = f.retagged(family, sub, full, full.replace(" ", ""))
|
|
62
|
+
return data, _ttf.TTF(data)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def eot(ttf_bytes, font, family, italic):
|
|
66
|
+
"""Wrap raw TTF bytes in an uncompressed EOT v2.1 container."""
|
|
67
|
+
os2 = font.os2
|
|
68
|
+
panose = os2["panose"]
|
|
69
|
+
|
|
70
|
+
def lpstr(s):
|
|
71
|
+
b = s.encode("utf-16-le")
|
|
72
|
+
return struct.pack("<HH", 0, len(b)) + b # padding + size + string
|
|
73
|
+
|
|
74
|
+
body = b""
|
|
75
|
+
body += panose # FontPANOSE[10]
|
|
76
|
+
body += struct.pack("<BB", 1, 1 if italic else 0) # Charset(DEFAULT), Italic
|
|
77
|
+
body += struct.pack("<I", os2["usWeightClass"]) # Weight
|
|
78
|
+
body += struct.pack("<HH", 0x0000, 0x504C) # fsType(installable), MagicNumber
|
|
79
|
+
body += struct.pack("<IIII", *os2["ulUnicodeRange"])
|
|
80
|
+
body += struct.pack("<II", *os2["ulCodePageRange"])
|
|
81
|
+
body += struct.pack("<I", font.check_sum_adjustment)
|
|
82
|
+
body += struct.pack("<IIII", 0, 0, 0, 0) # Reserved1-4
|
|
83
|
+
body += lpstr(family) # FamilyName
|
|
84
|
+
body += lpstr("Italic" if italic else "Regular") # StyleName
|
|
85
|
+
body += lpstr("Version 1.0") # VersionName
|
|
86
|
+
body += lpstr(f"{family} Italic" if italic else family) # FullName
|
|
87
|
+
body += struct.pack("<HH", 0, 0) # v2.1: RootString (empty)
|
|
88
|
+
|
|
89
|
+
# EOTSize, FontDataSize, Version, Flags(0 = uncompressed, not subsetted)
|
|
90
|
+
prefix = struct.pack("<IIII", 16 + len(body) + len(ttf_bytes),
|
|
91
|
+
len(ttf_bytes), 0x00020001, 0x00000000)
|
|
92
|
+
return prefix + body + ttf_bytes
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def brand_dir(name):
|
|
96
|
+
"""Locate the shared brand payload. Bundled beside the kit when packaged,
|
|
97
|
+
at the repo root when running from the repo. WAND_BRAND_DIR overrides."""
|
|
98
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
99
|
+
for d in filter(None, [os.environ.get("WAND_BRAND_DIR"),
|
|
100
|
+
here,
|
|
101
|
+
os.path.join(here, "..", "..", "..", "brand")]):
|
|
102
|
+
p = os.path.join(d, name)
|
|
103
|
+
# prove it is the brand payload, not a directory that shares its name
|
|
104
|
+
if os.path.isdir(p) and (name != "fonts" or os.path.isfile(os.path.join(p, "Geist-Regular.ttf"))):
|
|
105
|
+
return os.path.normpath(p)
|
|
106
|
+
raise SystemExit(f"brand/{name} not found")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
ap = argparse.ArgumentParser()
|
|
111
|
+
ap.add_argument("pptx")
|
|
112
|
+
ap.add_argument("-o", "--out")
|
|
113
|
+
ap.add_argument("--fonts", default=None)
|
|
114
|
+
ap.add_argument("--spec", help="carry this spec inside the deck, so the deck can be edited later "
|
|
115
|
+
"without the spec having to travel beside it")
|
|
116
|
+
a = ap.parse_args()
|
|
117
|
+
fonts = a.fonts or brand_dir("fonts")
|
|
118
|
+
out = a.out or a.pptx
|
|
119
|
+
|
|
120
|
+
work = tempfile.mkdtemp()
|
|
121
|
+
with zipfile.ZipFile(a.pptx) as z:
|
|
122
|
+
z.extractall(work)
|
|
123
|
+
|
|
124
|
+
fdir = os.path.join(work, "ppt", "fonts")
|
|
125
|
+
os.makedirs(fdir, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
rels_path = os.path.join(work, "ppt", "_rels", "presentation.xml.rels")
|
|
128
|
+
rels = open(rels_path, encoding="utf-8").read()
|
|
129
|
+
next_id = max([int(m) for m in re.findall(r'Id="rId(\d+)"', rels)] or [0]) + 1
|
|
130
|
+
|
|
131
|
+
new_rels, font_lst, made = [], [], 0
|
|
132
|
+
for family, stems in FAMILIES.items():
|
|
133
|
+
entries = []
|
|
134
|
+
for face, stem in zip(FACES, stems):
|
|
135
|
+
src = os.path.join(fonts, stem + ".ttf")
|
|
136
|
+
if not os.path.exists(src):
|
|
137
|
+
print(f" ! missing {stem}.ttf — skipping {family}/{face}", file=sys.stderr)
|
|
138
|
+
continue
|
|
139
|
+
italic = "italic" in face.lower()
|
|
140
|
+
data, font = retag(src, family, italic)
|
|
141
|
+
name = f"{family.replace(' ', '')}-{face}.fntdata"
|
|
142
|
+
open(os.path.join(fdir, name), "wb").write(eot(data, font, family, italic))
|
|
143
|
+
rid = f"rId{next_id}"; next_id += 1
|
|
144
|
+
new_rels.append(f'<Relationship Id="{rid}" Type="{REL_FONT}" Target="fonts/{name}"/>')
|
|
145
|
+
entries.append(f'<p:{face} r:id="{rid}"/>')
|
|
146
|
+
made += 1
|
|
147
|
+
if entries:
|
|
148
|
+
font_lst.append(
|
|
149
|
+
f'<p:embeddedFont><p:font typeface="{family}" pitchFamily="34" charset="0"/>'
|
|
150
|
+
+ "".join(entries) + "</p:embeddedFont>")
|
|
151
|
+
|
|
152
|
+
rels = rels.replace("</Relationships>", "".join(new_rels) + "</Relationships>")
|
|
153
|
+
open(rels_path, "w", encoding="utf-8").write(rels)
|
|
154
|
+
|
|
155
|
+
ct_path = os.path.join(work, "[Content_Types].xml")
|
|
156
|
+
ct = open(ct_path, encoding="utf-8").read()
|
|
157
|
+
if "fntdata" not in ct:
|
|
158
|
+
ct = ct.replace("<Default", '<Default Extension="fntdata" '
|
|
159
|
+
'ContentType="application/x-fontdata"/><Default', 1)
|
|
160
|
+
open(ct_path, "w", encoding="utf-8").write(ct)
|
|
161
|
+
|
|
162
|
+
p_path = os.path.join(work, "ppt", "presentation.xml")
|
|
163
|
+
p = open(p_path, encoding="utf-8").read()
|
|
164
|
+
p = re.sub(r'\s(embedTrueTypeFonts|saveSubsetFonts)="[^"]*"', "", p)
|
|
165
|
+
p = re.sub(r"(<p:presentation\b)",
|
|
166
|
+
r'\1 embedTrueTypeFonts="1" saveSubsetFonts="0"', p, count=1)
|
|
167
|
+
if "<p:embeddedFontLst" not in p:
|
|
168
|
+
block = "<p:embeddedFontLst>" + "".join(font_lst) + "</p:embeddedFontLst>"
|
|
169
|
+
# schema-fixed position: immediately after <p:notesSz/>
|
|
170
|
+
anchor = re.search(r"<p:notesSz[^/]*/>", p) or re.search(r"<p:sldSz[^/]*/>", p)
|
|
171
|
+
if not anchor:
|
|
172
|
+
sys.exit("no <p:notesSz>/<p:sldSz> anchor in presentation.xml")
|
|
173
|
+
p = p[:anchor.end()] + block + p[anchor.end():]
|
|
174
|
+
open(p_path, "w", encoding="utf-8").write(p)
|
|
175
|
+
|
|
176
|
+
if os.path.exists(out):
|
|
177
|
+
os.remove(out)
|
|
178
|
+
zf = zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED)
|
|
179
|
+
for root, _, files in os.walk(work):
|
|
180
|
+
for fn in files:
|
|
181
|
+
full = os.path.join(root, fn)
|
|
182
|
+
zf.write(full, os.path.relpath(full, work))
|
|
183
|
+
# A deck that carries its own spec can be edited a month later by someone
|
|
184
|
+
# who has only the .pptx. PowerPoint ignores parts it does not know about.
|
|
185
|
+
if a.spec and os.path.exists(a.spec):
|
|
186
|
+
zf.writestr("wand/spec.json", open(a.spec, "rb").read())
|
|
187
|
+
zf.close()
|
|
188
|
+
shutil.rmtree(work)
|
|
189
|
+
print(f"embedded {made} faces across {len(font_lst)} families{' + spec' if a.spec and os.path.exists(a.spec) else ''} -> {out}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
{
|
|
2
|
+
"deck": "WAND TEMPLATE",
|
|
3
|
+
"confidential": "TEMPLATE",
|
|
4
|
+
"slides": [
|
|
5
|
+
{ "layout": "cover", "eyebrow": "REPLACE THIS EYEBROW",
|
|
6
|
+
"titleLines": ["Your headline here.", "One line per line."],
|
|
7
|
+
"body": "The cover takes an eyebrow, up to three title lines, a body paragraph and a kicker. The title shrinks itself if it runs long.",
|
|
8
|
+
"kickerPlain": "Delete what you don't need. ", "kickerBold": "Keep the structure." },
|
|
9
|
+
|
|
10
|
+
{ "layout": "divider", "tag": "SECTION", "eyebrow": "Use a divider between sections",
|
|
11
|
+
"titleText": ["One idea. ", "Stated plainly", "."],
|
|
12
|
+
"body": "Dividers carry a tag, an eyebrow, one big line and a paragraph. They have no footer and no page number." },
|
|
13
|
+
|
|
14
|
+
{ "layout": "numberedList", "tag": "AGENDA", "dark": true,
|
|
15
|
+
"titleText": ["What we'll ", "cover", "."],
|
|
16
|
+
"subtitleText": "numberedList is the agenda and the ask. Up to six rows, numbered for you.",
|
|
17
|
+
"items": [
|
|
18
|
+
{ "title": "Where you are today", "body": "One sentence of context per row. The number is drawn automatically." },
|
|
19
|
+
{ "title": "What changes", "body": "Keep rows parallel in length — the layout reads best when they are." },
|
|
20
|
+
{ "title": "What it takes", "body": "Six rows is the maximum. Past that, split the slide." },
|
|
21
|
+
{ "title": "What happens next", "body": "The closing lines and sign-off below are both optional." }
|
|
22
|
+
],
|
|
23
|
+
"closing": { "line1": "One line to close.", "line2": "One more if you need it." },
|
|
24
|
+
"signoff": "A sign-off sits at the very bottom." },
|
|
25
|
+
|
|
26
|
+
{ "layout": "threeUp", "tag": "THREE THINGS", "dark": false, "arrows": true,
|
|
27
|
+
"titleText": ["Three comparable things. ", "One of them yours", "."],
|
|
28
|
+
"subtitleText": "threeUp is for three options, three stages, or three audiences. Set arrows: true when it is a progression.",
|
|
29
|
+
"items": [
|
|
30
|
+
{ "eyebrow": "TODAY", "title": "The status quo", "pill": "where most people are", "body": "Each card takes an eyebrow, a title, a pill and a body." },
|
|
31
|
+
{ "eyebrow": "THE SHIFT", "title": "What you propose", "pill": "the recommendation", "emph": true, "body": "Exactly one card per slide can be the emphasis card. Set emph: true on it." },
|
|
32
|
+
{ "eyebrow": "THE HORIZON", "title": "Where it leads", "pill": "the end state", "body": "Keep the three bodies roughly the same length." }
|
|
33
|
+
] },
|
|
34
|
+
|
|
35
|
+
{ "layout": "sixUpIcon", "tag": "CAPABILITIES", "dark": true,
|
|
36
|
+
"titleText": ["Six things, ", "evenly weighted", "."],
|
|
37
|
+
"subtitleText": "sixUpIcon is for features or capabilities. Icons are named — run `node wand.js glyphs` for the list.",
|
|
38
|
+
"items": [
|
|
39
|
+
{ "icon": "shield-check", "title": "Named icons", "body": "Say icon: \"shield-check\", not a filename. They recolour to the theme.", "note": "75 to choose from" },
|
|
40
|
+
{ "icon": "users", "title": "The note line", "body": "Each card can carry a small accent line under the body — a target, a figure, a date.", "note": "Optional" },
|
|
41
|
+
{ "icon": "network", "title": "Four to six", "body": "Fewer than four looks sparse; the layout draws at most six." },
|
|
42
|
+
{ "icon": "gauge", "title": "Keep bodies short", "body": "Two lines each is the sweet spot. Longer text shrinks to fit, which reads as an accident." },
|
|
43
|
+
{ "icon": "workflow", "title": "Don't repeat it", "body": "Three sixUpIcon slides in a row is a failure even if each one is correct." },
|
|
44
|
+
{ "icon": "sparkles", "title": "Vary the layouts", "body": "Across fifteen slides, aim for at least eight distinct ones." }
|
|
45
|
+
] },
|
|
46
|
+
|
|
47
|
+
{ "layout": "statGrid", "tag": "THE NUMBERS", "dark": false,
|
|
48
|
+
"titleText": ["Figures, ", "not adjectives", "."],
|
|
49
|
+
"subtitleText": "statGrid holds three to six figures. statQuad does four larger ones; heroQuad leads with a single headline number.",
|
|
50
|
+
"items": [
|
|
51
|
+
{ "value": "60%", "label": "The headline figure", "caption": "value, label and caption", "emph": true },
|
|
52
|
+
{ "value": "2×", "label": "Second figure", "caption": "One card can be the emphasis card" },
|
|
53
|
+
{ "value": "13 wks", "label": "Third figure", "caption": "Keep captions to one line" },
|
|
54
|
+
{ "value": "$1.2M", "label": "Fourth figure", "caption": "Currency and units read fine" },
|
|
55
|
+
{ "value": "30+", "label": "Fifth figure", "caption": "Six is the maximum" },
|
|
56
|
+
{ "value": "24/7", "label": "Sixth figure", "caption": "Cut to four if any caption wraps" }
|
|
57
|
+
],
|
|
58
|
+
"footnote": "A footnote line sits under the grid. Use it for the source." },
|
|
59
|
+
|
|
60
|
+
{ "layout": "colCards", "tag": "THE LAYERS", "dark": true,
|
|
61
|
+
"titleText": ["Two or three things, ", "each with a list", "."],
|
|
62
|
+
"items": [
|
|
63
|
+
{ "eyebrow": "Layer 01", "title": "Prose and bullets", "sub": "colCards carries both",
|
|
64
|
+
"body": "Use colCards when each of two or three things needs a paragraph and a list. The body is optional — drop it and the bullets take the room.",
|
|
65
|
+
"bullets": ["Up to five bullets", "Keep them short", "They shrink to fit", "Parallel phrasing reads best"] },
|
|
66
|
+
{ "eyebrow": "Layer 02", "title": "Keep them parallel", "sub": "same shape, same length",
|
|
67
|
+
"body": "Three columns of very different lengths is the most common way this layout goes wrong.",
|
|
68
|
+
"bullets": ["Same number of bullets", "Same rough length", "Same grammatical form"] },
|
|
69
|
+
{ "eyebrow": "Layer 03", "title": "Three is the maximum", "sub": "past that, use a grid",
|
|
70
|
+
"body": "For four or more categories, matrixGrid or stripGrid will hold them properly.",
|
|
71
|
+
"bullets": ["matrixGrid: 4 x 3 cells", "stripGrid: figures over columns", "compareGrid: 3 columns x 6 rows"] }
|
|
72
|
+
] },
|
|
73
|
+
|
|
74
|
+
{ "layout": "phaseCards", "tag": "ROADMAP", "dark": false,
|
|
75
|
+
"titleText": ["A plan in ", "four steps", "."],
|
|
76
|
+
"subtitleText": "phaseCards is the roadmap layout. Card one is the emphasis card.",
|
|
77
|
+
"phases": [
|
|
78
|
+
{ "period": "Weeks 1–4", "title": "Foundation", "bullets": ["A period, a title and up to five bullets", "Keep bullets to one line each"] },
|
|
79
|
+
{ "period": "Months 2–6", "title": "First results", "bullets": ["Four cards is the maximum", "Three reads well too"] },
|
|
80
|
+
{ "period": "Months 6–12", "title": "Scale", "bullets": ["Name the outcome, not the activity", "Dates beat adjectives"] },
|
|
81
|
+
{ "period": "Year 2", "title": "Steady state", "bullets": ["End on what changes for them", "Not on what you deliver"] }
|
|
82
|
+
] },
|
|
83
|
+
|
|
84
|
+
{ "layout": "splitPanel", "tag": "THE ARGUMENT", "dark": true,
|
|
85
|
+
"titleText": ["Half argument, ", "half evidence", "."],
|
|
86
|
+
"subtitleText": "splitPanel puts the claim on the left and the proof on the right.",
|
|
87
|
+
"left": { "eyebrow": "THE CLAIM", "heading": "State it in one line.",
|
|
88
|
+
"body": "The left panel is the emphasis card: an eyebrow, a heading, a paragraph and an optional note. Use it for the argument you want remembered.",
|
|
89
|
+
"note": "The note is italic and accented — good for a consequence or a result." },
|
|
90
|
+
"right": { "eyebrow": "THE EVIDENCE", "heading": "Up to four supporting rows",
|
|
91
|
+
"items": [
|
|
92
|
+
{ "label": "Label and body", "body": "render as one line, so the source reads exactly as written." },
|
|
93
|
+
{ "label": "Or no label", "body": "Leave it out and the row is a single sentence." },
|
|
94
|
+
{ "body": "Like this one — body only, no label." },
|
|
95
|
+
{ "label": "Four is the maximum", "body": "A fifth row will be refused by validate rather than silently dropped." }
|
|
96
|
+
] } },
|
|
97
|
+
|
|
98
|
+
{ "layout": "compareGrid", "tag": "BEFORE AND AFTER", "dark": false,
|
|
99
|
+
"titleText": ["A comparison ", "wider than two columns", "."],
|
|
100
|
+
"subtitleText": "compareGrid does up to three headed columns over six labelled rows. The last column is yours and is tinted.",
|
|
101
|
+
"heads": ["TODAY", "WITH YOU", "WHY ONLY YOU"],
|
|
102
|
+
"rows": [
|
|
103
|
+
{ "label": "Speed", "cells": [{ "body": "The status quo, stated plainly and without strawmanning it." }, { "lead": "The improvement.", "body": "A short second line if it helps." }, { "body": "The reason nobody else can claim it." }] },
|
|
104
|
+
{ "label": "Cost", "cells": [{ "body": "Keep every cell to roughly the same length." }, { "lead": "The number.", "body": "Lead with it." }, { "body": "The structural reason, not the feature." }] },
|
|
105
|
+
{ "label": "Risk", "cells": [{ "body": "Six rows is the maximum." }, { "lead": "What changes." }, { "body": "Two columns work too — drop a head and a cell." }] }
|
|
106
|
+
] },
|
|
107
|
+
|
|
108
|
+
{ "layout": "stripGrid", "tag": "THE YEAR", "dark": true,
|
|
109
|
+
"titleText": ["Figures over ", "a column grid", "."],
|
|
110
|
+
"subtitleText": "stripGrid is the densest layout here: a strip of figures, a statement, a column grid and a closing band — all optional except the geometry.",
|
|
111
|
+
"stats": [
|
|
112
|
+
{ "value": "8%", "label": "First metric" }, { "value": "12%", "label": "Second" },
|
|
113
|
+
{ "value": "8K", "label": "Third" }, { "value": "30%", "label": "Fourth" },
|
|
114
|
+
{ "value": "18%", "label": "Fifth" }, { "value": "25%", "label": "Sixth" }
|
|
115
|
+
],
|
|
116
|
+
"columns": [
|
|
117
|
+
{ "label": "WHAT YOU DELIVER", "items": [
|
|
118
|
+
{ "body": "Up to six rows per column, up to four columns." },
|
|
119
|
+
{ "body": "A row can be a body alone, or a title plus a body." },
|
|
120
|
+
{ "body": "Everything measures itself and shrinks to fit." } ] },
|
|
121
|
+
{ "label": "WHAT THEY COMMIT", "items": [
|
|
122
|
+
{ "body": "Use two columns for a delivers/commits split." },
|
|
123
|
+
{ "body": "Three or four for categories." },
|
|
124
|
+
{ "body": "One wide column for a single block of prose." } ] }
|
|
125
|
+
] },
|
|
126
|
+
|
|
127
|
+
{ "layout": "archStack", "tag": "ARCHITECTURE", "dark": false,
|
|
128
|
+
"titleText": ["One layer ", "above the rest", "."],
|
|
129
|
+
"subtitleText": "archStack is the platform slide: who sits on top, what you provide in the middle, what it governs below.",
|
|
130
|
+
"top": { "label": "THE PEOPLE ON TOP", "body": "Who uses it, and what they see" },
|
|
131
|
+
"middle": { "label": "YOUR LAYER — what it does",
|
|
132
|
+
"modules": [
|
|
133
|
+
{ "title": "Module one", "body": "Up to six modules. The band grows to fit the longest one." },
|
|
134
|
+
{ "title": "Module two", "body": "Keep the bodies short and parallel." },
|
|
135
|
+
{ "title": "Module three", "body": "Leave modules empty and the band becomes a connector line." },
|
|
136
|
+
{ "title": "Module four", "body": "A note line can sit under the row." }
|
|
137
|
+
] },
|
|
138
|
+
"note": "An optional note line sits between the modules and the systems below.",
|
|
139
|
+
"sources": { "eyebrow": "WHAT IT GOVERNS", "cards": [
|
|
140
|
+
{ "title": "System one", "body": "ours: true is a first-party card", "ours": true },
|
|
141
|
+
{ "title": "System two", "body": "ours: false gets the dashed treatment", "ours": false } ] },
|
|
142
|
+
"outcome": "THE OUTCOME BAND • ONE LINE • ALL CAPS READS BEST HERE" },
|
|
143
|
+
|
|
144
|
+
{ "layout": "logoBands", "tag": "PROOF", "dark": true,
|
|
145
|
+
"titleText": ["Figures over ", "names", "."],
|
|
146
|
+
"subtitleText": "logoBands is the proof slide: a few figures, then rows of customer or investor names.",
|
|
147
|
+
"stats": [
|
|
148
|
+
{ "value": "$120M+", "caption": "A figure and\na two-line caption" },
|
|
149
|
+
{ "value": ">$1B", "caption": "Up to four\nfigures" },
|
|
150
|
+
{ "value": "80+", "caption": "Keep the captions\nparallel" },
|
|
151
|
+
{ "value": "3", "caption": "Offices, markets,\nwhatever counts" }
|
|
152
|
+
],
|
|
153
|
+
"bands": [
|
|
154
|
+
{ "label": "CUSTOMERS", "names": "Name • Name • Name • Name • Name • Name",
|
|
155
|
+
"note": "A note under the row is optional." },
|
|
156
|
+
{ "label": "INVESTORS", "names": "Name • Name • Name • Name • Name" }
|
|
157
|
+
] },
|
|
158
|
+
|
|
159
|
+
{ "layout": "closing", "eyebrow": "THE CLOSE",
|
|
160
|
+
"line1": "The last thing they read.", "line2": "Make it the ask.",
|
|
161
|
+
"body": "closing is centred and full bleed, with no footer. Two lines, a paragraph and a URL.",
|
|
162
|
+
"url": "wand.ai" }
|
|
163
|
+
]
|
|
164
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Shared libraries
|
|
2
|
+
|
|
3
|
+
Code that is about the *brand payload* rather than about any one output format,
|
|
4
|
+
so decks and docs cannot drift apart on it.
|
|
5
|
+
|
|
6
|
+
| | |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `measure.js` | Real Geist metrics via fontkit. Fit is measured, never guessed |
|
|
9
|
+
| `glyphs.js` | The named Lucide set, recoloured per theme |
|
|
10
|
+
| `pdf.js` | A PDF writer — embedded TrueType, paths, images. Both kits render PDF |
|
|
11
|
+
| `ttf.py` | The 1% of TrueType `embed_fonts.py` needs, without fontTools |
|
|
12
|
+
| `wandxml.py` | XML parsing that works with nothing installed |
|
|
13
|
+
|
|
14
|
+
Each kit keeps its own `lib/brand.js` — that one *is* kit-local, because it
|
|
15
|
+
resolves paths relative to the kit — and reaches these through it:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const brand = require("./brand");
|
|
19
|
+
const M = require(path.join(brand.resolve("lib"), "measure.js"));
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`tools/package.mjs` copies this directory into every zip, so a packaged kit
|
|
23
|
+
still resolves everything locally.
|