remarque-tokens 0.18.0 → 0.19.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 CHANGED
@@ -146,6 +146,7 @@ project/
146
146
  ├── essay.css # Optional Essay module: sidenotes + sticky TOC rail (own subpath, NOT aggregated — import explicitly)
147
147
  ├── broadsheet.css # Optional Broadsheet pattern: masthead, lead, entry list, post kicker (own subpath, NOT aggregated — import explicitly)
148
148
  ├── forms.css # Optional form control primitives: field/input/checkbox/radio/button, state-color wiring (own subpath, NOT aggregated — import explicitly)
149
+ ├── deck.js # Optional Palette Deck module: switch/persist/restore between remarque-theme-generated palettes (own subpath, NOT aggregated — dependency-free ESM, no CSS of its own)
149
150
  ├── print.css # Print stylesheet (own subpath, NOT aggregated — import explicitly)
150
151
  ├── theme.css # Tailwind v4 adapter (@theme inline) — import after tailwindcss + tokens
151
152
  ├── tailwind.config.js # Tailwind v3 ONLY — v4 projects use theme.css instead
@@ -209,6 +210,7 @@ Before considering any implementation complete, verify:
209
210
  - [ ] If the Essay uses sidenotes/a TOC rail (`remarque-tokens/essay`): `.remarque-sidenote-ref`/`.remarque-sidenote` alternate in strict DOM order, the rail never intrudes into `.remarque-prose`'s own measure, and the page renders correctly with `essay.css`'s `@media` block deleted (the narrow-viewport/no-JS fallback)
210
211
  - [ ] If a Landing/archive page uses the Broadsheet pattern (`remarque-tokens/broadsheet`): entry numerals are generated from `data-entry-number` via `attr()` (not `counter()`), the entry list stays a `<ul>` (not `<ol>` — the numeral is `aria-hidden` and decorative), and every kicker/dateline row uses `font-variant-caps: all-small-caps`, never `text-transform: uppercase`
211
212
  - [ ] If a page uses form controls (`remarque-tokens/forms`): checkboxes/radios are styled with `accent-color` only — never `appearance: none` plus a hand-drawn replacement; every control (input, button, checkbox/radio label) is ≥44×44px; validation state lives on `.remarque-field[data-state]` AND a real `aria-invalid`/`aria-describedby` pair on the input, not `data-state` alone; disabled controls use `--color-disabled`, never a state color
213
+ - [ ] If a page uses the Palette Deck (`remarque-tokens/deck`): `data-palette` is set/cleared on the same root element as `data-theme` (they compose, never conflict); the FOUC-safe restore is a synchronous classic `<script>` physically in `<head>` (never `type="module"`, which defers); the switcher control itself is a native, keyboard-operable element (e.g. `.remarque-input` `<select>`) at ≥44×44px, not a hand-rolled widget; every registered palette was generated via `remarque-theme --scope`, never hand-authored
212
214
  - [ ] Mobile version is roomy — not a compressed desktop layout
213
215
  - [ ] Mobile nav links have ≥44px touch targets
214
216
  - [ ] No pure white or pure black backgrounds
