remarque-tokens 0.22.0 → 0.24.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 +12 -1
- package/CHANGELOG.md +43 -1
- package/README.md +3 -0
- package/REMARQUE.md +279 -4
- package/forms.css +48 -0
- package/package.json +13 -3
- package/registry/broadsheet.json +66 -0
- package/registry/essay.json +56 -0
- package/registry/forms.json +61 -0
- package/registry/palette-deck.json +21 -0
- package/registry-item.schema.json +75 -0
- package/registry.json +44 -0
- package/registry.schema.json +40 -0
- package/scripts/audit.mjs +10 -4
- package/scripts/build-registry.mjs +156 -0
- package/scripts/drift-check.mjs +8 -3
- package/scripts/lib/css-tokens.mjs +71 -0
- package/scripts/lib/registry-extract.mjs +87 -0
- package/scripts/tokens-json.mjs +10 -3
- package/tokens-palette.css +193 -232
- package/tokens.d.ts +1 -1
- package/tokens.json +1 -1
package/AGENT_RULES.md
CHANGED
|
@@ -6,6 +6,17 @@ Read `REMARQUE.md` first for the full system specification. This file tells you
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## Prefer the Registry Over Transcribing Prose
|
|
10
|
+
|
|
11
|
+
For the Essay Module, the Broadsheet pattern, and Forms (`remarque-tokens/essay`, `remarque-tokens/broadsheet`, `remarque-tokens/forms`), a machine-readable markup-contract registry ships alongside the spec (REMARQUE.md "The Registry"; issue #100). **Agents building a Remarque page SHOULD fetch the relevant registry item and apply its `usage.html`/CSS `content` verbatim, rather than re-typing the markup from this file's or REMARQUE.md's prose.**
|
|
12
|
+
|
|
13
|
+
- Fetch `https://williamzujkowski.github.io/remarque/registry/<name>.json` (`essay`, `broadsheet`, `forms`, or `palette-deck`), or read it from an installed package at `node_modules/remarque-tokens/registry/<name>.json` — same content either way.
|
|
14
|
+
- The known-good markup lives in `files[].content` where `type` is `"remarque:markup"`; the CSS module (if any) is in the `"remarque:css"` entry. Both are the exact, versioned, hash-pinned (`integrity`) bytes — copy them, don't re-derive them from memory.
|
|
15
|
+
- This exists specifically to prevent transcription bugs like #89 (a sidenote `aria-label` that drifted out of DOM order because an agent hand-copied ~80 lines of spec prose instead of applying known-good markup). Reading REMARQUE.md's prose for the *reasoning* behind a contract is still expected; reading it as the *source of the markup you type* is the failure mode the registry exists to close.
|
|
16
|
+
- No CLI installer ships this round — there is no `npx` command that writes these files into a project for you. Fetch the JSON, then apply the markup with your own editing tools.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
9
20
|
## Build Order
|
|
10
21
|
|
|
11
22
|
Execute in this exact order. Do not skip steps. Do not reorder.
|
|
@@ -177,7 +188,7 @@ Three further archetypes — Reference/Docs, Changelog, Gallery — are specifie
|
|
|
177
188
|
|
|
178
189
|
2. **Prose alignment:** The essay header and prose body must BOTH use centered max-width (`max-w-reading mx-auto`) to prevent misalignment. The `.content-reading` class auto-centers; match headers with `mx-auto`.
|
|
179
190
|
|
|
180
|
-
3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode.
|
|
191
|
+
3. **Dark mode specificity:** Define `[data-theme="light"]` and `[data-theme="dark"]` overrides in addition to the `:root` defaults. Without an explicit light override, users with `prefers-color-scheme: dark` cannot toggle to light mode. Since 0.24.0 (`light-dark()` + `color-scheme`, REMARQUE.md "Color Scheme & light-dark()"), the shipped default palette's own `[data-theme="light"]` override is just `color-scheme: light` — a consumer palette that still authors the older two-declaration convention needs the fuller override this rule originally describes.
|
|
181
192
|
|
|
182
193
|
4. **Self-hosted fonts:** Replace the Google Fonts CDN link with self-hosted woff2 files + `@font-face` declarations. Use `font-display: swap` and `<link rel="preload">` for performance.
|
|
183
194
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,49 @@ 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.
|
|
7
|
+
## 0.24.0 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
`light-dark()` + `color-scheme` palette migration (closes #95) and a `:has()` zero-JS forms enhancement layer (closes #97) — the ratified program's last two items.
|
|
10
|
+
|
|
11
|
+
### Browser support floor — raised (stated prominently per the new Governance & Deprecation "browser-floor" minor case)
|
|
12
|
+
|
|
13
|
+
`light-dark()` and `color-scheme` are baseline-stable but not ancient: this release raises remarque-tokens' minimum supported browser engine to **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. This is a **MINOR**, not a major — no token is removed or renamed, every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving exactly as before, and `tokens.json`/`tokens.d.ts`/`tokens.schema.json` are byte-identical to 0.23.0 (verified this release, version field aside). Only the rendering environment required to resolve the new CSS *correctly* changes. A site whose audience needs an older engine should stay on `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values (the audit accepts either authoring form). See REMARQUE.md's new "Color Scheme & light-dark()" section and its updated "Semver contract".
|
|
14
|
+
|
|
15
|
+
### Added / Changed
|
|
16
|
+
|
|
17
|
+
- **`tokens-palette.css`: every `--color-*` token is now a single `light-dark(<light>, <dark>)` declaration** under `:root { color-scheme: light dark; }`, replacing the old convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. System-preference default needs no color-specific rule at all now; `[data-theme="light"]`/`[data-theme="dark"]` become single `color-scheme` overrides instead of re-declaring every token's dark value; the `:root.dark` compatibility bridge (ratified sunset: 1.0, unchanged) carries the same minimal `color-scheme: dark` override. Values are unchanged — this is purely an authoring-form migration, golden-gated ΔE2000 = 0.000/0.000 against the previous values on every affected token (see `scripts/palette-golden.mjs` output).
|
|
18
|
+
- **`--weight-display` stays on the pre-migration mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` — now carrying only this one token instead of the whole palette. A registered-custom-property `light-dark()`-abuse workaround was considered and rejected: the conventional blocks are simpler, already audited, and now touch exactly one token — there's no problem left to solve by working around them.
|
|
19
|
+
- **`prefers-contrast: more` (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the bridge) — there is no dark-specific selector left to write.
|
|
20
|
+
- **The demo site's `[data-theme="light"]` mirror block is retired**, not merely updated. It used to hand-carry all 21 `--color-*` values from the package's light `:root` so a dark-OS visitor could still toggle back to light (AGENT_RULES Pitfall #3) — a known drift trap (`scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override shipped IN the package itself; the color values live in exactly one place (the `light-dark()` declaration), so there is no second copy to drift. `site/src/styles/globals.css`'s block shrinks to `[data-theme="light"] { color-scheme: light; }`. `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there, rather than comparing two copies of a value that no longer has a second copy.
|
|
21
|
+
- **Parser (`scripts/lib/css-tokens.mjs`): three new exports** — `splitLightDark(value)` (depth-aware split of a `light-dark(a, b)` value into its light/dark sides — a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for), `resolveSide(decls, side)` (collapses a flat decls map to one theme side; a no-op for any value that isn't a `light-dark()` call, which is what keeps every conventional-form palette parsing identically to before), and `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` (merges `light-dark()` dark sides from the top-level `:root` block with whatever a conventional dark block still declares — mixed-form palettes are supported, with the conventional block winning on conflict, matching real CSS cascade order). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these; none changed their own contrast/golden/drift logic, only how they extract light/dark values first. **This arguably simplifies the parser long-term**, as the issue predicted: both values now come from one declaration instead of being scattered across two selectors.
|
|
22
|
+
- **Fixture coverage (`scripts/test-audit.mjs`):** a pure `light-dark()` palette fixture, a mixed-form palette fixture (half the tokens migrated, half still in the two-block convention, in the same file), and a must-fail fixture where a `light-dark()` value's DARK side (only) fails contrast — proving the parser actually extracts and enforces both sides of a `light-dark()` declaration, not just the light side. Every pre-existing conventional-form fixture (attribute, class, media, palette-deck, nested/compound-media-not-dark, …) stays green, unmodified.
|
|
23
|
+
- **`scripts/theme.mjs` (the `remarque-theme` bridge) is deliberately unchanged** — still emits the conventional two-block form. Its output is a palette override a consumer imports after this package's own tokens; keeping it on the older, universally-parseable convention is the lower-churn choice (every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically). Modernizing the bridge's own emission to `light-dark()` is deferred to a later major.
|
|
24
|
+
- **`forms.css`: `:has()` zero-JS validation enhancement layer (issue #97).** A second progressive-enhancement layer over the existing `@supports selector(:user-invalid)` bonus border-recolor — `:user-invalid` alone can only style the input it matches (CSS has no "parent selector" without `:has()`), so it cannot reach the field's label. `.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label` extends the error recolor to the label; `.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label` brightens the label to `--color-fg` as a quiet secondary focus cue (not a replacement for the control's own native `:focus-visible` outline). Both guarded with `@supports selector(:has(...))`, same fallback shape as `:user-invalid` itself. `data-state` remains the canonical contract; neither block replaces authoring `data-state="error"` explicitly.
|
|
25
|
+
- **Registry regenerated** (`registry/forms.json`) — `forms.css`'s content changed (the new `:has()` blocks), single-sourced verbatim as always; no markup-contract change, so `usage.html` and the mechanical contract checks are untouched.
|
|
26
|
+
- **REMARQUE.md:** new "Color Scheme & light-dark()" section (under "Token Tiers") — the full mechanism, the browser-floor bump, the weight-display/mirror/bridge/parser decisions, and the `prefers-contrast` simplification. "Technology Stack" theming bullet, "DTCG Conformance", "Forced Colors & Contrast Preferences", "Governance & Deprecation" (semver contract's new browser-floor minor case; the `:root.dark` deprecation-table row's rationale), and "Forms" ("State wiring") all updated to match.
|
|
27
|
+
- Version-only regeneration of `tokens.json`/`tokens.d.ts` (no token name/value changes — verified byte-identical aside from the version field).
|
|
28
|
+
|
|
29
|
+
## 0.23.0 — 2026-07-23
|
|
30
|
+
|
|
31
|
+
Machine-readable markup-contract registry (closes #100) — the ratified program's flagship item. Funded on the panel's amended rationale: it mechanically prevents the markup-contract transcription bug class (the sidenote DOM-order/aria-label failure found in the flagship migration, #89), not on "category-defining" positioning.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **`registry.json` + `registry/{essay,broadsheet,forms,palette-deck}.json`** — four items, shaped like shadcn/ui's `registry-item.json`/`registry.json` (`https://ui.shadcn.com/schema/registry-item.json`, `.../registry.json`), adopted where the shape fits and explicitly NOT a parallel invented schema. Per-item: `name`/`type`/`title`/`description`/`dependencies`/`cssVars`/`docs`/`files` kept from shadcn's vocabulary; `type` mints one honest value (`remarque:contract` — none of shadcn's `registry:lib/component/ui/hook/...` describe a CSS+HTML contract with no installable component); `cssVars` is repurposed from "values to merge" to "the flat list of `--custom-property` names this item's CSS reads via `var(...)`, mechanically grepped"; `docs` is repurposed from a markdown blob to a single HTTPS URL into REMARQUE.md. `registry.json` is a lightweight index (pointer + pinned version/hash per item), not shadcn's full-item embedding — full payloads live only in `registry/<name>.json`. Every field-level adaptation and omission (`tailwind`/`css`/`envVars`/`font`/`extends`/`style`/`iconLibrary`/`baseColor`/`theme`/`files[].target`/`meta` — all omitted, none apply) is documented inline in the two vendored schemas and in REMARQUE.md's new "The Registry" section.
|
|
35
|
+
- **Security (panel-mandated, blocking), all enforced by `scripts/test-registry.mjs`:** every item pins a `version` (the exact package release) and an `integrity` hash (`sha256-<base64>`, W3C Subresource Integrity's own shape, computed over `files` at build time and independently recomputed in the test gate — not just carried over); every `docs`/`homepage` URL is `https://`; no item contains executable content anywhere (`scripts/test-registry.mjs` asserts no file's `content`, across every item, contains the substring `<script`). **`deck.js`** — the Palette Deck's runtime module, the one JS asset this package ships — is deliberately EXCLUDED from the registry entirely rather than included-and-hashed: the panel's "no executable content... files are HTML/CSS data" condition reads as absolute, so the honest call is a zero-`<script>`-tag bar with no case-by-case exception. `registry/palette-deck.json` ships only the `data-theme`/`data-palette` HTML fragment, not the FOUC-restore `<script>` sample documented beside it in REMARQUE.md — consumers still get `deck.js` via the ordinary `remarque-tokens/deck` package import.
|
|
36
|
+
- **Single-sourcing.** Every item's CSS `content` is read verbatim from the real `.css` file at build time (`essay.css`/`broadsheet.css`/`forms.css`) — never a second hand-copied literal. Every item's `usage.html` is extracted from REMARQUE.md's own "Markup contract" fenced samples via a `<!-- registry-usage:<name> -->` marker comment (new `scripts/lib/registry-extract.mjs`, `extractUsageHtml`) — the spec prose stays the ONE hand-authored copy; the registry is a derived build artifact, the same relationship `tokens.json` already has to the CSS.
|
|
37
|
+
- **`scripts/build-registry.mjs`** (`--check` for CI freshness, mirroring `scripts/tokens-json.mjs`) generates `registry.json` + `registry/*.json` in one pass from the CSS + REMARQUE.md markers + `package.json`'s version.
|
|
38
|
+
- **`scripts/test-registry.mjs`** (the enforce-don't-instruct gate this issue is funded on) — schema validation (`ajv`, draft 2020-12, with negative fixtures proving the schemas actually reject malformed input) plus mechanical re-derivation of the exact contract lines #89 was about, parsed straight out of each generated `usage.html`: every essay `.remarque-sidenote-ref` carries `aria-label="Note N"` and refs/notes strictly alternate in DOM order; the essay TOC rail carries `aria-label`; every broadsheet `.remarque-entry-numeral` carries `data-entry-number`; every forms `<input id>` has a matching `<label for>` and every `aria-describedby` target id exists in the sample.
|
|
39
|
+
- **Vendored schemas** `registry-item.schema.json` + `registry.schema.json` (draft 2020-12, hand-authored/adapted from shadcn's — see "Added" above for the field-by-field rationale).
|
|
40
|
+
- **CI wiring** (`deploy.yml`): `registry.json`/`registry/*.json` freshness-gated alongside `tokens.json` (`node scripts/build-registry.mjs --check`); `scripts/test-registry.mjs` runs alongside the other fixture suites.
|
|
41
|
+
- **Served by the demo site** (`site/scripts/copy-tokens-json.mjs`, extended): `registry.json`, `registry/*.json`, and both schemas copy into `public/` alongside `tokens.json`, published at `/registry.json`, `/registry/<name>.json`, `/registry-item.schema.json`, `/registry.schema.json`.
|
|
42
|
+
- **npm packaging:** `package.json` `files`/`exports` extended — `remarque-tokens/registry.json`, `remarque-tokens/registry/*` (subpath pattern → `registry/*.json`), `remarque-tokens/registry-item.schema.json`, `remarque-tokens/registry.schema.json`.
|
|
43
|
+
- **Docs.** REMARQUE.md's new "The Registry" section (what it is, the shape adaptation, the security model, the single-sourcing mechanism, what it deliberately is NOT — no CLI installer, no archetype "starter-page" items this round — and the fetch URLs). AGENT_RULES.md's new "Prefer the Registry Over Transcribing Prose" section — agents building Remarque pages should fetch a registry item rather than re-typing markup from spec prose. README's "For AI Agents" section and Files table updated with the registry's live endpoints.
|
|
44
|
+
- **Scope note:** the mandatory four items (essay, broadsheet, forms, palette-deck) are the whole of this release — per-archetype "starter-page" items were floated as an IF-cheap extra and SKIPPED: archetype pages live as demo-specific Astro files, not as a reusable module with one canonical markup sample, so there was nothing to single-source without inventing a new "canonical" sample from scratch.
|
|
45
|
+
- Version-only regeneration of `tokens.json`/`tokens.d.ts` (no CSS/token changes this release).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- **`peerDependencies` range for `@williamzujkowski/oklch-terminal-themes` was stale and broke every real consumer install.** Set to `>=0.1.0 <0.3.0` back when the dataset was at 0.1.0 and never widened as it shipped 0.4.0 (issue #94's dataviz fields) and 0.5.0 (the current devDependency pin) — `npm install remarque-tokens @williamzujkowski/oklch-terminal-themes@0.5.0` failed with `ERESOLVE` for any consumer following the documented "Color Providers" instructions, with no `--legacy-peer-deps` escape hatch offered or implied anywhere in the docs. Invisible from inside this repo because `npm ci` here installs both packages directly as siblings (root devDependency + peerDependency), which never exercises npm's peer-resolution algorithm the way a consumer's plain `npm install` does. Widened to `>=0.1.0 <1.0.0` — the dataset's changes have been strictly additive since 0.1.0 and `remarque-theme`/`remarque-audit` runtime-validate every field they read (see e.g. the dataviz-fallback error path added in 0.22.0), so the honest ceiling is the dataset's own 1.0 breaking-change boundary, not a minor-by-minor cap that recreates this exact break on every upstream release.
|
|
49
|
+
- **New `scripts/test-pack.mjs`** — a real consumer smoke test closing the gap that let the above go unnoticed: `npm pack`s the actual tarball (not the repo checkout), installs it plus the pinned dataset devDependency version into a scratch project with plain `npm install` (deliberately no `--legacy-peer-deps` — the assertion IS that peer resolution succeeds), then runs `npx remarque-theme remarque-light -o out.css` and `npx remarque-audit --palette out.css --src . --json` from that install and asserts `passed: true`. Wired into `deploy.yml` after the other gates (needs registry network access, which the existing devDependency-install step already has).
|
|
8
50
|
|
|
9
51
|
Dataviz categorical tokens — bridge-derived, golden-gated (closes #94). The syntax-palette pattern's fourth application, closing the "Dataviz Tokens" section's long-standing "guidance, not a palette" gap now that the upstream dataset ships a per-theme `dataviz` block (`@williamzujkowski/oklch-terminal-themes` 0.5.0).
|
|
10
52
|
|
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Packaging for agent tooling:
|
|
|
71
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.
|
|
72
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.
|
|
73
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.
|
|
74
|
+
- **Markup-contract registry:** a shadcn-`registry-item.json`-shaped, version-pinned, hash-verified registry of known-good markup for the Essay/Broadsheet/Forms/Palette Deck modules — **https://williamzujkowski.github.io/remarque/registry.json** (index) and **https://williamzujkowski.github.io/remarque/registry/essay.json** (per item; also `broadsheet`/`forms`/`palette-deck`). Fetch and apply the markup instead of transcribing it from spec prose — see REMARQUE.md's "The Registry" and AGENT_RULES.md's "Prefer the Registry Over Transcribing Prose."
|
|
74
75
|
|
|
75
76
|
## Files
|
|
76
77
|
|
|
@@ -86,6 +87,8 @@ Packaging for agent tooling:
|
|
|
86
87
|
| `scripts/drift-check.mjs` | `npx remarque-drift` — token drift check for consumers (`--json` for structured output) |
|
|
87
88
|
| `tokens.json` + `tokens.d.ts` | Generated machine-readable token inventory + TypeScript types (`scripts/tokens-json.mjs`) |
|
|
88
89
|
| `tokens.schema.json` | Generated JSON Schema (draft 2020-12) for `tokens.json`, published alongside it |
|
|
90
|
+
| `registry.json` + `registry/` | Generated markup-contract registry index + per-item files for essay/broadsheet/forms/palette-deck (`scripts/build-registry.mjs`) — see REMARQUE.md "The Registry" |
|
|
91
|
+
| `registry-item.schema.json` + `registry.schema.json` | Generated JSON Schemas (draft 2020-12) for the per-item and index registry files |
|
|
89
92
|
| `fonts.css` + `fonts/` | Self-hosted @font-face declarations and woff2 files (no CDN requests) |
|
|
90
93
|
| `tailwind.config.js` | Tailwind CSS **v3** configuration (v4 projects use an `@theme` block instead) |
|
|
91
94
|
| `package.json` | npm package manifest for `remarque-tokens` |
|
package/REMARQUE.md
CHANGED
|
@@ -43,7 +43,7 @@ Remarque projects should feel like a modern technical publication — not a gene
|
|
|
43
43
|
- **CSS framework:** Tailwind CSS — v4 via the shipped `remarque-tokens/theme.css` (`@theme inline` adapter; utilities reference the runtime tokens), or v3 via the shipped `tailwind.config.js`. One mechanism per project, never both.
|
|
44
44
|
- **Component primitives:** shadcn/ui (when reusable components are needed)
|
|
45
45
|
- **Markup:** Semantic HTML with ARIA landmarks
|
|
46
|
-
- **Theming:** Light and dark mode via the `[data-theme]` attribute (canonical
|
|
46
|
+
- **Theming:** Light and dark mode via `color-scheme` + `light-dark()` (issue #95, 0.24.0) — every `--color-*` token is one `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`; system preference resolves automatically, the `[data-theme]` attribute (canonical) overrides `color-scheme` explicitly, and `:root.dark` is a compatibility bridge for class-keyed sites (sunset: 1.0) carrying the same override. **Raises the package's minimum browser floor** to Chrome/Edge 123+, Firefox 120+, Safari 17.5+ — see "Color Scheme & light-dark()". The audit parses both the new single-declaration form and the older two-declaration convention (mixed within one file, too). Mind the specificity asymmetry on the one remaining non-color theme-conditional token (`--weight-display`): `[data-theme=\"dark\"]` is (0,1,0), `:root.dark` is (0,2,0).
|
|
47
47
|
- **Accessibility:** USWDS-informed. Keyboard navigation, skip-to-content link, ARIA labels, WCAG AA contrast compliance, 44px touch targets, 14px minimum small text
|
|
48
48
|
- **Tokens:** Centralized CSS custom properties in two tiers — `tokens-core.css` (immutable identity) + `tokens-palette.css` (sanctioned personalization). `tokens.css` aggregates both. See "Token Tiers"
|
|
49
49
|
- **Fonts:** Self-hosted woff2 (no CDN dependency). Preloaded via `<link rel="preload">`
|
|
@@ -68,6 +68,31 @@ 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
|
+
### Color Scheme & `light-dark()`
|
|
72
|
+
|
|
73
|
+
**0.24.0, closes #95.** Every `--color-*` token in `tokens-palette.css` is now authored as a single `light-dark(<light>, <dark>)` declaration under `:root { color-scheme: light dark; }`, replacing the previous convention of a full `:root` block plus a full, separately-maintained `@media (prefers-color-scheme: dark)` block re-listing every value a second time. This is the platform mechanism earlier releases of this file named as "the endgame to watch" — now adopted.
|
|
74
|
+
|
|
75
|
+
**Browser support floor — raised, stated prominently per "Governance & Deprecation":** `light-dark()` and `color-scheme` are baseline-stable but not ancient: **Chrome/Edge 123+, Firefox 120+, Safari 17.5+**. A site whose audience needs to support an older engine should stay on remarque-tokens `<0.24.0`, or override the affected `--color-*` tokens with plain `oklch()` values in its own palette (the audit accepts either form — see "How the parser handles this" below). This is the MINOR-with-rationale case the Governance section's semver contract now names explicitly: no token was removed or renamed, and every consumer's `var(--color-x)` still resolves — only the minimum browser engine that renders it *correctly* moved forward, which is why this ships as a minor rather than a major.
|
|
76
|
+
|
|
77
|
+
**How theme selection now works:**
|
|
78
|
+
- **System preference (no explicit choice).** `color-scheme: light dark` on `:root` is sufficient by itself — every `light-dark()` call resolves from `prefers-color-scheme` directly. No `@media (prefers-color-scheme: dark)` color block is needed anymore; it and its full duplicated value list are **retired**.
|
|
79
|
+
- **Explicit user choice.** `[data-theme="light"]`/`[data-theme="dark"]` now override `color-scheme` itself, once — not every token's dark value a second time. `[data-theme="dark"] { color-scheme: dark; }` / `[data-theme="light"] { color-scheme: light; }`.
|
|
80
|
+
- **`:root.dark` compatibility bridge.** Unchanged status — still bridges class-keyed consumer sites, still sunsets at 1.0 (see the Deprecation windows table). It carries the same `color-scheme: dark` override as `[data-theme="dark"]`, kept **minimal** by design: colors need nothing else now that they resolve through `color-scheme`.
|
|
81
|
+
|
|
82
|
+
**`--weight-display` is the one exception — and stays on the old mechanism, on purpose.** `light-dark()` is restricted to `<color>` values; `--weight-display` is a `font-weight` (400/500, dark-mode serif-hairline compensation, issue #59), so it cannot be expressed as `light-dark(400, 500)`. It keeps the pre-migration `@media (prefers-color-scheme: dark)` + `[data-theme="dark"], :root.dark` blocks — now carrying only this one token instead of the whole palette, which is the honest characterization: this isn't a compromise or a leftover, it is genuinely the smallest correct mechanism available for a non-color theme-conditional token. A registered-property `light-dark()`-abuse trick was considered and rejected — the conventional blocks are simpler, already-audited, and touch exactly one token; inventing a workaround to avoid two small blocks would be solving a non-problem.
|
|
83
|
+
|
|
84
|
+
**The `prefers-contrast: more` override (issue #93) simplifies from three blocks to one.** Pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override — three copies of two tokens, to cover system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. Under `light-dark()`, one un-scoped `light-dark(<light>, <dark>)` override per token replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there — there is no dark-specific selector left to write. See `tokens-palette.css`'s `@media (prefers-contrast: more)` block.
|
|
85
|
+
|
|
86
|
+
**The site's `[data-theme="light"]` mirror — retired, not merely updated.** Before this migration, an explicit light choice had no mechanism of its own, so the demo site hand-carried a full `[data-theme="light"]` block re-declaring every `--color-*` value (a known drift trap, guarded by `scripts/palette-golden.mjs`'s "site mirror" check). Explicit light selection is now a bare `color-scheme: light` override — the color values live in exactly one place (the `light-dark()` declaration itself) — so there is no second copy left to drift. The mirror block is deleted from `site/src/styles/globals.css`; `scripts/palette-golden.mjs`'s mirror check now asserts the *absence* of any `--color-*` redeclaration there instead of comparing two copies of a value that no longer has a second copy.
|
|
87
|
+
|
|
88
|
+
**How the parser handles this (`scripts/lib/css-tokens.mjs`).** `splitLightDark(value)` depth-aware-splits a `light-dark(a, b)` value into its light/dark sides (so a side that is itself `oklch(...)` or `var(...)` doesn't confuse the single top-level comma being looked for). `resolveSide(decls, side)` collapses a flat decls map to one theme side — a no-op for any value that isn't a `light-dark()` call, which is what makes this fully backward compatible: a palette using only the old two-block convention parses identically to before. `darkOverridesOf(rawLightRootDecls, rawDarkBlockDecls)` merges the `light-dark()` dark sides found in the top-level `:root` block with whatever a conventional dark block still declares (mixed-form palettes are supported — a consumer may migrate some tokens and leave others in the old convention in the same file), with the conventional-block value winning on conflict — matching real CSS cascade order (a later dark-block declaration overrides an earlier `:root` one at equal specificity). `scripts/audit.mjs`, `scripts/palette-golden.mjs`, `scripts/drift-check.mjs`, and `scripts/tokens-json.mjs` all route through these three functions now; none of them changed their own contrast/golden/drift logic, only how they extract light/dark values first. Fixture-tested in `scripts/test-audit.mjs`: a pure `light-dark()` palette, a mixed-form palette (some tokens migrated, some not), a `light-dark()` value that fails contrast on its dark side, and every pre-existing conventional-form fixture, all green.
|
|
89
|
+
|
|
90
|
+
**`scripts/theme.mjs` (the `remarque-theme` bridge) is intentionally unchanged — still emits the conventional two-block form.** Its output is a *palette override* a consumer imports after this package's own tokens; keeping it on the older, more universally-parseable convention is the lower-churn choice — every consumer regardless of remarque-tokens version can consume it, and the parser above already treats both forms identically. It is not a compromise either: modernizing the bridge's own emission to `light-dark()` is deferred to a later major, noted here so it isn't silently forgotten.
|
|
91
|
+
|
|
92
|
+
**`tokens.json` is unaffected in content.** The authoring-form change is purely how the CSS expresses a value, not what the value or token name is — regenerating `tokens.json`/`tokens.d.ts`/`tokens.schema.json` from the migrated CSS produces byte-identical output (aside from the `version` field), verified in this release.
|
|
93
|
+
|
|
94
|
+
**Visual regression: zero baseline changes.** Every `--color-*` value is unchanged — only the CSS mechanism producing it moved. The Playwright visual-regression suite's screenshot baselines are expected to be pixel-identical; any diff would indicate a transcription error in the migration, not an intended appearance change.
|
|
95
|
+
|
|
71
96
|
### DTCG Conformance
|
|
72
97
|
|
|
73
98
|
`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`).
|
|
@@ -75,7 +100,7 @@ This makes compliance mechanical: a site that overrides only palette-tier tokens
|
|
|
75
100
|
`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
101
|
|
|
77
102
|
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 (
|
|
103
|
+
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 (`light-dark()` + `color-scheme`, `[data-theme="dark"]`, the Palette Deck's `[data-palette]` scoping — see "Color Scheme & light-dark()") predates any such draft. **Gated on:** the DTCG multi-mode / resolver draft ratifying.
|
|
79
104
|
|
|
80
105
|
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
106
|
|
|
@@ -146,6 +171,8 @@ Remarque uses a three-slot font system. Each slot has a strict role, and each sl
|
|
|
146
171
|
|
|
147
172
|
This is why the measure is palette-tier: it is a property of the font choice, not of the system's identity. The *target character count* is the invariant.
|
|
148
173
|
|
|
174
|
+
**Known limitation: Latin script only.** The table above, and the `ch`-based measure math behind it, assume Latin-alphabet conventions — character width roughly proportional to em-height, word-space-separated tokens, left-to-right flow. None of that holds for CJK, Arabic, or Devanagari body text: CJK measure is conventionally counted in full-width characters per line, a different unit than a Latin `ch` entirely; Arabic's contextual letterforms and right-to-left flow change what "character count" even means; Devanagari's conjunct consonants and matras make per-character width too irregular for a single ch-target to describe. Remarque has not derived a measure-compensation row for any non-Latin script — a site setting CJK/Arabic/Devanagari body copy needs to work out its own measure the way this table works out Latin ones; the ch-target does not transfer by assumption. Named here honestly, in the same spirit as this spec's Butterick line-spacing deviation write-up further down, rather than left for a future non-Latin consumer to discover the hard way.
|
|
175
|
+
|
|
149
176
|
---
|
|
150
177
|
|
|
151
178
|
## Editorial Microtypography
|
|
@@ -494,8 +521,9 @@ Issue #93 — Remarque cites [USWDS](https://designsystem.digital.gov/documentat
|
|
|
494
521
|
|
|
495
522
|
**`prefers-contrast: more`.** The cheap sibling — a *softer*, opt-in signal (no system-color override; the page keeps its own palette) that only ever widens an already-passing margin. Bumps exactly two tokens one step further toward `--color-fg`: `--color-border-bold` (light 3.39:1 → 4.52:1, dark 3.23:1 → 4.53:1) and `--color-fg-muted` (light 7.55:1 → 10.11:1, dark 7.26:1 → 9.70:1). `--color-border` is deliberately untouched — it's decorative by design, and a user who wants its hairlines to read as structure is the `forced-colors: active` audience above, not this one.
|
|
496
523
|
|
|
497
|
-
- **Architectural home:** `tokens-palette.css`, immediately after the
|
|
498
|
-
- **
|
|
524
|
+
- **Architectural home:** `tokens-palette.css`, immediately after the theme-selection mechanism — these are `--color-*` values, palette tier by the Token Tiers contract, the same tier the base `light-dark()`/`color-scheme` regime already lives in.
|
|
525
|
+
- **Simplified to one block under `light-dark()` (0.24.0, issue #95):** pre-migration this needed a plain `:root` override, a `[data-theme="dark"], :root.dark` override, and a compound `@media (prefers-contrast: more) and (prefers-color-scheme: dark)` override — three copies of two tokens, covering system-default, explicit-dark, and system-dark-without-an-explicit-choice respectively. A single un-scoped `light-dark(<light>, <dark>)` override per token now replaces its whole value at once and re-resolves per element from whatever `color-scheme` is already active there (system, explicit, or the `:root.dark` bridge) — see "Color Scheme & light-dark()".
|
|
526
|
+
- **Audit-parser compatibility:** `scripts/lib/css-tokens.mjs`'s `isLightRoot` filter requires an empty at-rule context — a `@media (prefers-contrast: more)` block's context is that at-rule itself, so it matches neither `isLightRoot` nor `isDarkBlock`, and `scripts/audit.mjs`/`scripts/tokens-json.mjs` silently skip these declarations, unchanged from before the light-dark() migration. That's still correct: the block only ever strictly improves an already-passing contrast margin, so there is nothing for the automated gate to enforce beyond what the base palette already guarantees. The values are still hand-verified in-gamut and re-computed with the same OKLCH → sRGB → WCAG luminance math `scripts/audit.mjs` uses (see the ratios in `tokens-palette.css`'s comment).
|
|
499
527
|
|
|
500
528
|
**CI gate.** `site/tests/forced-colors.spec.ts` runs Chromium with `page.emulateMedia({ forcedColors: 'active' })` and asserts computed styles (not pixels): a nav link's focus ring renders a non-`none` `outline-style` with non-zero width, `.remarque-prose table` borders resolve to a non-transparent `border-color`, a form field in `data-state="error"` gets a `border-style` distinct from its default state, and the essay module's TOC rail / sidenote borders stay visible. Wired into the existing `visual-regression.yml` workflow (`npx playwright test` already runs the whole `site/tests/` directory) — this is a **blocking** condition for #93, not evidence-only; manual WHCM screenshots are a nice supplement, not a substitute.
|
|
501
529
|
|
|
@@ -528,6 +556,14 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
|
|
|
528
556
|
|
|
529
557
|
Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
|
|
530
558
|
|
|
559
|
+
### APCA: Considered, Not Adopted (2026-07-23)
|
|
560
|
+
|
|
561
|
+
APCA (Accessible Perceptual Contrast Algorithm) was pulled into the WCAG3 working draft in 2023 and remains exploratory — no ratified conformance model specifies it, and that status is unchanged as of this writing. Remarque holds **WCAG 2.x's ratio-based contrast** as the enforcement gate (see "Contrast Ratios" above and the Enforcement Checklist) rather than building the audit toward an unratified algorithm.
|
|
562
|
+
|
|
563
|
+
This is a deliberate policy/data split, not an oversight: the upstream `@williamzujkowski/oklch-terminal-themes` dataset is adding APCA Lc values as *data* on its themes (oklch-terminal-themes#151) — a reasonable thing for a dataset to expose so consumers can experiment. Remarque's own audit gate does not consume it. Data availability and policy adoption are two different questions, and this project is only answering one of them right now.
|
|
564
|
+
|
|
565
|
+
**Revisit trigger:** a ratified WCAG3 conformance model that specifies an actual APCA (or successor) algorithm and threshold set. Until then, this is dated rather than re-litigated on every PR that mentions contrast.
|
|
566
|
+
|
|
531
567
|
---
|
|
532
568
|
|
|
533
569
|
## Lineage: Butterick's Five Rules
|
|
@@ -753,6 +789,10 @@ for the value-by-value mapping).
|
|
|
753
789
|
|
|
754
790
|
### Markup contract
|
|
755
791
|
|
|
792
|
+
<!-- registry-usage:essay — scripts/build-registry.mjs (issue #100) extracts
|
|
793
|
+
the fenced block immediately below into registry/essay.json's
|
|
794
|
+
usage.html verbatim. Keep this the single, hand-authored known-good
|
|
795
|
+
sample — do not fork a second copy for the registry. -->
|
|
756
796
|
```html
|
|
757
797
|
<article class="remarque-essay">
|
|
758
798
|
<header class="content-reading">
|
|
@@ -934,6 +974,9 @@ Four primitives, composed independently — a page can use any subset:
|
|
|
934
974
|
|
|
935
975
|
### Markup contract
|
|
936
976
|
|
|
977
|
+
<!-- registry-usage:broadsheet — scripts/build-registry.mjs (issue #100)
|
|
978
|
+
extracts the fenced block immediately below into
|
|
979
|
+
registry/broadsheet.json's usage.html verbatim. -->
|
|
937
980
|
```html
|
|
938
981
|
<header class="remarque-masthead">
|
|
939
982
|
<p class="remarque-masthead-kicker"><span>Field Notes</span></p>
|
|
@@ -1064,6 +1107,9 @@ existing literal.
|
|
|
1064
1107
|
|
|
1065
1108
|
### Markup contract
|
|
1066
1109
|
|
|
1110
|
+
<!-- registry-usage:forms — scripts/build-registry.mjs (issue #100) extracts
|
|
1111
|
+
the fenced block immediately below into registry/forms.json's
|
|
1112
|
+
usage.html verbatim. -->
|
|
1067
1113
|
```html
|
|
1068
1114
|
<div class="remarque-field">
|
|
1069
1115
|
<label class="remarque-field-label" for="email">Email</label>
|
|
@@ -1129,6 +1175,22 @@ with an invalid field — it never replaces authoring `data-state="error"`
|
|
|
1129
1175
|
explicitly, since `:user-invalid` alone has no matching text-message
|
|
1130
1176
|
recolor and no `aria-invalid` wiring of its own.
|
|
1131
1177
|
|
|
1178
|
+
A second `:has()`-powered bonus layer (issue #97) goes one step further:
|
|
1179
|
+
`:user-invalid` alone can only style the input it matches — CSS has no
|
|
1180
|
+
"parent selector" without `:has()`, so it cannot reach the field's label.
|
|
1181
|
+
`.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label`
|
|
1182
|
+
recolors the label to `--color-error` too, and
|
|
1183
|
+
`.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label`
|
|
1184
|
+
brightens the label to `--color-fg` while any control in the field has
|
|
1185
|
+
keyboard focus — a quiet secondary cue, not a replacement for the
|
|
1186
|
+
control's own native `:focus-visible` outline. Both are guarded with
|
|
1187
|
+
`@supports selector(:has(...))` (the same "the browser lacking this
|
|
1188
|
+
falls back to `data-state`/`aria-invalid`" shape as `:user-invalid`
|
|
1189
|
+
itself) and, like that layer, are bonus visual cues rather than one of
|
|
1190
|
+
the canonical `data-state` states — `data-state` stays the one
|
|
1191
|
+
contract every consumer must author explicitly; `:has()` never replaces
|
|
1192
|
+
it, only adds a zero-JS enhancement on top for browsers that support it.
|
|
1193
|
+
|
|
1132
1194
|
### Restraint rules restated
|
|
1133
1195
|
|
|
1134
1196
|
- **Border radius**: form controls cap at `--radius-sm` (4px) — a
|
|
@@ -1271,6 +1333,15 @@ Set (or clear) `data-palette` on `<html>`, alongside the existing
|
|
|
1271
1333
|
carries *both* light and dark halves, so the light/dark toggle keeps
|
|
1272
1334
|
working no matter which palette (or none) is active:
|
|
1273
1335
|
|
|
1336
|
+
<!-- registry-usage:palette-deck — scripts/build-registry.mjs (issue #100)
|
|
1337
|
+
extracts the fenced block immediately below into
|
|
1338
|
+
registry/palette-deck.json's usage.html verbatim. The FOUC-safe
|
|
1339
|
+
`<script>` snippet a few lines down is deliberately NOT marked for
|
|
1340
|
+
extraction — see REMARQUE.md "The Registry" and the PR description
|
|
1341
|
+
for why (the registry's blocking no-executable-content condition is
|
|
1342
|
+
held to an unambiguous zero-`<script>`-tag bar across every item;
|
|
1343
|
+
that snippet's full text still lives here, one section up, and is
|
|
1344
|
+
linked from the registry item's `docs` URL). -->
|
|
1274
1345
|
```html
|
|
1275
1346
|
<html data-theme="dark" data-palette="gruvbox">
|
|
1276
1347
|
```
|
|
@@ -1323,6 +1394,175 @@ switching between them became the only piece worth a shared module.
|
|
|
1323
1394
|
|
|
1324
1395
|
---
|
|
1325
1396
|
|
|
1397
|
+
## The Registry
|
|
1398
|
+
|
|
1399
|
+
A machine-readable markup-contract registry — `registry.json` (index) +
|
|
1400
|
+
`registry/<name>.json` (four full items: `essay`, `broadsheet`, `forms`,
|
|
1401
|
+
`palette-deck`) — shipped for the ratified program's flagship item
|
|
1402
|
+
(issue #100). The funding rationale is narrow and mechanical, not
|
|
1403
|
+
"category-defining": it exists to prevent the exact bug class found in
|
|
1404
|
+
the flagship migration's sidenote implementation (#89,
|
|
1405
|
+
williamzujkowski.github.io#380) — an aria-label that drifted out of DOM
|
|
1406
|
+
order because the markup contract lived only as ~80 lines of spec prose
|
|
1407
|
+
an agent had to transcribe by hand. A registry item is that same known-
|
|
1408
|
+
good markup as versioned, hash-pinned DATA an agent fetches and applies
|
|
1409
|
+
instead.
|
|
1410
|
+
|
|
1411
|
+
**Shape.** Adopted from shadcn/ui's `registry-item.json`/`registry.json`
|
|
1412
|
+
(`https://ui.shadcn.com/schema/registry-item.json`,
|
|
1413
|
+
`https://ui.shadcn.com/schema/registry.json`) — field NAMES that fit a
|
|
1414
|
+
CSS+HTML contract are kept as-is (`name`, `title`, `description`,
|
|
1415
|
+
`dependencies`, `registryDependencies`, `files[].path`/`content`/`type`,
|
|
1416
|
+
`cssVars`, `docs`); this is deliberately **not** a parallel invented
|
|
1417
|
+
schema. The two vendored, hand-authored schemas
|
|
1418
|
+
(`registry-item.schema.json`, `registry.schema.json` at the package
|
|
1419
|
+
root) document every adaptation and omission inline, in the property
|
|
1420
|
+
descriptions themselves — the summary:
|
|
1421
|
+
|
|
1422
|
+
- **`type` is `"remarque:contract"`**, not one of shadcn's `registry:*`
|
|
1423
|
+
values (`registry:lib`/`component`/`ui`/`hook`/`page`/...). None of
|
|
1424
|
+
those describe a dependency-free CSS module plus its markup sample —
|
|
1425
|
+
every shadcn value assumes an installable React-ecosystem artifact.
|
|
1426
|
+
One honest value covers all four items today, since they're all the
|
|
1427
|
+
same KIND of thing; a second value would only be minted for a future
|
|
1428
|
+
item that is a genuinely different kind of contract.
|
|
1429
|
+
- **`cssVars` is repurposed**: shadcn's `cssVars` holds theme
|
|
1430
|
+
{`theme`/`light`/`dark`} VALUES to merge into a consumer's stylesheet.
|
|
1431
|
+
This registry ships whole CSS files verbatim instead (single-sourced
|
|
1432
|
+
from the real `.css` at build time — see "Single-sourcing" below), so
|
|
1433
|
+
there's nothing to merge; `cssVars` here is the flat, deduped list of
|
|
1434
|
+
`--custom-property` NAMES the item's CSS actually reads via `var(...)`
|
|
1435
|
+
— the palette-tier tokens it consumes, mechanically grepped, never
|
|
1436
|
+
hand-maintained.
|
|
1437
|
+
- **`docs` is a URL, not markdown.** shadcn's `docs` is a documentation
|
|
1438
|
+
blob; this registry's `docs` is a single HTTPS link into this file's
|
|
1439
|
+
relevant section. The registry doesn't duplicate spec prose — it
|
|
1440
|
+
points back at the one place that prose lives.
|
|
1441
|
+
- **Omitted fields** (documented once here, not repeated per-field):
|
|
1442
|
+
`tailwind`/`css`/`envVars` (no Tailwind config, direct CSS injection,
|
|
1443
|
+
or env vars — these items ship whole files, not fragments to splice
|
|
1444
|
+
in), `font`/`extends`/`style`/`iconLibrary`/`baseColor`/`theme` (all
|
|
1445
|
+
`registry:font`/`registry:base`-specific — not applicable to any item
|
|
1446
|
+
here), `files[].target` (no CLI installer — see "What this deliberately
|
|
1447
|
+
is NOT"), `meta` (no per-item metadata that doesn't already have a
|
|
1448
|
+
named field).
|
|
1449
|
+
- **`registry.json` is a lightweight index**, not shadcn's full-item
|
|
1450
|
+
embedding. shadcn's `items` array holds complete registry-item objects
|
|
1451
|
+
inline; this index instead lists one pointer per item (`name`, `type`,
|
|
1452
|
+
`title`, `description`, `version`, `integrity`, `file`) and leaves the
|
|
1453
|
+
full payload — all file content — in `registry/<name>.json` only, so a
|
|
1454
|
+
consumer can discover what exists and check its pinned hash before
|
|
1455
|
+
deciding whether to fetch the full item.
|
|
1456
|
+
|
|
1457
|
+
**Security (panel-mandated, blocking).** Every item carries a `version`
|
|
1458
|
+
(the exact `remarque-tokens` release it was generated from) and an
|
|
1459
|
+
`integrity` hash (`sha256-<base64>`, the same shape as W3C Subresource
|
|
1460
|
+
Integrity — computed over the item's `files` at build time by
|
|
1461
|
+
`scripts/lib/registry-extract.mjs`'s `sha256OfFiles`); `registry.json`'s
|
|
1462
|
+
index mirrors both per item, so a consumer can check the pin before
|
|
1463
|
+
fetching the full payload and must recheck it against what it actually
|
|
1464
|
+
receives. Every `docs`/`homepage` URL is `https://`. No item contains
|
|
1465
|
+
executable content — `scripts/test-registry.mjs` asserts no file's
|
|
1466
|
+
`content`, across every item without exception, contains the substring
|
|
1467
|
+
`<script`.
|
|
1468
|
+
|
|
1469
|
+
That last rule is why `deck.js` — the Palette Deck's runtime module, a
|
|
1470
|
+
dependency-free ~60-line ESM file, and the one JS asset this package
|
|
1471
|
+
ships — is **not embedded in the registry at all**, even hashed and
|
|
1472
|
+
labeled. The panel's condition reads as an absolute ("no executable
|
|
1473
|
+
content in registry items ... files are HTML/CSS data"), and the honest
|
|
1474
|
+
call under a *blocking* security condition is to hold that line without
|
|
1475
|
+
exception rather than carve out a documented-but-included special case.
|
|
1476
|
+
Concretely: `registry/palette-deck.json` ships **only** the
|
|
1477
|
+
`data-theme`/`data-palette` HTML fragment from this file's "Markup/wiring
|
|
1478
|
+
contract" section — it does *not* include the FOUC-safe restore
|
|
1479
|
+
`<script>` sample documented a few lines below that fragment, even
|
|
1480
|
+
though that snippet is (arguably) still "just an HTML example." Holding
|
|
1481
|
+
every item to a zero-`<script>`-tag bar, with no case-by-case judgment
|
|
1482
|
+
call about which scripts "count," is a simpler rule to mechanically
|
|
1483
|
+
enforce forever than "no script tags, except this one, which is fine
|
|
1484
|
+
because—". Consumers keep getting `deck.js` the normal way — the
|
|
1485
|
+
`remarque-tokens/deck` package import, listed as an ordinary
|
|
1486
|
+
`dependencies` entry on every item — and the FOUC snippet's full text
|
|
1487
|
+
from this section, linked via the item's `docs` URL.
|
|
1488
|
+
|
|
1489
|
+
**Modeling the tokens dependency.** essay/broadsheet/forms all consume
|
|
1490
|
+
palette-tier tokens, and the issue asked whether `tokens` should be a
|
|
1491
|
+
fifth registry item or a documented external dependency. It's the
|
|
1492
|
+
latter: every item's `dependencies` array lists `"remarque-tokens"` — an
|
|
1493
|
+
ordinary npm dependency, the same field shadcn uses for npm packages a
|
|
1494
|
+
component needs. Modeling tokens as a *registry item* would mean
|
|
1495
|
+
re-embedding (a subset of, or all of) `tokens.json`'s content a second
|
|
1496
|
+
time in a differently-shaped artifact; `tokens.json`/`tokens.schema.json`
|
|
1497
|
+
already exist as the purpose-built machine-readable form of the tokens
|
|
1498
|
+
(issue #99), served at the same site alongside this registry. Depending
|
|
1499
|
+
on the package that ships them is the smaller, honest mechanism.
|
|
1500
|
+
|
|
1501
|
+
**Single-sourcing.** Every item's CSS `content` is read verbatim from the
|
|
1502
|
+
real `.css` file at build time (`essay.css`, `broadsheet.css`,
|
|
1503
|
+
`forms.css`) — never a second hand-copied literal. Every item's
|
|
1504
|
+
`usage.html` is extracted from THIS file's own "Markup contract" fenced
|
|
1505
|
+
```html blocks via an HTML-comment marker
|
|
1506
|
+
(`<!-- registry-usage:<name> -->`) placed immediately before the fence —
|
|
1507
|
+
`scripts/lib/registry-extract.mjs`'s `extractUsageHtml` finds the marker,
|
|
1508
|
+
then the next fenced block, and lifts its content verbatim. This spec's
|
|
1509
|
+
prose stays the ONE hand-authored copy of each known-good sample; the
|
|
1510
|
+
registry is a build artifact derived from it, exactly like `tokens.json`
|
|
1511
|
+
is derived from the CSS. `scripts/build-registry.mjs --check` gates
|
|
1512
|
+
freshness in CI the same way `scripts/tokens-json.mjs --check` already
|
|
1513
|
+
does for tokens.
|
|
1514
|
+
|
|
1515
|
+
**Validation (the enforce-don't-instruct house pattern).**
|
|
1516
|
+
`scripts/test-registry.mjs` is the CI gate, wired alongside the other
|
|
1517
|
+
freshness/fixture checks in `deploy.yml`. It re-derives, mechanically,
|
|
1518
|
+
the exact contract lines this issue is funded on — not by trusting the
|
|
1519
|
+
prose, by parsing the generated `usage.html` itself:
|
|
1520
|
+
|
|
1521
|
+
- Every essay `.remarque-sidenote-ref` carries `aria-label="Note N"`, and
|
|
1522
|
+
refs/notes strictly alternate in DOM order — the precise shape of the
|
|
1523
|
+
#89 bug.
|
|
1524
|
+
- The essay TOC rail (`.remarque-toc-rail`) carries `aria-label`.
|
|
1525
|
+
- Every broadsheet `.remarque-entry-numeral` carries `data-entry-number`.
|
|
1526
|
+
- Every forms `<input id="...">` has a matching `<label for="...">`, and
|
|
1527
|
+
every `aria-describedby` target id exists in the sample.
|
|
1528
|
+
- No file, in any item, contains `<script` (the no-executable-content
|
|
1529
|
+
rule above).
|
|
1530
|
+
|
|
1531
|
+
Plus the structural gates any generated artifact needs: schema
|
|
1532
|
+
validation (via `ajv`, the same devDependency `tokens.schema.json`
|
|
1533
|
+
already uses) with negative fixtures proving the schemas actually reject
|
|
1534
|
+
malformed input, and an independent recomputation of every `integrity`
|
|
1535
|
+
hash proving it wasn't just carried over from a stale build.
|
|
1536
|
+
|
|
1537
|
+
**What this deliberately is NOT.** No CLI installer — there is no
|
|
1538
|
+
`npx remarque-registry add essay` command, and no `files[].target`
|
|
1539
|
+
field to resolve an install path against. An agent's expected workflow
|
|
1540
|
+
is fetch the JSON, read `usage.html`/the CSS `content`, and apply it with
|
|
1541
|
+
its own editing tools — the registry replaces transcription-from-prose,
|
|
1542
|
+
not an installer. No archetype "starter-page" items this round: the
|
|
1543
|
+
issue floated one per archetype (Essay, Dossier, Notebook, ...) as an
|
|
1544
|
+
IF-cheap extra, and the honest call is SKIP — archetype pages live as
|
|
1545
|
+
demo-specific Astro files (`site/src/pages/**`), not as a reusable
|
|
1546
|
+
module with one canonical markup sample the way the four shipped items
|
|
1547
|
+
are, so extracting one would mean inventing a new "canonical" sample
|
|
1548
|
+
rather than single-sourcing an existing one.
|
|
1549
|
+
|
|
1550
|
+
**Fetching it.** The demo site serves the registry the same way it
|
|
1551
|
+
serves `tokens.json` (`site/scripts/copy-tokens-json.mjs`, extended):
|
|
1552
|
+
|
|
1553
|
+
- Index: **https://williamzujkowski.github.io/remarque/registry.json**
|
|
1554
|
+
- Per-item: **https://williamzujkowski.github.io/remarque/registry/essay.json**
|
|
1555
|
+
(and `broadsheet.json`, `forms.json`, `palette-deck.json`)
|
|
1556
|
+
- Schemas: **https://williamzujkowski.github.io/remarque/registry-item.schema.json**,
|
|
1557
|
+
**https://williamzujkowski.github.io/remarque/registry.schema.json**
|
|
1558
|
+
|
|
1559
|
+
npm package exports mirror the same paths: `remarque-tokens/registry.json`,
|
|
1560
|
+
`remarque-tokens/registry/essay` (the `./registry/*` subpath maps to
|
|
1561
|
+
`./registry/*.json`), `remarque-tokens/registry-item.schema.json`,
|
|
1562
|
+
`remarque-tokens/registry.schema.json`.
|
|
1563
|
+
|
|
1564
|
+
---
|
|
1565
|
+
|
|
1326
1566
|
## Signature Moves
|
|
1327
1567
|
|
|
1328
1568
|
These are the repeatable visual tells that make a Remarque site recognizable:
|
|
@@ -1353,6 +1593,41 @@ An implementation succeeds when:
|
|
|
1353
1593
|
|
|
1354
1594
|
---
|
|
1355
1595
|
|
|
1596
|
+
## Governance & Deprecation
|
|
1597
|
+
|
|
1598
|
+
Remarque is small enough that governance fits on a page, but it now ships as a published npm package with multiple consuming sites and a CI gate other repos call into — enough surface that the rules deserve writing down rather than living only in maintainer habit. The precedent here is deliberately modest: closer to how a single-maintainer Carbon or Polaris component documents its own contract than to either project's full RFC process.
|
|
1599
|
+
|
|
1600
|
+
### Semver contract
|
|
1601
|
+
|
|
1602
|
+
Remarque ships as a CSS token system, not an application — "breaking" means something different than it does for a library with a JS API surface:
|
|
1603
|
+
|
|
1604
|
+
- **MAJOR** — a token is **removed** or **renamed**. Anything a consumer's `var(--color-x)` or a `tokens.json` lookup resolves today must keep resolving after a minor or patch bump; only a major bump may take that away.
|
|
1605
|
+
- **MINOR** — a token's **value changes**, or a token is **added**. Value changes always carry a stated rationale in the CHANGELOG entry (a contrast fix, a gamut correction, an upstream dataset repin) — the change is minor because the *name* stays stable and every consumer reads it through `var()` indirection, but it is never silent. Additive changes (new tokens, new optional CLI flags, new subpath exports) are minor by definition — nothing existing changes shape.
|
|
1606
|
+
- **PATCH** — tool/script fixes with no token or CSS output change (a `remarque-audit` parser bug, a `tokens-json.mjs` generation fix, a fixture correction). If `tokens.json`/`tokens.d.ts`'s content is byte-identical before and after, it's a patch.
|
|
1607
|
+
- **MINOR (browser-floor case)** — a change that raises the *minimum supported browser version* by adopting a newer CSS mechanism as load-bearing (0.24.0's `light-dark()`/`color-scheme` migration, issue #95, is the first instance). Still a minor, not a major: no token is removed or renamed, and every consumer's `var(--color-x)`/`tokens.json` lookup keeps resolving — only the rendering environment required to resolve it *correctly* changes. The new floor is stated prominently in both the CHANGELOG entry and the relevant REMARQUE.md section, not buried in prose.
|
|
1608
|
+
|
|
1609
|
+
The project is still pre-1.0 — under strict SemVer every 0.x release is technically allowed to break anything — but every release to date has in practice held to the contract above (no token has ever been removed or renamed; see the version history in CHANGELOG.md). 1.0 is the point where that becomes a promise instead of an observed pattern.
|
|
1610
|
+
|
|
1611
|
+
### Deprecation windows
|
|
1612
|
+
|
|
1613
|
+
A token or convention slated for removal is announced in the CHANGELOG at least one MINOR release before it disappears, named explicitly with a target version — not left implicit in a commit message. The table below is the centralized registry of every open sunset commitment; a PR that adds a new one should add a row here in the same commit, not just mention it once in the CHANGELOG.
|
|
1614
|
+
|
|
1615
|
+
| Deprecated / bridged item | Introduced | Sunset target | Rationale |
|
|
1616
|
+
|---|---|---|---|
|
|
1617
|
+
| `:root.dark` compatibility selector (alongside canonical `[data-theme="dark"]`) | 0.5.0 | 1.0 | Bridges class-keyed consumer sites onto the attribute-keyed convention `remarque-audit` treats as canonical — the audit already parses both forms, so the bridge costs nothing today. `color-scheme` + `light-dark()` — once "the platform endgame to watch" — landed in 0.24.0 (see "Color Scheme & light-dark()"); the bridge itself is unaffected, carrying a `color-scheme: dark` override now instead of a full color list, and its sunset target is unchanged. |
|
|
1618
|
+
|
|
1619
|
+
No other open sunset commitments exist as of this writing (grepped against REMARQUE.md and CHANGELOG.md for "sunset"/"deprecated"). This table is meant to be the single place to look — but only if every future commitment is added here, not just announced in passing.
|
|
1620
|
+
|
|
1621
|
+
### Decision-log habit
|
|
1622
|
+
|
|
1623
|
+
Every design decision with more than one reasonable answer — a token value, a deviation from a cited precedent (Butterick, Carbon, USWDS), a scope change to an in-flight issue — gets a recorded vote, not just an implementation. The convention has been practiced since 0.5.0; this section states it as policy rather than leaving it implicit:
|
|
1624
|
+
|
|
1625
|
+
- The CHANGELOG entry for the release states the vote count (e.g. "ratified 3-0 by consensus panel") and, when the outcome wasn't unanimous or shipped with conditions, what those conditions were.
|
|
1626
|
+
- REMARQUE.md carries the same treatment inline wherever a deviation is asserted against upstream guidance — see "Lineage: Butterick's Five Rules" below for the fullest example: the guidance, the deviation, the argued rationale, and the ratification line together.
|
|
1627
|
+
- The habit exists so a future maintainer — human or agent — can tell the difference between "nobody thought about this" and "this was considered and decided," without reconstructing the reasoning from a diff.
|
|
1628
|
+
|
|
1629
|
+
---
|
|
1630
|
+
|
|
1356
1631
|
## What Remarque Is Named For
|
|
1357
1632
|
|
|
1358
1633
|
In fine art printmaking, a *remarque* is a small original drawing made by the artist in the margin of a print. Found only in limited proof editions, the remarque is the mark that says: this was made by a specific person, with intention, by hand.
|
package/forms.css
CHANGED
|
@@ -358,6 +358,54 @@ textarea.remarque-input {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/* ─── `:has()` zero-JS validation enhancement layer (issue #97) ──────────
|
|
362
|
+
* A second, purely additive progressive-enhancement layer over the
|
|
363
|
+
* `:user-invalid` layer just above — same native-first shape, one step
|
|
364
|
+
* further. `:user-invalid` alone can only style the INPUT it matches;
|
|
365
|
+
* CSS has no "parent selector" without `:has()`, so it has no way to
|
|
366
|
+
* reach the field's label or wrapper. `:has()` is exactly the piece that
|
|
367
|
+
* closes that gap: `.remarque-field` can react to a descendant control's
|
|
368
|
+
* validity/focus state directly, with no `data-state` attribute and no
|
|
369
|
+
* JavaScript. Guarded with `@supports selector(:has(...))` — the same
|
|
370
|
+
* "degrades to the no-JS/no-support baseline" shape the `:user-invalid`
|
|
371
|
+
* block above uses — so a browser missing either pseudo-class in the
|
|
372
|
+
* compound query falls back cleanly to whatever `data-state`/
|
|
373
|
+
* `aria-invalid` (and, where supported, the plain `:user-invalid` layer
|
|
374
|
+
* above) already provide. `data-state` remains the CANONICAL contract;
|
|
375
|
+
* neither block below replaces authoring `data-state="error"` explicitly
|
|
376
|
+
* — see REMARQUE.md "State wiring".
|
|
377
|
+
*
|
|
378
|
+
* Like the bare `:user-invalid` layer above, this is a bonus visual cue,
|
|
379
|
+
* not one of the canonical `data-state` states the forced-colors
|
|
380
|
+
* reinforcement below was scoped to fix (REMARQUE.md "Forced Colors &
|
|
381
|
+
* Contrast Preferences") — it is not itself a new color-only-affordance
|
|
382
|
+
* regression, since the real accessible signal (`aria-invalid`, the
|
|
383
|
+
* `data-state` border/message wiring) is unchanged and unaffected by
|
|
384
|
+
* whether this bonus layer's color survives forced-colors mode.
|
|
385
|
+
*/
|
|
386
|
+
@supports selector(:has(:user-invalid)) {
|
|
387
|
+
/* Extends the :user-invalid border recolor (input only, above) up to
|
|
388
|
+
the field's label — the one thing plain :user-invalid cannot reach
|
|
389
|
+
on its own. */
|
|
390
|
+
.remarque-field:has(.remarque-input:user-invalid) .remarque-field-label {
|
|
391
|
+
color: var(--color-error);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Field-level focus affordance — a quiet secondary cue, NOT a
|
|
396
|
+
replacement for the control's own native `:focus-visible` outline
|
|
397
|
+
(tokens-core.css), which already renders on the control itself and is
|
|
398
|
+
forced-colors-safe by construction. Brightens the label from
|
|
399
|
+
`--color-muted` to `--color-fg` while any control inside the field has
|
|
400
|
+
keyboard focus — a restrained color wash, not a new border/background,
|
|
401
|
+
matching this module's "no component should be visually louder than
|
|
402
|
+
the content it contains" rule (AGENT_RULES.md Components). */
|
|
403
|
+
@supports selector(:has(:focus-visible)) {
|
|
404
|
+
.remarque-field:has(.remarque-input:focus-visible) .remarque-field-label {
|
|
405
|
+
color: var(--color-fg);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
361
409
|
/* ─── Forced-colors reinforcement (issue #93) ────────────────────────────
|
|
362
410
|
* The validation-state wiring above is the textbook color-only-affordance
|
|
363
411
|
* case the issue names explicitly: error/success/warning are distinguished
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remarque-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=18"
|
|
6
6
|
},
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"culori": "^4.0.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@williamzujkowski/oklch-terminal-themes": ">=0.1.0 <0.
|
|
40
|
+
"@williamzujkowski/oklch-terminal-themes": ">=0.1.0 <1.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@williamzujkowski/oklch-terminal-themes": {
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"./deck.js": "./deck.js",
|
|
81
81
|
"./agent-rules": "./AGENT_RULES.md",
|
|
82
82
|
"./spec": "./REMARQUE.md",
|
|
83
|
+
"./registry.json": "./registry.json",
|
|
84
|
+
"./registry-item.schema.json": "./registry-item.schema.json",
|
|
85
|
+
"./registry.schema.json": "./registry.schema.json",
|
|
86
|
+
"./registry/*": "./registry/*.json",
|
|
83
87
|
"./package.json": "./package.json"
|
|
84
88
|
},
|
|
85
89
|
"files": [
|
|
@@ -107,6 +111,12 @@
|
|
|
107
111
|
"broadsheet.css",
|
|
108
112
|
"forms.css",
|
|
109
113
|
"deck.js",
|
|
110
|
-
"scripts/lib/css-tokens.mjs"
|
|
114
|
+
"scripts/lib/css-tokens.mjs",
|
|
115
|
+
"registry.json",
|
|
116
|
+
"registry/",
|
|
117
|
+
"registry-item.schema.json",
|
|
118
|
+
"registry.schema.json",
|
|
119
|
+
"scripts/build-registry.mjs",
|
|
120
|
+
"scripts/lib/registry-extract.mjs"
|
|
111
121
|
]
|
|
112
122
|
}
|