@natjswenson/press 0.4.0 → 0.5.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 CHANGED
@@ -5,6 +5,55 @@ 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.5.0] - 2026-08-01
9
+
10
+ The last hand-maintained copy of the brand is gone.
11
+
12
+ ### Added
13
+
14
+ - **`python-consts` emitter** — flat module constants plus optional dicts, for a
15
+ script that reads tokens as Python names rather than a theme dict. The profile
16
+ README's SVG build has no override file to deep-merge and no stylesheet, so
17
+ `python-theme` would be dead weight around four strings.
18
+ - **`font_files`** — the vendorable faces, for engines that EMBED a font file
19
+ rather than resolving a CSS stack. Same type intent as `fonts.profiles`,
20
+ expressed where a stack is not an option: SF is not licensable, so Inter
21
+ stands in for the display voice and IBM Plex for serif and mono.
22
+
23
+ ### Changed
24
+
25
+ - **Migrated `natejswenson/natejswenson`** (the GitHub profile README). Its
26
+ header named `natejswenson.io/src/styles/global.css` as canonical without
27
+ anything keeping it honest — the ninth copy, and the last one. Every value is
28
+ byte-identical; the only content change is the comment header becoming a
29
+ region marker.
30
+ - That repo had **no CI at all**, so it gains a workflow running its existing
31
+ lint and 82 tests plus the brand check.
32
+
33
+ ### On verifying that migration
34
+
35
+ Its SVG tiles embed subsetted WOFF2, and **font subsetting is not
36
+ byte-reproducible** — rebuilding the untouched repo produced different base64
37
+ payloads. A byte comparison would have failed constantly while proving nothing.
38
+ Parity was verified by stripping the font payloads and comparing everything
39
+ else: 7 of 7 SVGs and the generated README identical.
40
+
41
+ ## [0.4.1] - 2026-08-01
42
+
43
+ ### Fixed
44
+
45
+ - **The propagate workflow could never fire on a release.** It triggered on
46
+ `push: tags: [press-v*]`, but the release tag is created by `_release.yml`
47
+ using the repository's `GITHUB_TOKEN`, and GitHub does not start new workflow
48
+ runs for events created with that token. The trigger looked correct and
49
+ executed exactly zero times — confirmed on the 0.4.0 release, which tagged and
50
+ published with no propagate run at all.
51
+
52
+ The release path now calls the workflow directly (`workflow_call` from
53
+ `press.yml`'s `propagate` job, `needs: release`), which runs in the same chain
54
+ and is not subject to that restriction. The weekly schedule and manual
55
+ dispatch were always unaffected.
56
+
8
57
  ## [0.4.0] - 2026-08-01
9
58
 
10
59
  Closes the propagation gap: a pinned check can tell you a consumer is intact,
@@ -167,6 +216,8 @@ source of truth and a CI drift gate.
167
216
  rasterised cards, whose eyebrow legitimately runs at `.16em`. Scoped rather
168
217
  than waived, and caught by linting the real shipped corpus.
169
218
 
219
+ [0.5.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.5.0
220
+ [0.4.1]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.4.1
170
221
  [0.4.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.4.0
171
222
  [0.3.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.3.0
172
223
  [0.2.0]: https://github.com/natejswenson/claude-skills/releases/tag/press-v0.2.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.4.0
5
+ version: 0.5.0
6
6
  ---
7
7
 
8
8
  # /press — the brand system
@@ -161,7 +161,7 @@ node bin/press.js propagate --repo ../budget --dry-run
161
161
  the region takes over, so the duplicate is **swallowed**, not left behind.
162
162
  5. `node bin/press.js emit --target <id> --init`
163
163
  6. Add a `press check` step to that repo's CI, **pinned to an exact version**:
164
- `npx -y @natjswenson/press@0.4.0 check --repo .`. Never write a bare
164
+ `npx -y @natjswenson/press@0.5.0 check --repo .`. Never write a bare
165
165
  `npx @natjswenson/press` — with no version reference at all, npx silently
166
166
  prefers a stale global install over the registry, which cost this repo a
167
167
  release once already. An exact pin also keeps a mutable dependency out of a
package/brand/tokens.json CHANGED
@@ -38,7 +38,11 @@
38
38
  "border_alpha": 0.16,
39
39
  "border_hover_alpha": 0.38,
40
40
  "accent_dim_alpha": 0.12,
41
- "fill_steps": ["ink", "ink_mid", "dim"]
41
+ "fill_steps": [
42
+ "ink",
43
+ "ink_mid",
44
+ "dim"
45
+ ]
42
46
  },
43
47
  "fonts": {
44
48
  "$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.",
@@ -58,6 +62,13 @@
58
62
  }
59
63
  }
60
64
  },
65
+ "font_files": {
66
+ "$comment": "For engines that EMBED a real font file rather than resolving a CSS stack — the profile README's SVG build subsets these into each tile. SF is not vendorable, so Inter stands in for the display voice and IBM Plex for serif/mono. Same type intent as fonts.profiles, expressed where a stack is not an option; a consumer must vendor matching files under its own assets/fonts.",
67
+ "display_black": "Inter-Black.ttf",
68
+ "display_bold": "Inter-Bold.ttf",
69
+ "serif_italic": "IBMPlexSerif-Italic.ttf",
70
+ "mono": "IBMPlexMono-Regular.ttf"
71
+ },
61
72
  "identity": {
62
73
  "stamp": "NS",
63
74
  "byline": "linkedin.com/in/natejswenson"
package/lib/emit.mjs CHANGED
@@ -264,8 +264,37 @@ function jsonTokens(tokens, params) {
264
264
 
265
265
  // --------------------------------------------------------------------------
266
266
 
267
+
268
+ /**
269
+ * Flat module constants plus optional dicts, for a script that reads tokens as
270
+ * Python names rather than a theme dict. The profile README's SVG build is the
271
+ * case: it has no override file to deep-merge and no stylesheet, so the full
272
+ * `python-theme` machinery would be dead weight around four strings.
273
+ */
274
+ function pythonConsts(tokens, params) {
275
+ const out = [];
276
+ for (const spec of params.consts ?? []) {
277
+ const name = required(spec, 'name', 'python-consts');
278
+ out.push(`${name} = ${py(lookup(tokens, required(spec, 'token', 'python-consts'), params))}`);
279
+ }
280
+ for (const spec of params.dicts ?? []) {
281
+ const group = tokens[required(spec, 'group', 'python-consts')];
282
+ if (!group) throw new EmitError(`python-consts: unknown token group "${spec.group}"`);
283
+ if (out.length) out.push('');
284
+ out.push(`${required(spec, 'name', 'python-consts')} = {`);
285
+ for (const [key, value] of Object.entries(group)) {
286
+ const k = spec.key_style === 'kebab' ? key.replace(/_/g, '-') : key;
287
+ out.push(` ${py(k)}: ${py(value)},`);
288
+ }
289
+ out.push('}');
290
+ }
291
+ if (out.length === 0) throw new EmitError('python-consts needs consts and/or dicts');
292
+ return out.join('\n');
293
+ }
294
+
267
295
  export const EMITTERS = {
268
296
  'python-theme': pythonTheme,
297
+ 'python-consts': pythonConsts,
269
298
  'css-vars': cssVars,
270
299
  'md-palette': mdPalette,
271
300
  'markdown-block': markdownBlock,
@@ -317,7 +346,7 @@ const DERIVED = {
317
346
 
318
347
  function lookup(tokens, name, params = {}) {
319
348
  if (name in DERIVED) return tokens.derived[DERIVED[name]];
320
- for (const group of [tokens.colors, tokens.terminal, fontsFor(tokens, params), tokens.identity, tokens.marks]) {
349
+ for (const group of [tokens.colors, tokens.terminal, fontsFor(tokens, params), tokens.fontFiles, tokens.identity, tokens.marks]) {
321
350
  if (name in group) return group[name];
322
351
  }
323
352
  throw new EmitError(`unknown token "${name}"`);
package/lib/tokens.mjs CHANGED
@@ -88,6 +88,7 @@ function resolve(raw) {
88
88
  ),
89
89
  defaultFontProfile: raw.fonts?.default_profile,
90
90
  identity: clean(raw.identity),
91
+ fontFiles: clean(raw.font_files),
91
92
  marks: clean(raw.marks),
92
93
  limits: clean(raw.limits),
93
94
  derived: { hair, hairAlpha, border, borderHover, accentDim, fillSteps },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natjswenson/press",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "One brand system — tokens, laws, run presentation and voice core — generated into every consumer with a CI drift gate",
5
5
  "license": "MIT",
6
6
  "author": "Nate Swenson",
package/targets.json CHANGED
@@ -359,6 +359,45 @@
359
359
  "replaceFrom": "^:root \\{$",
360
360
  "replaceTo": "^\\}$"
361
361
  }
362
+ },
363
+ {
364
+ "id": "profile-readme",
365
+ "repo": "natejswenson",
366
+ "path": "build_svg.py",
367
+ "region": "tokens",
368
+ "syntax": "python",
369
+ "emitter": "python-consts",
370
+ "params": {
371
+ "consts": [
372
+ {
373
+ "name": "PAPER",
374
+ "token": "paper"
375
+ },
376
+ {
377
+ "name": "INK",
378
+ "token": "ink"
379
+ },
380
+ {
381
+ "name": "DIM",
382
+ "token": "dim"
383
+ },
384
+ {
385
+ "name": "ACCENT",
386
+ "token": "accent"
387
+ }
388
+ ],
389
+ "dicts": [
390
+ {
391
+ "name": "FACES",
392
+ "group": "fontFiles",
393
+ "key_style": "kebab"
394
+ }
395
+ ]
396
+ },
397
+ "init": {
398
+ "replaceFrom": "^# --- PRESS tokens",
399
+ "replaceTo": "^\\}$"
400
+ }
362
401
  }
363
402
  ]
364
403
  }