@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 +29 -27
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -0
- package/claude/skills/draft-and-pick/SKILL.md +2 -1
- package/claude/skills/draft-and-pick/references/frame.html +118 -0
- package/docs/agents/capture.md +2 -2
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +33 -1
- package/src/slides/styles.ts +28 -3
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
|
[](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
|
-
|
|
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
|
-
|
|
15
|
+
</div>
|
|
16
16
|
|
|
17
|
-

|
|
18
18
|
|
|
19
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
+

|
|
69
71
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+

|
|
75
77
|
|
|
76
|
-

|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
## It runs on itself
|
|
79
81
|
|
|
80
|
-
The
|
|
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
|
-

|
|
83
85
|
|
|
84
|
-
|
|
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
|
|
|
@@ -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>
|
package/docs/agents/capture.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
@@ -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,
|
package/src/slides/styles.ts
CHANGED
|
@@ -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: '
|
|
28
|
-
body: '
|
|
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,
|