remarque-tokens 0.19.0 → 0.20.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/AGENT_RULES.md +85 -0
- package/CHANGELOG.md +12 -0
- package/README.md +8 -3
- package/REMARQUE.md +20 -1
- package/package.json +4 -1
- package/scripts/audit.mjs +59 -10
- package/scripts/drift-check.mjs +28 -1
- package/scripts/tokens-json.mjs +191 -10
- package/tokens.d.ts +12 -1
- package/tokens.json +18 -1
- package/tokens.schema.json +210 -0
package/AGENT_RULES.md
CHANGED
|
@@ -227,6 +227,91 @@ Before considering any implementation complete, verify:
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
+
## Machine-Readable Output
|
|
231
|
+
|
|
232
|
+
Two tools emit structured JSON for agents/tooling instead of colored
|
|
233
|
+
stdout — pass `--json` and parse stdout as a single JSON document. Exit
|
|
234
|
+
codes are unchanged in both cases; `--json` only changes what stdout
|
|
235
|
+
contains (all human progress lines, including error lines normally on
|
|
236
|
+
stderr, are suppressed — nothing but the JSON document is written).
|
|
237
|
+
|
|
238
|
+
### `remarque-audit --json`
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
node scripts/audit.mjs --palette <file> --src <dir> --json
|
|
242
|
+
npx remarque-audit --palette <file> --src <dir> --json
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
{
|
|
247
|
+
version: string; // the remarque-tokens package version running the audit
|
|
248
|
+
palette: string; // the --palette path as given
|
|
249
|
+
src: string; // the --src path as given
|
|
250
|
+
passed: boolean; // true iff failures.length === 0
|
|
251
|
+
contrast: Array<{
|
|
252
|
+
theme: 'light' | 'dark';
|
|
253
|
+
fg: string; // fg token name, no leading --
|
|
254
|
+
bg: string; // bg token name, no leading --
|
|
255
|
+
label: string; // human label from the CHECKS table (e.g. "primary text")
|
|
256
|
+
required: number; // minimum ratio (e.g. 4.5, 7.0, 3.0)
|
|
257
|
+
actual: number | null; // computed ratio rounded to 2dp, or null if fg/bg was unresolvable
|
|
258
|
+
ok: boolean;
|
|
259
|
+
error?: string; // present only when actual is null (unresolved var()/missing token)
|
|
260
|
+
}>;
|
|
261
|
+
gamut: Array<{
|
|
262
|
+
theme: 'light' | 'dark';
|
|
263
|
+
token: string; // token name, no leading --
|
|
264
|
+
value: string; // e.g. "oklch(0.5 0.14 250)"
|
|
265
|
+
ok: boolean; // false = outside sRGB gamut (browsers will clip it)
|
|
266
|
+
}>;
|
|
267
|
+
srcScans: {
|
|
268
|
+
fontFloor: Array<{ file: string; line: number; size: number; unit: 'rem' | 'px' }>;
|
|
269
|
+
unverifiableFontSize: Array<{ file: string; line: number; snippet: string }>;
|
|
270
|
+
hardcodedColors: Array<{ file: string; line: number; snippet: string }>;
|
|
271
|
+
oklchLiteral: Array<{ file: string; line: number; snippet: string }>;
|
|
272
|
+
};
|
|
273
|
+
failures: string[]; // every failure as a human-readable message, same text the non-JSON run prints
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`contrast` and `gamut` list EVERY check performed (passing and failing);
|
|
278
|
+
only `srcScans` and `failures` are violation-only lists, since a source
|
|
279
|
+
scan that finds nothing has nothing to report per line.
|
|
280
|
+
|
|
281
|
+
### `remarque-drift --json`
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
node scripts/drift-check.mjs --css-file <path> --package-dir <dir> --json
|
|
285
|
+
npx remarque-drift --css-file <path> --package-dir <dir> --json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
{
|
|
290
|
+
cssFile: string;
|
|
291
|
+
packageDir: string; // resolved node_modules/remarque-tokens path
|
|
292
|
+
installedVersion: string; // version of the INSTALLED remarque-tokens being checked against
|
|
293
|
+
deviationDoc: string | null; // path to DESIGN-DEVIATIONS.md / DESIGN-NOTES.md, or null if none found
|
|
294
|
+
passed: boolean; // true iff fail.length === 0
|
|
295
|
+
fail: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string }>;
|
|
296
|
+
warn: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string; doc: string }>;
|
|
297
|
+
info: Array<{ name: string; theme: 'light' | 'dark'; canonical: string; value: string }>;
|
|
298
|
+
summary: { fail: number; warn: number; info: number }; // counts of distinct TOKENS with an entry, not records (one token can produce two records — light + dark)
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`fail`/`warn`/`info` are one record per (token, theme) mismatch — see
|
|
303
|
+
`scripts/drift-check.mjs`'s doc comment and REMARQUE.md's "Token Tiers"
|
|
304
|
+
for the FAIL/WARN/INFO classification these mirror.
|
|
305
|
+
|
|
306
|
+
### `tokens.json` / `tokens.schema.json`
|
|
307
|
+
|
|
308
|
+
`remarque-tokens/tokens.json` is always machine-readable (no flag
|
|
309
|
+
needed) and now ships with a published JSON Schema — see REMARQUE.md
|
|
310
|
+
"DTCG Conformance" for the schema's location, the `$schema` pointer, and
|
|
311
|
+
the documented DTCG divergences.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
230
315
|
## How to Reference This System
|
|
231
316
|
|
|
232
317
|
In prompts, issues, or agent instructions, use:
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `remarque-tokens` are documented here. Token value
|
|
|
4
4
|
changes always state the design rationale — downstream sites pin against
|
|
5
5
|
these entries when syncing.
|
|
6
6
|
|
|
7
|
+
## 0.20.0 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
Machine-readable surface: `--json` for `remarque-audit`/`remarque-drift`, and a published JSON Schema for `tokens.json` (closes #98, closes #99).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`--json` on `remarque-audit`** (issue #98). Suppresses all human console output (both the progress lines and the error lines normally on stderr) and emits ONE JSON document to stdout instead — exit codes unchanged. Shape: `{ version, palette, src, passed, contrast: [{theme,fg,bg,label,required,actual,ok,error?}], gamut: [{theme,token,value,ok}], srcScans: {fontFloor,unverifiableFontSize,hardcodedColors,oklchLiteral}, failures: [] }`. `contrast`/`gamut` list every check performed (pass and fail); `srcScans`/`failures` are violation-only. Documented in `AGENT_RULES.md` ("Machine-Readable Output") — agents are the intended audience, parsing structure instead of scraping colored stdout.
|
|
13
|
+
- **`--json` on `remarque-drift`** (issue #98). Same suppression contract; emits `{ cssFile, packageDir, installedVersion, deviationDoc, passed, fail: [], warn: [], info: [], summary: {fail,warn,info} }` — one record per (token, theme) mismatch, mirroring the FAIL/WARN/INFO classification the human report already prints.
|
|
14
|
+
- **`tokens.schema.json`** (issue #99). JSON Schema (draft 2020-12) describing `tokens.json`'s actual shape, generated by `scripts/tokens-json.mjs` in the same pass as `tokens.json`/`tokens.d.ts`, same `--check` freshness gate. Token *names* are open (`patternProperties` on the kebab-case grammar); the `$value`/`$type`/light-dark VALUE shapes are fixed. Published as the `remarque-tokens/tokens.schema.json` export, shipped in `files`, and served by the demo site at `/tokens.schema.json` (alongside the existing `/tokens.json`) via an extended `site/scripts/copy-tokens-json.mjs` — shadcn's schema-URL precedent. `tokens.json` now carries a `"$schema"` pointer to that URL.
|
|
15
|
+
- **DTCG conformance note** (issue #99, ratified option ii). `tokens.json`'s `$extensions.remarque.dtcg` documents that the file is conformant in spirit with the Design Tokens Community Group format (`$value`/`$type` on every token) with two **deliberate** divergences — color values as `oklch()` CSS strings (not DTCG's structured color object) and per-token `light`/`dark` nesting (DTCG has no ratified multi-mode/resolver mechanism yet) — each with a named ratification trigger, so a future agent can't "fix" `tokens.json` toward an unratified target. Same argument restated for humans in REMARQUE.md's new "DTCG Conformance" section (under "Token Tiers").
|
|
16
|
+
- **Schema validation in CI** (`scripts/test-types.mjs`). Real JSON-Schema validation of `tokens.json` against `tokens.schema.json` via `ajv` (new devDependency, pinned to a version with zero known advisories; the `$data` option that the one applicable ajv CVE requires is never used) — plus three negative fixtures (unknown `$extensions` key, invalid `$type` enum value, missing required `dark` side) proving the schema actually rejects malformed input rather than accepting anything.
|
|
17
|
+
- **`--json` fixture coverage** (`scripts/test-audit.mjs`, `scripts/test-drift.mjs`). Parses the JSON output of a passing and a must-fail fixture for each tool; asserts the documented shape and that the must-fail fixture's offending pairing/token is actually present with `ok`/`passed: false`.
|
|
18
|
+
|
|
7
19
|
## 0.19.0 — 2026-07-23
|
|
8
20
|
|
|
9
21
|
Palette Deck module (closes #56).
|
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ Copy the `fonts/` woff2 files from `node_modules/remarque-tokens/fonts/` into yo
|
|
|
44
44
|
npx remarque-audit --palette src/styles/my-palette.css --src src
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Add `--json` to either `remarque-audit` or `remarque-drift` for a single structured JSON document on stdout instead of colored console output (exit codes unchanged) — for agents and CI tooling to parse rather than scrape. Shape documented in `AGENT_RULES.md` ("Machine-Readable Output").
|
|
48
|
+
|
|
47
49
|
**Tailwind v4** projects add the shipped adapter — utilities that track the tokens through every theme switch, no value duplication:
|
|
48
50
|
|
|
49
51
|
```css
|
|
@@ -52,7 +54,7 @@ npx remarque-audit --palette src/styles/my-palette.css --src src
|
|
|
52
54
|
@import "remarque-tokens/theme.css";
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
**Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
|
|
57
|
+
**Tailwind v3** projects use the shipped config (`remarque-tokens/tailwind`) instead. A machine-readable token inventory ships as `remarque-tokens/tokens.json` (generated from the CSS — core/palette tiers, light+dark values) for tooling and AI agents, with a published JSON Schema at `remarque-tokens/tokens.schema.json` (also referenced by tokens.json's own `$schema` field) so tooling can validate it structurally instead of hand-parsing. tokens.json is conformant in spirit with the [Design Tokens Community Group](https://www.designtokens.org/) format (`$value`/`$type` on every token), with two deliberate divergences — see REMARQUE.md's "DTCG Conformance" section. Prefer copy-paste? Grab `fonts.css`, `tokens.css`, `tokens-core.css`, `tokens-palette.css`, and `fonts/` directly — `tokens.css` aggregates the two tier files, so all three CSS files travel together. Use string-form `@import './tokens.css'` only (some bundlers silently drop `@import url(...)` for local files).
|
|
56
58
|
|
|
57
59
|
## For AI Agents
|
|
58
60
|
|
|
@@ -68,7 +70,7 @@ The agent rules define build order, non-negotiable rules, disallowed patterns, a
|
|
|
68
70
|
Packaging for agent tooling:
|
|
69
71
|
- **npm exports:** `remarque-tokens/agent-rules` (→ `AGENT_RULES.md`) and `remarque-tokens/spec` (→ `REMARQUE.md`), alongside the existing `remarque-tokens/tokens.json`, so a project can point an agent at `node_modules/remarque-tokens/AGENT_RULES.md` without hardcoding a filename.
|
|
70
72
|
- **Claude Code skill:** [`.claude/skills/remarque/SKILL.md`](.claude/skills/remarque/SKILL.md) — triggers on "remarque" / "design system" / new-page work, loads all three files, and states the tier rules, the audit command, and the two build-time pitfalls (unlayered-token-import, string-form `@import`) that pass a green build while silently breaking.
|
|
71
|
-
- **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json** — a remote agent can fetch current token values directly instead of trusting training data.
|
|
73
|
+
- **Live tokens endpoint:** the demo site serves the current `tokens.json` at **https://williamzujkowski.github.io/remarque/tokens.json**, and its schema at **https://williamzujkowski.github.io/remarque/tokens.schema.json** — a remote agent can fetch current token values (and validate their shape) directly instead of trusting training data.
|
|
72
74
|
|
|
73
75
|
## Files
|
|
74
76
|
|
|
@@ -80,7 +82,10 @@ Packaging for agent tooling:
|
|
|
80
82
|
| `tokens-core.css` | Core tier — type scale, spacing, widths, radius, motion, prose styling. Never overridden |
|
|
81
83
|
| `tokens-palette.css` | Palette tier — font slots, colors, accent, reading measure. The sanctioned personalization surface |
|
|
82
84
|
| `prose.css` | `.remarque-prose` long-form styling — own subpath so sites with their own prose system can skip it |
|
|
83
|
-
| `scripts/audit.mjs` | `npm run audit` — enforces the spec's contrast/gamut/font-floor/no-hardcoded-color checklist |
|
|
85
|
+
| `scripts/audit.mjs` | `npm run audit` — enforces the spec's contrast/gamut/font-floor/no-hardcoded-color checklist (`--json` for structured output) |
|
|
86
|
+
| `scripts/drift-check.mjs` | `npx remarque-drift` — token drift check for consumers (`--json` for structured output) |
|
|
87
|
+
| `tokens.json` + `tokens.d.ts` | Generated machine-readable token inventory + TypeScript types (`scripts/tokens-json.mjs`) |
|
|
88
|
+
| `tokens.schema.json` | Generated JSON Schema (draft 2020-12) for `tokens.json`, published alongside it |
|
|
84
89
|
| `fonts.css` + `fonts/` | Self-hosted @font-face declarations and woff2 files (no CDN requests) |
|
|
85
90
|
| `tailwind.config.js` | Tailwind CSS **v3** configuration (v4 projects use an `@theme` block instead) |
|
|
86
91
|
| `package.json` | npm package manifest for `remarque-tokens` |
|
package/REMARQUE.md
CHANGED
|
@@ -68,6 +68,17 @@ Package subpaths for consumers: `remarque-tokens` (aggregator), `remarque-tokens
|
|
|
68
68
|
|
|
69
69
|
This makes compliance mechanical: a site that overrides only palette-tier tokens is *authored*; one that touches core-tier tokens has *forked*. `tokens.css` imports both tiers, so existing consumers are unaffected.
|
|
70
70
|
|
|
71
|
+
### DTCG Conformance
|
|
72
|
+
|
|
73
|
+
`tokens-core.css` and `tokens-palette.css` remain the single source of truth; `scripts/tokens-json.mjs` regenerates `tokens.json`, `tokens.d.ts`, and `tokens.schema.json` from them in one pass (`node scripts/tokens-json.mjs`; `--check` gates CI freshness). `tokens.json` carries a `$schema` pointer to the published schema (served by the demo site at `/tokens.schema.json`, alongside `/tokens.json` — also exported as `remarque-tokens/tokens.schema.json`).
|
|
74
|
+
|
|
75
|
+
`tokens.json` is **conformant in spirit** with the [Design Tokens Community Group (DTCG)](https://www.designtokens.org/) format — every token carries `$value`/`$type` — with two **deliberate** structural divergences, documented in the generated file itself (`$extensions.remarque.dtcg`) so a future regeneration can't lose the rationale:
|
|
76
|
+
|
|
77
|
+
1. **Color values are `oklch()` CSS strings**, not the DTCG structured color object (`{ colorSpace, components, alpha }`). Remarque's color pipeline (audit, drift-check, `remarque-theme`) is built entirely on parsing/emitting `oklch(L C H)` strings; adopting the structured form would mean carrying two color representations in parallel for no present benefit. **Gated on:** the DTCG color `$type` structured-value format ratifying.
|
|
78
|
+
2. **Palette-tier tokens nest per-token `light`/`dark` groups**, each with its own `$value`, rather than a single `$value` plus a modes/resolver mechanism. The DTCG spec has no ratified multi-mode/theming primitive yet; this repo's own theming (`@media (prefers-color-scheme: dark)`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping) predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
|
|
79
|
+
|
|
80
|
+
Full DTCG conformance is gated on those two drafts ratifying — a named, checkable trigger, not "someday." Until then, do not "fix" `tokens.json` toward either unratified draft; the CSS is the source of truth, and both divergences are load-bearing for the tooling that already depends on this shape.
|
|
81
|
+
|
|
71
82
|
---
|
|
72
83
|
|
|
73
84
|
## Color Providers
|
|
@@ -706,7 +717,8 @@ for the value-by-value mapping).
|
|
|
706
717
|
<h2 id="section-one">Section One</h2>
|
|
707
718
|
<p>
|
|
708
719
|
A claim worth a citation<a href="#note-1" id="note-1-ref"
|
|
709
|
-
class="remarque-sidenote-ref" aria-
|
|
720
|
+
class="remarque-sidenote-ref" aria-label="Note 1"
|
|
721
|
+
aria-describedby="note-1"></a>.
|
|
710
722
|
</p>
|
|
711
723
|
<aside class="remarque-sidenote" id="note-1" role="note">
|
|
712
724
|
The note text, in real DOM order right after the paragraph that
|
|
@@ -729,6 +741,13 @@ Requirements the CSS depends on:
|
|
|
729
741
|
`.remarque-sidenote` after it; later citations of the same note use
|
|
730
742
|
`.remarque-sidenote-ref--repeat` (styled identically, does not advance
|
|
731
743
|
the counter) so the shared numbering doesn't skip.
|
|
744
|
+
- `aria-label="Note N"` on every `.remarque-sidenote-ref`, numbered in
|
|
745
|
+
the same order the CSS counter advances — the ref's visible digit is
|
|
746
|
+
counter-generated, so the anchor itself has no text content, and an
|
|
747
|
+
empty focusable link with no accessible name fails WCAG 4.1.2 / 2.4.4
|
|
748
|
+
(axe: `link-name`). Because both the label and the counter derive from
|
|
749
|
+
DOM order, they cannot drift apart. (Found in the flagship's migration,
|
|
750
|
+
williamzujkowski.github.io#380.)
|
|
732
751
|
- `role="note"` on every `.remarque-sidenote` — `<aside>`'s implicit role
|
|
733
752
|
is `complementary`, a landmark; an essay with several notes would
|
|
734
753
|
otherwise scatter that many unlabeled landmark regions through the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remarque-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=18"
|
|
6
6
|
},
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@williamzujkowski/oklch-terminal-themes": "0.4.0",
|
|
36
|
+
"ajv": "^8.20.0",
|
|
36
37
|
"culori": "^4.0.2"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"default": "./tokens.json"
|
|
65
66
|
},
|
|
66
67
|
"./tokens.d.ts": "./tokens.d.ts",
|
|
68
|
+
"./tokens.schema.json": "./tokens.schema.json",
|
|
67
69
|
"./prose": "./prose.css",
|
|
68
70
|
"./prose.css": "./prose.css",
|
|
69
71
|
"./print": "./print.css",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"theme.css",
|
|
98
100
|
"tokens.json",
|
|
99
101
|
"tokens.d.ts",
|
|
102
|
+
"tokens.schema.json",
|
|
100
103
|
"scripts/tokens-json.mjs",
|
|
101
104
|
"prose.css",
|
|
102
105
|
"print.css",
|
package/scripts/audit.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* remarque audit — enforces REMARQUE.md's checklist mechanically.
|
|
4
4
|
*
|
|
5
|
-
* node scripts/audit.mjs [--palette tokens-palette.css] [--src <dir>]
|
|
5
|
+
* node scripts/audit.mjs [--palette tokens-palette.css] [--src <dir>] [--json]
|
|
6
6
|
* npx remarque-audit --palette your-palette.css --src src
|
|
7
7
|
*
|
|
8
8
|
* Checks (exit 1 on any failure):
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* 2. FONT FLOOR — no font-size declaration below 0.8125rem / 13px in src.
|
|
20
20
|
* 3. NO HARDCODED COLORS — no hex/rgb()/hsl() literals in src styles;
|
|
21
21
|
* oklch() literals are allowed only in token files.
|
|
22
|
+
*
|
|
23
|
+
* --json: suppresses all human console output and emits ONE JSON document
|
|
24
|
+
* to stdout instead (exit codes unchanged) — for agents/tooling to parse
|
|
25
|
+
* instead of scraping colored stdout. Shape documented in AGENT_RULES.md
|
|
26
|
+
* ("Machine-Readable Output — remarque-audit --json").
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
29
|
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
@@ -33,11 +38,35 @@ function argOf(flag, dflt) {
|
|
|
33
38
|
}
|
|
34
39
|
const PALETTE = argOf('--palette', 'tokens-palette.css');
|
|
35
40
|
const SRC = argOf('--src', existsSync('site/src') ? 'site/src' : '.');
|
|
41
|
+
const JSON_MODE = args.includes('--json');
|
|
42
|
+
|
|
43
|
+
// The audit's OWN package version (this script's remarque-tokens, resolved
|
|
44
|
+
// via import.meta.url so it is correct whether run in this repo or as
|
|
45
|
+
// npx remarque-audit from a consumer's node_modules/remarque-tokens) —
|
|
46
|
+
// not the consumer project's package.json.
|
|
47
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
48
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
49
|
+
|
|
50
|
+
const report = {
|
|
51
|
+
version: PKG_VERSION,
|
|
52
|
+
palette: PALETTE,
|
|
53
|
+
src: SRC,
|
|
54
|
+
passed: false,
|
|
55
|
+
contrast: [],
|
|
56
|
+
gamut: [],
|
|
57
|
+
srcScans: { fontFloor: [], unverifiableFontSize: [], hardcodedColors: [], oklchLiteral: [] },
|
|
58
|
+
failures: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function log(...a) {
|
|
62
|
+
if (!JSON_MODE) console.log(...a);
|
|
63
|
+
}
|
|
36
64
|
|
|
37
65
|
let failures = 0;
|
|
38
66
|
function fail(msg) {
|
|
39
67
|
failures++;
|
|
40
|
-
|
|
68
|
+
report.failures.push(msg);
|
|
69
|
+
if (!JSON_MODE) console.error(` ✗ ${msg}`);
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
if (!existsSync(PALETTE)) {
|
|
@@ -48,6 +77,9 @@ if (!existsSync(SRC)) {
|
|
|
48
77
|
console.error(`src directory not found: ${SRC} (use --src <dir>)`);
|
|
49
78
|
process.exit(1);
|
|
50
79
|
}
|
|
80
|
+
// These two checks are fatal usage errors (exit 1) unrelated to the
|
|
81
|
+
// checked project's compliance, so they print unconditionally even in
|
|
82
|
+
// --json mode — there is no report to emit yet.
|
|
51
83
|
|
|
52
84
|
/* ── OKLCH → sRGB → WCAG luminance ──────────────────────── */
|
|
53
85
|
|
|
@@ -139,11 +171,14 @@ const CHECKS = [
|
|
|
139
171
|
];
|
|
140
172
|
|
|
141
173
|
for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
|
|
142
|
-
|
|
174
|
+
log(`\n${themeName} theme (${PALETTE})`);
|
|
143
175
|
// gamut check on every color-valued token
|
|
144
176
|
for (const name of Object.keys(decls)) {
|
|
145
177
|
const r = resolveColor(decls, name);
|
|
146
|
-
if (
|
|
178
|
+
if (!r.color) continue; // unresolvable tokens are covered by the contrast checks' own error path
|
|
179
|
+
const ok = inGamut(oklchToLinearSrgb(...r.color));
|
|
180
|
+
report.gamut.push({ theme: themeName, token: name, value: `oklch(${r.color.join(' ')})`, ok });
|
|
181
|
+
if (!ok) {
|
|
147
182
|
fail(`--${name} oklch(${r.color.join(' ')}) is outside sRGB gamut — browsers will clip it; author an in-gamut value`);
|
|
148
183
|
}
|
|
149
184
|
}
|
|
@@ -151,12 +186,16 @@ for (const [themeName, decls] of [['light', lightDecls], ['dark', darkDecls]]) {
|
|
|
151
186
|
const fg = resolveColor(decls, fgName);
|
|
152
187
|
const bg = resolveColor(decls, bgName);
|
|
153
188
|
if (fg.err || bg.err) {
|
|
189
|
+
report.contrast.push({ theme: themeName, fg: fgName, bg: bgName, label, required: min, actual: null, ok: false, error: fg.err || bg.err });
|
|
154
190
|
fail(`${fgName}/${bgName} (${label}): ${fg.err || bg.err}`);
|
|
155
191
|
continue;
|
|
156
192
|
}
|
|
157
193
|
const r = ratio(fg.color, bg.color);
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
const actual = Math.round(r * 100) / 100;
|
|
195
|
+
const ok = r >= min;
|
|
196
|
+
report.contrast.push({ theme: themeName, fg: fgName, bg: bgName, label, required: min, actual, ok });
|
|
197
|
+
if (!ok) fail(`${fgName}/${bgName} = ${r.toFixed(2)}:1 < ${min}:1 (${label})`);
|
|
198
|
+
else log(` ✓ ${fgName}/${bgName} = ${r.toFixed(2)}:1 (${label})`);
|
|
160
199
|
}
|
|
161
200
|
}
|
|
162
201
|
|
|
@@ -171,7 +210,7 @@ function* walk(dir) {
|
|
|
171
210
|
}
|
|
172
211
|
}
|
|
173
212
|
|
|
174
|
-
|
|
213
|
+
log(`\nsource scans (${SRC})`);
|
|
175
214
|
const FONT_FLOOR_REM = 0.8125, FONT_FLOOR_PX = 13;
|
|
176
215
|
// tokens.astro is the token *reference page* — its job is displaying literal
|
|
177
216
|
// values (issue #48 tracks generating it from a machine-readable source).
|
|
@@ -198,26 +237,36 @@ for (const file of walk(SRC)) {
|
|
|
198
237
|
for (const m of line.matchAll(/font-size:\s*([\d.]+)(rem|px)/g)) {
|
|
199
238
|
const v = parseFloat(m[1]);
|
|
200
239
|
if ((m[2] === 'rem' && v < FONT_FLOOR_REM) || (m[2] === 'px' && v < FONT_FLOOR_PX)) {
|
|
240
|
+
report.srcScans.fontFloor.push({ file: rel, line: i + 1, size: v, unit: m[2] });
|
|
201
241
|
fail(`${rel}:${i + 1} font-size ${m[1]}${m[2]} below the 13px floor`);
|
|
202
242
|
}
|
|
203
243
|
}
|
|
204
244
|
if (!isTokenFile(rel) && /font-size:\s*(clamp\(|[\d.]+%)/.test(line)) {
|
|
245
|
+
report.srcScans.unverifiableFontSize.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
|
|
205
246
|
fail(`${rel}:${i + 1} statically unverifiable font-size (clamp/%) — use var(--text-*) tokens: ${line.trim().slice(0, 80)}`);
|
|
206
247
|
}
|
|
207
248
|
// 3. hardcoded colors (hex / rgb / hsl anywhere; oklch outside token files).
|
|
208
249
|
// Note: hex fills/strokes in inline SVG are flagged deliberately —
|
|
209
250
|
// Remarque icons use currentColor, per the no-hardcoded-colors rule.
|
|
210
251
|
if (/(#[0-9a-fA-F]{3,8}\b(?![0-9a-zA-Z]))|(\brgba?\()|(\bhsla?\()/.test(line) && !/xmlns|href|url\(#|\{#/.test(line)) {
|
|
252
|
+
report.srcScans.hardcodedColors.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
|
|
211
253
|
fail(`${rel}:${i + 1} hardcoded hex/rgb/hsl color: ${line.trim().slice(0, 80)}`);
|
|
212
254
|
}
|
|
213
255
|
if (!isTokenFile(rel) && /\boklch\(/.test(line) && !line.includes('var(--')) {
|
|
256
|
+
report.srcScans.oklchLiteral.push({ file: rel, line: i + 1, snippet: line.trim().slice(0, 80) });
|
|
214
257
|
fail(`${rel}:${i + 1} oklch() literal outside token files: ${line.trim().slice(0, 80)}`);
|
|
215
258
|
}
|
|
216
259
|
});
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
|
|
262
|
+
report.passed = failures === 0;
|
|
263
|
+
|
|
264
|
+
if (JSON_MODE) {
|
|
265
|
+
console.log(JSON.stringify(report, null, 2));
|
|
266
|
+
} else if (failures) {
|
|
220
267
|
console.error(`\naudit FAILED — ${failures} problem(s)\n`);
|
|
221
|
-
|
|
268
|
+
} else {
|
|
269
|
+
console.log('\naudit passed ✓\n');
|
|
222
270
|
}
|
|
223
|
-
|
|
271
|
+
|
|
272
|
+
if (failures) process.exit(1);
|
package/scripts/drift-check.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* remarque-drift — token drift check for consumers of remarque-tokens.
|
|
4
4
|
*
|
|
5
|
-
* node scripts/drift-check.mjs --css-file <path> --package-dir <dir>
|
|
5
|
+
* node scripts/drift-check.mjs --css-file <path> --package-dir <dir> [--json]
|
|
6
6
|
* npx remarque-drift --css-file src/styles/global.css --package-dir .
|
|
7
7
|
*
|
|
8
8
|
* Compares a consumer's stylesheet against the INSTALLED remarque-tokens
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Exit code: 1 iff there is at least one undocumented (FAIL) core-tier
|
|
29
29
|
* mismatch. WARN and INFO never fail the build.
|
|
30
|
+
*
|
|
31
|
+
* --json: suppresses all human console output and emits ONE JSON document
|
|
32
|
+
* to stdout instead (exit codes unchanged) — records mirror the FAIL/
|
|
33
|
+
* WARN/INFO classification above. Shape documented in AGENT_RULES.md
|
|
34
|
+
* ("Machine-Readable Output — remarque-drift --json").
|
|
30
35
|
*/
|
|
31
36
|
|
|
32
37
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -41,6 +46,7 @@ function argOf(flag, dflt) {
|
|
|
41
46
|
|
|
42
47
|
const CSS_FILE = argOf('--css-file', null);
|
|
43
48
|
const PACKAGE_DIR = resolve(argOf('--package-dir', '.'));
|
|
49
|
+
const JSON_MODE = args.includes('--json');
|
|
44
50
|
|
|
45
51
|
if (!CSS_FILE) {
|
|
46
52
|
console.error('usage: drift-check.mjs --css-file <path> --package-dir <dir>');
|
|
@@ -162,6 +168,27 @@ for (const [name, tok] of Object.entries(tokens.palette || {})) {
|
|
|
162
168
|
|
|
163
169
|
/* ── Report ─────────────────────────────────────────────────────────── */
|
|
164
170
|
|
|
171
|
+
if (JSON_MODE) {
|
|
172
|
+
// Flatten the internal per-token fails/warns/infos (each may bundle a
|
|
173
|
+
// light+dark mismatch) into one record per theme mismatch — the FAIL/
|
|
174
|
+
// WARN/INFO records the human report prints one line per, above.
|
|
175
|
+
const fail = fails.flatMap((f) => f.mismatches.map((m) => ({ name: f.name, theme: m.theme, canonical: f.canonical, value: m.value })));
|
|
176
|
+
const warn = warns.flatMap((w) => w.mismatches.map((m) => ({ name: w.name, theme: m.theme, canonical: w.canonical, value: m.value, doc: w.doc })));
|
|
177
|
+
const info = infos.flatMap((i) => i.diffs.map((d) => ({ name: i.name, theme: d.theme, canonical: d.theme === 'light' ? i.canonicalLight : i.canonicalDark, value: d.value })));
|
|
178
|
+
console.log(JSON.stringify({
|
|
179
|
+
cssFile: CSS_FILE,
|
|
180
|
+
packageDir: PKG_ROOT,
|
|
181
|
+
installedVersion,
|
|
182
|
+
deviationDoc: deviationDocPath,
|
|
183
|
+
passed: fails.length === 0,
|
|
184
|
+
fail,
|
|
185
|
+
warn,
|
|
186
|
+
info,
|
|
187
|
+
summary: { fail: fails.length, warn: warns.length, info: infos.length },
|
|
188
|
+
}, null, 2));
|
|
189
|
+
process.exit(fails.length ? 1 : 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
console.log('remarque-drift — token drift check');
|
|
166
193
|
console.log(` consumer stylesheet: ${CSS_FILE}`);
|
|
167
194
|
console.log(` installed remarque-tokens: ${installedVersion} (${PKG_ROOT})`);
|
package/scripts/tokens-json.mjs
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
* Generates tokens.json (W3C design-tokens flavored)
|
|
4
|
-
* (TypeScript literal-union types)
|
|
5
|
-
* tokens
|
|
3
|
+
* Generates tokens.json (DTCG/W3C design-tokens flavored), tokens.d.ts
|
|
4
|
+
* (TypeScript literal-union types), and tokens.schema.json (JSON Schema
|
|
5
|
+
* draft 2020-12 for tokens.json) FROM the CSS — tokens-core.css and
|
|
6
|
+
* tokens-palette.css remain the single source of truth; all three files
|
|
6
7
|
* here are derived output for tooling, AI agents, and TS editors.
|
|
7
8
|
*
|
|
8
|
-
* node scripts/tokens-json.mjs # (re)write tokens.json + tokens.d.ts
|
|
9
|
-
* node scripts/tokens-json.mjs --check # exit 1 if
|
|
9
|
+
* node scripts/tokens-json.mjs # (re)write tokens.json + tokens.d.ts + tokens.schema.json
|
|
10
|
+
* node scripts/tokens-json.mjs --check # exit 1 if any of the three is stale (CI)
|
|
10
11
|
*
|
|
11
12
|
* Multi-theme values use per-token light/dark groups with $value on each
|
|
12
|
-
* (the W3C spec has no native theming; this follows its $extensions
|
|
13
|
-
* escape hatch conservatively).
|
|
13
|
+
* (the DTCG/W3C spec has no native theming; this follows its $extensions
|
|
14
|
+
* escape hatch conservatively). tokens.json is CONFORMANT IN SPIRIT with
|
|
15
|
+
* the Design Tokens Community Group format ($value/$type on every token)
|
|
16
|
+
* but has two deliberate divergences — see the `dtcg` block below and
|
|
17
|
+
* REMARQUE.md's "DTCG Conformance" section for the full rationale and the
|
|
18
|
+
* named ratification triggers that would close each gap.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
@@ -37,13 +42,41 @@ function valueFor(type, value) {
|
|
|
37
42
|
return value;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
// Published alongside tokens.json/tokens.schema.json by the demo site
|
|
46
|
+
// (site/scripts/copy-tokens-json.mjs) — issue #99's "shadcn schema-URL
|
|
47
|
+
// precedent". Bump the path only in lockstep with an actual site move.
|
|
48
|
+
const SCHEMA_URL = 'https://williamzujkowski.github.io/remarque/tokens.schema.json';
|
|
49
|
+
|
|
40
50
|
const version = JSON.parse(readFileSync('package.json', 'utf8')).version;
|
|
41
51
|
const coreDecls = declsOf(extractBlocks(readFileSync('tokens-core.css', 'utf8')), isLightRoot);
|
|
42
52
|
const paletteBlocks = extractBlocks(readFileSync('tokens-palette.css', 'utf8'));
|
|
43
53
|
const lightDecls = declsOf(paletteBlocks, isLightRoot);
|
|
44
54
|
const darkOverrides = declsOf(paletteBlocks, isDarkBlock);
|
|
45
55
|
|
|
56
|
+
// DTCG conformance note (issue #99, ratified option ii): documented IN the
|
|
57
|
+
// generated artifact, not just in prose, so a future agent regenerating
|
|
58
|
+
// tokens.json can't lose it. Two deliberate divergences from the Design
|
|
59
|
+
// Tokens Community Group draft, each with a named ratification trigger —
|
|
60
|
+
// full conformance is gated on those drafts landing, not "someday soon."
|
|
61
|
+
const DTCG_NOTE = {
|
|
62
|
+
conformance: 'partial — $value/$type present on every token (conformant in spirit); two deliberate structural divergences below',
|
|
63
|
+
divergences: [
|
|
64
|
+
{
|
|
65
|
+
aspect: 'color-value-encoding',
|
|
66
|
+
detail: 'Color $value is an oklch() CSS string, not the DTCG structured color object ({ colorSpace, components, alpha }).',
|
|
67
|
+
gatedOn: 'DTCG color $type structured-value format ratifying',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
aspect: 'multi-mode-theming',
|
|
71
|
+
detail: 'Palette-tier tokens nest per-token { light: {$value}, dark: {$value} } groups instead of a single $value plus a modes/resolver mechanism.',
|
|
72
|
+
gatedOn: 'DTCG multi-mode / resolver draft ratifying',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
note: 'Deliberate, not oversight — see REMARQUE.md "DTCG Conformance" for the argument. Do not "fix" these toward the current unratified drafts; re-derive from the CSS via scripts/tokens-json.mjs instead.',
|
|
76
|
+
};
|
|
77
|
+
|
|
46
78
|
const out = {
|
|
79
|
+
$schema: SCHEMA_URL,
|
|
47
80
|
$description: 'Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.',
|
|
48
81
|
$extensions: {
|
|
49
82
|
remarque: {
|
|
@@ -52,6 +85,7 @@ const out = {
|
|
|
52
85
|
core: 'immutable identity — overriding forks the system',
|
|
53
86
|
palette: 'sanctioned personalization surface — override freely, then run remarque-audit',
|
|
54
87
|
},
|
|
88
|
+
dtcg: DTCG_NOTE,
|
|
55
89
|
},
|
|
56
90
|
},
|
|
57
91
|
core: {},
|
|
@@ -171,6 +205,7 @@ export interface RemarquePaletteTokenEntry {
|
|
|
171
205
|
* below applying, etc).
|
|
172
206
|
*/
|
|
173
207
|
export interface RemarqueTokensFile {
|
|
208
|
+
readonly $schema: string;
|
|
174
209
|
readonly $description: string;
|
|
175
210
|
readonly $extensions: {
|
|
176
211
|
readonly remarque: {
|
|
@@ -179,6 +214,16 @@ export interface RemarqueTokensFile {
|
|
|
179
214
|
readonly core: string;
|
|
180
215
|
readonly palette: string;
|
|
181
216
|
};
|
|
217
|
+
/** DTCG conformance note (issue #99) — see REMARQUE.md "DTCG Conformance". */
|
|
218
|
+
readonly dtcg: {
|
|
219
|
+
readonly conformance: string;
|
|
220
|
+
readonly divergences: ReadonlyArray<{
|
|
221
|
+
readonly aspect: string;
|
|
222
|
+
readonly detail: string;
|
|
223
|
+
readonly gatedOn: string;
|
|
224
|
+
}>;
|
|
225
|
+
readonly note: string;
|
|
226
|
+
};
|
|
182
227
|
};
|
|
183
228
|
};
|
|
184
229
|
readonly core: { readonly [K in RemarqueCoreToken]: RemarqueCoreTokenEntry };
|
|
@@ -212,19 +257,155 @@ declare module 'remarque-tokens/tokens.json' {
|
|
|
212
257
|
|
|
213
258
|
const dts = renderDts(out, version);
|
|
214
259
|
|
|
260
|
+
/* ── tokens.schema.json ────────────────────────────────────────────────
|
|
261
|
+
* JSON Schema draft 2020-12 describing tokens.json's ACTUAL shape —
|
|
262
|
+
* generated (not hand-maintained) so it can never drift from what this
|
|
263
|
+
* script emits. Token *names* are open (patternProperties on the
|
|
264
|
+
* kebab-case grammar every generated name follows) rather than an
|
|
265
|
+
* enumerated list of the current token set, so adding/removing a token
|
|
266
|
+
* in the CSS doesn't require a schema edit — only its documented VALUE
|
|
267
|
+
* shape ($value/$type, the light/dark palette grouping, the $extensions
|
|
268
|
+
* escape hatches) is fixed.
|
|
269
|
+
*/
|
|
270
|
+
function renderSchema() {
|
|
271
|
+
const kebabName = '^[a-z][a-z0-9-]*$';
|
|
272
|
+
const scalarValue = { oneOf: [{ type: 'string' }, { type: 'number' }] };
|
|
273
|
+
return {
|
|
274
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
275
|
+
$id: SCHEMA_URL,
|
|
276
|
+
title: 'Remarque design tokens (tokens.json)',
|
|
277
|
+
description:
|
|
278
|
+
'JSON Schema for remarque-tokens\' generated tokens.json — GENERATED by scripts/tokens-json.mjs, do not hand-edit. ' +
|
|
279
|
+
'Conformant in spirit with the Design Tokens Community Group format ($value/$type on every token), with two ' +
|
|
280
|
+
'deliberate divergences (oklch()-string color values; per-token light/dark nesting) — see the tokens.json ' +
|
|
281
|
+
'$extensions.remarque.dtcg block and REMARQUE.md "DTCG Conformance" for the full rationale and ratification triggers.',
|
|
282
|
+
type: 'object',
|
|
283
|
+
additionalProperties: false,
|
|
284
|
+
required: ['$description', '$extensions', 'core', 'palette'],
|
|
285
|
+
properties: {
|
|
286
|
+
// No format: 'uri' keyword — ajv's strict mode rejects unknown
|
|
287
|
+
// formats without the separate ajv-formats package, and this
|
|
288
|
+
// project deliberately keeps the schema-validation devDependency
|
|
289
|
+
// surface to ajv alone (see scripts/test-types.mjs).
|
|
290
|
+
$schema: { type: 'string' },
|
|
291
|
+
$description: { type: 'string' },
|
|
292
|
+
$extensions: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
required: ['remarque'],
|
|
296
|
+
properties: {
|
|
297
|
+
remarque: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
additionalProperties: false,
|
|
300
|
+
required: ['version', 'tiers', 'dtcg'],
|
|
301
|
+
properties: {
|
|
302
|
+
version: { type: 'string' },
|
|
303
|
+
tiers: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
additionalProperties: false,
|
|
306
|
+
required: ['core', 'palette'],
|
|
307
|
+
properties: { core: { type: 'string' }, palette: { type: 'string' } },
|
|
308
|
+
},
|
|
309
|
+
dtcg: {
|
|
310
|
+
type: 'object',
|
|
311
|
+
additionalProperties: false,
|
|
312
|
+
required: ['conformance', 'divergences', 'note'],
|
|
313
|
+
properties: {
|
|
314
|
+
conformance: { type: 'string' },
|
|
315
|
+
divergences: {
|
|
316
|
+
type: 'array',
|
|
317
|
+
items: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
additionalProperties: false,
|
|
320
|
+
required: ['aspect', 'detail', 'gatedOn'],
|
|
321
|
+
properties: {
|
|
322
|
+
aspect: { type: 'string' },
|
|
323
|
+
detail: { type: 'string' },
|
|
324
|
+
gatedOn: { type: 'string' },
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
note: { type: 'string' },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
core: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
patternProperties: { [kebabName]: { $ref: '#/$defs/coreEntry' } },
|
|
339
|
+
},
|
|
340
|
+
palette: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
additionalProperties: false,
|
|
343
|
+
patternProperties: { [kebabName]: { $ref: '#/$defs/paletteEntry' } },
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
$defs: {
|
|
347
|
+
tokenType: {
|
|
348
|
+
type: 'string',
|
|
349
|
+
enum: ['color', 'dimension', 'number', 'fontFamily', 'fontWeight', 'duration', 'string'],
|
|
350
|
+
},
|
|
351
|
+
coreEntry: {
|
|
352
|
+
type: 'object',
|
|
353
|
+
additionalProperties: false,
|
|
354
|
+
required: ['$value', '$type'],
|
|
355
|
+
properties: { $value: scalarValue, $type: { $ref: '#/$defs/tokenType' } },
|
|
356
|
+
},
|
|
357
|
+
paletteSideValue: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
additionalProperties: false,
|
|
360
|
+
required: ['$value'],
|
|
361
|
+
properties: {
|
|
362
|
+
$value: scalarValue,
|
|
363
|
+
$extensions: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
additionalProperties: false,
|
|
366
|
+
properties: {
|
|
367
|
+
remarque: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
properties: { inheritedFromLight: { type: 'boolean' } },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
paletteEntry: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
required: ['$type', 'light', 'dark'],
|
|
380
|
+
properties: {
|
|
381
|
+
$type: { $ref: '#/$defs/tokenType' },
|
|
382
|
+
light: { $ref: '#/$defs/paletteSideValue' },
|
|
383
|
+
dark: { $ref: '#/$defs/paletteSideValue' },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const schema = JSON.stringify(renderSchema(), null, 2) + '\n';
|
|
391
|
+
|
|
215
392
|
if (process.argv.includes('--check')) {
|
|
216
393
|
const currentJson = existsSync('tokens.json') ? readFileSync('tokens.json', 'utf8') : '';
|
|
217
394
|
const currentDts = existsSync('tokens.d.ts') ? readFileSync('tokens.d.ts', 'utf8') : '';
|
|
395
|
+
const currentSchema = existsSync('tokens.schema.json') ? readFileSync('tokens.schema.json', 'utf8') : '';
|
|
218
396
|
const staleJson = currentJson !== json;
|
|
219
397
|
const staleDts = currentDts !== dts;
|
|
220
|
-
|
|
398
|
+
const staleSchema = currentSchema !== schema;
|
|
399
|
+
if (staleJson || staleDts || staleSchema) {
|
|
221
400
|
if (staleJson) console.error('tokens.json is stale — run: node scripts/tokens-json.mjs');
|
|
222
401
|
if (staleDts) console.error('tokens.d.ts is stale — run: node scripts/tokens-json.mjs');
|
|
402
|
+
if (staleSchema) console.error('tokens.schema.json is stale — run: node scripts/tokens-json.mjs');
|
|
223
403
|
process.exit(1);
|
|
224
404
|
}
|
|
225
|
-
console.log('tokens.json
|
|
405
|
+
console.log('tokens.json, tokens.d.ts, and tokens.schema.json are fresh ✓');
|
|
226
406
|
} else {
|
|
227
407
|
writeFileSync('tokens.json', json);
|
|
228
408
|
writeFileSync('tokens.d.ts', dts);
|
|
229
|
-
|
|
409
|
+
writeFileSync('tokens.schema.json', schema);
|
|
410
|
+
console.log(`tokens.json + tokens.d.ts + tokens.schema.json written — ${Object.keys(out.core).length} core + ${Object.keys(out.palette).length} palette tokens (v${version})`);
|
|
230
411
|
}
|
package/tokens.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Remarque design tokens — GENERATED from tokens.json by
|
|
3
|
-
* scripts/tokens-json.mjs (v0.
|
|
3
|
+
* scripts/tokens-json.mjs (v0.20.0). Do not edit — the CSS
|
|
4
4
|
* (tokens-core.css + tokens-palette.css) is the source of truth;
|
|
5
5
|
* tokens.json is the intermediate machine-readable form this file is
|
|
6
6
|
* generated from. Regenerate with: node scripts/tokens-json.mjs
|
|
@@ -152,6 +152,7 @@ export interface RemarquePaletteTokenEntry {
|
|
|
152
152
|
* below applying, etc).
|
|
153
153
|
*/
|
|
154
154
|
export interface RemarqueTokensFile {
|
|
155
|
+
readonly $schema: string;
|
|
155
156
|
readonly $description: string;
|
|
156
157
|
readonly $extensions: {
|
|
157
158
|
readonly remarque: {
|
|
@@ -160,6 +161,16 @@ export interface RemarqueTokensFile {
|
|
|
160
161
|
readonly core: string;
|
|
161
162
|
readonly palette: string;
|
|
162
163
|
};
|
|
164
|
+
/** DTCG conformance note (issue #99) — see REMARQUE.md "DTCG Conformance". */
|
|
165
|
+
readonly dtcg: {
|
|
166
|
+
readonly conformance: string;
|
|
167
|
+
readonly divergences: ReadonlyArray<{
|
|
168
|
+
readonly aspect: string;
|
|
169
|
+
readonly detail: string;
|
|
170
|
+
readonly gatedOn: string;
|
|
171
|
+
}>;
|
|
172
|
+
readonly note: string;
|
|
173
|
+
};
|
|
163
174
|
};
|
|
164
175
|
};
|
|
165
176
|
readonly core: { readonly [K in RemarqueCoreToken]: RemarqueCoreTokenEntry };
|
package/tokens.json
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/tokens.schema.json",
|
|
2
3
|
"$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
|
|
3
4
|
"$extensions": {
|
|
4
5
|
"remarque": {
|
|
5
|
-
"version": "0.
|
|
6
|
+
"version": "0.20.0",
|
|
6
7
|
"tiers": {
|
|
7
8
|
"core": "immutable identity — overriding forks the system",
|
|
8
9
|
"palette": "sanctioned personalization surface — override freely, then run remarque-audit"
|
|
10
|
+
},
|
|
11
|
+
"dtcg": {
|
|
12
|
+
"conformance": "partial — $value/$type present on every token (conformant in spirit); two deliberate structural divergences below",
|
|
13
|
+
"divergences": [
|
|
14
|
+
{
|
|
15
|
+
"aspect": "color-value-encoding",
|
|
16
|
+
"detail": "Color $value is an oklch() CSS string, not the DTCG structured color object ({ colorSpace, components, alpha }).",
|
|
17
|
+
"gatedOn": "DTCG color $type structured-value format ratifying"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"aspect": "multi-mode-theming",
|
|
21
|
+
"detail": "Palette-tier tokens nest per-token { light: {$value}, dark: {$value} } groups instead of a single $value plus a modes/resolver mechanism.",
|
|
22
|
+
"gatedOn": "DTCG multi-mode / resolver draft ratifying"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"note": "Deliberate, not oversight — see REMARQUE.md \"DTCG Conformance\" for the argument. Do not \"fix\" these toward the current unratified drafts; re-derive from the CSS via scripts/tokens-json.mjs instead."
|
|
9
26
|
}
|
|
10
27
|
}
|
|
11
28
|
},
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://williamzujkowski.github.io/remarque/tokens.schema.json",
|
|
4
|
+
"title": "Remarque design tokens (tokens.json)",
|
|
5
|
+
"description": "JSON Schema for remarque-tokens' generated tokens.json — GENERATED by scripts/tokens-json.mjs, do not hand-edit. Conformant in spirit with the Design Tokens Community Group format ($value/$type on every token), with two deliberate divergences (oklch()-string color values; per-token light/dark nesting) — see the tokens.json $extensions.remarque.dtcg block and REMARQUE.md \"DTCG Conformance\" for the full rationale and ratification triggers.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"$description",
|
|
10
|
+
"$extensions",
|
|
11
|
+
"core",
|
|
12
|
+
"palette"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"$schema": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"$description": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"$extensions": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": [
|
|
25
|
+
"remarque"
|
|
26
|
+
],
|
|
27
|
+
"properties": {
|
|
28
|
+
"remarque": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": [
|
|
32
|
+
"version",
|
|
33
|
+
"tiers",
|
|
34
|
+
"dtcg"
|
|
35
|
+
],
|
|
36
|
+
"properties": {
|
|
37
|
+
"version": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"tiers": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": [
|
|
44
|
+
"core",
|
|
45
|
+
"palette"
|
|
46
|
+
],
|
|
47
|
+
"properties": {
|
|
48
|
+
"core": {
|
|
49
|
+
"type": "string"
|
|
50
|
+
},
|
|
51
|
+
"palette": {
|
|
52
|
+
"type": "string"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"dtcg": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": [
|
|
60
|
+
"conformance",
|
|
61
|
+
"divergences",
|
|
62
|
+
"note"
|
|
63
|
+
],
|
|
64
|
+
"properties": {
|
|
65
|
+
"conformance": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"divergences": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"items": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"required": [
|
|
74
|
+
"aspect",
|
|
75
|
+
"detail",
|
|
76
|
+
"gatedOn"
|
|
77
|
+
],
|
|
78
|
+
"properties": {
|
|
79
|
+
"aspect": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"detail": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"gatedOn": {
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"note": {
|
|
92
|
+
"type": "string"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"core": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"patternProperties": {
|
|
104
|
+
"^[a-z][a-z0-9-]*$": {
|
|
105
|
+
"$ref": "#/$defs/coreEntry"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"palette": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"additionalProperties": false,
|
|
112
|
+
"patternProperties": {
|
|
113
|
+
"^[a-z][a-z0-9-]*$": {
|
|
114
|
+
"$ref": "#/$defs/paletteEntry"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"$defs": {
|
|
120
|
+
"tokenType": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"enum": [
|
|
123
|
+
"color",
|
|
124
|
+
"dimension",
|
|
125
|
+
"number",
|
|
126
|
+
"fontFamily",
|
|
127
|
+
"fontWeight",
|
|
128
|
+
"duration",
|
|
129
|
+
"string"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"coreEntry": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"additionalProperties": false,
|
|
135
|
+
"required": [
|
|
136
|
+
"$value",
|
|
137
|
+
"$type"
|
|
138
|
+
],
|
|
139
|
+
"properties": {
|
|
140
|
+
"$value": {
|
|
141
|
+
"oneOf": [
|
|
142
|
+
{
|
|
143
|
+
"type": "string"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"type": "number"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
"$type": {
|
|
151
|
+
"$ref": "#/$defs/tokenType"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
"paletteSideValue": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": [
|
|
159
|
+
"$value"
|
|
160
|
+
],
|
|
161
|
+
"properties": {
|
|
162
|
+
"$value": {
|
|
163
|
+
"oneOf": [
|
|
164
|
+
{
|
|
165
|
+
"type": "string"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"type": "number"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
"$extensions": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"additionalProperties": false,
|
|
175
|
+
"properties": {
|
|
176
|
+
"remarque": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"additionalProperties": false,
|
|
179
|
+
"properties": {
|
|
180
|
+
"inheritedFromLight": {
|
|
181
|
+
"type": "boolean"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"paletteEntry": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"additionalProperties": false,
|
|
192
|
+
"required": [
|
|
193
|
+
"$type",
|
|
194
|
+
"light",
|
|
195
|
+
"dark"
|
|
196
|
+
],
|
|
197
|
+
"properties": {
|
|
198
|
+
"$type": {
|
|
199
|
+
"$ref": "#/$defs/tokenType"
|
|
200
|
+
},
|
|
201
|
+
"light": {
|
|
202
|
+
"$ref": "#/$defs/paletteSideValue"
|
|
203
|
+
},
|
|
204
|
+
"dark": {
|
|
205
|
+
"$ref": "#/$defs/paletteSideValue"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|