@erclx/canon 4.97.0 → 4.98.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/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/brand/mark-accent.svg" alt="canon" width="56" height="56">
4
+
1
5
  # canon
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/@erclx/canon)](https://www.npmjs.com/package/@erclx/canon)
@@ -6,17 +10,13 @@
6
10
 
7
11
  canon is a CLI and Claude Code plugin that stops your AI conventions drifting apart across repositories. It keeps one authoritative copy and installs it into each project on demand.
8
12
 
9
- ![The canon catalog, listing skills, governance rules, and standards with the count each ships, the workflow skills named, and a sample of the rule and standard names](assets/evidence/hero.png)
10
-
11
- The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today. See it live at [canon.erclx.dev](https://canon.erclx.dev).
12
-
13
- ## It runs on itself
13
+ **[See it run at canon.erclx.dev](https://canon.erclx.dev)**
14
14
 
15
- The workflow this toolkit ships is the workflow that built it. Several Claude Code sessions run at once, each in its own git worktree on its own branch, and each opens its own pull request.
15
+ </div>
16
16
 
17
- ![The canon landing page's session list, with the orchestrator pinned above bands of working and completed sessions, workers and planners in both, each naming what it is doing and the pull request it opened, and one row moving from Working into Completed](demos/agent-view.gif)
17
+ ![The first screen of the canon landing page, a project before and after one canon init, with the file counts each step lays down](assets/evidence/arrival.png)
18
18
 
19
- That is the landing page's own agent view, recorded from a local build by `canon demo run`. The session rows behind it are a real `canon sessions list --json` read. The activity text and the pull request number are transcribed, because no verb here reports either.
19
+ The page is one real session the toolkit ran on itself, and every count on it is read from the repo when the page builds.
20
20
 
21
21
  ## Why
22
22
 
@@ -53,35 +53,37 @@ bun install --global @erclx/canon
53
53
 
54
54
  ## What is inside
55
55
 
56
- Each domain has a canonical source in this repo and a thin install or sync CLI on your side.
56
+ Each domain has a canonical source in this repo and a thin install or sync CLI on your side. The domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
57
57
 
58
- - Claude Code plugin: skills that plan a feature, review a diff, sync the planning docs, and run the ship chain from branch through pull request
59
- - Governance rules: coding and authoring rules that load into a Claude session when a matching path is edited, installed per project and refreshed by sync
60
- - Standards: authoring conventions for commits, branches, plans, tasks, and markdown, read by name with `canon standards <name>` rather than copied into your project
61
- - Snippets: reusable prompts fired by `@` reference in a Claude Code session, resolved live from the plugin with no install step
62
- - Tooling stacks: golden configs, seeds, and a reference per framework, laid down by `canon init` and reconciled by `canon tooling sync`
63
- - Design system: a `DESIGN.md` token format, a skill that drafts one from an existing project or from scratch, and a render command
64
- - Slides: a `SLIDES.md` source format with a layout catalog, rendered to PowerPoint by `canon slides render`
65
- - Transcripts: `canon transcripts` pulls a YouTube transcript with metadata frontmatter into any repo
66
- - Sandbox: scenario-based scaffolds that provision an isolated project state for verifying each domain flow
58
+ | Domain | What it is | How it reaches you |
59
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
60
+ | Plugin skills | Skills that plan a feature, review a diff, sync the planning docs, and run the ship chain | Loaded live from the plugin, never copied |
61
+ | Governance | Coding and authoring rules that load into a Claude session when a matching path is edited | Installed per project by `canon gov install`, refreshed by sync |
62
+ | Standards | Authoring conventions for commits, branches, plans, tasks, and markdown | Opened by name, read by name with `canon standards <name>` |
63
+ | Snippets | Reusable prompts fired by `@` reference | Resolved live from the plugin |
64
+ | Tooling stacks | Golden configs, seeds, and a reference per framework | Laid down by `canon init`, reconciled by `canon tooling sync` |
65
+ | Design system | Ships a `DESIGN.md` token format, a skill that drafts one from an existing project or from scratch, and a render command | `canon design render` |
66
+ | Slides | A `SLIDES.md` source format with a layout catalog | `canon slides render` writes PowerPoint |
67
+ | Transcripts | A YouTube transcript with metadata frontmatter | `canon transcripts <url>` writes it into any repo |
68
+ | Sandbox | Scenario scaffolds that provision an isolated project state for verifying each domain flow | `canon sandbox` |
67
69
 
68
- Those domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
70
+ ![The canon catalog as a ledger of five rows, with the count each ships and a few real names: skills, rules, standards, commands, and stacks](assets/evidence/catalog.png)
69
71
 
70
- ![canon tooling list and canon standards list side by side, the first showing five stacks with their dependency and script counts, the second showing standards against the artifact each governs](assets/evidence/install-surface.png)
72
+ Every count in that image is read from the catalogs when it is built, so they're what the repo ships today. A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A standard stays here and is opened by name, so there is no copy in your repo to drift from this one. Governance is the third shape. A rule with a path glob loads only when a matching path is edited, and a rule with none loads every session. Run `canon gov list` to see the glob beside each rule.
71
73
 
72
- A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A standard stays here and is opened by name, so there is no copy in your repo to drift from this one.
74
+ Two of the outputs are documents you open rather than files you run. A `SLIDES.md` renders to a deck, and a learning workspace renders to a small site.
73
75
 
74
- Governance is the third shape, and it is worth seeing rather than reading about, because the glob beside each rule is what decides whether it reaches a session at all.
76
+ ![A slide from the showcase deck rendered by canon slides render, a bulleted list under a heading on a dark warm background](examples/slides/images/showcase-dark-04.png)
75
77
 
76
- ![canon gov list, showing seven stacks with the rules each carries and a sample of rules beside the path glob that loads each one](assets/evidence/governance.png)
78
+ ![The learning workspace listing rendered by canon teach, three workspaces with their lesson and term counts, one still a stub](examples/teach/evidence/root-listing.png)
77
79
 
78
- Stacks compose, so a react project inherits node and node inherits base. A rule with a glob loads only when a matching path is edited, and a rule with none loads every session. Both counts and every row above are read from the catalogs when the image is built.
80
+ ## It runs on itself
79
81
 
80
- The toolkit tracks its own work the same way, in a task board no catalog can read back.
82
+ The workflow this toolkit ships is the workflow that built it. Several Claude Code sessions run at once, each in its own git worktree on its own branch, and each opens its own pull request.
81
83
 
82
- ![the task board's Run now table, one row per task in flight, each naming the files it touches and the plan it runs under](assets/evidence/task-board.png)
84
+ ![The canon landing page scrolling from the dispatch section to the branch graph, three workers and a planner on disjoint file sets with the pull request each opened](demos/agent-view.gif)
83
85
 
84
- `.canon/tasks/` is gitignored session scratch, so unlike the two frames above, this one is a hand-taken snapshot rather than something the build reads live, and it goes stale the moment the board moves.
86
+ That recording is the landing page's own `dispatch` and `workers` sections, driven by `canon demo run` against a local build. The branch graph beside the pull request numbers is authored rather than read, because nothing on a build machine records which files four sessions held.
85
87
 
86
88
  ## Documentation
87
89
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.97.0",
4
+ "version": "4.98.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -28,6 +28,7 @@ Without this skill, a session facing a decision nobody can settle from a diff:
28
28
  - Names no layer at all, since the instruction to name one carries no catalog to name from and nothing reports the omission. Four consecutive rounds against one page each varied a property at the top layer while the defect sat two below it, and three of the four changed nothing the operator could see.
29
29
  - Judges a lower layer through a finished higher one, so an operator asked about composition looks at the palette instead and answers about that.
30
30
  - Hands over the combined page alone for a decision about layout, where every arm sits in a column while the media queries answer to the whole window, so no arm is ever seen at its own viewport and a fault that appears only narrower survives the pick.
31
+ - Rebuilds a navigation harness by hand on a run with several rounds, since each round writes its own folder and nothing reaches all of them from one address. Two design tracks hand-wrote the same page picker and theme toggle.
31
32
  - Sets the theme on an arm that persists its own and reads it back on load, so the control reports one theme while the arm renders the other and the set is compared across two.
32
33
 
33
34
  ## Must
@@ -38,6 +39,7 @@ Without this skill, a session facing a decision nobody can settle from a diff:
38
39
  - Grey-box the set when the declared layer sits below typography, and say the set is grey-boxed when handing it over, so the operator reads a flat page as the question rather than as unfinished work.
39
40
  - Render every arm in one theme at a time, with a control that sets the whole set, so the comparison holds still while the operator reads it, and set the theme in a way that survives an arm reading its own stored preference back on load.
40
41
  - Hand over the per-arm file addresses beside the combined page where the declared layer is composition, layout or space, since only a whole-page file at a real viewport answers how an arm behaves at a width, and the browser's own device toolbar is the control.
42
+ - Write a frame from `references/frame.html` where the run has several rounds, so a picker reaches every round from one address. The frame carries no width control of its own.
41
43
  - Judge each render against a stated bar before handing it over, name the weakest thing on the page, and fix it where that sentence would embarrass the work.
42
44
  - Write each iteration to its own folder rather than narrowing the previous one in place, so every earlier round stays openable.
43
45
  - Write the run's renders inside the record that cites them wherever one exists, and reserve the scratch path for inputs that are re-runnable and cited by nothing.
@@ -31,6 +31,7 @@ Write every arm side by side on one self-contained HTML page at `<dest>/candidat
31
31
  - Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
32
32
  - Give the combined page one control that sets every arm's theme at once, beside whatever per-arm control the arms carry. A set spanning both themes cannot be compared, since the operator has to toggle each arm and hold the earlier ones in memory, which is the failure the single-page rule exists to prevent.
33
33
  - Set that theme so it survives the arm reading its own preference back. An arm that stores a theme and applies it on load will overwrite whatever the control set, which shows the control in one state and the arm in the other, and it passes a fresh browser profile because the stored value is not there yet.
34
+ - Write `<dest>/frame.html` from `${CLAUDE_SKILL_DIR}/references/frame.html` where the run has more than one round, and hand over its address beside the combined page. Replace the template's one data token with the JSON object its comment describes, listing the combined page and every per-arm file of this round and each earlier one, with each `src` relative to the frame and `themeKey` set to the key the arms read their theme from. The frame is the route to every round from one address, with a page picker and the global theme toggle. Earlier rounds are sibling folders, so serve the parent folder with `canon serve <parent> --entry <dest-name>/frame.html` rather than `<dest>`, and keep the per-arm files as the fallback where nothing is served.
34
35
  - Hand over the per-arm file addresses beside the combined page where the declared layer is composition, layout or space, and say that an arm is judged at a width by opening its own file in the browser's device toolbar. The combined page cannot answer that question at all: every arm sits in a column there while the media queries answer to the whole window, so each one renders its widest layout in a narrow space no matter how the window is sized. Build no width control of your own, since the browser already has one.
35
36
  - Take the live-app branch instead when the surface under decision is a running app: lift the rendered markup and link a copy of the built stylesheet rather than inlining, per `${CLAUDE_SKILL_DIR}/references/live-arms.md`.
36
37
  - On the default path, inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
@@ -70,7 +71,7 @@ Put the choice to the operator through the structured question surface, since a
70
71
  ## Step 5: loop on the pick
71
72
 
72
73
  1. Write each iteration to its own `<dest>` rather than narrowing the previous one in place, suffixing the slug so the folders sort. An iteration overwritten is one a later pass cannot open, and the losing round is what stops a correction re-proposing something already rejected.
73
- 2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not.
74
+ 2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not. Each pass through Step 2 rewrites the frame with every earlier round's arms, since the list is baked in when the frame is written.
74
75
  3. Re-render, hand off again, and take the next answer.
75
76
  4. Repeat until the operator says it is right. The loop stops on their word and on nothing else, so a run stopping because the arms stopped differing has stopped early.
76
77
  5. Hold the real surface untouched across every iteration. Nothing outside the run's own folders changes until the pick is final.
@@ -0,0 +1,118 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Draft and pick frame</title>
6
+ <style>
7
+ :root {
8
+ --bg: #f4f4f5;
9
+ --fg: #18181b;
10
+ --bar: #ffffff;
11
+ --line: #d4d4d8;
12
+ }
13
+ :root[data-theme='dark'] {
14
+ --bg: #18181b;
15
+ --fg: #f4f4f5;
16
+ --bar: #27272a;
17
+ --line: #3f3f46;
18
+ }
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ background: var(--bg);
28
+ color: var(--fg);
29
+ font-family: system-ui, sans-serif;
30
+ font-size: 14px;
31
+ }
32
+ header {
33
+ display: flex;
34
+ gap: 12px;
35
+ align-items: center;
36
+ padding: 8px 12px;
37
+ background: var(--bar);
38
+ border-bottom: 1px solid var(--line);
39
+ }
40
+ select,
41
+ button {
42
+ font: inherit;
43
+ color: inherit;
44
+ background: var(--bg);
45
+ border: 1px solid var(--line);
46
+ border-radius: 4px;
47
+ padding: 4px 8px;
48
+ }
49
+ main {
50
+ flex: 1;
51
+ overflow: auto;
52
+ display: flex;
53
+ justify-content: center;
54
+ padding: 12px;
55
+ }
56
+ iframe {
57
+ width: 100%;
58
+ height: 100%;
59
+ border: 1px solid var(--line);
60
+ background: #fff;
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <header>
66
+ <label
67
+ >Page
68
+ <select id="picker"></select
69
+ ></label>
70
+ <button id="theme" type="button">Theme: light</button>
71
+ <span>F12 device toolbar to resize</span>
72
+ </header>
73
+ <main><iframe id="page" title="Candidate page"></iframe></main>
74
+ <!-- The skill replaces the token below with one JSON object: {"themeKey": "<key the arms read>", "rounds": [{"label": "...", "pages": [{"label": "...", "src": "<path relative to this file>"}]}]} -->
75
+ <script type="application/json" id="frame-data">
76
+ __FRAME_DATA__
77
+ </script>
78
+ <script>
79
+ const data = JSON.parse(document.getElementById('frame-data').textContent)
80
+ const picker = document.getElementById('picker')
81
+ const themeButton = document.getElementById('theme')
82
+ const frame = document.getElementById('page')
83
+ let theme = 'light'
84
+
85
+ for (const round of data.rounds) {
86
+ for (const page of round.pages) {
87
+ const option = document.createElement('option')
88
+ option.value = page.src
89
+ option.textContent = round.label + ' · ' + page.label
90
+ picker.append(option)
91
+ }
92
+ }
93
+
94
+ // A page that stores its own theme and reads it back on load overwrites the
95
+ // attribute alone, so write the stored key as well as the attribute.
96
+ function applyTheme() {
97
+ document.documentElement.dataset.theme = theme
98
+ themeButton.textContent = 'Theme: ' + theme
99
+ try {
100
+ const win = frame.contentWindow
101
+ win.document.documentElement.setAttribute('data-theme', theme)
102
+ win.localStorage.setItem(data.themeKey, theme)
103
+ } catch {}
104
+ }
105
+
106
+ picker.addEventListener('change', () => {
107
+ frame.src = picker.value
108
+ })
109
+ themeButton.addEventListener('click', () => {
110
+ theme = theme === 'light' ? 'dark' : 'light'
111
+ applyTheme()
112
+ })
113
+ frame.addEventListener('load', applyTheme)
114
+
115
+ if (picker.options.length > 0) frame.src = picker.value
116
+ </script>
117
+ </body>
118
+ </html>
@@ -18,9 +18,9 @@ canon capture https://example.com --selector .window --out preview.png
18
18
 
19
19
  ## What this repository captures
20
20
 
21
- A capture set here spans two folders. `assets/captures/` holds five sources and the template each is written from, and `assets/evidence/` holds the five images a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the five is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them, and `bun run check` regenerates them and fails on the difference.
21
+ A capture set here spans two folders. `assets/captures/` holds three sources and the template each is written from, and `assets/evidence/` holds their images and the built-page capture a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the three sources is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them, and `bun run check` regenerates them and fails on the difference.
22
22
 
23
- Three of the five take catalog data, so a stack gaining a rule moves the frame on the next run. `install.html` and `task-board.html` are the two exceptions: the first holds terminal text from a real run and the second holds a hand-frozen snapshot of a gitignored board, each in its template rather than derived from a live catalog at build time.
23
+ Two of the three take catalog data, so a stack gaining a rule moves the frame on the next run. `install.html` is the exception, since it holds terminal text from a real run in its template rather than derived from a live catalog at build time. `assets/evidence/arrival.png` is the one image with no source here: it is the built page's `.fold` element captured in dark mode against a local `web/` preview, since `canon capture` takes no color scheme and the page follows the browser's default of light, so it is retaken by hand when the page's first screen changes.
24
24
 
25
25
  `assets/captures/` is read flat and never descends, by the regeneration script, by this command, and by the drift stage alike. A source nested a further folder down is skipped by all three with nothing reported, so a new frame is a template dropped directly in rather than a folder of its own. The split is what retired the name prefix the flat layout used to need: a frame is named for itself now, since `assets/captures/` no longer mixes markup in with the images.
26
26
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.97.0",
4
+ "version": "4.98.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -77,8 +77,11 @@ GOV_JSON="$(catalog gov list)"
77
77
  STANDARDS_JSON="$(catalog standards list)"
78
78
  SNIPPETS_JSON="$(catalog snippets list)"
79
79
  TOOLING_JSON="$(catalog tooling list)"
80
+ # The catalog ledger names three commands, and the registered set is only
81
+ # readable from the root help, which lists one per line under `Commands:`.
82
+ COMMANDS_HELP="$(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts --help 2>/dev/null)"
80
83
 
81
- for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON"; do
84
+ for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON" "$COMMANDS_HELP"; do
82
85
  if [ -z "$payload" ]; then
83
86
  echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
84
87
  exit 1
@@ -98,6 +101,7 @@ printf '%s' "$GOV_JSON" >"$PAYLOAD_DIR/gov.json"
98
101
  printf '%s' "$STANDARDS_JSON" >"$PAYLOAD_DIR/standards.json"
99
102
  printf '%s' "$SNIPPETS_JSON" >"$PAYLOAD_DIR/snippets.json"
100
103
  printf '%s' "$TOOLING_JSON" >"$PAYLOAD_DIR/tooling.json"
104
+ printf '%s' "$COMMANDS_HELP" >"$PAYLOAD_DIR/commands-help.txt"
101
105
  printf '%s' "$TOKEN_CSS" >"$PAYLOAD_DIR/tokens.css"
102
106
 
103
107
  export PAYLOAD_DIR
@@ -326,8 +330,36 @@ for (const [label, rows] of [
326
330
  }
327
331
  }
328
332
 
333
+ // The ledger names a few real entries per domain. Chosen by hand for the same
334
+ // reason FEATURED_SKILLS is, and asserted against the catalog so a rename fails
335
+ // the run rather than leaving a name the repository no longer ships.
336
+ const commandNames = readFileSync(PAYLOAD_DIR + "/commands-help.txt", "utf8")
337
+ .split("\n")
338
+ .map((line) => line.match(/^[│\s]{2,}(?<name>[a-z][a-z-]*)\s/)?.groups?.name)
339
+ .filter(Boolean)
340
+ const LEDGER = [
341
+ ["skills", skills.length, ["plan-feature", "role-orchestrator", "review-pr"], skills],
342
+ ["rules", rules.length, ["behavior", "memory", "testing"], rules],
343
+ ["standards", standards.length, ["markdown", "plan", "teach"], standards],
344
+ ["commands", commandCount, ["gov", "tasks", "design"], commandNames],
345
+ ["stacks", toolingStacks.length, ["astro", "python", "web"], toolingStacks.map((stack) => stack.name)],
346
+ ]
347
+ for (const [domain, , chosen, names] of LEDGER) {
348
+ const known = new Set(names)
349
+ const missing = chosen.filter((name) => !known.has(name))
350
+ if (missing.length > 0) {
351
+ console.error(`regen-hero: ledger ${domain} names missing from the catalog: ${missing.join(", ")}`)
352
+ process.exit(1)
353
+ }
354
+ }
355
+ const ledgerRows = LEDGER.map(
356
+ ([domain, count, chosen]) =>
357
+ ` <div class="row"><span class="count">${count}</span><span class="domain">${domain}</span><span class="names">${escape(chosen.join(", "))}</span></div>`,
358
+ ).join("\n")
359
+
329
360
  const values = {
330
361
  TOKENS: tokenCss,
362
+ LEDGER_ROWS: ledgerRows,
331
363
  GOV_STACK_ROWS: govStackRows,
332
364
  GOV_RULE_ROWS: govRuleRows,
333
365
  STANDARD_ROWS: standardRows,
@@ -1,4 +1,4 @@
1
- import { bareHex, colorValue } from '@/design/tokens'
1
+ import { bareHex, colorValue, TOKENS } from '@/design/tokens'
2
2
 
3
3
  export type Variant = 'light' | 'dark'
4
4
 
@@ -23,11 +23,36 @@ const ROLES: Record<keyof Theme, readonly [dark: string, light: string]> = {
23
23
  accent: ['accent', 'light-accent'],
24
24
  }
25
25
 
26
+ /**
27
+ * `PptxGenJS` takes one installed family name where the record declares a
28
+ * stack, and a `.pptx` cannot embed the face. The first family is taken with
29
+ * its ` Variable` suffix dropped, since that suffix names the CSS package and
30
+ * not the installed family, and PowerPoint picks its own substitute when the
31
+ * face is missing.
32
+ */
33
+ function familyName(role: string): string {
34
+ const token = TOKENS.typography.find((entry) => entry.role === role)
35
+ const family = token?.family
36
+ .split(',')[0]
37
+ ?.trim()
38
+ .replace(/ Variable$/, '')
39
+ if (!family) {
40
+ throw new Error(`The design record declares no ${role} type role`)
41
+ }
42
+
43
+ return family
44
+ }
45
+
26
46
  export const FONTS = {
27
- heading: 'Arial',
28
- body: 'Calibri',
47
+ heading: familyName('display'),
48
+ body: familyName('body'),
29
49
  } as const
30
50
 
51
+ /**
52
+ * Deck-owned point sizes. The record's `t0` to `t6` steps are screen pixels,
53
+ * which read as points on a 13.33 inch slide would shrink the cover to 38 and
54
+ * the body to 11, so only the face and the colors come from the record.
55
+ */
31
56
  export const TYPE = {
32
57
  cover: 44,
33
58
  section: 40,