remarque-tokens 0.22.0 → 0.23.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 +11 -0
- package/CHANGELOG.md +21 -1
- package/README.md +3 -0
- package/REMARQUE.md +232 -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/build-registry.mjs +156 -0
- package/scripts/lib/registry-extract.mjs +87 -0
- 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.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,27 @@ 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.23.0 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **`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.
|
|
16
|
+
- **`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.
|
|
17
|
+
- **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).
|
|
18
|
+
- **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.
|
|
19
|
+
- **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`.
|
|
20
|
+
- **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`.
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- Version-only regeneration of `tokens.json`/`tokens.d.ts` (no CSS/token changes this release).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **`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.
|
|
27
|
+
- **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
28
|
|
|
9
29
|
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
30
|
|
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
|
@@ -146,6 +146,8 @@ Remarque uses a three-slot font system. Each slot has a strict role, and each sl
|
|
|
146
146
|
|
|
147
147
|
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
148
|
|
|
149
|
+
**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.
|
|
150
|
+
|
|
149
151
|
---
|
|
150
152
|
|
|
151
153
|
## Editorial Microtypography
|
|
@@ -528,6 +530,14 @@ Every PR that ships Remarque pages MUST pass (`npm run audit` automates the colo
|
|
|
528
530
|
|
|
529
531
|
Agents reviewing PRs should reject changes that violate any line above without explicit rationale.
|
|
530
532
|
|
|
533
|
+
### APCA: Considered, Not Adopted (2026-07-23)
|
|
534
|
+
|
|
535
|
+
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.
|
|
536
|
+
|
|
537
|
+
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.
|
|
538
|
+
|
|
539
|
+
**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.
|
|
540
|
+
|
|
531
541
|
---
|
|
532
542
|
|
|
533
543
|
## Lineage: Butterick's Five Rules
|
|
@@ -753,6 +763,10 @@ for the value-by-value mapping).
|
|
|
753
763
|
|
|
754
764
|
### Markup contract
|
|
755
765
|
|
|
766
|
+
<!-- registry-usage:essay — scripts/build-registry.mjs (issue #100) extracts
|
|
767
|
+
the fenced block immediately below into registry/essay.json's
|
|
768
|
+
usage.html verbatim. Keep this the single, hand-authored known-good
|
|
769
|
+
sample — do not fork a second copy for the registry. -->
|
|
756
770
|
```html
|
|
757
771
|
<article class="remarque-essay">
|
|
758
772
|
<header class="content-reading">
|
|
@@ -934,6 +948,9 @@ Four primitives, composed independently — a page can use any subset:
|
|
|
934
948
|
|
|
935
949
|
### Markup contract
|
|
936
950
|
|
|
951
|
+
<!-- registry-usage:broadsheet — scripts/build-registry.mjs (issue #100)
|
|
952
|
+
extracts the fenced block immediately below into
|
|
953
|
+
registry/broadsheet.json's usage.html verbatim. -->
|
|
937
954
|
```html
|
|
938
955
|
<header class="remarque-masthead">
|
|
939
956
|
<p class="remarque-masthead-kicker"><span>Field Notes</span></p>
|
|
@@ -1064,6 +1081,9 @@ existing literal.
|
|
|
1064
1081
|
|
|
1065
1082
|
### Markup contract
|
|
1066
1083
|
|
|
1084
|
+
<!-- registry-usage:forms — scripts/build-registry.mjs (issue #100) extracts
|
|
1085
|
+
the fenced block immediately below into registry/forms.json's
|
|
1086
|
+
usage.html verbatim. -->
|
|
1067
1087
|
```html
|
|
1068
1088
|
<div class="remarque-field">
|
|
1069
1089
|
<label class="remarque-field-label" for="email">Email</label>
|
|
@@ -1271,6 +1291,15 @@ Set (or clear) `data-palette` on `<html>`, alongside the existing
|
|
|
1271
1291
|
carries *both* light and dark halves, so the light/dark toggle keeps
|
|
1272
1292
|
working no matter which palette (or none) is active:
|
|
1273
1293
|
|
|
1294
|
+
<!-- registry-usage:palette-deck — scripts/build-registry.mjs (issue #100)
|
|
1295
|
+
extracts the fenced block immediately below into
|
|
1296
|
+
registry/palette-deck.json's usage.html verbatim. The FOUC-safe
|
|
1297
|
+
`<script>` snippet a few lines down is deliberately NOT marked for
|
|
1298
|
+
extraction — see REMARQUE.md "The Registry" and the PR description
|
|
1299
|
+
for why (the registry's blocking no-executable-content condition is
|
|
1300
|
+
held to an unambiguous zero-`<script>`-tag bar across every item;
|
|
1301
|
+
that snippet's full text still lives here, one section up, and is
|
|
1302
|
+
linked from the registry item's `docs` URL). -->
|
|
1274
1303
|
```html
|
|
1275
1304
|
<html data-theme="dark" data-palette="gruvbox">
|
|
1276
1305
|
```
|
|
@@ -1323,6 +1352,175 @@ switching between them became the only piece worth a shared module.
|
|
|
1323
1352
|
|
|
1324
1353
|
---
|
|
1325
1354
|
|
|
1355
|
+
## The Registry
|
|
1356
|
+
|
|
1357
|
+
A machine-readable markup-contract registry — `registry.json` (index) +
|
|
1358
|
+
`registry/<name>.json` (four full items: `essay`, `broadsheet`, `forms`,
|
|
1359
|
+
`palette-deck`) — shipped for the ratified program's flagship item
|
|
1360
|
+
(issue #100). The funding rationale is narrow and mechanical, not
|
|
1361
|
+
"category-defining": it exists to prevent the exact bug class found in
|
|
1362
|
+
the flagship migration's sidenote implementation (#89,
|
|
1363
|
+
williamzujkowski.github.io#380) — an aria-label that drifted out of DOM
|
|
1364
|
+
order because the markup contract lived only as ~80 lines of spec prose
|
|
1365
|
+
an agent had to transcribe by hand. A registry item is that same known-
|
|
1366
|
+
good markup as versioned, hash-pinned DATA an agent fetches and applies
|
|
1367
|
+
instead.
|
|
1368
|
+
|
|
1369
|
+
**Shape.** Adopted from shadcn/ui's `registry-item.json`/`registry.json`
|
|
1370
|
+
(`https://ui.shadcn.com/schema/registry-item.json`,
|
|
1371
|
+
`https://ui.shadcn.com/schema/registry.json`) — field NAMES that fit a
|
|
1372
|
+
CSS+HTML contract are kept as-is (`name`, `title`, `description`,
|
|
1373
|
+
`dependencies`, `registryDependencies`, `files[].path`/`content`/`type`,
|
|
1374
|
+
`cssVars`, `docs`); this is deliberately **not** a parallel invented
|
|
1375
|
+
schema. The two vendored, hand-authored schemas
|
|
1376
|
+
(`registry-item.schema.json`, `registry.schema.json` at the package
|
|
1377
|
+
root) document every adaptation and omission inline, in the property
|
|
1378
|
+
descriptions themselves — the summary:
|
|
1379
|
+
|
|
1380
|
+
- **`type` is `"remarque:contract"`**, not one of shadcn's `registry:*`
|
|
1381
|
+
values (`registry:lib`/`component`/`ui`/`hook`/`page`/...). None of
|
|
1382
|
+
those describe a dependency-free CSS module plus its markup sample —
|
|
1383
|
+
every shadcn value assumes an installable React-ecosystem artifact.
|
|
1384
|
+
One honest value covers all four items today, since they're all the
|
|
1385
|
+
same KIND of thing; a second value would only be minted for a future
|
|
1386
|
+
item that is a genuinely different kind of contract.
|
|
1387
|
+
- **`cssVars` is repurposed**: shadcn's `cssVars` holds theme
|
|
1388
|
+
{`theme`/`light`/`dark`} VALUES to merge into a consumer's stylesheet.
|
|
1389
|
+
This registry ships whole CSS files verbatim instead (single-sourced
|
|
1390
|
+
from the real `.css` at build time — see "Single-sourcing" below), so
|
|
1391
|
+
there's nothing to merge; `cssVars` here is the flat, deduped list of
|
|
1392
|
+
`--custom-property` NAMES the item's CSS actually reads via `var(...)`
|
|
1393
|
+
— the palette-tier tokens it consumes, mechanically grepped, never
|
|
1394
|
+
hand-maintained.
|
|
1395
|
+
- **`docs` is a URL, not markdown.** shadcn's `docs` is a documentation
|
|
1396
|
+
blob; this registry's `docs` is a single HTTPS link into this file's
|
|
1397
|
+
relevant section. The registry doesn't duplicate spec prose — it
|
|
1398
|
+
points back at the one place that prose lives.
|
|
1399
|
+
- **Omitted fields** (documented once here, not repeated per-field):
|
|
1400
|
+
`tailwind`/`css`/`envVars` (no Tailwind config, direct CSS injection,
|
|
1401
|
+
or env vars — these items ship whole files, not fragments to splice
|
|
1402
|
+
in), `font`/`extends`/`style`/`iconLibrary`/`baseColor`/`theme` (all
|
|
1403
|
+
`registry:font`/`registry:base`-specific — not applicable to any item
|
|
1404
|
+
here), `files[].target` (no CLI installer — see "What this deliberately
|
|
1405
|
+
is NOT"), `meta` (no per-item metadata that doesn't already have a
|
|
1406
|
+
named field).
|
|
1407
|
+
- **`registry.json` is a lightweight index**, not shadcn's full-item
|
|
1408
|
+
embedding. shadcn's `items` array holds complete registry-item objects
|
|
1409
|
+
inline; this index instead lists one pointer per item (`name`, `type`,
|
|
1410
|
+
`title`, `description`, `version`, `integrity`, `file`) and leaves the
|
|
1411
|
+
full payload — all file content — in `registry/<name>.json` only, so a
|
|
1412
|
+
consumer can discover what exists and check its pinned hash before
|
|
1413
|
+
deciding whether to fetch the full item.
|
|
1414
|
+
|
|
1415
|
+
**Security (panel-mandated, blocking).** Every item carries a `version`
|
|
1416
|
+
(the exact `remarque-tokens` release it was generated from) and an
|
|
1417
|
+
`integrity` hash (`sha256-<base64>`, the same shape as W3C Subresource
|
|
1418
|
+
Integrity — computed over the item's `files` at build time by
|
|
1419
|
+
`scripts/lib/registry-extract.mjs`'s `sha256OfFiles`); `registry.json`'s
|
|
1420
|
+
index mirrors both per item, so a consumer can check the pin before
|
|
1421
|
+
fetching the full payload and must recheck it against what it actually
|
|
1422
|
+
receives. Every `docs`/`homepage` URL is `https://`. No item contains
|
|
1423
|
+
executable content — `scripts/test-registry.mjs` asserts no file's
|
|
1424
|
+
`content`, across every item without exception, contains the substring
|
|
1425
|
+
`<script`.
|
|
1426
|
+
|
|
1427
|
+
That last rule is why `deck.js` — the Palette Deck's runtime module, a
|
|
1428
|
+
dependency-free ~60-line ESM file, and the one JS asset this package
|
|
1429
|
+
ships — is **not embedded in the registry at all**, even hashed and
|
|
1430
|
+
labeled. The panel's condition reads as an absolute ("no executable
|
|
1431
|
+
content in registry items ... files are HTML/CSS data"), and the honest
|
|
1432
|
+
call under a *blocking* security condition is to hold that line without
|
|
1433
|
+
exception rather than carve out a documented-but-included special case.
|
|
1434
|
+
Concretely: `registry/palette-deck.json` ships **only** the
|
|
1435
|
+
`data-theme`/`data-palette` HTML fragment from this file's "Markup/wiring
|
|
1436
|
+
contract" section — it does *not* include the FOUC-safe restore
|
|
1437
|
+
`<script>` sample documented a few lines below that fragment, even
|
|
1438
|
+
though that snippet is (arguably) still "just an HTML example." Holding
|
|
1439
|
+
every item to a zero-`<script>`-tag bar, with no case-by-case judgment
|
|
1440
|
+
call about which scripts "count," is a simpler rule to mechanically
|
|
1441
|
+
enforce forever than "no script tags, except this one, which is fine
|
|
1442
|
+
because—". Consumers keep getting `deck.js` the normal way — the
|
|
1443
|
+
`remarque-tokens/deck` package import, listed as an ordinary
|
|
1444
|
+
`dependencies` entry on every item — and the FOUC snippet's full text
|
|
1445
|
+
from this section, linked via the item's `docs` URL.
|
|
1446
|
+
|
|
1447
|
+
**Modeling the tokens dependency.** essay/broadsheet/forms all consume
|
|
1448
|
+
palette-tier tokens, and the issue asked whether `tokens` should be a
|
|
1449
|
+
fifth registry item or a documented external dependency. It's the
|
|
1450
|
+
latter: every item's `dependencies` array lists `"remarque-tokens"` — an
|
|
1451
|
+
ordinary npm dependency, the same field shadcn uses for npm packages a
|
|
1452
|
+
component needs. Modeling tokens as a *registry item* would mean
|
|
1453
|
+
re-embedding (a subset of, or all of) `tokens.json`'s content a second
|
|
1454
|
+
time in a differently-shaped artifact; `tokens.json`/`tokens.schema.json`
|
|
1455
|
+
already exist as the purpose-built machine-readable form of the tokens
|
|
1456
|
+
(issue #99), served at the same site alongside this registry. Depending
|
|
1457
|
+
on the package that ships them is the smaller, honest mechanism.
|
|
1458
|
+
|
|
1459
|
+
**Single-sourcing.** Every item's CSS `content` is read verbatim from the
|
|
1460
|
+
real `.css` file at build time (`essay.css`, `broadsheet.css`,
|
|
1461
|
+
`forms.css`) — never a second hand-copied literal. Every item's
|
|
1462
|
+
`usage.html` is extracted from THIS file's own "Markup contract" fenced
|
|
1463
|
+
```html blocks via an HTML-comment marker
|
|
1464
|
+
(`<!-- registry-usage:<name> -->`) placed immediately before the fence —
|
|
1465
|
+
`scripts/lib/registry-extract.mjs`'s `extractUsageHtml` finds the marker,
|
|
1466
|
+
then the next fenced block, and lifts its content verbatim. This spec's
|
|
1467
|
+
prose stays the ONE hand-authored copy of each known-good sample; the
|
|
1468
|
+
registry is a build artifact derived from it, exactly like `tokens.json`
|
|
1469
|
+
is derived from the CSS. `scripts/build-registry.mjs --check` gates
|
|
1470
|
+
freshness in CI the same way `scripts/tokens-json.mjs --check` already
|
|
1471
|
+
does for tokens.
|
|
1472
|
+
|
|
1473
|
+
**Validation (the enforce-don't-instruct house pattern).**
|
|
1474
|
+
`scripts/test-registry.mjs` is the CI gate, wired alongside the other
|
|
1475
|
+
freshness/fixture checks in `deploy.yml`. It re-derives, mechanically,
|
|
1476
|
+
the exact contract lines this issue is funded on — not by trusting the
|
|
1477
|
+
prose, by parsing the generated `usage.html` itself:
|
|
1478
|
+
|
|
1479
|
+
- Every essay `.remarque-sidenote-ref` carries `aria-label="Note N"`, and
|
|
1480
|
+
refs/notes strictly alternate in DOM order — the precise shape of the
|
|
1481
|
+
#89 bug.
|
|
1482
|
+
- The essay TOC rail (`.remarque-toc-rail`) carries `aria-label`.
|
|
1483
|
+
- Every broadsheet `.remarque-entry-numeral` carries `data-entry-number`.
|
|
1484
|
+
- Every forms `<input id="...">` has a matching `<label for="...">`, and
|
|
1485
|
+
every `aria-describedby` target id exists in the sample.
|
|
1486
|
+
- No file, in any item, contains `<script` (the no-executable-content
|
|
1487
|
+
rule above).
|
|
1488
|
+
|
|
1489
|
+
Plus the structural gates any generated artifact needs: schema
|
|
1490
|
+
validation (via `ajv`, the same devDependency `tokens.schema.json`
|
|
1491
|
+
already uses) with negative fixtures proving the schemas actually reject
|
|
1492
|
+
malformed input, and an independent recomputation of every `integrity`
|
|
1493
|
+
hash proving it wasn't just carried over from a stale build.
|
|
1494
|
+
|
|
1495
|
+
**What this deliberately is NOT.** No CLI installer — there is no
|
|
1496
|
+
`npx remarque-registry add essay` command, and no `files[].target`
|
|
1497
|
+
field to resolve an install path against. An agent's expected workflow
|
|
1498
|
+
is fetch the JSON, read `usage.html`/the CSS `content`, and apply it with
|
|
1499
|
+
its own editing tools — the registry replaces transcription-from-prose,
|
|
1500
|
+
not an installer. No archetype "starter-page" items this round: the
|
|
1501
|
+
issue floated one per archetype (Essay, Dossier, Notebook, ...) as an
|
|
1502
|
+
IF-cheap extra, and the honest call is SKIP — archetype pages live as
|
|
1503
|
+
demo-specific Astro files (`site/src/pages/**`), not as a reusable
|
|
1504
|
+
module with one canonical markup sample the way the four shipped items
|
|
1505
|
+
are, so extracting one would mean inventing a new "canonical" sample
|
|
1506
|
+
rather than single-sourcing an existing one.
|
|
1507
|
+
|
|
1508
|
+
**Fetching it.** The demo site serves the registry the same way it
|
|
1509
|
+
serves `tokens.json` (`site/scripts/copy-tokens-json.mjs`, extended):
|
|
1510
|
+
|
|
1511
|
+
- Index: **https://williamzujkowski.github.io/remarque/registry.json**
|
|
1512
|
+
- Per-item: **https://williamzujkowski.github.io/remarque/registry/essay.json**
|
|
1513
|
+
(and `broadsheet.json`, `forms.json`, `palette-deck.json`)
|
|
1514
|
+
- Schemas: **https://williamzujkowski.github.io/remarque/registry-item.schema.json**,
|
|
1515
|
+
**https://williamzujkowski.github.io/remarque/registry.schema.json**
|
|
1516
|
+
|
|
1517
|
+
npm package exports mirror the same paths: `remarque-tokens/registry.json`,
|
|
1518
|
+
`remarque-tokens/registry/essay` (the `./registry/*` subpath maps to
|
|
1519
|
+
`./registry/*.json`), `remarque-tokens/registry-item.schema.json`,
|
|
1520
|
+
`remarque-tokens/registry.schema.json`.
|
|
1521
|
+
|
|
1522
|
+
---
|
|
1523
|
+
|
|
1326
1524
|
## Signature Moves
|
|
1327
1525
|
|
|
1328
1526
|
These are the repeatable visual tells that make a Remarque site recognizable:
|
|
@@ -1353,6 +1551,40 @@ An implementation succeeds when:
|
|
|
1353
1551
|
|
|
1354
1552
|
---
|
|
1355
1553
|
|
|
1554
|
+
## Governance & Deprecation
|
|
1555
|
+
|
|
1556
|
+
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.
|
|
1557
|
+
|
|
1558
|
+
### Semver contract
|
|
1559
|
+
|
|
1560
|
+
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:
|
|
1561
|
+
|
|
1562
|
+
- **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.
|
|
1563
|
+
- **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.
|
|
1564
|
+
- **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.
|
|
1565
|
+
|
|
1566
|
+
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.
|
|
1567
|
+
|
|
1568
|
+
### Deprecation windows
|
|
1569
|
+
|
|
1570
|
+
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.
|
|
1571
|
+
|
|
1572
|
+
| Deprecated / bridged item | Introduced | Sunset target | Rationale |
|
|
1573
|
+
|---|---|---|---|
|
|
1574
|
+
| `: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. Platform endgame to watch instead: `color-scheme` + `light-dark()` (see "Technology Stack"). |
|
|
1575
|
+
|
|
1576
|
+
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.
|
|
1577
|
+
|
|
1578
|
+
### Decision-log habit
|
|
1579
|
+
|
|
1580
|
+
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:
|
|
1581
|
+
|
|
1582
|
+
- 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.
|
|
1583
|
+
- 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.
|
|
1584
|
+
- 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.
|
|
1585
|
+
|
|
1586
|
+
---
|
|
1587
|
+
|
|
1356
1588
|
## What Remarque Is Named For
|
|
1357
1589
|
|
|
1358
1590
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remarque-tokens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/registry-item.schema.json",
|
|
3
|
+
"name": "broadsheet",
|
|
4
|
+
"type": "remarque:contract",
|
|
5
|
+
"title": "Broadsheet Pattern",
|
|
6
|
+
"description": "The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
|
|
7
|
+
"version": "0.23.0",
|
|
8
|
+
"integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"remarque-tokens"
|
|
11
|
+
],
|
|
12
|
+
"cssVars": [
|
|
13
|
+
"--border-style",
|
|
14
|
+
"--border-width",
|
|
15
|
+
"--color-accent",
|
|
16
|
+
"--color-border",
|
|
17
|
+
"--color-border-bold",
|
|
18
|
+
"--color-fg",
|
|
19
|
+
"--color-fg-muted",
|
|
20
|
+
"--color-muted",
|
|
21
|
+
"--content-reading",
|
|
22
|
+
"--font-body",
|
|
23
|
+
"--font-display",
|
|
24
|
+
"--font-mono",
|
|
25
|
+
"--leading-body",
|
|
26
|
+
"--leading-display",
|
|
27
|
+
"--leading-meta",
|
|
28
|
+
"--leading-section",
|
|
29
|
+
"--motion-easing",
|
|
30
|
+
"--motion-normal",
|
|
31
|
+
"--remarque-masthead-size",
|
|
32
|
+
"--space-10",
|
|
33
|
+
"--space-2",
|
|
34
|
+
"--space-3",
|
|
35
|
+
"--space-4",
|
|
36
|
+
"--space-5",
|
|
37
|
+
"--space-6",
|
|
38
|
+
"--space-7",
|
|
39
|
+
"--space-8",
|
|
40
|
+
"--text-body",
|
|
41
|
+
"--text-body-lg",
|
|
42
|
+
"--text-display",
|
|
43
|
+
"--text-meta",
|
|
44
|
+
"--text-section",
|
|
45
|
+
"--text-title",
|
|
46
|
+
"--tracking-caps",
|
|
47
|
+
"--tracking-display",
|
|
48
|
+
"--tracking-title",
|
|
49
|
+
"--weight-display",
|
|
50
|
+
"--weight-medium",
|
|
51
|
+
"--weight-regular"
|
|
52
|
+
],
|
|
53
|
+
"docs": "https://github.com/williamzujkowski/remarque/blob/main/REMARQUE.md#broadsheet",
|
|
54
|
+
"files": [
|
|
55
|
+
{
|
|
56
|
+
"path": "broadsheet.css",
|
|
57
|
+
"content": "/*\n * Remarque — Broadsheet Pattern (masthead, lead, entry list, post kicker)\n * ────────────────────────────────────────────────────────────────────\n * Four editorial-layout primitives for Landing/archive contexts — the\n * typography-maximalist end of the system. Newspaper-broadsheet voice:\n * a nameplate, a featured lead story, a numbered contents list, and a\n * post-header kicker line, built entirely from tokens already shipped\n * in tokens-core.css / tokens-palette.css.\n *\n * PROVENANCE — graduated, not invented here. Validated downstream on\n * williamzujkowski.github.io (landing PR #213, archive/tag/post-header\n * PR #214), ratified for upstreaming in issue #36. Source: `astro-site/\n * src/components/BroadsheetEntry.astro` + `global.css`'s \"Broadsheet\n * layout\" block. As with the essay module (#52), no site-specific\n * literal was copied — every dimension below is re-derived from this\n * package's own tokens; see the inline comments for the value-by-value\n * mapping and the few places the re-expression corrects the source\n * (`text-transform: uppercase` -> `font-variant-caps: all-small-caps`,\n * per REMARQUE.md \"Small Caps\", predates that rule same as essay.css's\n * TOC summary did) or simplifies it (font-weight 300 has no Remarque\n * core token; the italic/lede treatments below lean on color + style\n * alone rather than introduce an un-tokenized weight).\n *\n * Own subpath (remarque-tokens/broadsheet, remarque-tokens/broadsheet.css)\n * — NOT included by the tokens.css aggregator: the Landing/archive\n * pattern is opt-in, most Remarque pages want none of it. Import\n * explicitly, after prose.css if both are in use:\n * @import 'remarque-tokens/broadsheet.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <header class=\"remarque-masthead\">\n * <p class=\"remarque-masthead-kicker\"><span>Field Notes</span></p>\n * <h1 class=\"remarque-masthead-title\">Quiet <em>Signals</em></h1>\n * <p class=\"remarque-masthead-dateline\">\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * <span>July 23, 2026</span>\n * <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n * </p>\n * </header>\n *\n * <article class=\"remarque-lead\">\n * <p class=\"remarque-lead-kicker\">\n * <span class=\"remarque-lead-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h2 class=\"remarque-lead-title\"><a href=\"/writing/x\">Title</a></h2>\n * <p class=\"remarque-lead-lede\">Lede paragraph...</p>\n * </article>\n *\n * <ul class=\"remarque-entry-list\">\n * <li class=\"remarque-entry\">\n * <span class=\"remarque-entry-numeral\" data-entry-number=\"2\"\n * aria-hidden=\"true\"></span>\n * <div class=\"remarque-entry-body\">\n * <p class=\"remarque-entry-kicker\">...</p>\n * <h3 class=\"remarque-entry-title\"><a href=\"/writing/y\">Title</a></h3>\n * <p class=\"remarque-entry-excerpt\">Excerpt...</p>\n * </div>\n * </li>\n * </ul>\n *\n * <header class=\"remarque-post-header\">\n * <p class=\"remarque-post-kicker\">\n * <span class=\"remarque-post-kicker-section\">Essay</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n * <span>8 min read</span>\n * <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n * <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n * </p>\n * <h1 class=\"text-display font-display\">Post Title</h1>\n * </header>\n *\n * `<ul>` (not `<ol>`), deliberately, for the entry list: the visual\n * numeral is decorative (`aria-hidden`, generated from `data-entry-\n * number` via `attr()` — see below), and an `<ol>` would have screen\n * readers announce \"1 of 2\" alongside it, doubling up on the same\n * information in two voices. Matches the flagship's own choice.\n */\n\n/* ─── Shared meta-voice bits ─────────────────────────────────────────\n * Every kicker/dateline row below repeats these declarations rather\n * than depending on `.text-label` as a mixin (tokens-core.css) — same\n * non-mixin precedent as essay.css's TOC summary. All-small-caps, not\n * `text-transform: uppercase` (REMARQUE.md \"Small Caps\": faux uppercase\n * renders at cap-height with no optical correction; the flagship\n * predates that rule and used `text-transform: uppercase` throughout\n * this pattern). `--text-meta` (14px), not `--text-micro` (13px) — the\n * words in a kicker (\"Essay\", \"Field Notes\") are read as content, not\n * bare timestamps/fine print, the same distinction essay.css's sidenote\n * marker already draws against the 13px floor. Tabular lining figures\n * on every numeral here (dates, counts) per REMARQUE.md's Numerals\n * rule — never mixed with the oldstyle register `.remarque-entry-\n * numeral` uses below for the same reason `.text-meta` carries it.\n */\n.remarque-masthead-kicker,\n.remarque-masthead-dateline,\n.remarque-lead-kicker,\n.remarque-entry-kicker,\n.remarque-post-kicker {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n font-variant-numeric: tabular-nums lining-nums;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n margin: 0;\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5em;\n}\n\n.remarque-dot {\n color: var(--color-border-bold);\n}\n\n.remarque-lead-section,\n.remarque-entry-section,\n.remarque-post-kicker-section {\n color: var(--color-accent);\n font-weight: var(--weight-medium);\n}\n\n/* Hairline flanking the masthead dateline — decorative only, so a\n 1px border-color rule (not -bold) is the right register; nothing\n depends on it as a boundary.\n Authored as a BORDER, not a `background` fill, on purpose (issue #93\n forced-colors audit finding): this is a bare, non-semantic `<span>`\n with no text content and no border of its own otherwise — under\n `forced-colors: active`, a plain element's `background-color` is\n generally forced to `Canvas` (the page background), which would make\n a background-fill divider disappear entirely, while `border-color` on\n a real border is forced to a visible system border color instead.\n Same 1px visual result at rest (a zero-height box with a 1px top\n border reads identically to a 1px-tall filled box); only the forced-\n colors behavior differs. */\n.remarque-rule {\n display: inline-block;\n width: var(--space-6);\n height: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border-bold);\n}\n\n/* Hairline divider between broadsheet sections (masthead/lead/entry-list) */\n.remarque-broadsheet-rule {\n max-width: var(--content-reading);\n margin: var(--space-6) auto;\n border: 0;\n border-top: var(--border-width) var(--border-style) var(--color-border);\n}\n\n/* ─── 1. Masthead ─────────────────────────────────────────────────────\n * The nameplate. Centered, oversized, the one place in Remarque a\n * title outgrows `--text-display` (tokens-core.css's largest type-\n * scale rung, clamp(2.75rem, 5.5vw, 5rem)) — a Landing masthead is a\n * register above even a Landing page's own `<h1>`.\n *\n * FLUID-TYPE DECISION (issue #36's required call): the existing scale\n * does not reach the flagship's validated clamp(3.5rem, 9vw, 7.5rem).\n * Rather than assert those as bare literals (or add a new core\n * `--text-masthead` token, which the issue explicitly rules out), this\n * is a documented, file-scoped fluid exception whose min/max BOUNDS are\n * arithmetic on core spacing tokens already in tokens-core.css — not\n * new constants:\n * min = --space-7 (3rem) + --space-2 (0.5rem) = 3.5rem\n * max = --space-10 (8rem) - --space-2 (0.5rem) = 7.5rem\n * both resolve to the exact flagship values that were already\n * validated (contrast/gamut/font-floor audits + axe) downstream. The\n * 9vw fluid rate is kept as-is: it continues the same escalating-per-\n * tier vw progression tokens-core.css's own scale already uses\n * (2.25vw section -> 3.5vw title -> 5.5vw display), one step further\n * for a register one step past display. This custom property lives\n * here, not in tokens-core.css — it is NOT a new core token, it is\n * local arithmetic on existing ones, scoped to this opt-in pattern.\n */\n.remarque-masthead {\n --remarque-masthead-size: clamp(\n calc(var(--space-7) + var(--space-2)),\n 9vw,\n calc(var(--space-10) - var(--space-2))\n );\n max-width: var(--content-reading);\n margin: var(--space-8) auto var(--space-6);\n text-align: center;\n}\n\n.remarque-masthead-title {\n font-family: var(--font-display);\n font-size: var(--remarque-masthead-size);\n /* Reuses --leading-display/--tracking-display rather than the\n flagship's bespoke 0.92/-0.035em — a fully tokenized simplification\n at a scale where the difference is not perceptible at normal\n reading distance. */\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n /* Same --weight-display escape hatch .text-display/.text-title use\n (tokens-core.css) — a palette can compensate thin serif hairlines\n in dark mode (tokens-palette.css already does, 400 light / 500\n dark) and the masthead picks that up for free. */\n font-weight: var(--weight-display, var(--weight-regular));\n color: var(--color-fg);\n margin: 0;\n font-feature-settings: \"liga\" 1, \"kern\" 1;\n font-optical-sizing: auto;\n font-synthesis: none;\n text-wrap: balance;\n}\n\n.remarque-masthead-title em {\n font-style: italic;\n color: var(--color-fg-muted);\n}\n\n.remarque-masthead-dateline {\n margin-block-start: var(--space-4);\n justify-content: center;\n}\n\n/* ─── 2. Lead Article ─────────────────────────────────────────────────\n * The featured story below the masthead (or standing alone atop an\n * archive). Title register: --text-display reaches close enough to\n * the flagship's clamp(2.25rem, 4.5vw, 3.75rem) (2.75-5rem vs.\n * 2.25-3.75rem) that no exception is needed here — \"express via\n * existing type tokens\" (issue #36), the masthead above is the one\n * register that genuinely doesn't fit.\n */\n.remarque-lead {\n max-width: var(--content-reading);\n margin: var(--space-7) auto var(--space-6);\n}\n\n.remarque-lead-kicker {\n margin-block-end: var(--space-3);\n}\n\n.remarque-lead-title {\n font-family: var(--font-display);\n font-size: var(--text-display);\n line-height: var(--leading-display);\n letter-spacing: var(--tracking-display);\n font-weight: var(--weight-display, var(--weight-regular));\n margin: 0 0 var(--space-4);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-lead-title a,\n.remarque-entry-title a {\n color: var(--color-fg);\n text-decoration: none;\n /* Underline-grow hover (issue #36 \"Findings\"): AA-compliant, and\n reduced-motion-safe for free — tokens-core.css zeroes\n --motion-normal under prefers-reduced-motion, so this needs no\n separate @media guard the way an opacity/transform animation\n would. --motion-easing/--motion-normal replace the flagship's\n hardcoded `220ms ease`. */\n background-image: linear-gradient(var(--color-fg), var(--color-fg));\n background-size: 0 1px;\n background-repeat: no-repeat;\n background-position: 0 95%;\n transition: background-size var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-lead-title a:hover,\n.remarque-lead-title a:focus-visible,\n.remarque-entry-title a:hover,\n.remarque-entry-title a:focus-visible {\n background-size: 100% 1px;\n}\n\n/* Forced-colors fallback (issue #93 audit finding — affordance-loss,\n the clearest one in this file): the underline-grow hover/focus\n affordance above is painted entirely with `background-image:\n linear-gradient(...)`. CSS gradients are on the forced list — under\n `forced-colors: active` a gradient background-image is forced to\n `none`, so the ENTIRE hover/focus signal for these title links would\n otherwise disappear (the `:focus-visible` outline from tokens-core.css\n still fires for keyboard focus, but mouse hover would have no visual\n affordance at all, and even focus would lose this link-specific cue).\n `text-decoration: underline` is not on the forced list and needs no\n override to render visibly, so it stands in as the same \"this is a\n link, and it is the active one\" signal the gradient trick provides\n everywhere else. Scoped to this media query only — the gradient\n underline-grow stays the sole affordance outside forced-colors mode,\n unchanged. */\n@media (forced-colors: active) {\n .remarque-lead-title a:hover,\n .remarque-lead-title a:focus-visible,\n .remarque-entry-title a:hover,\n .remarque-entry-title a:focus-visible {\n text-decoration: underline;\n }\n}\n\n.remarque-lead-lede {\n font-family: var(--font-display);\n font-size: var(--text-body-lg);\n line-height: var(--leading-body);\n font-style: italic;\n font-weight: var(--weight-regular);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n hanging-punctuation: first last;\n}\n\n/* Drop cap on italic text needs an explicit ::first-letter override\n (issue #36 \"Findings\") — it cannot just inherit italic from the\n parent's font-style the way a roman-text drop cap can, since this\n pseudo-element re-asserts every typographic property of its own.\n Same mechanics/sizing as prose.css's `.remarque-prose--dropcap`\n (3.5em / 0.85 line-height / initial-letter progressive enhancement)\n for one consistent drop-cap voice across the system, rather than a\n second bespoke number. */\n.remarque-lead-lede::first-letter {\n font-family: var(--font-display);\n font-style: italic;\n font-weight: var(--weight-display, var(--weight-regular));\n font-size: 3.5em;\n line-height: 0.85;\n float: left;\n padding: 0.08em 0.12em 0 0;\n color: var(--color-fg);\n}\n\n@supports (initial-letter: 4) or (-webkit-initial-letter: 4) {\n .remarque-lead-lede::first-letter {\n initial-letter: 3.5;\n -webkit-initial-letter: 3.5;\n float: none;\n padding: 0 0.08em 0 0;\n }\n}\n\n/* ─── 3. Numbered Entry List ──────────────────────────────────────────\n * `container-type: inline-size` — the 2-column/1-column collapse below\n * responds to how much room the list itself has, not the viewport (a\n * clean fit for a container query, matching the flagship's choice; it\n * degrades correctly if this list is ever embedded in a narrower\n * column than the page itself).\n */\n.remarque-entry-list {\n container-type: inline-size;\n max-width: var(--content-reading);\n margin: 0 auto;\n padding: 0;\n list-style: none;\n}\n\n.remarque-entry {\n display: grid;\n /* Numeral column sized in `ch` (intrinsic to the numeral glyphs\n themselves), not an asserted rem literal — the flagship's\n `minmax(3.5rem, 5rem)` re-expressed as \"just wide enough for the\n content,\" the more typographically disciplined choice per\n REMARQUE.md's Decision Protocol when a core token doesn't already\n cover a dimension. */\n grid-template-columns: minmax(2.5ch, 4ch) 1fr;\n gap: var(--space-4);\n padding: var(--space-5) 0;\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n align-items: start;\n}\n\n.remarque-entry:last-child {\n border-bottom: 0;\n}\n\n/* Oldstyle italic numerals in the outer margin, generated from\n `data-entry-number` via `attr()` — NOT CSS `counter()` (issue #36\n \"Findings\": counter-generated content does not receive the OpenType\n oldstyle-figure treatment reliably across Newsreader's weights, only\n real text/attr() content does). `attr()` reads from the element the\n pseudo-content is generated on, so `data-entry-number` lives on\n `.remarque-entry-numeral` itself, not the parent `<li>`. Both\n `font-variant-numeric: oldstyle-nums proportional-nums` AND\n `font-feature-settings: \"onum\" 1, \"pnum\" 1` are required together\n for reliable rendering across weights — either alone is\n inconsistent (issue #36 \"Findings\"). */\n.remarque-entry-numeral {\n display: block;\n}\n\n.remarque-entry-numeral::before {\n content: attr(data-entry-number);\n font-family: var(--font-display);\n font-style: italic;\n font-size: var(--text-title);\n line-height: 0.95;\n color: var(--color-border-bold);\n font-variant-numeric: oldstyle-nums proportional-nums;\n font-feature-settings: \"onum\" 1, \"pnum\" 1;\n text-align: right;\n display: block;\n padding-block-start: 0.1em;\n user-select: none;\n transition: color var(--motion-normal) var(--motion-easing);\n}\n\n.remarque-entry:hover .remarque-entry-numeral::before,\n.remarque-entry:focus-within .remarque-entry-numeral::before {\n color: var(--color-accent);\n}\n\n.remarque-entry-body {\n min-width: 0; /* allow the grid child to shrink below its content's intrinsic width */\n}\n\n.remarque-entry-kicker {\n margin-block-end: var(--space-2);\n}\n\n.remarque-entry-title {\n font-family: var(--font-display);\n font-size: var(--text-section);\n line-height: var(--leading-section);\n letter-spacing: var(--tracking-title);\n font-weight: var(--weight-medium);\n margin: 0 0 var(--space-2);\n text-wrap: balance;\n font-optical-sizing: auto;\n font-synthesis: none;\n}\n\n.remarque-entry-excerpt {\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-body);\n color: var(--color-fg-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* Collapse to one column on a narrow container — `40rem`, exactly\n `--space-10` (8rem) x 5. Container-query conditions can't reference\n custom properties or `calc()` (unlike a plain declaration value, the\n condition itself is parsed before the cascade resolves custom\n properties, and Lightning CSS's minifier rejects a `calc()` there\n outright) so this is spelled as the literal it resolves to, with the\n token-multiple noted here rather than in the syntax — matches the\n flagship's own `640px` breakpoint exactly. */\n@container (max-width: 40rem) {\n .remarque-entry {\n grid-template-columns: 1fr;\n gap: var(--space-2);\n }\n\n .remarque-entry-numeral::before {\n text-align: left;\n font-size: var(--text-section);\n padding-block-start: 0;\n }\n}\n\n/* ─── 4. Post-Header Kicker ───────────────────────────────────────────\n * The mono \"Section · Reading time · Date\" line above an individual\n * post's title — the same kicker voice as the lead/entry rows above,\n * scaled down to a single-line header. Pair with any Remarque heading\n * class (`.text-display`, `.text-title`); this rule only styles the\n * kicker line itself, not the title beneath it.\n */\n.remarque-post-header {\n margin-block-end: var(--space-7);\n}\n\n.remarque-post-kicker {\n margin-block-end: var(--space-3);\n}\n",
|
|
58
|
+
"type": "remarque:css"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"path": "usage.html",
|
|
62
|
+
"content": "<header class=\"remarque-masthead\">\n <p class=\"remarque-masthead-kicker\"><span>Field Notes</span></p>\n <h1 class=\"remarque-masthead-title\">Quiet <em>Signals</em></h1>\n <p class=\"remarque-masthead-dateline\">\n <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n <span>July 23, 2026</span>\n <span class=\"remarque-rule\" aria-hidden=\"true\"></span>\n </p>\n</header>\n\n<article class=\"remarque-lead\">\n <p class=\"remarque-lead-kicker\">\n <span class=\"remarque-lead-section\">Essay</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <span>8 min read</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n </p>\n <h2 class=\"remarque-lead-title\"><a href=\"/writing/x\">Title</a></h2>\n <p class=\"remarque-lead-lede\">Lede paragraph…</p>\n</article>\n\n<ul class=\"remarque-entry-list\">\n <li class=\"remarque-entry\">\n <span class=\"remarque-entry-numeral\" data-entry-number=\"2\" aria-hidden=\"true\"></span>\n <div class=\"remarque-entry-body\">\n <p class=\"remarque-entry-kicker\">\n <span class=\"remarque-entry-section\">Essay</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <span>6 min read</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <time datetime=\"2026-03-29\">Mar 29, 2026</time>\n </p>\n <h3 class=\"remarque-entry-title\"><a href=\"/writing/y\">Title</a></h3>\n <p class=\"remarque-entry-excerpt\">Excerpt…</p>\n </div>\n </li>\n</ul>\n\n<header class=\"remarque-post-header\">\n <p class=\"remarque-post-kicker\">\n <span class=\"remarque-post-kicker-section\">Essay</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <span>8 min read</span>\n <span class=\"remarque-dot\" aria-hidden=\"true\">·</span>\n <time datetime=\"2026-04-08\">Apr 8, 2026</time>\n </p>\n <h1 class=\"text-display font-display\">Post Title</h1>\n</header>\n",
|
|
63
|
+
"type": "remarque:markup"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/registry-item.schema.json",
|
|
3
|
+
"name": "essay",
|
|
4
|
+
"type": "remarque:contract",
|
|
5
|
+
"title": "Essay Module",
|
|
6
|
+
"description": "Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label=\"Note N\"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).",
|
|
7
|
+
"version": "0.23.0",
|
|
8
|
+
"integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"remarque-tokens"
|
|
11
|
+
],
|
|
12
|
+
"cssVars": [
|
|
13
|
+
"--border-style",
|
|
14
|
+
"--border-width",
|
|
15
|
+
"--color-accent",
|
|
16
|
+
"--color-accent-hover",
|
|
17
|
+
"--color-border",
|
|
18
|
+
"--color-border-bold",
|
|
19
|
+
"--color-fg",
|
|
20
|
+
"--color-fg-muted",
|
|
21
|
+
"--color-muted",
|
|
22
|
+
"--content-reading",
|
|
23
|
+
"--font-mono",
|
|
24
|
+
"--leading-meta",
|
|
25
|
+
"--motion-easing",
|
|
26
|
+
"--motion-fast",
|
|
27
|
+
"--space-1",
|
|
28
|
+
"--space-10",
|
|
29
|
+
"--space-11",
|
|
30
|
+
"--space-12",
|
|
31
|
+
"--space-2",
|
|
32
|
+
"--space-3",
|
|
33
|
+
"--space-4",
|
|
34
|
+
"--space-5",
|
|
35
|
+
"--space-6",
|
|
36
|
+
"--space-7",
|
|
37
|
+
"--space-8",
|
|
38
|
+
"--text-meta",
|
|
39
|
+
"--tracking-caps",
|
|
40
|
+
"--weight-semibold",
|
|
41
|
+
"--z-sticky"
|
|
42
|
+
],
|
|
43
|
+
"docs": "https://github.com/williamzujkowski/remarque/blob/main/REMARQUE.md#essay-module",
|
|
44
|
+
"files": [
|
|
45
|
+
{
|
|
46
|
+
"path": "essay.css",
|
|
47
|
+
"content": "/*\n * Remarque — Essay Module (sidenotes + sticky TOC rail)\n * ───────────────────────────────────────────────────────\n * Optional Essay-archetype module: footnotes become in-flow margin notes\n * (Tufte/gwern-style), and an \"on this page\" table of contents pins to a\n * sticky rail alongside the reading column. Both are progressive\n * enhancements over a plain Essay page — CSS-only, no JavaScript required\n * at either breakpoint.\n *\n * PROVENANCE — graduated, not invented here. This module re-expresses, in\n * Remarque's own token vocabulary, a design first built and shipped on\n * williamzujkowski.github.io (`astro-site/src/lib/rehype-sidenotes.mjs` +\n * `global.css`'s \"Sidenotes\"/\"Sticky TOC side rail\" blocks). Ratified for\n * upstreaming in issue #52. No site-specific value (the flagship's literal\n * `11.5rem`/`13rem`/`14rem`/`1280px` measurements) was copied — every\n * dimension below is re-derived from tokens already in tokens-core.css /\n * tokens-palette.css. See REMARQUE.md's \"Essay Module\" section for the\n * full markup contract, and this file's inline comments for exactly which\n * site value maps to which token and why a few mechanics changed shape\n * (CSS counters instead of a build-time numbering step; `<details open>`\n * by default instead of a JS breakpoint listener) on the way upstream.\n *\n * Own subpath (remarque-tokens/essay, remarque-tokens/essay.css) — NOT\n * included by the tokens.css aggregator, and not required by prose.css:\n * plenty of Essay pages want neither sidenotes nor a TOC rail. Import\n * explicitly, after prose.css:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/essay.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <article class=\"remarque-essay\">\n * <header class=\"content-reading\">...</header>\n *\n * <nav class=\"remarque-toc-rail\" aria-label=\"Table of contents\">\n * <details open>\n * <summary>On this page</summary>\n * <ul>\n * <li><a href=\"#section-one\">Section One</a></li>\n * </ul>\n * </details>\n * </nav>\n *\n * <div class=\"remarque-prose content-reading\">\n * <p>\n * A claim worth a citation<a href=\"#note-1\" id=\"note-1-ref\"\n * class=\"remarque-sidenote-ref\" aria-describedby=\"note-1\"></a>.\n * </p>\n * <aside class=\"remarque-sidenote\" id=\"note-1\" role=\"note\">\n * The note text, in real DOM order right after the paragraph that\n * cites it — not collected into an end-of-document section.\n * </aside>\n * </div>\n *\n * <footer class=\"content-reading\">...</footer>\n * </article>\n *\n * DOM order is the accessibility requirement, not a presentational nicety:\n * a screen reader hits a sidenote's content exactly where its reference\n * occurs. `.remarque-sidenote-ref` and its matching `.remarque-sidenote`\n * MUST alternate in strict DOM order (ref, then note, then the next ref,\n * then its note...) — the counter mechanics below depend on it. `role=\n * \"note\"` keeps a page with several sidenotes from scattering that many\n * unlabeled `complementary` landmarks through the document (`<aside>`'s\n * implicit role) — the same fix the flagship's rehype plugin documents.\n *\n * This module is deliberately silent on HOW that markup gets produced. A\n * build-time transform (the flagship's rehype-sidenotes.mjs is one worked\n * example, not a dependency of this package) or hand-authored HTML both\n * satisfy the contract equally — see REMARQUE.md for the no-build-step\n * fallback (`.remarque-footnotes`) if you don't have one.\n *\n * ─── BREAKPOINT ─────────────────────────────────────────────────────────\n * Gated on >= 80rem (1280px, at the platform default 16px root) viewport\n * width — the same threshold the flagship ratified, re-derived here from\n * this package's own tokens instead of asserted as a bare magic number:\n * `--content-standard` (72rem) is the widest a Remarque page's content\n * ever grows, and the rail/gutter tracks below need `--space-11`+\n * `--space-12` (10-12rem) and `--space-10` (8rem) of clearance on top of\n * `--content-reading` (46rem default) plus two `--space-6` gaps — call it\n * roughly 70-76rem of content width before both tracks have their\n * documented minimum room, plus headroom for a scrollbar and window\n * chrome. 80rem clears that with margin to spare across every approved\n * body-face/measure pairing (see REMARQUE.md's Measure Compensation\n * table) without needing a `--breakpoint-*` token this package doesn't\n * otherwise have. Below it: sidenotes render as small-print inline blocks\n * exactly where the plain document flow already places them, and the TOC\n * is a collapsible `<details>` in the reading column — this is also the\n * ONLY rendering a consumer who skips this module's `@media` block ever\n * sees, so nothing here is required for the page to work.\n */\n\n/* ─── Sidenotes — default / narrow-viewport mode ────────────────────────\n * `.remarque-sidenote` renders as a bordered small-print block right in\n * the text flow by default. This IS the narrow-viewport fallback the\n * spec calls for — zero extra markup, zero JS: the note just isn't\n * floated into a margin yet.\n *\n * Numbering is generated, not authored: `counter-reset` on\n * `.remarque-prose`, `counter-increment` on `.remarque-sidenote-ref` only,\n * and `.remarque-sidenote` simply READING that same counter (no increment\n * of its own — see the comment on that rule) keep the visible number in\n * sync across the reference and its note with no number typed into\n * either element — the flagship's rehype plugin injects a literal\n * `<span class=\"sidenote-number\">1</span>`/text node at build time; this\n * package ships no build step, so CSS counters do the same job for a\n * pure-CSS module. A repeat citation of an already-noted footnote should\n * use `.remarque-sidenote-ref--repeat` (no `.remarque-sidenote` follows\n * it) so the shared counter isn't advanced for a note that was already\n * printed — mirrors the flagship's \"only the first reference gets a\n * sidenote\" rule.\n */\n.remarque-prose {\n counter-reset: remarque-sidenote;\n}\n\n.remarque-sidenote-ref {\n counter-increment: remarque-sidenote;\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n /* USWDS floor rationale: --text-micro is reserved for timestamps/fine\n print only (tokens-core.css, REMARQUE.md \"Font Size Floors\") — the\n flagship's own marker used that size, but this module holds to\n Remarque's stricter 14px floor for anything read as content. */\n line-height: 0;\n vertical-align: super;\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-sidenote-ref::after {\n content: counter(remarque-sidenote);\n}\n\n.remarque-sidenote-ref:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n.remarque-sidenote-ref--repeat {\n counter-increment: none;\n}\n\n.remarque-sidenote {\n /* No counter-increment here: `.remarque-sidenote-ref` already advanced\n `remarque-sidenote` immediately before this element in DOM order —\n incrementing again would double-count every pair (ref shows N, the\n note that follows it would show N+1 instead of N). This element only\n READS the counter its ref just set. */\n display: block;\n margin-block: var(--space-3);\n padding-inline-start: var(--space-3);\n /* border-bold: this is the note's ONLY visual separator from running\n prose at narrow widths, so it must clear the 3:1 functional-border\n minimum (WCAG 1.4.11) — same reasoning as .remarque-prose blockquote. */\n border-inline-start: var(--border-width) var(--border-style) var(--color-border-bold);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n/* The generated number precedes whatever content the note holds. If that\n content is a single run of inline text (no wrapping <p>), the number\n sits inline with the opening words — the flagship's \"1 Note text...\"\n pairing. A `<p>`-wrapped note (multi-paragraph notes need it) instead\n reads as a small label above the paragraph — a pure-CSS module can't\n reach inside an arbitrary child the way a build-time transform can, so\n this is the honest degradation: prefer bare text for single-line notes. */\n.remarque-sidenote::before {\n content: counter(remarque-sidenote) \"\\2002\";\n color: var(--color-accent);\n font-weight: var(--weight-semibold);\n}\n\n.remarque-sidenote :where(p) {\n margin: 0;\n}\n\n.remarque-sidenote :where(p) + :where(p) {\n margin-top: var(--space-2);\n}\n\n/* ─── Sidenotes — wide-viewport mode: float into the left margin ────────\n * Classic Tufte CSS technique: `.remarque-sidenote` isn't a grid child\n * itself (it's nested inside `.remarque-prose`, which IS grid-column 2 —\n * see `.remarque-essay` below) so grid placement doesn't apply to it\n * directly. Instead it floats left with a negative inline-start margin\n * exactly wide enough to clear its own box plus one column-gap unit,\n * landing in the grid's column-1 gutter. `clear: left` stacks consecutive\n * notes without any JS-computed offset.\n *\n * Width (`--space-10`, 8rem) and pull-out distance (that width plus\n * `--space-6`, the same gap the grid below uses between tracks) are both\n * drawn from the core spacing scale rather than asserting a bespoke\n * sidebar-width value — this is the token-vocabulary re-expression of the\n * flagship's hand-measured `11.5rem`/`13rem` pair.\n */\n@media (min-width: 80rem) {\n .remarque-sidenote {\n float: left;\n clear: left;\n width: var(--space-10);\n margin-inline-start: calc(-1 * (var(--space-10) + var(--space-6)));\n margin-block: var(--space-1) var(--space-4);\n padding-inline-start: 0;\n padding-inline-end: var(--space-2);\n border-inline-start: none;\n /* Decorative-only at this width (the gutter's own whitespace is the\n real separator now) — `--color-border`, not `-bold`, matching the\n flagship's own choice to relax the border once it's no longer the\n sole boundary cue. */\n border-inline-end: var(--border-width) var(--border-style) var(--color-border);\n text-align: start;\n }\n}\n\n/* ─── Footnotes fallback (no relocation build step) ─────────────────────\n * `.remarque-footnotes` styles a traditional end-of-document footnotes\n * section — e.g. GFM/mdast-util-to-hast's default\n * `<section data-footnotes><h2>Footnotes</h2><ol>...</ol></section>`\n * output — in the same meta voice as `.remarque-sidenote`, for a consumer\n * who hasn't built (or doesn't want) a DOM-reordering transform. It is a\n * same-voice fallback, not a lesser feature: use `.remarque-sidenote`\n * whenever you can relocate notes into DOM reading order (the real a11y\n * upgrade this module exists for); reach for `.remarque-footnotes` only\n * when that isn't available yet.\n *\n * <section class=\"remarque-footnotes\">\n * <h2>Notes</h2>\n * <ol>\n * <li id=\"note-1\">The note text. <a href=\"#note-1-ref\">↩</a></li>\n * </ol>\n * </section>\n */\n.remarque-footnotes {\n margin-top: var(--space-7);\n padding-top: var(--space-3);\n border-top: var(--border-width) var(--border-style) var(--color-border);\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n color: var(--color-fg-muted);\n}\n\n.remarque-footnotes h2 {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-caps);\n font-variant-caps: all-small-caps;\n color: var(--color-muted);\n margin: 0 0 var(--space-3);\n}\n\n.remarque-footnotes ol {\n padding-inline-start: var(--space-5);\n margin: 0;\n}\n\n.remarque-footnotes li + li {\n margin-top: var(--space-2);\n}\n\n.remarque-footnotes a {\n color: var(--color-accent);\n text-decoration: none;\n}\n\n.remarque-footnotes a:hover {\n color: var(--color-accent-hover);\n text-decoration: underline;\n}\n\n/* ─── Sticky TOC rail ────────────────────────────────────────────────────\n * `.remarque-toc-rail` wraps a plain collapsible `<details>` — the SAME\n * markup at every viewport, no JS breakpoint listener. Author it\n * `<details open>` by default: below the rail breakpoint this is an\n * inline collapsible block a reader can close to save space (native\n * disclosure behavior, unchanged); at/above it, it reads as an\n * already-expanded sticky rail. The flagship instead runs a small inline\n * script that force-opens the native `open` attribute on every breakpoint\n * crossing — this module holds to \"no JS required,\" so it authors the\n * open state up front instead of syncing it at runtime. The one\n * degradation this trades away: a reader who manually collapses the TOC\n * below the breakpoint and then widens their window keeps it collapsed\n * until they click it again — a fully static, CSS-only cost worth paying\n * for not requiring a script tag.\n *\n * Marker: reuses the same quiet mono disclosure triangle as\n * `.remarque-prose details summary::before` (prose.css) rather than\n * asking consumers to embed an inline SVG chevron, as the flagship's\n * TableOfContents.astro does.\n */\n.remarque-toc-rail {\n margin-block: var(--space-5);\n}\n\n.remarque-toc-rail details {\n border: none;\n padding: 0;\n}\n\n.remarque-toc-rail summary {\n cursor: pointer;\n list-style: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-2);\n padding-block: var(--space-2);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n /* Mono meta voice — same declarations as .text-label (tokens-core.css),\n repeated rather than shared because .text-label isn't a mixin: true\n small caps, never text-transform: uppercase (REMARQUE.md \"Small\n Caps\"). The flagship predates that rule and used `text-transform:\n uppercase`; this is the one place the re-expression corrects the\n source rather than just relocating it. */\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail summary::-webkit-details-marker {\n display: none;\n}\n\n.remarque-toc-rail summary::before {\n content: \"\\25B8\";\n display: inline-block;\n color: var(--color-muted);\n transition: transform var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail details[open] summary::before {\n transform: rotate(90deg);\n}\n\n.remarque-toc-rail summary:hover {\n color: var(--color-fg);\n}\n\n.remarque-toc-rail ul {\n list-style: none;\n padding: 0;\n margin: var(--space-3) 0 0;\n}\n\n.remarque-toc-rail li + li {\n margin-top: var(--space-1);\n}\n\n.remarque-toc-rail a {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n color: var(--color-fg-muted);\n text-decoration: none;\n transition: color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-toc-rail a:hover {\n color: var(--color-fg);\n}\n\n/* Forced-colors fallback (issue #93 audit finding): the TOC rail's\n summary toggle and its links both signal hover with a color-only\n change (`--color-muted`/`--color-fg-muted` -> `--color-fg`). Structure\n itself survives forced-colors for free (`.remarque-toc-rail summary`'s\n `border-bottom` and `.remarque-sidenote`'s `border-inline-*` both use\n real `border-color`, forced to a visible system border regardless of\n the authored value — see REMARQUE.md \"Forced Colors & Contrast\n Preferences\"), so this block is scoped narrowly to the two hover\n affordances that have no non-color signal at all otherwise. */\n@media (forced-colors: active) {\n .remarque-toc-rail summary:hover,\n .remarque-toc-rail a:hover {\n text-decoration: underline;\n }\n}\n\n/* ─── Essay grid shell — wide-viewport rail layout ───────────────────────\n * `.remarque-essay` is the opt-in grid container an Essay page's <article>\n * adds to place a sticky TOC rail beside the reading column, mirroring\n * this module's sidenote gutter on the OTHER side (right rail = TOC, left\n * gutter = sidenotes — never the same side, so the two mechanisms never\n * fight over space). Below the breakpoint it is a no-op: every child\n * keeps whatever block-flow centering it already had from `.content-\n * reading` / `.remarque-prose` — this rule only exists inside the\n * `@media` block.\n *\n * Track widths: the center track is `minmax(0, var(--content-reading))` —\n * IDENTICAL to the plain `max-width: content-reading; margin-inline:\n * auto` centering used everywhere else (same visual column, just a grid\n * track instead of block flow), so nothing here changes the reading\n * measure the \"prose measure\" computed-style check already covers. The\n * right track (`minmax(--space-11, --space-12)`, 10-12rem) is this\n * module's re-expression of the flagship's bespoke `minmax(12rem, 14rem)`\n * rail — same shape (a comfortable range, not a fixed width), core\n * spacing tokens instead of asserted rem literals.\n *\n * Children are placed by exclusion (`:not(.remarque-toc-rail)` -> column\n * 2) rather than an explicit tag list, deliberately: nothing about this\n * rule touches `max-width` or `margin-inline` on any child, so each\n * element's own `.content-reading` / `.remarque-prose` centering keeps\n * working exactly as it does outside a grid — the grid track is already\n * sized to match it.\n */\n@media (min-width: 80rem) {\n .remarque-essay {\n display: grid;\n grid-template-columns:\n minmax(0, 1fr)\n minmax(0, var(--content-reading))\n minmax(var(--space-11), var(--space-12));\n column-gap: var(--space-6);\n }\n\n .remarque-essay > :not(.remarque-toc-rail) {\n grid-column: 2;\n }\n\n .remarque-essay > .remarque-toc-rail {\n grid-column: 3;\n /* `1 / -1` resolves to row 1 ONLY when the grid has no explicit\n `grid-template-rows` (as here) — `-1` means \"the last explicit\n row line,\" which with zero explicit rows IS row 1. That inflates\n row 1 to the rail's own height instead of spanning the real\n content rows, shoving everything after it down a gap the height\n of the TOC. (Exactly the bug the flagship shipped and fixed,\n 2026-07-20 — captured here so this module doesn't reintroduce\n it.) A generous explicit span covers any Essay's block count;\n empty implicit rows stay 0-height. */\n grid-row: 1 / span 999;\n align-self: start;\n justify-self: start;\n position: sticky;\n /* --z-sticky (tokens-core.css, issue #29) — never a bare z-index\n number in consumer CSS. */\n z-index: var(--z-sticky);\n top: var(--space-6);\n width: 100%;\n max-height: calc(100vh - var(--space-8));\n overflow-y: auto;\n }\n}\n",
|
|
48
|
+
"type": "remarque:css"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "usage.html",
|
|
52
|
+
"content": "<article class=\"remarque-essay\">\n <header class=\"content-reading\">\n <h1 class=\"text-display font-display\">Essay Title</h1>\n </header>\n\n <nav class=\"remarque-toc-rail\" aria-label=\"Table of contents\">\n <details open>\n <summary>On this page</summary>\n <ul>\n <li><a href=\"#section-one\">Section One</a></li>\n <li><a href=\"#section-two\">Section Two</a></li>\n </ul>\n </details>\n </nav>\n\n <div class=\"remarque-prose content-reading\">\n <h2 id=\"section-one\">Section One</h2>\n <p>\n A claim worth a citation<a href=\"#note-1\" id=\"note-1-ref\"\n class=\"remarque-sidenote-ref\" aria-label=\"Note 1\"\n aria-describedby=\"note-1\"></a>.\n </p>\n <aside class=\"remarque-sidenote\" id=\"note-1\" role=\"note\">\n The note text, in real DOM order right after the paragraph that\n cites it — not collected into an end-of-document section.\n </aside>\n </div>\n\n <footer class=\"content-reading\">...</footer>\n</article>\n",
|
|
53
|
+
"type": "remarque:markup"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/registry-item.schema.json",
|
|
3
|
+
"name": "forms",
|
|
4
|
+
"type": "remarque:contract",
|
|
5
|
+
"title": "Forms Primitives",
|
|
6
|
+
"description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
|
|
7
|
+
"version": "0.23.0",
|
|
8
|
+
"integrity": "sha256-Ft+jRmZtY6HBdTCFoIIJbM3S/3dx7iSVl7NRWD/40rY=",
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"remarque-tokens"
|
|
11
|
+
],
|
|
12
|
+
"cssVars": [
|
|
13
|
+
"--border-style",
|
|
14
|
+
"--border-width",
|
|
15
|
+
"--color-accent",
|
|
16
|
+
"--color-accent-hover",
|
|
17
|
+
"--color-accent-subtle",
|
|
18
|
+
"--color-bg",
|
|
19
|
+
"--color-bg-subtle",
|
|
20
|
+
"--color-border",
|
|
21
|
+
"--color-border-bold",
|
|
22
|
+
"--color-disabled",
|
|
23
|
+
"--color-error",
|
|
24
|
+
"--color-fg",
|
|
25
|
+
"--color-fg-muted",
|
|
26
|
+
"--color-muted",
|
|
27
|
+
"--color-success",
|
|
28
|
+
"--color-warning",
|
|
29
|
+
"--content-reading",
|
|
30
|
+
"--font-body",
|
|
31
|
+
"--font-mono",
|
|
32
|
+
"--leading-meta",
|
|
33
|
+
"--leading-title",
|
|
34
|
+
"--motion-easing",
|
|
35
|
+
"--motion-fast",
|
|
36
|
+
"--radius-sm",
|
|
37
|
+
"--space-2",
|
|
38
|
+
"--space-3",
|
|
39
|
+
"--space-4",
|
|
40
|
+
"--space-5",
|
|
41
|
+
"--text-body",
|
|
42
|
+
"--text-meta",
|
|
43
|
+
"--tracking-caps",
|
|
44
|
+
"--tracking-meta",
|
|
45
|
+
"--weight-medium",
|
|
46
|
+
"--weight-semibold"
|
|
47
|
+
],
|
|
48
|
+
"docs": "https://github.com/williamzujkowski/remarque/blob/main/REMARQUE.md#forms",
|
|
49
|
+
"files": [
|
|
50
|
+
{
|
|
51
|
+
"path": "forms.css",
|
|
52
|
+
"content": "/*\n * Remarque — Form Control Primitives (issue #27)\n * ────────────────────────────────────────────────\n * Namespaced classes that style native form elements — no fake replacement\n * widgets. A `<select>` is a `<select>`, a checkbox stays a checkbox with\n * `accent-color` doing the recoloring work a browser already supports\n * natively. This module exists because Remarque is editorial-first: forms\n * are for the contact/search/newsletter moments a written-word site\n * occasionally needs, not a component library for building app UIs. See\n * \"When NOT to use\" in REMARQUE.md's \"Forms\" section before reaching for\n * this on a page that wants more than a handful of fields. Also carries a\n * small standalone-table section (`.remarque-table`) at the end of this\n * file — see that section's header comment for why it lives here rather\n * than in prose.css (issue #30's reference `Table.astro` needs the same\n * table visual language OUTSIDE `.remarque-prose`).\n *\n * PROVENANCE — spec-native, not graduated from a downstream site (unlike\n * essay.css/broadsheet.css). Built directly from issue #27; there was no\n * prior flagship implementation to re-express in token vocabulary, so\n * every value below is original to this module rather than a mapped-over\n * site literal.\n *\n * Own subpath (remarque-tokens/forms, remarque-tokens/forms.css) — NOT\n * included by the tokens.css aggregator, matching essay.css/broadsheet.css/\n * print.css: most Remarque pages have zero forms. Import explicitly, after\n * prose.css if both are in use:\n * @import 'remarque-tokens/prose.css';\n * @import 'remarque-tokens/forms.css';\n * (string form — see AGENT_RULES.md Pitfall #6.)\n *\n * ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────\n *\n * <div class=\"remarque-field\">\n * <label class=\"remarque-field-label\" for=\"email\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email\" name=\"email\"\n * aria-describedby=\"email-help\" />\n * <p class=\"remarque-field-message\" id=\"email-help\">\n * We'll never share your email.\n * </p>\n * </div>\n *\n * <div class=\"remarque-field\" data-state=\"error\">\n * <label class=\"remarque-field-label\" for=\"email2\">Email</label>\n * <input class=\"remarque-input\" type=\"email\" id=\"email2\" name=\"email2\"\n * aria-invalid=\"true\" aria-describedby=\"email2-message\" required />\n * <p class=\"remarque-field-message\" id=\"email2-message\" role=\"alert\">\n * Enter a valid email address.\n * </p>\n * </div>\n *\n * <label class=\"remarque-checkbox\">\n * <input type=\"checkbox\" name=\"subscribe\" />\n * <span>Subscribe to updates</span>\n * </label>\n *\n * <label class=\"remarque-radio\">\n * <input type=\"radio\" name=\"plan\" value=\"a\" />\n * <span>Plan A</span>\n * </label>\n *\n * <button class=\"remarque-button\" type=\"button\">Cancel</button>\n * <button class=\"remarque-button remarque-button--primary\" type=\"submit\">\n * Submit\n * </button>\n *\n * `data-state` lives on `.remarque-field` (not the input) — it recolors\n * both the control's border AND the message text together from one\n * attribute, the same \"one attribute drives several descendants\" shape\n * essay.css's counter-driven numbering uses. `aria-invalid=\"true\"` is the\n * REAL accessibility signal (screen readers announce it); `data-state` is\n * the visual layer on top. Author both together — see \"State wiring\"\n * below for what happens if a consumer only sets one.\n */\n\n/* ─── Field wrapper: label + control + help/error text stack ────────────\n * A vertical rhythm of three parts. `--space-2` between label and control\n * (tight — they read as one unit), `--space-2` again before the message\n * line. No border, no background on the wrapper itself — `.remarque-field`\n * is layout only, never a bordered \"card\" around the control (Components:\n * \"no component should be visually louder than the content it contains\").\n */\n.remarque-field {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n max-width: var(--content-reading);\n}\n\n.remarque-field + .remarque-field {\n margin-top: var(--space-5);\n}\n\n/* Label voice: same declarations as `.text-label` (tokens-core.css),\n repeated rather than depended-on-as-a-mixin — the same non-mixin\n precedent essay.css's TOC summary and broadsheet.css's kicker rows\n establish. True small caps (`font-variant-caps`), never `text-transform:\n uppercase` (REMARQUE.md \"Small Caps\"). Quiet mono metadata voice for the\n label, not the body face — a form label identifies the field, the VALUE\n typed into it is the content, and that distinction is the same one\n `.text-label` already draws for dates/tags/status elsewhere. */\n.remarque-field-label {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n font-variant-caps: all-small-caps;\n letter-spacing: var(--tracking-caps);\n color: var(--color-muted);\n}\n\n/* Required-field marker: a plain asterisk in the accent color is the one\n sanctioned use here (it is unambiguously \"this is the interactive thing\n that needs attention right now\" — not decoration). Author it inside the\n label: <label>Email <span class=\"remarque-field-required\"\n aria-hidden=\"true\">*</span></label> — aria-hidden because `required` on\n the input itself is the real signal a screen reader announces. */\n.remarque-field-required {\n color: var(--color-accent);\n}\n\n/* Help/error/success message line — meta voice, body-face-adjacent size\n floor (--text-meta, 14px, clears the USWDS small-text minimum). Neutral\n (muted) by default; recolored by `.remarque-field[data-state]` below.\n `text-wrap: pretty` matches `.remarque-prose`'s optical default for\n short justified-feeling message copy. */\n.remarque-field-message {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n line-height: var(--leading-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n margin: 0;\n text-wrap: pretty;\n}\n\n/* ─── Text inputs: input / textarea / select ─────────────────────────────\n * One shared class across all three native elements — same border,\n * radius, spacing, and body-voice typography (Font Slots: \"Body ... UI\n * labels, navigation, buttons\" — form values are UI labels' closest\n * relative here, not metadata). `--color-border-bold`, not `-bold`'s\n * quieter sibling: an input's boundary is a FUNCTIONAL edge (WCAG 1.4.11\n * non-text contrast), the same reasoning `.remarque-sidenote`'s narrow-\n * viewport border and `.remarque-toc-rail`'s hairline both already apply\n * decoratively-vs-functionally in essay.css.\n */\n.remarque-input {\n display: block;\n width: 100%;\n min-height: 44px; /* USWDS/WCAG 2.5.5 touch-target floor — see AGENT_RULES.md */\n padding: var(--space-2) var(--space-3);\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: var(--color-bg);\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n /* Restraint rule (AGENT_RULES.md \"Border radius never exceeds\n --radius-md\" + this module's own tighter floor): form controls stay\n at --radius-sm, never the 8px ceiling other chrome is allowed. A\n smaller control reads as more precise at a smaller radius. */\n border-radius: var(--radius-sm);\n transition: border-color var(--motion-fast) var(--motion-easing);\n}\n\ntextarea.remarque-input {\n min-height: 6rem; /* several lines tall by default — still clears the 44px floor */\n resize: vertical;\n}\n\n/* Native select's disclosure triangle is left as the platform default —\n restyling it means either `appearance: none` (which then requires a\n hand-drawn replacement icon, exactly the \"fake replacement\" this module\n avoids) or accepting a single browser's rendering as the reference,\n neither of which is worth it for one triangle. */\n.remarque-input:hover {\n border-color: var(--color-fg-muted);\n}\n\n/* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used\n everywhere else at this contrast tier — a conscious choice, not the\n browser default (which is often far below AA). Placeholder text is\n supplementary (never the only label — every `.remarque-input` pairs\n with a real `<label>`), so 4.5:1 rather than `--color-fg-muted`'s 7:1\n is the correct tier: AA is sufficient for non-essential hint copy. */\n.remarque-input::placeholder {\n color: var(--color-muted);\n opacity: 1; /* Firefox lowers placeholder opacity by default; pin it so the token value is what actually renders */\n}\n\n.remarque-input:disabled {\n color: var(--color-disabled);\n background-color: var(--color-bg-subtle);\n border-color: var(--color-border);\n cursor: not-allowed;\n}\n\n/* ─── Checkbox / Radio — accent-color, no fake replacements ──────────────\n * `accent-color` is the ENTIRE recoloring mechanism: no `appearance:\n * none`, no hand-drawn checkmark/dot, no absolutely-positioned pseudo-\n * element standing in for the native control. This is a deliberate\n * ceiling on customization, not an oversight — every browser Remarque\n * targets supports `accent-color` on checkboxes/radios, and a real native\n * control keeps every platform accessibility behavior (keyboard,\n * high-contrast mode, forced-colors mode, screen-reader state\n * announcements) for free, at the cost of not controlling the checkmark\n * glyph's exact shape.\n *\n * Sizing: --space-5 (1.5rem / 24px) — comfortably inside the 44px touch\n * target once the wrapping <label> below adds its own min-height, and\n * large enough to keep the accent-colored fill legible (a 16px default\n * checkbox reads as fussy at this system's generous spacing scale).\n */\n.remarque-checkbox,\n.remarque-radio {\n display: inline-flex;\n align-items: center;\n gap: var(--space-2);\n min-height: 44px; /* touch-target floor on the whole label, same shape as .nav-link */\n font-family: var(--font-body);\n font-size: var(--text-body);\n line-height: var(--leading-title);\n color: var(--color-fg);\n cursor: pointer;\n}\n\n.remarque-checkbox input,\n.remarque-radio input {\n accent-color: var(--color-accent);\n width: var(--space-5);\n height: var(--space-5);\n margin: 0;\n flex-shrink: 0;\n}\n\n/* `:has()` is a light progressive enhancement here, not a functional\n dependency — a browser without it still gets a fully working, natively\n disabled control; it just skips the muted label-text recolor. */\n.remarque-checkbox:has(input:disabled),\n.remarque-radio:has(input:disabled) {\n color: var(--color-disabled);\n cursor: not-allowed;\n}\n\n.remarque-checkbox input:disabled,\n.remarque-radio input:disabled {\n cursor: not-allowed;\n}\n\n/* ─── Buttons ─────────────────────────────────────────────────────────────\n * Quiet by rule (AGENT_RULES.md Components: \"Buttons are quiet: text-only\n * or subtle bordered. Never filled/solid as default.\"). The default\n * variant is bordered, transparent background, body-voice text — reads as\n * \"a clickable label,\" not a colored block. `--primary` is the ONE\n * sanctioned accent placement per viewport (Visual Rules: accent for\n * \"inline links and one interactive element per viewport\") — even there,\n * it stays unfilled: accent text + accent border, never a solid accent\n * fill, so the same restraint the default variant holds carries through\n * to the emphasized one. Hover on `--primary` washes in\n * `--color-accent-subtle` (the same quiet-highlight token\n * `.remarque-prose mark` already uses) rather than darkening to a filled\n * block — a wash, not a fill.\n */\n.remarque-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-2);\n min-height: 44px;\n padding: var(--space-2) var(--space-4);\n font-family: var(--font-body);\n font-size: var(--text-body);\n font-weight: var(--weight-medium);\n line-height: var(--leading-title);\n color: var(--color-fg);\n background-color: transparent;\n border: var(--border-width) var(--border-style) var(--color-border-bold);\n border-radius: var(--radius-sm);\n cursor: pointer;\n transition:\n color var(--motion-fast) var(--motion-easing),\n border-color var(--motion-fast) var(--motion-easing),\n background-color var(--motion-fast) var(--motion-easing);\n}\n\n.remarque-button:hover:not(:disabled) {\n border-color: var(--color-fg-muted);\n color: var(--color-fg);\n}\n\n.remarque-button--primary {\n color: var(--color-accent);\n border-color: var(--color-accent);\n}\n\n.remarque-button--primary:hover:not(:disabled) {\n color: var(--color-accent-hover);\n border-color: var(--color-accent-hover);\n background-color: var(--color-accent-subtle);\n}\n\n.remarque-button:disabled {\n color: var(--color-disabled);\n border-color: var(--color-border);\n background-color: transparent;\n cursor: not-allowed;\n}\n\n/* ─── Validation state wiring (0.17.0 state-color tokens) ────────────────\n * `data-state` lives on `.remarque-field`, cascading down to the input's\n * border and the message line's color together — author it alongside a\n * real `aria-invalid=\"true\"`/`aria-describedby` pair on the input (the\n * markup contract above); `data-state` alone changes only the paint,\n * `aria-invalid` is what a screen reader actually announces. `--warning`\n * covers a caution/needs-attention message that isn't yet a hard failure\n * (REMARQUE.md \"State Colors\" — \"states needing attention before\n * proceeding\"), e.g. a password-strength hint. No `-subtle` background is\n * used here: `-subtle` is sized for banner/callout backgrounds (REMARQUE.md\n * \"State Colors\"), and painting a per-field background would make the\n * field louder than the content it holds (Components rule) — border +\n * message-text color is enough signal at this scale.\n */\n.remarque-field[data-state=\"error\"] .remarque-input,\n.remarque-input[aria-invalid=\"true\"] {\n border-color: var(--color-error);\n}\n\n.remarque-field[data-state=\"error\"] .remarque-field-message {\n color: var(--color-error);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-input {\n border-color: var(--color-success);\n}\n\n.remarque-field[data-state=\"success\"] .remarque-field-message {\n color: var(--color-success);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-input {\n border-color: var(--color-warning);\n}\n\n.remarque-field[data-state=\"warning\"] .remarque-field-message {\n color: var(--color-warning);\n}\n\n/* `:user-invalid` — native, zero-JS validity feedback (matches only after\n the user has interacted with the field and left it invalid, unlike\n `:invalid`, which can match a required-but-untouched field on page\n load). Guarded with `@supports selector(...)`: a browser lacking the\n pseudo-class simply never matches this block and falls back to\n whatever `data-state`/`aria-invalid` already provide — the same\n \"degrades to the no-JS/no-support baseline\" shape essay.css's `@media`\n gate uses for its own progressive enhancement. This is a bonus layer,\n not a replacement for authoring `data-state=\"error\"` explicitly. */\n@supports selector(:user-invalid) {\n .remarque-input:user-invalid {\n border-color: var(--color-error);\n }\n}\n\n/* ─── Forced-colors reinforcement (issue #93) ────────────────────────────\n * The validation-state wiring above is the textbook color-only-affordance\n * case the issue names explicitly: error/success/warning are distinguished\n * ENTIRELY by `border-color`/`color`, both of which `forced-colors: active`\n * remaps to a system color regardless of which state authored them — so\n * all three states (and the plain, untouched control) would render\n * IDENTICALLY under forced colors, exactly the failure `aria-invalid` +\n * `role=\"alert\"` already prevent for screen-reader users but do nothing\n * for a sighted low-vision user in Windows High Contrast Mode.\n *\n * Fix is geometry, not color, since color is exactly what forced-colors\n * discards: each state gets a distinct `border-style`/`border-width`\n * combination on the control itself (a shape that reads under ANY forced\n * system border color), plus a small glyph prefixed to the message line\n * — scoped to this media query only, so the quiet default (message text\n * alone) is unchanged everywhere else. The glyph is supplementary, not\n * the primary signal (a screen reader user already gets `aria-invalid` +\n * the message text; a sighted forced-colors user gets the border shape);\n * it is not marked `aria-hidden` because CSS generated content cannot\n * carry that attribute, but it is inert in the accessibility tree by\n * default in every current UA/AT combination tested (Chromium+NVDA,\n * Chromium+VoiceOver) — it is not read as if it were message content.\n */\n@media (forced-colors: active) {\n .remarque-field[data-state=\"error\"] .remarque-input,\n .remarque-input[aria-invalid=\"true\"] {\n border-style: double;\n border-width: 3px;\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-input {\n border-style: dashed;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"success\"] .remarque-input {\n border-style: solid;\n border-width: 2px;\n }\n\n .remarque-field[data-state=\"error\"] .remarque-field-message::before {\n content: \"\\2715\\0020\"; /* ✕ */\n }\n\n .remarque-field[data-state=\"warning\"] .remarque-field-message::before {\n content: \"\\26A0\\0020\"; /* ⚠ */\n }\n\n .remarque-field[data-state=\"success\"] .remarque-field-message::before {\n content: \"\\2713\\0020\"; /* ✓ */\n }\n}\n\n/* ─── Standalone data tables (issue #30) ──────────────────────────────────\n * `.remarque-table` re-scopes prose.css's table rules (mono `th` voice,\n * 2px `border-bottom` under the header row, 1px row rules only, `.num`\n * for tabular-lining right-aligned numeric columns) under a top-level\n * class instead of `.remarque-prose table`. This is intentionally a\n * near-verbatim copy of those declarations, not a shared mixin (CSS has\n * none) and not a requirement to wrap a standalone data table in the full\n * `.remarque-prose` container — `.remarque-prose` also applies oldstyle\n * proportional numerals, `> * + *` paragraph-spacing rhythm, and prose\n * max-width, none of which a reference data grid (e.g. `Table.astro`)\n * wants. Living in forms.css rather than a new subpath: this module is\n * already \"the supplementary-UI module\" in AGENT_RULES.md's build order\n * (step 7 groups buttons, tags, cards, code blocks, AND tables together),\n * and a table this small does not justify its own subpath the way the\n * Essay/Broadsheet modules' much larger surface area did.\n *\n * Wrap a wide table exactly like prose.css's convention:\n * <div class=\"remarque-table-wrap\"><table class=\"remarque-table\">...</table></div>\n */\n.remarque-table-wrap {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.remarque-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--text-body);\n}\n\n.remarque-table caption {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n color: var(--color-muted);\n text-align: left;\n caption-side: top;\n margin-bottom: var(--space-2);\n}\n\n.remarque-table th {\n font-family: var(--font-mono);\n font-size: var(--text-meta);\n letter-spacing: var(--tracking-meta);\n font-weight: var(--weight-semibold);\n text-align: left;\n padding: var(--space-2) var(--space-3);\n border-bottom: 2px solid var(--color-border-bold);\n color: var(--color-fg);\n}\n\n.remarque-table td {\n font-family: var(--font-body);\n font-size: var(--text-body);\n padding: var(--space-2) var(--space-3);\n border-bottom: var(--border-width) var(--border-style) var(--color-border);\n color: var(--color-fg);\n}\n\n.remarque-table th.num,\n.remarque-table td.num {\n font-variant-numeric: tabular-nums lining-nums;\n text-align: right;\n}\n",
|
|
53
|
+
"type": "remarque:css"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"path": "usage.html",
|
|
57
|
+
"content": "<div class=\"remarque-field\">\n <label class=\"remarque-field-label\" for=\"email\">Email</label>\n <input class=\"remarque-input\" type=\"email\" id=\"email\" name=\"email\"\n aria-describedby=\"email-help\" />\n <p class=\"remarque-field-message\" id=\"email-help\">\n We'll never share your email.\n </p>\n</div>\n\n<div class=\"remarque-field\" data-state=\"error\">\n <label class=\"remarque-field-label\" for=\"email2\">Email</label>\n <input class=\"remarque-input\" type=\"email\" id=\"email2\" name=\"email2\"\n aria-invalid=\"true\" aria-describedby=\"email2-message\" required />\n <p class=\"remarque-field-message\" id=\"email2-message\" role=\"alert\">\n Enter a valid email address.\n </p>\n</div>\n\n<label class=\"remarque-checkbox\">\n <input type=\"checkbox\" name=\"subscribe\" />\n <span>Subscribe to updates</span>\n</label>\n\n<label class=\"remarque-radio\">\n <input type=\"radio\" name=\"plan\" value=\"a\" />\n <span>Plan A</span>\n</label>\n\n<button class=\"remarque-button\" type=\"button\">Cancel</button>\n<button class=\"remarque-button remarque-button--primary\" type=\"submit\">Submit</button>\n",
|
|
58
|
+
"type": "remarque:markup"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/registry-item.schema.json",
|
|
3
|
+
"name": "palette-deck",
|
|
4
|
+
"type": "remarque:contract",
|
|
5
|
+
"title": "Palette Deck (markup contract only — no executable content)",
|
|
6
|
+
"description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
|
|
7
|
+
"version": "0.23.0",
|
|
8
|
+
"integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"remarque-tokens"
|
|
11
|
+
],
|
|
12
|
+
"cssVars": [],
|
|
13
|
+
"docs": "https://github.com/williamzujkowski/remarque/blob/main/REMARQUE.md#palette-deck",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "usage.html",
|
|
17
|
+
"content": "<html data-theme=\"dark\" data-palette=\"gruvbox\">\n",
|
|
18
|
+
"type": "remarque:markup"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://williamzujkowski.github.io/remarque/registry-item.schema.json",
|
|
4
|
+
"title": "Remarque registry item",
|
|
5
|
+
"description": "JSON Schema for one item in Remarque's machine-readable markup-contract registry (registry/<name>.json) — GENERATED by scripts/build-registry.mjs, do not hand-edit. Vendored and adapted from shadcn/ui's registry-item.json (https://ui.shadcn.com/schema/registry-item.json): field NAMES that fit a CSS+HTML markup contract are kept (name, type, title, description, dependencies, registryDependencies, cssVars, docs, files[].path/content/type); fields specific to installable React component registries (tailwind, css, envVars, meta, font, extends/style/iconLibrary/baseColor/theme, files[].target) are omitted because nothing here is installed by a CLI — see REMARQUE.md \"The Registry\" for the full per-field adaptation/omission rationale. `type` does not reuse shadcn's registry:* enum (registry:lib/component/ui/hook/page/...) — none of those describe a dependency-free CSS module plus its known-good markup sample, so this schema mints one honest value instead of stretching an ill-fitting one. `cssVars` and `docs` keep shadcn's field NAME but narrow its MEANING (documented per-property below) rather than its shape, so this stays one schema, not a parallel one.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["name", "type", "title", "description", "version", "integrity", "dependencies", "cssVars", "docs", "files"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": { "type": "string" },
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]*$",
|
|
14
|
+
"description": "Unique kebab-case identifier — matches the registry/<name>.json filename and the module/pattern's own subpath name (essay, broadsheet, forms, palette-deck)."
|
|
15
|
+
},
|
|
16
|
+
"type": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"enum": ["remarque:contract"],
|
|
19
|
+
"description": "Adapted from shadcn's per-item `type`. Every Remarque registry item is the same kind of thing — a versioned, hash-pinned pairing of a CSS module (if any) and its known-good markup usage sample — so one value covers all items today; a second value would only be minted for a future item that is genuinely a different KIND of contract, not just a different module."
|
|
20
|
+
},
|
|
21
|
+
"title": { "type": "string", "description": "Human-readable title. Required here (optional in shadcn's schema) — every Remarque item is a public artifact, not local dev scaffolding." },
|
|
22
|
+
"description": { "type": "string", "description": "One- or two-sentence summary of what the module/pattern is for." },
|
|
23
|
+
"version": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "The remarque-tokens package version this item was generated from — SECURITY: pinned so a consumer can assert freshness/provenance against a known release, not an unversioned floating fetch."
|
|
26
|
+
},
|
|
27
|
+
"integrity": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"pattern": "^sha256-[A-Za-z0-9+/]+=*$",
|
|
30
|
+
"description": "SECURITY: sha256 content hash of this item's files (scripts/lib/registry-extract.mjs's sha256OfFiles, same sha256-<base64> shape as W3C Subresource Integrity), computed at build time. A consumer that fetches this item over HTTPS can recompute the hash over `files` and refuse to apply the payload if it doesn't match — the same trust model as any other SRI-pinned fetch."
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "string" },
|
|
35
|
+
"description": "NPM package(s) this item's CSS assumes is already loaded. Every item lists \"remarque-tokens\" here — the design-tokens package (tokens-core.css/tokens-palette.css) this item's `cssVars` consume is modeled as this ordinary npm dependency, not as a fifth registry item and not as a registryDependencies entry: tokens.json/tokens.schema.json already exist as a separate, purpose-built machine-readable artifact (issue #99), and a registry item that re-embedded the full token set would duplicate that artifact rather than single-source it."
|
|
36
|
+
},
|
|
37
|
+
"registryDependencies": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": { "type": "string" },
|
|
40
|
+
"description": "Adapted from shadcn's field of the same name (other registry items this one depends on). Empty/omitted for every current item — essay/broadsheet/forms/palette-deck are independent opt-ins with no inter-item dependency, per REMARQUE.md's Essay Module and Broadsheet \"When to use\" sections."
|
|
41
|
+
},
|
|
42
|
+
"cssVars": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": { "type": "string", "pattern": "^--[a-z0-9-]+$" },
|
|
45
|
+
"description": "DIVERGENCE from shadcn: shadcn's `cssVars` holds {theme,light,dark} VALUES to merge into a consumer's stylesheet. This registry ships whole CSS files verbatim in `files` instead (single-sourced from the real .css, never a second hand-copied value), so there is nothing to merge — `cssVars` here is repurposed as the flat, deduped list of palette-tier `--color-*`/other custom-property NAMES this item's CSS actually reads (derived mechanically via `var(...)` grep over the shipped CSS, never hand-maintained). Empty for palette-deck, which ships no CSS of its own."
|
|
46
|
+
},
|
|
47
|
+
"docs": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^https://",
|
|
50
|
+
"description": "DIVERGENCE from shadcn: shadcn's `docs` is a markdown blob. This registry's `docs` is a single HTTPS URL into the relevant REMARQUE.md section (SECURITY: https:// only, checked by scripts/test-registry.mjs) — the registry deliberately does not duplicate spec prose; it points back at the one place that prose lives."
|
|
51
|
+
},
|
|
52
|
+
"files": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"minItems": 1,
|
|
55
|
+
"items": { "$ref": "#/$defs/file" },
|
|
56
|
+
"description": "The payload. Adapted from shadcn's `files` (path/content/type kept; `target` omitted — see the top-level description) — every entry is inline HTML/CSS text data. SECURITY (blocking): no entry may contain executable content; scripts/test-registry.mjs asserts no file's content contains a literal `<script` substring, across every item without exception."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"$defs": {
|
|
60
|
+
"file": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"required": ["path", "content", "type"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"path": { "type": "string", "description": "Path relative to this item's own directory, e.g. \"essay.css\" or \"usage.html\"." },
|
|
66
|
+
"content": { "type": "string", "description": "The file's full, verbatim text content — for a CSS file, read from the real .css at build time (never duplicated by hand); for usage.html, extracted from REMARQUE.md's own fenced markup-contract sample via a `registry-usage:<name>` marker (scripts/lib/registry-extract.mjs)." },
|
|
67
|
+
"type": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"enum": ["remarque:css", "remarque:markup"],
|
|
70
|
+
"description": "DIVERGENCE from shadcn: shadcn's per-file `type` reuses the same registry:* vocabulary as the item-level `type`, for CLI install-target resolution. There is no installer here, so this names what the file actually IS instead — a whole CSS module (remarque:css) or an HTML markup-contract sample (remarque:markup)."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://williamzujkowski.github.io/remarque/registry.schema.json",
|
|
3
|
+
"name": "remarque-tokens",
|
|
4
|
+
"homepage": "https://williamzujkowski.github.io/remarque/",
|
|
5
|
+
"version": "0.23.0",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"name": "essay",
|
|
9
|
+
"type": "remarque:contract",
|
|
10
|
+
"title": "Essay Module",
|
|
11
|
+
"description": "Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label=\"Note N\"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).",
|
|
12
|
+
"version": "0.23.0",
|
|
13
|
+
"integrity": "sha256-zr04H0+0xf01OWLk6B2A9ziZOYJd4Sq5q5/cib65lbU=",
|
|
14
|
+
"file": "registry/essay.json"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "broadsheet",
|
|
18
|
+
"type": "remarque:contract",
|
|
19
|
+
"title": "Broadsheet Pattern",
|
|
20
|
+
"description": "The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.",
|
|
21
|
+
"version": "0.23.0",
|
|
22
|
+
"integrity": "sha256-BZea38PRdob4OfmdGN0eDw3U0adl7SEsdrjKgSewK9A=",
|
|
23
|
+
"file": "registry/broadsheet.json"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "forms",
|
|
27
|
+
"type": "remarque:contract",
|
|
28
|
+
"title": "Forms Primitives",
|
|
29
|
+
"description": "Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.",
|
|
30
|
+
"version": "0.23.0",
|
|
31
|
+
"integrity": "sha256-Ft+jRmZtY6HBdTCFoIIJbM3S/3dx7iSVl7NRWD/40rY=",
|
|
32
|
+
"file": "registry/forms.json"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "palette-deck",
|
|
36
|
+
"type": "remarque:contract",
|
|
37
|
+
"title": "Palette Deck (markup contract only — no executable content)",
|
|
38
|
+
"description": "Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.",
|
|
39
|
+
"version": "0.23.0",
|
|
40
|
+
"integrity": "sha256-WGgBZ2j0VnLPRAZlzv/4G1Wu9rKqTdXf1536dWWu8nU=",
|
|
41
|
+
"file": "registry/palette-deck.json"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://williamzujkowski.github.io/remarque/registry.schema.json",
|
|
4
|
+
"title": "Remarque markup-contract registry index",
|
|
5
|
+
"description": "JSON Schema for registry.json — GENERATED by scripts/build-registry.mjs, do not hand-edit. Vendored and adapted from shadcn/ui's registry.json (https://ui.shadcn.com/schema/registry.json): `name`/`homepage`/`items` are kept; `include` (chunked multi-file registries) is omitted — Remarque's registry is small enough to stay one index file. DIVERGENCE from shadcn: shadcn's `items` embeds each FULL registry-item object inline. This index instead lists a lightweight pointer per item (name/type/title/description/version/integrity/file) and leaves the full payload — including all file content — in registry/<name>.json only. Embedding full CSS/HTML content in both the index and the per-item file would mean two copies of the same bytes that could silently drift; the index exists so a consumer can discover what's available and its pinned version+hash WITHOUT fetching every item's full payload, then fetch only the item(s) it needs.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["$schema", "name", "homepage", "version", "items"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": { "type": "string" },
|
|
11
|
+
"name": { "type": "string", "description": "The registry name — \"remarque-tokens\"." },
|
|
12
|
+
"homepage": { "type": "string", "pattern": "^https://", "description": "SECURITY: HTTPS-only, checked by scripts/test-registry.mjs." },
|
|
13
|
+
"version": { "type": "string", "description": "The remarque-tokens package version this index was generated from. Every entry in `items` carries this same version (scripts/test-registry.mjs asserts they match) — the whole registry is generated in one pass from one package version, never a mix." },
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"minItems": 1,
|
|
17
|
+
"items": { "$ref": "#/$defs/indexEntry" }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"$defs": {
|
|
21
|
+
"indexEntry": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["name", "type", "title", "description", "version", "integrity", "file"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
27
|
+
"type": { "type": "string", "enum": ["remarque:contract"] },
|
|
28
|
+
"title": { "type": "string" },
|
|
29
|
+
"description": { "type": "string" },
|
|
30
|
+
"version": { "type": "string", "description": "SECURITY: pinned version, mirrors the full item's `version` at `file`." },
|
|
31
|
+
"integrity": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^sha256-[A-Za-z0-9+/]+=*$",
|
|
34
|
+
"description": "SECURITY: mirrors the full item's `integrity` at `file` — a consumer can check this before deciding whether to fetch the full payload at all, and must recheck it against the fetched payload either way."
|
|
35
|
+
},
|
|
36
|
+
"file": { "type": "string", "pattern": "^registry/[a-z0-9-]+\\.json$", "description": "Path (relative to this index) to the full registry item — registry/<name>.json." }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Generates registry.json (the index) + registry/<name>.json (the four
|
|
4
|
+
* full items) — Remarque's machine-readable markup-contract registry,
|
|
5
|
+
* shaped like shadcn's registry-item.json/registry.json (issue #100). See
|
|
6
|
+
* registry-item.schema.json / registry.schema.json for the field-by-field
|
|
7
|
+
* adaptation from shadcn, and REMARQUE.md's "The Registry" section for the
|
|
8
|
+
* prose version of the same argument.
|
|
9
|
+
*
|
|
10
|
+
* node scripts/build-registry.mjs # (re)write registry.json + registry/*.json
|
|
11
|
+
* node scripts/build-registry.mjs --check # exit 1 if either is stale (CI)
|
|
12
|
+
*
|
|
13
|
+
* Single-sourcing: every item's CSS content is read verbatim from the real
|
|
14
|
+
* .css file at build time (never duplicated by hand); every item's
|
|
15
|
+
* usage.html is extracted from REMARQUE.md's own "Markup contract" fenced
|
|
16
|
+
* samples via a `registry-usage:<name>` marker comment (see
|
|
17
|
+
* scripts/lib/registry-extract.mjs) — REMARQUE.md's prose stays the one
|
|
18
|
+
* hand-authored copy of each known-good sample.
|
|
19
|
+
*
|
|
20
|
+
* Mandatory scope is the four modules named in issue #100 (essay,
|
|
21
|
+
* broadsheet, forms, palette-deck) — the per-archetype "starter-page"
|
|
22
|
+
* items floated as an IF-cheap extra are SKIPPED this round: archetype
|
|
23
|
+
* pages live as demo-specific Astro files (site/src/pages/**), not as a
|
|
24
|
+
* reusable module with its own CSS + one canonical markup sample the way
|
|
25
|
+
* these four are, so extracting one would mean inventing a new kind of
|
|
26
|
+
* "canonical" sample rather than single-sourcing an existing one. Revisit
|
|
27
|
+
* if/when an archetype grows a dedicated, package-shipped reference markup
|
|
28
|
+
* block the way essay/broadsheet/forms/palette-deck already have.
|
|
29
|
+
*
|
|
30
|
+
* SECURITY (blocking, panel-mandated): pinned version + sha256 integrity
|
|
31
|
+
* per item (scripts/lib/registry-extract.mjs's sha256OfFiles); every docs/
|
|
32
|
+
* homepage URL is HTTPS; no executable content — deck.js (the one JS asset
|
|
33
|
+
* this package ships) is deliberately EXCLUDED from the registry entirely,
|
|
34
|
+
* not merely hashed-and-included, and palette-deck's usage.html omits even
|
|
35
|
+
* the FOUC-restore <script> sample that REMARQUE.md documents alongside it
|
|
36
|
+
* — see the palette-deck item's `description` below and REMARQUE.md "The
|
|
37
|
+
* Registry" for the full argument.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
41
|
+
import { extractUsageHtml, sha256OfFiles, cssVarsOf } from './lib/registry-extract.mjs';
|
|
42
|
+
|
|
43
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
44
|
+
const version = pkg.version;
|
|
45
|
+
const remarqueMd = readFileSync('REMARQUE.md', 'utf8');
|
|
46
|
+
|
|
47
|
+
const SPEC_BASE = 'https://github.com/williamzujkowski/remarque/blob/main/REMARQUE.md';
|
|
48
|
+
|
|
49
|
+
function cssFile(path) {
|
|
50
|
+
return { path, content: readFileSync(path, 'utf8'), type: 'remarque:css' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function markupFile(name) {
|
|
54
|
+
return { path: 'usage.html', content: extractUsageHtml(remarqueMd, name), type: 'remarque:markup' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ITEM_DEFS = [
|
|
58
|
+
{
|
|
59
|
+
name: 'essay',
|
|
60
|
+
title: 'Essay Module',
|
|
61
|
+
description:
|
|
62
|
+
'Sidenotes + sticky TOC rail for long-form Essay pages: margin notes relocated into real DOM order, right after the paragraph that cites them, numbered by CSS counter and labeled `aria-label="Note N"` in the same order the counter advances — the mechanism that prevents the sidenote aria-label/DOM-order transcription bug found in the flagship migration (#89, williamzujkowski.github.io#380).',
|
|
63
|
+
docs: `${SPEC_BASE}#essay-module`,
|
|
64
|
+
cssPaths: ['essay.css'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'broadsheet',
|
|
68
|
+
title: 'Broadsheet Pattern',
|
|
69
|
+
description:
|
|
70
|
+
'The editorial Landing/archive pattern: masthead, lead article, numbered entry list, and post-header kicker. Entry numerals are generated from `data-entry-number` via `attr()`, never `counter()`, and every kicker/dateline row is true `font-variant-caps: all-small-caps`, never `text-transform: uppercase`.',
|
|
71
|
+
docs: `${SPEC_BASE}#broadsheet`,
|
|
72
|
+
cssPaths: ['broadsheet.css'],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'forms',
|
|
76
|
+
title: 'Forms Primitives',
|
|
77
|
+
description:
|
|
78
|
+
'Native field/input/checkbox/radio/button primitives with state-color wiring. Every `.remarque-input` pairs `for`/`id` with its `.remarque-field-label`, and every `.remarque-field-message` is wired via `aria-describedby` on the control — the paint-layer `data-state` attribute is never the only signal; `aria-invalid`/`aria-describedby` on the input carry the real accessibility contract.',
|
|
79
|
+
docs: `${SPEC_BASE}#forms`,
|
|
80
|
+
cssPaths: ['forms.css'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'palette-deck',
|
|
84
|
+
title: 'Palette Deck (markup contract only — no executable content)',
|
|
85
|
+
description:
|
|
86
|
+
'Markup/wiring contract for the Palette Deck: set `data-palette` on the same root element as `data-theme` and the two compose independently. This item ships ONLY that HTML fragment. `deck.js` — the runtime module itself, a dependency-free ~60-line ESM file — is deliberately NOT embedded here: the registry’s blocking no-executable-content condition is held to an unambiguous zero-`<script>`-tag bar across every item, so neither `deck.js` nor the FOUC-restore `<script>` sample documented alongside it in REMARQUE.md is included. Consume `deck.js` the normal way, via the `remarque-tokens/deck` package import (`dependencies` below); read the FOUC-restore snippet from the `docs` URL.',
|
|
87
|
+
docs: `${SPEC_BASE}#palette-deck`,
|
|
88
|
+
cssPaths: [],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
mkdirSync('registry', { recursive: true });
|
|
93
|
+
|
|
94
|
+
const indexItems = [];
|
|
95
|
+
const itemFiles = {}; // name -> { path, json }
|
|
96
|
+
|
|
97
|
+
for (const def of ITEM_DEFS) {
|
|
98
|
+
const files = [...def.cssPaths.map(cssFile), markupFile(def.name)];
|
|
99
|
+
const allCss = def.cssPaths.map((p) => readFileSync(p, 'utf8')).join('\n');
|
|
100
|
+
|
|
101
|
+
const item = {
|
|
102
|
+
$schema: 'https://williamzujkowski.github.io/remarque/registry-item.schema.json',
|
|
103
|
+
name: def.name,
|
|
104
|
+
type: 'remarque:contract',
|
|
105
|
+
title: def.title,
|
|
106
|
+
description: def.description,
|
|
107
|
+
version,
|
|
108
|
+
integrity: sha256OfFiles(files),
|
|
109
|
+
dependencies: ['remarque-tokens'],
|
|
110
|
+
cssVars: cssVarsOf(allCss),
|
|
111
|
+
docs: def.docs,
|
|
112
|
+
files,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const path = `registry/${def.name}.json`;
|
|
116
|
+
const json = JSON.stringify(item, null, 2) + '\n';
|
|
117
|
+
itemFiles[def.name] = { path, json };
|
|
118
|
+
|
|
119
|
+
indexItems.push({
|
|
120
|
+
name: item.name,
|
|
121
|
+
type: item.type,
|
|
122
|
+
title: item.title,
|
|
123
|
+
description: item.description,
|
|
124
|
+
version: item.version,
|
|
125
|
+
integrity: item.integrity,
|
|
126
|
+
file: path,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const registryIndex = {
|
|
131
|
+
$schema: 'https://williamzujkowski.github.io/remarque/registry.schema.json',
|
|
132
|
+
name: 'remarque-tokens',
|
|
133
|
+
homepage: pkg.homepage,
|
|
134
|
+
version,
|
|
135
|
+
items: indexItems,
|
|
136
|
+
};
|
|
137
|
+
const indexJson = JSON.stringify(registryIndex, null, 2) + '\n';
|
|
138
|
+
|
|
139
|
+
if (process.argv.includes('--check')) {
|
|
140
|
+
let stale = [];
|
|
141
|
+
const currentIndex = existsSync('registry.json') ? readFileSync('registry.json', 'utf8') : '';
|
|
142
|
+
if (currentIndex !== indexJson) stale.push('registry.json');
|
|
143
|
+
for (const [name, { path, json }] of Object.entries(itemFiles)) {
|
|
144
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
145
|
+
if (current !== json) stale.push(path);
|
|
146
|
+
}
|
|
147
|
+
if (stale.length) {
|
|
148
|
+
for (const f of stale) console.error(`${f} is stale — run: node scripts/build-registry.mjs`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
console.log(`registry.json + registry/*.json are fresh ✓ (${indexItems.length} items)`);
|
|
152
|
+
} else {
|
|
153
|
+
writeFileSync('registry.json', indexJson);
|
|
154
|
+
for (const { path, json } of Object.values(itemFiles)) writeFileSync(path, json);
|
|
155
|
+
console.log(`registry.json + ${indexItems.length} registry/*.json item(s) written (v${version})`);
|
|
156
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared helpers for scripts/build-registry.mjs and scripts/test-registry.mjs
|
|
3
|
+
* (issue #100 — the machine-readable markup-contract registry).
|
|
4
|
+
*
|
|
5
|
+
* extractUsageHtml() is the single-sourcing mechanism for registry usage.html
|
|
6
|
+
* content: REMARQUE.md's hand-authored "Markup contract" fenced ```html
|
|
7
|
+
* blocks stay the ONE canonical copy of each known-good sample. A marker
|
|
8
|
+
* comment (`<!-- registry-usage:<name> -->`) placed immediately before a
|
|
9
|
+
* fence in REMARQUE.md tells this extractor which fenced block belongs to
|
|
10
|
+
* which registry item — no second copy of the markup is ever hand-authored
|
|
11
|
+
* for the registry, so the spec prose and the registry payload can't drift
|
|
12
|
+
* apart the way two independently maintained copies could.
|
|
13
|
+
*
|
|
14
|
+
* sha256OfFiles() is the integrity-hash algorithm shared by the generator
|
|
15
|
+
* (which computes it) and the test gate (which recomputes it independently
|
|
16
|
+
* and asserts the two agree) — defined once so there is exactly one place
|
|
17
|
+
* that could get the algorithm wrong, not two that could disagree.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find every `<!-- registry-usage:<name> -->` marker in `markdown` and
|
|
24
|
+
* return the content of the ```html fenced block that immediately follows
|
|
25
|
+
* each one, concatenated in document order (blank-line separated). Throws
|
|
26
|
+
* if the marker has no following ```html fence, the fence is unterminated,
|
|
27
|
+
* or the marker doesn't appear at all — a silently-empty usage.html would
|
|
28
|
+
* be a worse failure mode than a loud build error.
|
|
29
|
+
*/
|
|
30
|
+
export function extractUsageHtml(markdown, name) {
|
|
31
|
+
// A word-boundary regex, not a fixed `<!-- registry-usage:NAME -->`
|
|
32
|
+
// string — the marker comments carry trailing explanatory prose (see
|
|
33
|
+
// REMARQUE.md), so only the `registry-usage:<name>` token itself, not
|
|
34
|
+
// the whole comment, is load-bearing. The negative lookahead keeps
|
|
35
|
+
// "essay" from matching inside a longer name that happened to start
|
|
36
|
+
// with it (none do today; this is defense-in-depth, not a fix for an
|
|
37
|
+
// observed collision).
|
|
38
|
+
const markerRe = new RegExp(`registry-usage:${name}(?![a-z0-9-])`, 'g');
|
|
39
|
+
const parts = [];
|
|
40
|
+
let searchFrom = 0;
|
|
41
|
+
while (true) {
|
|
42
|
+
markerRe.lastIndex = searchFrom;
|
|
43
|
+
const m = markerRe.exec(markdown);
|
|
44
|
+
if (!m) break;
|
|
45
|
+
const markerAt = m.index;
|
|
46
|
+
const fenceOpen = markdown.indexOf('```html', markerAt);
|
|
47
|
+
if (fenceOpen === -1) {
|
|
48
|
+
throw new Error(`registry-usage marker "${name}" has no following \`\`\`html fence`);
|
|
49
|
+
}
|
|
50
|
+
const contentStart = markdown.indexOf('\n', fenceOpen) + 1;
|
|
51
|
+
const fenceClose = markdown.indexOf('\n```', contentStart);
|
|
52
|
+
if (fenceClose === -1) {
|
|
53
|
+
throw new Error(`registry-usage marker "${name}": \`\`\`html fence is unterminated`);
|
|
54
|
+
}
|
|
55
|
+
parts.push(markdown.slice(contentStart, fenceClose));
|
|
56
|
+
searchFrom = fenceClose;
|
|
57
|
+
}
|
|
58
|
+
if (parts.length === 0) {
|
|
59
|
+
throw new Error(`no "registry-usage:${name}" marker found in REMARQUE.md`);
|
|
60
|
+
}
|
|
61
|
+
return parts.join('\n\n').trim() + '\n';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Content-addressed integrity hash for a registry item's files, in the same
|
|
66
|
+
* `sha256-<base64>` shape as the W3C Subresource Integrity attribute — a
|
|
67
|
+
* deliberate borrow of an existing web security convention rather than a
|
|
68
|
+
* bespoke format, since the property being asserted (this exact byte
|
|
69
|
+
* content, and no other) is the same one SRI already names.
|
|
70
|
+
*
|
|
71
|
+
* Hashes the ordered file contents joined by a NUL separator (never a
|
|
72
|
+
* legal byte inside either CSS or HTML text here) so two files whose
|
|
73
|
+
* contents happen to concatenate identically under a different join
|
|
74
|
+
* cannot collide.
|
|
75
|
+
*/
|
|
76
|
+
export function sha256OfFiles(files) {
|
|
77
|
+
const hash = createHash('sha256');
|
|
78
|
+
for (const file of files) hash.update(file.content, 'utf8').update('\0');
|
|
79
|
+
return `sha256-${hash.digest('base64')}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every `--custom-property` name referenced via `var(...)` in `css`, deduped and sorted. */
|
|
83
|
+
export function cssVarsOf(css) {
|
|
84
|
+
const names = new Set();
|
|
85
|
+
for (const m of css.matchAll(/var\((--[a-z0-9-]+)/g)) names.add(m[1]);
|
|
86
|
+
return [...names].sort();
|
|
87
|
+
}
|
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.23.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
|
package/tokens.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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.",
|
|
4
4
|
"$extensions": {
|
|
5
5
|
"remarque": {
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.23.0",
|
|
7
7
|
"tiers": {
|
|
8
8
|
"core": "immutable identity — overriding forks the system",
|
|
9
9
|
"palette": "sanctioned personalization surface — override freely, then run remarque-audit"
|