package/CHANGELOG.md CHANGED
@@ -4,6 +4,65 @@ 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.19.0 — 2026-07-23
8
+
9
+ Palette Deck module (closes #56).
10
+
11
+ ### Added
12
+ - **`deck.js` — palette-deck module (issue #56, re-scoped by 2026-07-23
13
+ consensus panel).** New own-subpath export (`remarque-tokens/deck`,
14
+ `remarque-tokens/deck.js`) — a dependency-free ~60-line ESM file, NOT
15
+ aggregated into `tokens.css` (it ships no CSS at all). Deliberately
16
+ THIN: the original theme-deck graduation proposal (12 terminal
17
+ palettes as `:root[data-theme-deck]` overrides) predates
18
+ `remarque-theme`; the panel re-scoped it to cover only what that
19
+ pipeline doesn't already provide — switching, persisting, and
20
+ FOUC-safely restoring a choice among already-generated palettes.
21
+ Generation, contrast solving, and hue/pairing remain entirely
22
+ `remarque-theme`'s job.
23
+ - `createDeck(names, opts?)` — registers a set of palette names,
24
+ returns `{ names, current, apply, restore }`. `apply(name)` sets/
25
+ clears `data-palette` on the root element and persists the choice to
26
+ `localStorage` (default key `remarque-palette`); `apply(null)`
27
+ reverts to the unscoped default. `restore()` re-applies a persisted
28
+ choice without re-persisting it (for the post-paint sync step — the
29
+ actual FOUC guard is a duplicated inline snippet, documented in
30
+ REMARQUE.md, matching how the light/dark theme toggle's own `<head>`
31
+ script already works). Unknown names throw rather than setting an
32
+ unvalidated attribute value.
33
+ - **`--scope <name>` on `remarque-theme`.** Emits the derived palette
34
+ under `[data-palette="<name>"]` / `[data-palette="<name>"][data-theme="dark"]`
35
+ instead of `:root` / `[data-theme="dark"]`, so several generated
36
+ palettes can coexist in one stylesheet. `<name>` is validated with the
37
+ same slug grammar as `<light-slug>`/`--dark` before it is interpolated
38
+ into the attribute selector (security review precedent from #75/#76 —
39
+ it is the only control here, since a scope name has no upstream index
40
+ to check against). Derivation and self-verification are unaffected by
41
+ scoping — a scoped and unscoped run of the same pair emit
42
+ byte-identical declarations, differing only in the wrapping selector
43
+ (fixture-tested).
44
+ - **`remarque-audit` recognizes scoped palettes directly.**
45
+ `scripts/lib/css-tokens.mjs`'s `isLightRoot` now treats a bare
46
+ `[data-palette="name"]` block as that scope's light root (one more
47
+ exact-match case, symmetric with the existing `[data-theme="light"]`
48
+ special case) — `isDarkBlock` needed no change, since
49
+ `[data-palette="name"][data-theme="dark"]` already matched its
50
+ existing rule. A `--scope`'d file is auditable exactly like any other
51
+ palette file; no "audit the unscoped version first" workaround is
52
+ needed. See REMARQUE.md "Palette Deck" for the full audit story and
53
+ why this was the smaller correct fix over a bespoke attribute-selector
54
+ grammar.
55
+ - **Demo (`site/`):** a Palette Deck section on `/tokens` with a native
56
+ `<select class="remarque-input">` switcher (gruvbox / rosé-pine,
57
+ pinned slugs `gruvbox-light`/`gruvbox-dark` and
58
+ `rose-pine-dawn`/`rose-pine`), generated at build time by
59
+ `site/scripts/build-palette-deck.mjs` into `public/palettes/*.css`
60
+ (gitignored, regenerated every build — same pattern as
61
+ `public/tokens.json`). FOUC-safe restore snippet added to
62
+ `BaseLayout.astro`'s `<head>`, alongside the existing theme-toggle
63
+ script. VR baselines: `tokens.astro` only (new section on that page);
64
+ every other page is untouched by this release.
65
+
7
66
  ## 0.18.0 — 2026-07-23
8
67
 
