@natjswenson/press 0.2.0 → 0.3.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/CHANGELOG.md +36 -0
- package/SKILL.md +1 -1
- package/brand/tokens.json +16 -4
- package/lib/emit.mjs +24 -7
- package/lib/tokens.mjs +23 -1
- package/package.json +1 -1
- package/targets.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to the **press** skill are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
6
6
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-08-01
|
|
9
|
+
|
|
10
|
+
Font stacks become per-engine profiles. Migrating `local-fitness` measured a real
|
|
11
|
+
regression the previous single-stack model would have shipped.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`fonts.profiles`** — the brand's type *intent* is one thing; the fallback
|
|
16
|
+
chain that achieves it depends on the rendering engine. Targets pick one with
|
|
17
|
+
`params.font_profile`; omitting it uses `fonts.default_profile` (`browser`), so
|
|
18
|
+
every existing consumer is byte-for-byte unchanged.
|
|
19
|
+
- **`browser`** — Chromium/WebKit. `-apple-system` resolves to SF and the
|
|
20
|
+
fallbacks are never reached, so depth is free.
|
|
21
|
+
- **`fontconfig`** — WeasyPrint and anything else walking the chain for real.
|
|
22
|
+
Deliberately shallower: every extra face is one that can *win*.
|
|
23
|
+
- An unknown `font_profile` is an error, never a silent fall back to the default.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **The union stack visibly broke WeasyPrint output.** `local-fitness` renders
|
|
28
|
+
through WeasyPrint, which resolves none of the `ui-*` / `-apple-system`
|
|
29
|
+
keywords and walks the chain for real. The browser-tuned list put `Inter` and
|
|
30
|
+
`'Helvetica Neue'` ahead of `Arial`, and headlines resolved to
|
|
31
|
+
**Helvetica Neue Heavy Condensed** — measured with `pdffonts`, not assumed.
|
|
32
|
+
Under the `fontconfig` profile it renders `Arial-Bold` / `Georgia-Bold` /
|
|
33
|
+
`Menlo-Bold`, exactly as it always has.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Migrated `budget` and `local-fitness`.** budget renders through headless
|
|
38
|
+
Chrome, where the widened stacks are provably inert: identical embedded faces
|
|
39
|
+
and byte-identical rendered pages. local-fitness takes the `fontconfig`
|
|
40
|
+
profile and its emitted theme changed **zero values**. Suites stay green
|
|
41
|
+
(894 and 2063 tests).
|
|
42
|
+
|
|
8
43
|
## [0.2.0] - 2026-08-01
|
|
9
44
|
|
|
10
45
|
Everything `natejswenson.io` needed to adopt the brand without changing by a
|
|
@@ -92,5 +127,6 @@ source of truth and a CI drift gate.
|
|
|
92
127
|
rasterised cards, whose eyebrow legitimately runs at `.16em`. Scoped rather
|
|
93
128
|
than waived, and caught by linting the real shipped corpus.
|
|
94
129
|
|
|
130
|
+
[0.3.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.3.0
|
|
95
131
|
[0.2.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.2.0
|
|
96
132
|
[0.1.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.1.0
|
package/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: press
|
|
3
3
|
description: The one brand system for everything produced in Claude — design tokens, the visual laws, the run-presentation contract, and the universal voice core. Use when composing or restyling any artifact (report, résumé, card, cover, PDF, HTML page, chart), when asked about brand colors, fonts, the accent law or "the PRESS look", when adding a new skill that renders anything, or when a brand value needs to change everywhere at once. Also handles "check the brand is in sync", "why do my colors differ", and onboarding a new consumer repo.
|
|
4
4
|
user_invocable: true
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.3.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# /press — the brand system
|
package/brand/tokens.json
CHANGED
|
@@ -41,10 +41,22 @@
|
|
|
41
41
|
"fill_steps": ["ink", "ink_mid", "dim"]
|
|
42
42
|
},
|
|
43
43
|
"fonts": {
|
|
44
|
-
"$comment": "The
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
44
|
+
"$comment": "The brand's type INTENT is one thing — SF-like sans for structure, New York-like serif for commentary, SF Mono-like mono for data. The fallback chain that actually achieves it depends on the rendering engine, so there are profiles rather than one list. Chromium resolves -apple-system to SF and never reaches the fallbacks; fontconfig (WeasyPrint) resolves none of the ui-*/-apple-system keywords and walks the chain for real, where a browser-tuned list lands on Helvetica Neue *Heavy Condensed* — measured with pdffonts, not assumed. Pick with params.font_profile; omitting it gives you `default_profile`. Single quotes so the same string is valid CSS and needs no escaping in a Python literal.",
|
|
45
|
+
"default_profile": "browser",
|
|
46
|
+
"profiles": {
|
|
47
|
+
"browser": {
|
|
48
|
+
"$comment": "Chromium/WebKit. -apple-system wins on macOS; the rest is depth for other platforms. The union of every browser consumer's chain, since fallbacks are additive.",
|
|
49
|
+
"display_stack": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
50
|
+
"serif_stack": "'New York', ui-serif, Georgia, 'Iowan Old Style', 'Times New Roman', serif",
|
|
51
|
+
"mono_stack": "ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono', Menlo, Consolas, monospace"
|
|
52
|
+
},
|
|
53
|
+
"fontconfig": {
|
|
54
|
+
"$comment": "WeasyPrint and anything else going through fontconfig, where the chain is walked for real. Measured to land on Arial-Bold / Georgia-Bold / Menlo-Bold — the faces local-fitness has always rendered. Deliberately shallower than the browser profile: every extra face here is one that can win, so depth is risk rather than robustness.",
|
|
55
|
+
"display_stack": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif",
|
|
56
|
+
"serif_stack": "'New York', ui-serif, Georgia, 'Times New Roman', serif",
|
|
57
|
+
"mono_stack": "ui-monospace, 'SF Mono', Menlo, monospace"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
48
60
|
},
|
|
49
61
|
"identity": {
|
|
50
62
|
"stamp": "NS",
|
package/lib/emit.mjs
CHANGED
|
@@ -48,6 +48,7 @@ function pythonTheme(tokens, params) {
|
|
|
48
48
|
const brandLine = required(params, 'brand_line', 'python-theme');
|
|
49
49
|
const byline = params.byline ?? tokens.identity.byline;
|
|
50
50
|
const docKind = params.document_kind ?? 'DOCUMENT';
|
|
51
|
+
const fonts = fontsFor(tokens, params);
|
|
51
52
|
|
|
52
53
|
const out = [];
|
|
53
54
|
out.push('import copy');
|
|
@@ -88,11 +89,11 @@ function pythonTheme(tokens, params) {
|
|
|
88
89
|
out.push(' },');
|
|
89
90
|
out.push(' "fonts": {');
|
|
90
91
|
out.push(' # Display/structure voice, set 800-900 with tight tracking by the CSS.');
|
|
91
|
-
out.push(` "display_stack": ${py(
|
|
92
|
+
out.push(` "display_stack": ${py(fonts.display_stack)},`);
|
|
92
93
|
out.push(' # Commentary voice: serif italics for standfirsts and captions.');
|
|
93
|
-
out.push(` "serif_stack": ${py(
|
|
94
|
+
out.push(` "serif_stack": ${py(fonts.serif_stack)},`);
|
|
94
95
|
out.push(' # Data voice: labels, dates, tables, provenance.');
|
|
95
|
-
out.push(` "mono_stack": ${py(
|
|
96
|
+
out.push(` "mono_stack": ${py(fonts.mono_stack)},`);
|
|
96
97
|
if (extras.has('mono_file')) {
|
|
97
98
|
out.push(' # Point at a real TTF to load an authentic mono face via @font-face.');
|
|
98
99
|
out.push(' "mono_file": None,');
|
|
@@ -191,7 +192,7 @@ function cssVars(tokens, params) {
|
|
|
191
192
|
const name = (spec.name ?? spec.token).replace(/_/g, '-');
|
|
192
193
|
const raw = spec.token === 'stamp'
|
|
193
194
|
? (params.stamp ?? tokens.identity.stamp)
|
|
194
|
-
: lookup(tokens, spec.token);
|
|
195
|
+
: lookup(tokens, spec.token, params);
|
|
195
196
|
const value = spec.quote ? JSON.stringify(raw) : raw;
|
|
196
197
|
// `comments: "explicit"` keeps only hand-written hints; the token notes are
|
|
197
198
|
// written for a reader of tokens.json and are far too long for a CSS block.
|
|
@@ -253,7 +254,7 @@ function jsonTokens(tokens, params) {
|
|
|
253
254
|
const payload = {
|
|
254
255
|
colors: tokens.colors,
|
|
255
256
|
terminal: tokens.terminal,
|
|
256
|
-
fonts: tokens
|
|
257
|
+
fonts: fontsFor(tokens, params),
|
|
257
258
|
identity: { ...tokens.identity, ...(params.stamp ? { stamp: params.stamp } : {}) },
|
|
258
259
|
derived: { hair: tokens.derived.hair, fill_steps: tokens.derived.fillSteps },
|
|
259
260
|
};
|
|
@@ -291,6 +292,22 @@ function required(params, key, emitter) {
|
|
|
291
292
|
return value;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
/**
|
|
296
|
+
* The font stacks for this target's rendering engine. Defaults to the token
|
|
297
|
+
* set's own default profile, so a target that says nothing is unchanged.
|
|
298
|
+
*/
|
|
299
|
+
function fontsFor(tokens, params = {}) {
|
|
300
|
+
const name = params.font_profile;
|
|
301
|
+
if (!name) return tokens.fonts;
|
|
302
|
+
const profile = tokens.fontProfiles?.[name];
|
|
303
|
+
if (!profile) {
|
|
304
|
+
throw new EmitError(
|
|
305
|
+
`unknown font_profile "${name}" (available: ${Object.keys(tokens.fontProfiles ?? {}).join(', ')})`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return profile;
|
|
309
|
+
}
|
|
310
|
+
|
|
294
311
|
const DERIVED = {
|
|
295
312
|
hair: 'hair',
|
|
296
313
|
border: 'border',
|
|
@@ -298,9 +315,9 @@ const DERIVED = {
|
|
|
298
315
|
accent_dim: 'accentDim',
|
|
299
316
|
};
|
|
300
317
|
|
|
301
|
-
function lookup(tokens, name) {
|
|
318
|
+
function lookup(tokens, name, params = {}) {
|
|
302
319
|
if (name in DERIVED) return tokens.derived[DERIVED[name]];
|
|
303
|
-
for (const group of [tokens.colors, tokens.terminal, tokens
|
|
320
|
+
for (const group of [tokens.colors, tokens.terminal, fontsFor(tokens, params), tokens.identity, tokens.marks]) {
|
|
304
321
|
if (name in group) return group[name];
|
|
305
322
|
}
|
|
306
323
|
throw new EmitError(`unknown token "${name}"`);
|
package/lib/tokens.mjs
CHANGED
|
@@ -37,6 +37,24 @@ const clean = (group) => {
|
|
|
37
37
|
return out;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The three stacks for one rendering engine. An unknown profile name is an
|
|
42
|
+
* error rather than a silent fall back to the default — a target asking for
|
|
43
|
+
* `fontconfig` and quietly getting the browser chain is precisely the bug this
|
|
44
|
+
* whole mechanism exists to prevent.
|
|
45
|
+
*/
|
|
46
|
+
function fontProfile(raw, name) {
|
|
47
|
+
const profiles = raw.fonts?.profiles ?? {};
|
|
48
|
+
const key = name ?? raw.fonts?.default_profile;
|
|
49
|
+
const profile = profiles[key];
|
|
50
|
+
if (!profile) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`unknown font profile "${key}" (available: ${Object.keys(profiles).join(', ')})`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return clean(profile);
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
function resolve(raw) {
|
|
41
59
|
const colors = clean(raw.colors);
|
|
42
60
|
const terminal = clean(raw.terminal);
|
|
@@ -64,7 +82,11 @@ function resolve(raw) {
|
|
|
64
82
|
colors,
|
|
65
83
|
terminal,
|
|
66
84
|
notes: clean(raw.color_notes),
|
|
67
|
-
fonts:
|
|
85
|
+
fonts: fontProfile(raw, raw.fonts?.default_profile),
|
|
86
|
+
fontProfiles: Object.fromEntries(
|
|
87
|
+
Object.keys(raw.fonts?.profiles ?? {}).map((p) => [p, fontProfile(raw, p)]),
|
|
88
|
+
),
|
|
89
|
+
defaultFontProfile: raw.fonts?.default_profile,
|
|
68
90
|
identity: clean(raw.identity),
|
|
69
91
|
marks: clean(raw.marks),
|
|
70
92
|
limits: clean(raw.limits),
|
package/package.json
CHANGED