9
68
  Form control primitives + reference components (closes #27, closes #30).
package/REMARQUE.md CHANGED
@@ -1121,6 +1121,133 @@ instead and keep Remarque to the page's editorial chrome around it.
1121
1121
 
1122
1122
  ---
1123
1123
 
1124
+ ## Palette Deck
1125
+
1126
+ A tiny runtime (`remarque-tokens/deck`, `deck.js`) for switching between
1127
+ several `remarque-theme`-generated palettes on one site — register a set
1128
+ of palette names, switch the active one, persist the choice, restore it
1129
+ before first paint. Issue #56.
1130
+
1131
+ **What this module deliberately does NOT do.** The flagship site's
1132
+ `theme-deck.css` (12 terminal palettes as `:root[data-theme-deck=…]`
1133
+ overrides) was originally proposed for upstreaming as-is. A 2026-07-23
1134
+ consensus panel (3-0) re-scoped it: generation, contrast solving, and
1135
+ hue/pairing all now come from `remarque-theme` (`scripts/theme.mjs`,
1136
+ "Color Providers" above) — that pipeline makes deriving a new palette
1137
+ nearly free, so graduating a second, parallel generator would just
1138
+ duplicate it. What's left — and what this module is *only* — is the part
1139
+ `remarque-theme` doesn't provide: switching between already-generated
1140
+ palettes at runtime. No color math, no contrast solving, no palette
1141
+ authoring lives in `deck.js`; it is a dependency-free ~60-line ESM file
1142
+ that sets an attribute, reads/writes `localStorage`, and hands back
1143
+ enough to build a FOUC-safe restore. If you need to *generate* a palette,
1144
+ see "Color Providers"; this section is exclusively about *switching*
1145
+ between ones you already have.
1146
+
1147
+ ### The mechanism: `--scope`
1148
+
1149
+ `remarque-theme` gained a `--scope <name>` flag for this module. Without
1150
+ it, output owns `:root` / `[data-theme="dark"]` outright — the normal,
1151
+ single-palette case. With `--scope <name>`, the same derivation emits
1152
+ under `[data-palette="<name>"]` / `[data-palette="<name>"][data-theme="dark"]`
1153
+ instead, so several generated palettes can coexist in one stylesheet
1154
+ (or several stylesheets) without fighting over `:root`:
1155
+
1156
+ ```
1157
+ npx remarque-theme gruvbox-light --dark gruvbox-dark --scope gruvbox -o palettes/gruvbox.css
1158
+ npx remarque-theme rose-pine-dawn --dark rose-pine --scope rose-pine -o palettes/rose-pine.css
1159
+ ```
1160
+
1161
+ `--scope`'s value is interpolated into a CSS attribute selector, so it is
1162
+ validated with the exact same slug grammar as `<light-slug>`/`--dark`
1163
+ (lowercase alphanumeric segments joined by single hyphens) before it
1164
+ touches any output — a scope name is a caller-supplied label with no
1165
+ upstream index to check it against, which is exactly why the regex
1166
+ carries the full weight here rather than acting as defense-in-depth for
1167
+ a separate lookup. Derivation, self-verification, and every contrast/
1168
+ gamut guarantee in "Color Providers" are **completely unaffected by
1169
+ scoping** — `theme.mjs` self-verifies the derived `[L, C, H]` values
1170
+ before it ever chooses a selector to emit them under, so a scoped and
1171
+ unscoped run of the same light/dark pair produce byte-identical
1172
+ declarations, differing only in which selector wraps them (fixture-
1173
+ tested in `scripts/test-theme.mjs`).
1174
+
1175
+ ### The audit story for scoped palettes
1176
+
1177
+ `remarque-audit`'s selector classification (`scripts/lib/css-tokens.mjs`,
1178
+ `isLightRoot`/`isDarkBlock`) recognizes `[data-palette="name"]` directly:
1179
+ a bare `[data-palette="name"]` block counts as that scope's light root
1180
+ (the same kind of exact-match special case the file already carries for
1181
+ `[data-theme="light"]`), and `[data-palette="name"][data-theme="dark"]`
1182
+ was already recognized as dark by the existing rule (it contains the
1183
+ `[data-theme="dark"]` substring and starts with `[`) — no change needed
1184
+ there. This was the smaller correct fix, not a bespoke new
1185
+ attribute-selector grammar: one exact-match regex, symmetric with a
1186
+ pattern the file already used, rather than teaching the parser to
1187
+ understand arbitrary compound attribute selectors. The result: a
1188
+ `--scope`'d file is auditable exactly like any other palette file —
1189
+ `npx remarque-audit --palette palettes/gruvbox.css --src <dir>` — no
1190
+ "generate the unscoped version first" workaround required.
1191
+
1192
+ ### Markup/wiring contract
1193
+
1194
+ Set (or clear) `data-palette` on `<html>`, alongside the existing
1195
+ `data-theme` attribute — the two compose independently. A deck palette
1196
+ carries *both* light and dark halves, so the light/dark toggle keeps
1197
+ working no matter which palette (or none) is active:
1198
+
1199
+ ```html
1200
+ <html data-theme="dark" data-palette="gruvbox">
1201
+ ```
1202
+
1203
+ ```js
1204
+ import { createDeck } from 'remarque-tokens/deck';
1205
+
1206
+ const deck = createDeck(['gruvbox', 'rose-pine']); // registered names
1207
+ deck.restore(); // re-apply a persisted choice, if any
1208
+ deck.apply('rose-pine'); // switch + persist
1209
+ deck.apply(null); // back to the unscoped default palette
1210
+ deck.current(); // 'rose-pine' | null
1211
+ ```
1212
+
1213
+ `createDeck` validates every `apply()` call against the registered name
1214
+ list and throws on an unknown one — the deck never sets an attribute
1215
+ value it wasn't told about. Persistence (`localStorage`, default key
1216
+ `remarque-palette`) degrades to in-memory-only if storage is unavailable
1217
+ (private browsing, SSR) rather than throwing.
1218
+
1219
+ **FOUC-safe restore.** Exactly like the light/dark theme toggle's own
1220
+ `<head>` script (see the demo's `BaseLayout.astro`), the deck's restore
1221
+ must run as a synchronous, render-blocking **classic** inline script
1222
+ physically in `<head>` — an ESM `<script type="module">` is deferred by
1223
+ the HTML spec and would let the default palette flash before the stored
1224
+ one applies. So the head snippet duplicates the ~3-line read-and-apply
1225
+ logic directly rather than importing `deck.js` (the same reason the
1226
+ theme toggle's head script doesn't import anything either):
1227
+
1228
+ ```html
1229
+ <script is:inline>
1230
+ (function() {
1231
+ var p = localStorage.getItem('remarque-palette');
1232
+ if (p) document.documentElement.setAttribute('data-palette', p);
1233
+ })();
1234
+ </script>
1235
+ ```
1236
+
1237
+ `deck.js` itself is loaded later (deferred/module, after first paint) to
1238
+ wire the switcher UI's change handler and to keep `restore()` available
1239
+ for pages that need to re-derive `current()` after the fact.
1240
+
1241
+ ### Provenance
1242
+
1243
+ Re-scoped from the flagship site's theme-deck on the way up (issue #56,
1244
+ consensus panel, 2026-07-23) — see the root README's "Graduation"
1245
+ section. The original proposal predated `remarque-theme`; once that
1246
+ pipeline existed, generating N palettes stopped being the hard part and
1247
+ switching between them became the only piece worth a shared module.
1248
+
1249
+ ---
1250
+
1124
1251
  ## Signature Moves
1125
1252
 
1126
1253
  These are the repeatable visual tells that make a Remarque site recognizable:
package/deck.js ADDED
@@ -0,0 +1,70 @@
1
+ /*
2
+ * Remarque — Palette Deck (runtime switching between remarque-theme
3
+ * generated palettes)
4
+ * ───────────────────────────────────────────────────────────────────
5
+ * PROVENANCE — re-scoped, not graduated as-is. The flagship site's
6
+ * theme-deck (12 terminal palettes, `[data-theme-deck]`) was proposed for
7
+ * upstreaming whole in issue #56; a 2026-07-23 consensus panel (3-0)
8
+ * re-scoped it: generation, contrast solving, and pairing all now come
9
+ * from `remarque-theme` (`scripts/theme.mjs`, "Color Providers"), so this
10
+ * module is deliberately THIN — it does none of that. All it does:
11
+ *
12
+ * 1. register the set of valid palette names,
13
+ * 2. switch the active one (`data-palette` on the root element),
14
+ * 3. persist the choice (localStorage), and
15
+ * 4. hand back enough to build a FOUC-safe restore (see REMARQUE.md
16
+ * "Palette Deck" for the inline <head> snippet — that snippet does
17
+ * NOT import this module; it duplicates the ~3 lines of restore
18
+ * logic directly, the same way the light/dark theme toggle's own
19
+ * head script duplicates rather than imports, because a FOUC guard
20
+ * must be a synchronous classic script and this file is an ESM
21
+ * module).
22
+ *
23
+ * No framework, no build step, no CSS authored here — `--scope`'d palette
24
+ * files (npx remarque-theme <slug> --scope <name>) are the caller's job.
25
+ */
26
+
27
+ const ATTR = 'data-palette';
28
+ const DEFAULT_STORAGE_KEY = 'remarque-palette';
29
+
30
+ /**
31
+ * @param {(string | { name: string })[]} palettes registered palette names
32
+ * (or `{ name }` objects, for callers that also carry a label/slug pair)
33
+ * @param {{ root?: HTMLElement, storageKey?: string }} [opts]
34
+ */
35
+ export function createDeck(palettes, opts = {}) {
36
+ const root = opts.root || document.documentElement;
37
+ const storageKey = opts.storageKey || DEFAULT_STORAGE_KEY;
38
+ const names = palettes.map((p) => (typeof p === 'string' ? p : p.name));
39
+
40
+ function isValid(name) {
41
+ return name == null || names.includes(name);
42
+ }
43
+
44
+ function current() {
45
+ return root.getAttribute(ATTR);
46
+ }
47
+
48
+ /** Switch palettes. `name === null` reverts to the unscoped default. */
49
+ function apply(name, { persist = true } = {}) {
50
+ if (!isValid(name)) throw new Error(`remarque palette-deck: unknown palette "${name}" (registered: ${names.join(', ') || '(none)'})`);
51
+ if (name) root.setAttribute(ATTR, name);
52
+ else root.removeAttribute(ATTR);
53
+ if (persist) {
54
+ try {
55
+ if (name) localStorage.setItem(storageKey, name);
56
+ else localStorage.removeItem(storageKey);
57
+ } catch { /* storage unavailable (private mode) — in-memory only for this load */ }
58
+ }
59
+ }
60
+
61
+ /** Re-apply a previously persisted choice (no-op if none / unknown). */
62
+ function restore() {
63
+ let stored = null;
64
+ try { stored = localStorage.getItem(storageKey); } catch { /* ignore */ }
65
+ if (stored && isValid(stored)) apply(stored, { persist: false });
66
+ return current();
67
+ }
68
+
69
+ return { names, current, apply, restore };
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remarque-tokens",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
@@ -32,7 +32,7 @@
32
32
  "drift": "node scripts/drift-check.mjs"
33
33
  },
34
34
  "devDependencies": {
35
- "@williamzujkowski/oklch-terminal-themes": "0.3.0",
35
+ "@williamzujkowski/oklch-terminal-themes": "0.4.0",
36
36
  "culori": "^4.0.2"
37
37
  },
38
38
  "peerDependencies": {
@@ -74,6 +74,8 @@
74
74
  "./broadsheet.css": "./broadsheet.css",
75
75
  "./forms": "./forms.css",
76
76
  "./forms.css": "./forms.css",
77
+ "./deck": "./deck.js",
78
+ "./deck.js": "./deck.js",
77
79
  "./agent-rules": "./AGENT_RULES.md",
78
80
  "./spec": "./REMARQUE.md",
79
81
  "./package.json": "./package.json"
@@ -101,6 +103,7 @@
101
103
  "essay.css",
102
104
  "broadsheet.css",
103
105
  "forms.css",
106
+ "deck.js",
104
107
  "scripts/lib/css-tokens.mjs"
105
108
  ]
106
109
  }
@@ -54,12 +54,21 @@ const parts = (prelude) => prelude.split(',').map((s) => s.trim());
54
54
 
55
55
  const DARKISH = /(\.dark\b|\[data-theme="dark"\])/;
56
56
  const ROOTISH = /^(:root|html|body)\b/;
57
+ // A bare palette-deck scope selector (remarque-tokens/deck, issue #56) — no
58
+ // [data-theme] of its own, so it plays the same "light root" role for that
59
+ // scope that :root plays sitewide. Exact-match, same shape as the existing
60
+ // `[data-theme="light"]` special case immediately below — not a general
61
+ // attribute-selector grammar, just one more enumerated light-ish selector.
62
+ const PALETTE_SCOPE_LIGHT = /^\[data-palette="[a-z0-9-]+"\]$/;
57
63
  export const isLightRoot = (b) =>
58
64
  b.context === '' && parts(b.prelude).some((s) =>
59
- (ROOTISH.test(s) && !DARKISH.test(s)) || s === '[data-theme="light"]');
65
+ (ROOTISH.test(s) && !DARKISH.test(s)) || s === '[data-theme="light"]' || PALETTE_SCOPE_LIGHT.test(s));
60
66
 
61
67
  /* Dark: media-query :root, the canonical [data-theme="dark"], or the
62
- class-convention :root.dark / html.dark (compatibility bridge). */
68
+ class-convention :root.dark / html.dark (compatibility bridge). A scoped
69
+ deck palette's dark block ([data-palette="name"][data-theme="dark"])
70
+ already matches unchanged: DARKISH finds the [data-theme="dark"]
71
+ substring, and the selector starts with "[". */
63
72
  export const isDarkBlock = (b) =>
64
73
  (b.context.includes('prefers-color-scheme') && b.context.includes('dark') &&
65
74
  parts(b.prelude).some((s) => ROOTISH.test(s) && !DARKISH.test(s))) ||
package/scripts/theme.mjs CHANGED
@@ -50,13 +50,22 @@ try {
50
50
  OWN_VERSION = JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8')).version;
51
51
  } catch { /* provenance is best-effort */ }
52
52
 
53
- const USAGE = `usage: remarque-theme <light-slug> [--dark <dark-slug>] [-o out.css]
53
+ const USAGE = `usage: remarque-theme <light-slug> [--dark <dark-slug>] [-o out.css] [--scope <name>]
54
54
 
55
55
  <light-slug> slug of a theme with isDark=false in ${PKG_SPEC}
56
56
  --dark <slug> slug of a theme with isDark=true; defaults to the light
57
57
  theme's "counterpart" from the dataset (0.2.0+) when it
58
58
  has one, and is required when it doesn't
59
- -o, --output write the derived palette here instead of stdout`;
59
+ -o, --output write the derived palette here instead of stdout
60
+ --scope <name> emit under [data-palette="<name>"] instead of :root — for
61
+ the palette-deck module (remarque-tokens/deck), where
62
+ several generated palettes coexist in one stylesheet and
63
+ are switched at runtime. <name> is validated with the
64
+ same slug grammar as a theme slug (lowercase
65
+ alphanumeric + hyphen) — it is interpolated into a CSS
66
+ attribute selector, so it goes through the same
67
+ byte-for-byte validation as light-slug/dark-slug above,
68
+ not just a shape check (see "Security" above)`;
60
69
 
61
70
  function die(msg) {
62
71
  console.error(msg);
@@ -66,18 +75,33 @@ function die(msg) {
66
75
  /* ── CLI args ─────────────────────────────────────────────────────── */
67
76
 
68
77
  const argv = process.argv.slice(2);
69
- let darkSlug, outFile;
78
+ let darkSlug, outFile, scopeName;
70
79
  const positional = [];
71
80
  for (let i = 0; i < argv.length; i++) {
72
81
  const a = argv[i];
73
82
  if (a === '--dark') darkSlug = argv[++i];
74
83
  else if (a === '-o' || a === '--output') outFile = argv[++i];
84
+ else if (a === '--scope') scopeName = argv[++i];
75
85
  else if (a === '-h' || a === '--help') { console.log(USAGE); process.exit(0); }
76
86
  else positional.push(a);
77
87
  }
78
88
  const lightSlug = positional[0];
79
89
  if (!lightSlug) die(USAGE);
80
90
 
91
+ /* --scope is interpolated into a CSS attribute selector
92
+ * ([data-palette="<name>"]) — validated against the SAME slug grammar as
93
+ * light-slug/dark-slug below (SLUG_RE), not a looser check. Unlike those
94
+ * two, there's no upstream index to match it against (a scope name is the
95
+ * caller's own label, not a theme lookup key), so the regex IS the whole
96
+ * control here — which is exactly why it stays as strict as the lookup
97
+ * keys' defense-in-depth regex: lowercase alphanumeric + hyphen only,
98
+ * nothing that could break out of the quoted attribute value or inject
99
+ * additional CSS. */
100
+ const SCOPE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
101
+ if (scopeName !== undefined && !SCOPE_RE.test(scopeName)) {
102
+ die(`--scope "${scopeName}" is invalid — must be lowercase alphanumeric segments joined by single hyphens (e.g. "gruvbox", "rose-pine")`);
103
+ }
104
+
81
105
  /* ── Resolve @williamzujkowski/oklch-terminal-themes ─────────────────
82
106
  * Tried from the consumer's cwd first (a project that installs the
83
107
  * themes package at its own top level), then from this script's own
@@ -734,9 +758,16 @@ selfVerify('dark', dark.raw);
734
758
 
735
759
  const decls = (tokens, indent = ' ') => Object.entries(tokens).map(([k, v]) => `${indent}--${k}: ${v};`).join('\n');
736
760
 
761
+ const regenCmd = `npx remarque-theme ${lightSlug} --dark ${darkSlug}${scopeName ? ` --scope ${scopeName}` : ''}`;
762
+ const lightSelector = scopeName ? `[data-palette="${scopeName}"]` : ':root';
763
+ const darkSelector = scopeName ? `[data-palette="${scopeName}"][data-theme="dark"]` : '[data-theme="dark"]';
764
+ const scopeNote = scopeName
765
+ ? `\n *\n * Scoped for remarque-tokens/deck (--scope "${scopeName}") — this is one\n * palette among several coexisting in a stylesheet, switched at runtime\n * via [data-palette] rather than owning :root outright. Self-verification\n * below runs on the same derived [L,C,H] values regardless of scope; see\n * REMARQUE.md "Palette Deck" for the audit story on scoped output.`
766
+ : '';
767
+
737
768
  const css = `/*
738
769
  * Generated by remarque-theme (remarque-tokens ${OWN_VERSION}) — DO NOT HAND-EDIT.
739
- * Regenerate instead: npx remarque-theme ${lightSlug} --dark ${darkSlug}
770
+ * Regenerate instead: ${regenCmd}
740
771
  *
741
772
  * Source themes (${PKG_SPEC}@${themesVersion}):
742
773
  * light = "${lightSlug}" dark = "${darkSlug}"
@@ -744,15 +775,15 @@ const css = `/*
744
775
  *
745
776
  * Palette tier only (--color-*, --weight-display) — see REMARQUE.md
746
777
  * "Token Tiers" / "Color Providers". Self-verified against the same
747
- * contrast + gamut checks as remarque-audit before being emitted.
778
+ * contrast + gamut checks as remarque-audit before being emitted.${scopeNote}
748
779
  */
749
780
 
750
- :root {
781
+ ${lightSelector} {
751
782
  ${decls(light.tokens)}
752
783
  --weight-display: 400;
753
784
  }
754
785
 
755
- [data-theme="dark"] {
786
+ ${darkSelector} {
756
787
  ${decls(dark.tokens)}
757
788
  }
758
789
  `;
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.18.0). Do not edit — the CSS
3
+ * scripts/tokens-json.mjs (v0.19.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
@@ -2,7 +2,7 @@
2
2
  "$description": "Remarque design tokens — GENERATED from tokens-core.css + tokens-palette.css by scripts/tokens-json.mjs. Do not edit; the CSS is the source of truth.",
3
3
  "$extensions": {
4
4
  "remarque": {
5
- "version": "0.18.0",
5
+ "version": "0.19.0",
6
6
  "tiers": {
7
7
  "core": "immutable identity — overriding forks the system",
8
8
  "palette": "sanctioned personalization surface — override freely, then run remarque-audit"