remarque-tokens 0.17.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 +4 -0
- package/CHANGELOG.md +119 -0
- package/REMARQUE.md +270 -0
- package/deck.js +70 -0
- package/forms.css +424 -0
- package/package.json +8 -2
- package/scripts/lib/css-tokens.mjs +11 -2
- package/scripts/theme.mjs +38 -7
- package/tokens.d.ts +1 -1
- package/tokens.json +1 -1
package/AGENT_RULES.md
CHANGED
|
@@ -145,6 +145,8 @@ project/
|
|
|
145
145
|
├── prose.css # .remarque-prose long-form styling (aggregated by tokens.css; own subpath for opt-out)
|
|
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
|
+
├── 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)
|
|
148
150
|
├── print.css # Print stylesheet (own subpath, NOT aggregated — import explicitly)
|
|
149
151
|
├── theme.css # Tailwind v4 adapter (@theme inline) — import after tailwindcss + tokens
|
|
150
152
|
├── tailwind.config.js # Tailwind v3 ONLY — v4 projects use theme.css instead
|
|
@@ -207,6 +209,8 @@ Before considering any implementation complete, verify:
|
|
|
207
209
|
- [ ] Every page maps to an archetype (Essay, Dossier, Notebook, Landing, Reference/Docs, Changelog, Gallery)
|
|
208
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)
|
|
209
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`
|
|
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
|
|
210
214
|
- [ ] Mobile version is roomy — not a compressed desktop layout
|
|
211
215
|
- [ ] Mobile nav links have ≥44px touch targets
|
|
212
216
|
- [ ] No pure white or pure black backgrounds
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,125 @@ 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
|
+
|
|
66
|
+
## 0.18.0 — 2026-07-23
|
|
67
|
+
|
|
68
|
+
Form control primitives + reference components (closes #27, closes #30).
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
- **`forms.css` — form control primitives module (issue #27).** New own-subpath
|
|
72
|
+
module (`remarque-tokens/forms`, `remarque-tokens/forms.css`), NOT aggregated
|
|
73
|
+
into `tokens.css` — matches `essay.css`/`broadsheet.css`/`print.css`'s
|
|
74
|
+
opt-in convention. Spec-native (built directly from the issue, not
|
|
75
|
+
graduated from a downstream site — no prior flagship implementation to
|
|
76
|
+
re-express).
|
|
77
|
+
- `.remarque-field` (label + control + help/error message vertical stack),
|
|
78
|
+
`.remarque-field-label` (meta voice, same declarations as `.text-label`),
|
|
79
|
+
`.remarque-field-required`, `.remarque-field-message`.
|
|
80
|
+
- `.remarque-input` — shared class across `<input>`/`<textarea>`/`<select>`:
|
|
81
|
+
body-voice text, `--color-border-bold` boundary (functional, 3:1-checked),
|
|
82
|
+
`--radius-sm` (tighter than the general 8px ceiling — form controls stay
|
|
83
|
+
more precise-reading), ≥44px tall, `::placeholder` at `--color-muted`
|
|
84
|
+
(a conscious AA choice, documented — placeholder is supplementary hint
|
|
85
|
+
copy, never the field's only label).
|
|
86
|
+
- `.remarque-checkbox` / `.remarque-radio` — `accent-color` only, no
|
|
87
|
+
`appearance: none`, no hand-drawn replacement; native controls keep every
|
|
88
|
+
platform accessibility behavior. Sized at `--space-5` (24px), wrapped in
|
|
89
|
+
a `<label>` carrying the 44px touch target.
|
|
90
|
+
- `.remarque-button` (+ `--primary` variant) — quiet by rule: bordered,
|
|
91
|
+
transparent, body-voice text by default; `--primary` is the one
|
|
92
|
+
sanctioned accent placement per viewport, still unfilled (accent
|
|
93
|
+
text/border, hover washes in `--color-accent-subtle`, never a solid
|
|
94
|
+
fill). Disabled state (native `:disabled`) uses `--color-disabled`.
|
|
95
|
+
- Validation state wiring on the 0.17.0 state-color tokens:
|
|
96
|
+
`.remarque-field[data-state="error"|"success"|"warning"]` recolors the
|
|
97
|
+
input border and message text together; always pairs with a real
|
|
98
|
+
`aria-invalid`/`aria-describedby` on the input (`data-state` is paint
|
|
99
|
+
only). `.remarque-input:user-invalid`, guarded with
|
|
100
|
+
`@supports selector(:user-invalid)`, is a zero-JS bonus layer.
|
|
101
|
+
- `.remarque-table` / `.remarque-table-wrap` — a standalone-table
|
|
102
|
+
re-scoping of `prose.css`'s table rules (mono `th`, 2px header rule, 1px
|
|
103
|
+
row rules, `.num` tabular-lining columns) for data tables that shouldn't
|
|
104
|
+
be wrapped in the full `.remarque-prose` container. Lives in `forms.css`
|
|
105
|
+
rather than a fifth subpath — AGENT_RULES.md's build order already
|
|
106
|
+
groups tables with buttons/cards as one supplementary-UI step.
|
|
107
|
+
- REMARQUE.md "Forms" section — full markup contract, state-wiring rules,
|
|
108
|
+
restraint rules restated (radius/touch-targets/no-fake-replacements/
|
|
109
|
+
quiet-buttons/placeholder-contrast), standalone-tables note, and "when
|
|
110
|
+
NOT to use" (Remarque is editorial-first — contact/search/newsletter
|
|
111
|
+
moments, not app UIs). AGENT_RULES.md's File Structure Convention and
|
|
112
|
+
Quality Checklist gain matching entries.
|
|
113
|
+
- `scripts/audit.mjs`'s `--src .` invocation covers `forms.css` for free
|
|
114
|
+
(font-floor + no-hardcoded-color scans — no new pairings needed, this
|
|
115
|
+
module introduces no new color tokens).
|
|
116
|
+
- **Reference components (demo site, issue #30)** — `site/src/components/`:
|
|
117
|
+
`Button.astro`, `Input.astro` (a `.remarque-field` wrapper — label/help/
|
|
118
|
+
error props, wires `for`/`id`/`aria-describedby`/`aria-invalid`),
|
|
119
|
+
`Table.astro` (`.remarque-table`, standalone). New demo page
|
|
120
|
+
`site/pages/components` exercises every input state (default/focus/error/
|
|
121
|
+
success/disabled), both button variants, and a sample table in both
|
|
122
|
+
themes — added to `site/tests/helpers.ts`'s `PAGES` for visual-regression
|
|
123
|
+
coverage (new baselines for this page only; no existing baseline changed)
|
|
124
|
+
and linked from the site nav.
|
|
125
|
+
|
|
7
126
|
## 0.17.0 — 2026-07-23
|
|
8
127
|
|
|
9
128
|
Semantic state colors + z-index scale (closes #26, closes #29).
|
package/REMARQUE.md
CHANGED
|
@@ -978,6 +978,276 @@ the archive itself.
|
|
|
978
978
|
|
|
979
979
|
---
|
|
980
980
|
|
|
981
|
+
## Forms
|
|
982
|
+
|
|
983
|
+
Native form-control primitives — field wrapper, text input, checkbox,
|
|
984
|
+
radio, button — shipped as their own subpath, `remarque-tokens/forms`
|
|
985
|
+
(issue #27). Unlike the Essay Module and the Broadsheet pattern, this
|
|
986
|
+
module was not graduated from a downstream site implementation; it is
|
|
987
|
+
spec-native, built directly from the issue rather than re-expressing an
|
|
988
|
+
existing literal.
|
|
989
|
+
|
|
990
|
+
### Markup contract
|
|
991
|
+
|
|
992
|
+
```html
|
|
993
|
+
<div class="remarque-field">
|
|
994
|
+
<label class="remarque-field-label" for="email">Email</label>
|
|
995
|
+
<input class="remarque-input" type="email" id="email" name="email"
|
|
996
|
+
aria-describedby="email-help" />
|
|
997
|
+
<p class="remarque-field-message" id="email-help">
|
|
998
|
+
We'll never share your email.
|
|
999
|
+
</p>
|
|
1000
|
+
</div>
|
|
1001
|
+
|
|
1002
|
+
<div class="remarque-field" data-state="error">
|
|
1003
|
+
<label class="remarque-field-label" for="email2">Email</label>
|
|
1004
|
+
<input class="remarque-input" type="email" id="email2" name="email2"
|
|
1005
|
+
aria-invalid="true" aria-describedby="email2-message" required />
|
|
1006
|
+
<p class="remarque-field-message" id="email2-message" role="alert">
|
|
1007
|
+
Enter a valid email address.
|
|
1008
|
+
</p>
|
|
1009
|
+
</div>
|
|
1010
|
+
|
|
1011
|
+
<label class="remarque-checkbox">
|
|
1012
|
+
<input type="checkbox" name="subscribe" />
|
|
1013
|
+
<span>Subscribe to updates</span>
|
|
1014
|
+
</label>
|
|
1015
|
+
|
|
1016
|
+
<label class="remarque-radio">
|
|
1017
|
+
<input type="radio" name="plan" value="a" />
|
|
1018
|
+
<span>Plan A</span>
|
|
1019
|
+
</label>
|
|
1020
|
+
|
|
1021
|
+
<button class="remarque-button" type="button">Cancel</button>
|
|
1022
|
+
<button class="remarque-button remarque-button--primary" type="submit">Submit</button>
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
| Class | Applies to | Notes |
|
|
1026
|
+
|---|---|---|
|
|
1027
|
+
| `.remarque-field` | wrapping `<div>` | Label + control + message vertical stack. Layout only — no border/background of its own. |
|
|
1028
|
+
| `.remarque-field-label` | `<label>` | Meta voice (mono, small caps, `--color-muted`) — same declarations as `.text-label`, repeated per the non-mixin precedent essay.css/broadsheet.css already set. |
|
|
1029
|
+
| `.remarque-field-required` | `<span aria-hidden="true">` inside the label | Accent-colored asterisk. `aria-hidden` — the input's own `required` attribute is the real signal a screen reader announces. |
|
|
1030
|
+
| `.remarque-field-message` | `<p>` after the control | Help text by default; recolored by the parent's `data-state`. Wire `aria-describedby` on the input to this element's `id` in every case, valid or not. |
|
|
1031
|
+
| `.remarque-input` | `<input>` / `<textarea>` / `<select>` | Body-voice text, `--color-border-bold` boundary, `--radius-sm`, ≥44px tall. |
|
|
1032
|
+
| `.remarque-checkbox` / `.remarque-radio` | wrapping `<label>` | `accent-color` on the native control — no `appearance: none`, no hand-drawn replacement. The wrapping label carries the 44px touch target; the control itself stays at its natural (enlarged) size. |
|
|
1033
|
+
| `.remarque-button` | `<button>` | Quiet default: bordered, transparent, body-voice text. |
|
|
1034
|
+
| `.remarque-button--primary` | `<button>` | The one sanctioned accent placement per viewport (Visual Rules) — accent text + accent border, never a solid accent fill. Hover washes in `--color-accent-subtle`, it does not darken to a filled block. |
|
|
1035
|
+
|
|
1036
|
+
### State wiring
|
|
1037
|
+
|
|
1038
|
+
`data-state="error"` / `"success"` / `"warning"` on `.remarque-field`
|
|
1039
|
+
recolors both the input's border and the message text from the 0.17.0
|
|
1040
|
+
state-color tokens (`--color-error`/`--color-success`/`--color-warning`).
|
|
1041
|
+
`data-state` is the PAINT layer only — always pair it with the real
|
|
1042
|
+
accessibility signal on the input itself: `aria-invalid="true"` (screen
|
|
1043
|
+
readers announce this; `data-state` alone is invisible to them) and
|
|
1044
|
+
`aria-describedby` pointing at the `.remarque-field-message` id. A
|
|
1045
|
+
disabled control never gets a `data-state` — it uses the native
|
|
1046
|
+
`:disabled`/`disabled` state and `--color-disabled`, which is a muted
|
|
1047
|
+
register, not one of the three feedback hues (REMARQUE.md "State Colors":
|
|
1048
|
+
disabled is deliberately not ANSI-derived).
|
|
1049
|
+
|
|
1050
|
+
As a zero-JS bonus layer, `.remarque-input:user-invalid` (guarded with
|
|
1051
|
+
`@supports selector(:user-invalid)`) picks up the same error border once
|
|
1052
|
+
a browser supports the pseudo-class and the user has actually interacted
|
|
1053
|
+
with an invalid field — it never replaces authoring `data-state="error"`
|
|
1054
|
+
explicitly, since `:user-invalid` alone has no matching text-message
|
|
1055
|
+
recolor and no `aria-invalid` wiring of its own.
|
|
1056
|
+
|
|
1057
|
+
### Restraint rules restated
|
|
1058
|
+
|
|
1059
|
+
- **Border radius**: form controls cap at `--radius-sm` (4px) — a
|
|
1060
|
+
tighter ceiling than AGENT_RULES.md's general `--radius-md` (8px)
|
|
1061
|
+
maximum for other chrome. A smaller, more precise-reading radius suits
|
|
1062
|
+
a control the user directly manipulates.
|
|
1063
|
+
- **Touch targets**: every interactive control — input, textarea,
|
|
1064
|
+
select, button, and the `<label>` wrapping a checkbox/radio — is
|
|
1065
|
+
≥44×44px (WCAG 2.5.5 AAA, USWDS floor; see "Touch Targets" above and
|
|
1066
|
+
`.nav-link`'s identical convention in the demo site's `globals.css`).
|
|
1067
|
+
- **No fake replacements**: checkboxes and radios stay native elements
|
|
1068
|
+
recolored with `accent-color` only. No `appearance: none` plus a
|
|
1069
|
+
hand-drawn box/dot — that would trade away native keyboard behavior,
|
|
1070
|
+
forced-colors-mode support, and screen-reader state announcements for
|
|
1071
|
+
a purely cosmetic win this system doesn't need.
|
|
1072
|
+
- **Buttons stay quiet**: text-only or bordered, never filled/solid by
|
|
1073
|
+
default — the primary variant included. Accent appears as text/border
|
|
1074
|
+
color, never as a background fill (Visual Rules' one-accent rule and
|
|
1075
|
+
Components' "never filled/solid as default" both still govern the
|
|
1076
|
+
emphasized variant).
|
|
1077
|
+
- **Placeholder contrast**: `--color-muted` (4.5:1 on `--color-bg`/
|
|
1078
|
+
`--color-surface`), a conscious choice — placeholder text is
|
|
1079
|
+
supplementary hint copy, never the field's only label, so AA is the
|
|
1080
|
+
correct tier rather than `--color-fg-muted`'s AAA.
|
|
1081
|
+
|
|
1082
|
+
### Standalone tables
|
|
1083
|
+
|
|
1084
|
+
`forms.css` also ships `.remarque-table` / `.remarque-table-wrap`
|
|
1085
|
+
(issue #30) — the same visual language as `.remarque-prose table`
|
|
1086
|
+
(mono `th` voice, 2px header rule, 1px row rules, `.num` for tabular-
|
|
1087
|
+
lining right-aligned numeric columns), re-scoped under a top-level
|
|
1088
|
+
class so a standalone data grid doesn't have to wrap itself in the full
|
|
1089
|
+
`.remarque-prose` container (which also applies oldstyle proportional
|
|
1090
|
+
numerals and prose spacing rhythm a data table doesn't want):
|
|
1091
|
+
|
|
1092
|
+
```html
|
|
1093
|
+
<div class="remarque-table-wrap">
|
|
1094
|
+
<table class="remarque-table">
|
|
1095
|
+
<caption>Example</caption>
|
|
1096
|
+
<thead>
|
|
1097
|
+
<tr><th>Name</th><th class="num">Count</th></tr>
|
|
1098
|
+
</thead>
|
|
1099
|
+
<tbody>
|
|
1100
|
+
<tr><td>Rows</td><td class="num">42</td></tr>
|
|
1101
|
+
</tbody>
|
|
1102
|
+
</table>
|
|
1103
|
+
</div>
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Lives in `forms.css` rather than its own subpath — AGENT_RULES.md's
|
|
1107
|
+
build order already groups tables with buttons/cards/code blocks as one
|
|
1108
|
+
"supplementary UI" step, and this table variant is a small re-scoping,
|
|
1109
|
+
not enough surface area to justify a fourth optional module.
|
|
1110
|
+
|
|
1111
|
+
### When NOT to use
|
|
1112
|
+
|
|
1113
|
+
Remarque is editorial-first. This module exists for the handful of
|
|
1114
|
+
form moments a written-word site actually needs — a contact form, a
|
|
1115
|
+
site search box, a newsletter signup — not for building application
|
|
1116
|
+
UIs. If a page needs more than a few fields, conditional multi-step
|
|
1117
|
+
logic, inline data tables of editable rows, or anything that starts to
|
|
1118
|
+
resemble a settings/dashboard screen, that is out of scope for
|
|
1119
|
+
Remarque's forms primitives; reach for a dedicated UI/form library
|
|
1120
|
+
instead and keep Remarque to the page's editorial chrome around it.
|
|
1121
|
+
|
|
1122
|
+
---
|
|
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
|
+
|
|
981
1251
|
## Signature Moves
|
|
982
1252
|
|
|
983
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/forms.css
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Remarque — Form Control Primitives (issue #27)
|
|
3
|
+
* ────────────────────────────────────────────────
|
|
4
|
+
* Namespaced classes that style native form elements — no fake replacement
|
|
5
|
+
* widgets. A `<select>` is a `<select>`, a checkbox stays a checkbox with
|
|
6
|
+
* `accent-color` doing the recoloring work a browser already supports
|
|
7
|
+
* natively. This module exists because Remarque is editorial-first: forms
|
|
8
|
+
* are for the contact/search/newsletter moments a written-word site
|
|
9
|
+
* occasionally needs, not a component library for building app UIs. See
|
|
10
|
+
* "When NOT to use" in REMARQUE.md's "Forms" section before reaching for
|
|
11
|
+
* this on a page that wants more than a handful of fields. Also carries a
|
|
12
|
+
* small standalone-table section (`.remarque-table`) at the end of this
|
|
13
|
+
* file — see that section's header comment for why it lives here rather
|
|
14
|
+
* than in prose.css (issue #30's reference `Table.astro` needs the same
|
|
15
|
+
* table visual language OUTSIDE `.remarque-prose`).
|
|
16
|
+
*
|
|
17
|
+
* PROVENANCE — spec-native, not graduated from a downstream site (unlike
|
|
18
|
+
* essay.css/broadsheet.css). Built directly from issue #27; there was no
|
|
19
|
+
* prior flagship implementation to re-express in token vocabulary, so
|
|
20
|
+
* every value below is original to this module rather than a mapped-over
|
|
21
|
+
* site literal.
|
|
22
|
+
*
|
|
23
|
+
* Own subpath (remarque-tokens/forms, remarque-tokens/forms.css) — NOT
|
|
24
|
+
* included by the tokens.css aggregator, matching essay.css/broadsheet.css/
|
|
25
|
+
* print.css: most Remarque pages have zero forms. Import explicitly, after
|
|
26
|
+
* prose.css if both are in use:
|
|
27
|
+
* @import 'remarque-tokens/prose.css';
|
|
28
|
+
* @import 'remarque-tokens/forms.css';
|
|
29
|
+
* (string form — see AGENT_RULES.md Pitfall #6.)
|
|
30
|
+
*
|
|
31
|
+
* ─── MARKUP CONTRACT (summary — full version in REMARQUE.md) ──────────
|
|
32
|
+
*
|
|
33
|
+
* <div class="remarque-field">
|
|
34
|
+
* <label class="remarque-field-label" for="email">Email</label>
|
|
35
|
+
* <input class="remarque-input" type="email" id="email" name="email"
|
|
36
|
+
* aria-describedby="email-help" />
|
|
37
|
+
* <p class="remarque-field-message" id="email-help">
|
|
38
|
+
* We'll never share your email.
|
|
39
|
+
* </p>
|
|
40
|
+
* </div>
|
|
41
|
+
*
|
|
42
|
+
* <div class="remarque-field" data-state="error">
|
|
43
|
+
* <label class="remarque-field-label" for="email2">Email</label>
|
|
44
|
+
* <input class="remarque-input" type="email" id="email2" name="email2"
|
|
45
|
+
* aria-invalid="true" aria-describedby="email2-message" required />
|
|
46
|
+
* <p class="remarque-field-message" id="email2-message" role="alert">
|
|
47
|
+
* Enter a valid email address.
|
|
48
|
+
* </p>
|
|
49
|
+
* </div>
|
|
50
|
+
*
|
|
51
|
+
* <label class="remarque-checkbox">
|
|
52
|
+
* <input type="checkbox" name="subscribe" />
|
|
53
|
+
* <span>Subscribe to updates</span>
|
|
54
|
+
* </label>
|
|
55
|
+
*
|
|
56
|
+
* <label class="remarque-radio">
|
|
57
|
+
* <input type="radio" name="plan" value="a" />
|
|
58
|
+
* <span>Plan A</span>
|
|
59
|
+
* </label>
|
|
60
|
+
*
|
|
61
|
+
* <button class="remarque-button" type="button">Cancel</button>
|
|
62
|
+
* <button class="remarque-button remarque-button--primary" type="submit">
|
|
63
|
+
* Submit
|
|
64
|
+
* </button>
|
|
65
|
+
*
|
|
66
|
+
* `data-state` lives on `.remarque-field` (not the input) — it recolors
|
|
67
|
+
* both the control's border AND the message text together from one
|
|
68
|
+
* attribute, the same "one attribute drives several descendants" shape
|
|
69
|
+
* essay.css's counter-driven numbering uses. `aria-invalid="true"` is the
|
|
70
|
+
* REAL accessibility signal (screen readers announce it); `data-state` is
|
|
71
|
+
* the visual layer on top. Author both together — see "State wiring"
|
|
72
|
+
* below for what happens if a consumer only sets one.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/* ─── Field wrapper: label + control + help/error text stack ────────────
|
|
76
|
+
* A vertical rhythm of three parts. `--space-2` between label and control
|
|
77
|
+
* (tight — they read as one unit), `--space-2` again before the message
|
|
78
|
+
* line. No border, no background on the wrapper itself — `.remarque-field`
|
|
79
|
+
* is layout only, never a bordered "card" around the control (Components:
|
|
80
|
+
* "no component should be visually louder than the content it contains").
|
|
81
|
+
*/
|
|
82
|
+
.remarque-field {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: var(--space-2);
|
|
86
|
+
max-width: var(--content-reading);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.remarque-field + .remarque-field {
|
|
90
|
+
margin-top: var(--space-5);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Label voice: same declarations as `.text-label` (tokens-core.css),
|
|
94
|
+
repeated rather than depended-on-as-a-mixin — the same non-mixin
|
|
95
|
+
precedent essay.css's TOC summary and broadsheet.css's kicker rows
|
|
96
|
+
establish. True small caps (`font-variant-caps`), never `text-transform:
|
|
97
|
+
uppercase` (REMARQUE.md "Small Caps"). Quiet mono metadata voice for the
|
|
98
|
+
label, not the body face — a form label identifies the field, the VALUE
|
|
99
|
+
typed into it is the content, and that distinction is the same one
|
|
100
|
+
`.text-label` already draws for dates/tags/status elsewhere. */
|
|
101
|
+
.remarque-field-label {
|
|
102
|
+
font-family: var(--font-mono);
|
|
103
|
+
font-size: var(--text-meta);
|
|
104
|
+
line-height: var(--leading-meta);
|
|
105
|
+
font-variant-caps: all-small-caps;
|
|
106
|
+
letter-spacing: var(--tracking-caps);
|
|
107
|
+
color: var(--color-muted);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Required-field marker: a plain asterisk in the accent color is the one
|
|
111
|
+
sanctioned use here (it is unambiguously "this is the interactive thing
|
|
112
|
+
that needs attention right now" — not decoration). Author it inside the
|
|
113
|
+
label: <label>Email <span class="remarque-field-required"
|
|
114
|
+
aria-hidden="true">*</span></label> — aria-hidden because `required` on
|
|
115
|
+
the input itself is the real signal a screen reader announces. */
|
|
116
|
+
.remarque-field-required {
|
|
117
|
+
color: var(--color-accent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Help/error/success message line — meta voice, body-face-adjacent size
|
|
121
|
+
floor (--text-meta, 14px, clears the USWDS small-text minimum). Neutral
|
|
122
|
+
(muted) by default; recolored by `.remarque-field[data-state]` below.
|
|
123
|
+
`text-wrap: pretty` matches `.remarque-prose`'s optical default for
|
|
124
|
+
short justified-feeling message copy. */
|
|
125
|
+
.remarque-field-message {
|
|
126
|
+
font-family: var(--font-mono);
|
|
127
|
+
font-size: var(--text-meta);
|
|
128
|
+
line-height: var(--leading-meta);
|
|
129
|
+
letter-spacing: var(--tracking-meta);
|
|
130
|
+
color: var(--color-muted);
|
|
131
|
+
margin: 0;
|
|
132
|
+
text-wrap: pretty;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ─── Text inputs: input / textarea / select ─────────────────────────────
|
|
136
|
+
* One shared class across all three native elements — same border,
|
|
137
|
+
* radius, spacing, and body-voice typography (Font Slots: "Body ... UI
|
|
138
|
+
* labels, navigation, buttons" — form values are UI labels' closest
|
|
139
|
+
* relative here, not metadata). `--color-border-bold`, not `-bold`'s
|
|
140
|
+
* quieter sibling: an input's boundary is a FUNCTIONAL edge (WCAG 1.4.11
|
|
141
|
+
* non-text contrast), the same reasoning `.remarque-sidenote`'s narrow-
|
|
142
|
+
* viewport border and `.remarque-toc-rail`'s hairline both already apply
|
|
143
|
+
* decoratively-vs-functionally in essay.css.
|
|
144
|
+
*/
|
|
145
|
+
.remarque-input {
|
|
146
|
+
display: block;
|
|
147
|
+
width: 100%;
|
|
148
|
+
min-height: 44px; /* USWDS/WCAG 2.5.5 touch-target floor — see AGENT_RULES.md */
|
|
149
|
+
padding: var(--space-2) var(--space-3);
|
|
150
|
+
font-family: var(--font-body);
|
|
151
|
+
font-size: var(--text-body);
|
|
152
|
+
line-height: var(--leading-title);
|
|
153
|
+
color: var(--color-fg);
|
|
154
|
+
background-color: var(--color-bg);
|
|
155
|
+
border: var(--border-width) var(--border-style) var(--color-border-bold);
|
|
156
|
+
/* Restraint rule (AGENT_RULES.md "Border radius never exceeds
|
|
157
|
+
--radius-md" + this module's own tighter floor): form controls stay
|
|
158
|
+
at --radius-sm, never the 8px ceiling other chrome is allowed. A
|
|
159
|
+
smaller control reads as more precise at a smaller radius. */
|
|
160
|
+
border-radius: var(--radius-sm);
|
|
161
|
+
transition: border-color var(--motion-fast) var(--motion-easing);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
textarea.remarque-input {
|
|
165
|
+
min-height: 6rem; /* several lines tall by default — still clears the 44px floor */
|
|
166
|
+
resize: vertical;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Native select's disclosure triangle is left as the platform default —
|
|
170
|
+
restyling it means either `appearance: none` (which then requires a
|
|
171
|
+
hand-drawn replacement icon, exactly the "fake replacement" this module
|
|
172
|
+
avoids) or accepting a single browser's rendering as the reference,
|
|
173
|
+
neither of which is worth it for one triangle. */
|
|
174
|
+
.remarque-input:hover {
|
|
175
|
+
border-color: var(--color-fg-muted);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Placeholder: --color-muted, the same 4.5:1-on-bg/-surface token used
|
|
179
|
+
everywhere else at this contrast tier — a conscious choice, not the
|
|
180
|
+
browser default (which is often far below AA). Placeholder text is
|
|
181
|
+
supplementary (never the only label — every `.remarque-input` pairs
|
|
182
|
+
with a real `<label>`), so 4.5:1 rather than `--color-fg-muted`'s 7:1
|
|
183
|
+
is the correct tier: AA is sufficient for non-essential hint copy. */
|
|
184
|
+
.remarque-input::placeholder {
|
|
185
|
+
color: var(--color-muted);
|
|
186
|
+
opacity: 1; /* Firefox lowers placeholder opacity by default; pin it so the token value is what actually renders */
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.remarque-input:disabled {
|
|
190
|
+
color: var(--color-disabled);
|
|
191
|
+
background-color: var(--color-bg-subtle);
|
|
192
|
+
border-color: var(--color-border);
|
|
193
|
+
cursor: not-allowed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* ─── Checkbox / Radio — accent-color, no fake replacements ──────────────
|
|
197
|
+
* `accent-color` is the ENTIRE recoloring mechanism: no `appearance:
|
|
198
|
+
* none`, no hand-drawn checkmark/dot, no absolutely-positioned pseudo-
|
|
199
|
+
* element standing in for the native control. This is a deliberate
|
|
200
|
+
* ceiling on customization, not an oversight — every browser Remarque
|
|
201
|
+
* targets supports `accent-color` on checkboxes/radios, and a real native
|
|
202
|
+
* control keeps every platform accessibility behavior (keyboard,
|
|
203
|
+
* high-contrast mode, forced-colors mode, screen-reader state
|
|
204
|
+
* announcements) for free, at the cost of not controlling the checkmark
|
|
205
|
+
* glyph's exact shape.
|
|
206
|
+
*
|
|
207
|
+
* Sizing: --space-5 (1.5rem / 24px) — comfortably inside the 44px touch
|
|
208
|
+
* target once the wrapping <label> below adds its own min-height, and
|
|
209
|
+
* large enough to keep the accent-colored fill legible (a 16px default
|
|
210
|
+
* checkbox reads as fussy at this system's generous spacing scale).
|
|
211
|
+
*/
|
|
212
|
+
.remarque-checkbox,
|
|
213
|
+
.remarque-radio {
|
|
214
|
+
display: inline-flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: var(--space-2);
|
|
217
|
+
min-height: 44px; /* touch-target floor on the whole label, same shape as .nav-link */
|
|
218
|
+
font-family: var(--font-body);
|
|
219
|
+
font-size: var(--text-body);
|
|
220
|
+
line-height: var(--leading-title);
|
|
221
|
+
color: var(--color-fg);
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.remarque-checkbox input,
|
|
226
|
+
.remarque-radio input {
|
|
227
|
+
accent-color: var(--color-accent);
|
|
228
|
+
width: var(--space-5);
|
|
229
|
+
height: var(--space-5);
|
|
230
|
+
margin: 0;
|
|
231
|
+
flex-shrink: 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* `:has()` is a light progressive enhancement here, not a functional
|
|
235
|
+
dependency — a browser without it still gets a fully working, natively
|
|
236
|
+
disabled control; it just skips the muted label-text recolor. */
|
|
237
|
+
.remarque-checkbox:has(input:disabled),
|
|
238
|
+
.remarque-radio:has(input:disabled) {
|
|
239
|
+
color: var(--color-disabled);
|
|
240
|
+
cursor: not-allowed;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.remarque-checkbox input:disabled,
|
|
244
|
+
.remarque-radio input:disabled {
|
|
245
|
+
cursor: not-allowed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ─── Buttons ─────────────────────────────────────────────────────────────
|
|
249
|
+
* Quiet by rule (AGENT_RULES.md Components: "Buttons are quiet: text-only
|
|
250
|
+
* or subtle bordered. Never filled/solid as default."). The default
|
|
251
|
+
* variant is bordered, transparent background, body-voice text — reads as
|
|
252
|
+
* "a clickable label," not a colored block. `--primary` is the ONE
|
|
253
|
+
* sanctioned accent placement per viewport (Visual Rules: accent for
|
|
254
|
+
* "inline links and one interactive element per viewport") — even there,
|
|
255
|
+
* it stays unfilled: accent text + accent border, never a solid accent
|
|
256
|
+
* fill, so the same restraint the default variant holds carries through
|
|
257
|
+
* to the emphasized one. Hover on `--primary` washes in
|
|
258
|
+
* `--color-accent-subtle` (the same quiet-highlight token
|
|
259
|
+
* `.remarque-prose mark` already uses) rather than darkening to a filled
|
|
260
|
+
* block — a wash, not a fill.
|
|
261
|
+
*/
|
|
262
|
+
.remarque-button {
|
|
263
|
+
display: inline-flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
gap: var(--space-2);
|
|
267
|
+
min-height: 44px;
|
|
268
|
+
padding: var(--space-2) var(--space-4);
|
|
269
|
+
font-family: var(--font-body);
|
|
270
|
+
font-size: var(--text-body);
|
|
271
|
+
font-weight: var(--weight-medium);
|
|
272
|
+
line-height: var(--leading-title);
|
|
273
|
+
color: var(--color-fg);
|
|
274
|
+
background-color: transparent;
|
|
275
|
+
border: var(--border-width) var(--border-style) var(--color-border-bold);
|
|
276
|
+
border-radius: var(--radius-sm);
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
transition:
|
|
279
|
+
color var(--motion-fast) var(--motion-easing),
|
|
280
|
+
border-color var(--motion-fast) var(--motion-easing),
|
|
281
|
+
background-color var(--motion-fast) var(--motion-easing);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.remarque-button:hover:not(:disabled) {
|
|
285
|
+
border-color: var(--color-fg-muted);
|
|
286
|
+
color: var(--color-fg);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.remarque-button--primary {
|
|
290
|
+
color: var(--color-accent);
|
|
291
|
+
border-color: var(--color-accent);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.remarque-button--primary:hover:not(:disabled) {
|
|
295
|
+
color: var(--color-accent-hover);
|
|
296
|
+
border-color: var(--color-accent-hover);
|
|
297
|
+
background-color: var(--color-accent-subtle);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.remarque-button:disabled {
|
|
301
|
+
color: var(--color-disabled);
|
|
302
|
+
border-color: var(--color-border);
|
|
303
|
+
background-color: transparent;
|
|
304
|
+
cursor: not-allowed;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ─── Validation state wiring (0.17.0 state-color tokens) ────────────────
|
|
308
|
+
* `data-state` lives on `.remarque-field`, cascading down to the input's
|
|
309
|
+
* border and the message line's color together — author it alongside a
|
|
310
|
+
* real `aria-invalid="true"`/`aria-describedby` pair on the input (the
|
|
311
|
+
* markup contract above); `data-state` alone changes only the paint,
|
|
312
|
+
* `aria-invalid` is what a screen reader actually announces. `--warning`
|
|
313
|
+
* covers a caution/needs-attention message that isn't yet a hard failure
|
|
314
|
+
* (REMARQUE.md "State Colors" — "states needing attention before
|
|
315
|
+
* proceeding"), e.g. a password-strength hint. No `-subtle` background is
|
|
316
|
+
* used here: `-subtle` is sized for banner/callout backgrounds (REMARQUE.md
|
|
317
|
+
* "State Colors"), and painting a per-field background would make the
|
|
318
|
+
* field louder than the content it holds (Components rule) — border +
|
|
319
|
+
* message-text color is enough signal at this scale.
|
|
320
|
+
*/
|
|
321
|
+
.remarque-field[data-state="error"] .remarque-input,
|
|
322
|
+
.remarque-input[aria-invalid="true"] {
|
|
323
|
+
border-color: var(--color-error);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.remarque-field[data-state="error"] .remarque-field-message {
|
|
327
|
+
color: var(--color-error);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.remarque-field[data-state="success"] .remarque-input {
|
|
331
|
+
border-color: var(--color-success);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.remarque-field[data-state="success"] .remarque-field-message {
|
|
335
|
+
color: var(--color-success);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.remarque-field[data-state="warning"] .remarque-input {
|
|
339
|
+
border-color: var(--color-warning);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.remarque-field[data-state="warning"] .remarque-field-message {
|
|
343
|
+
color: var(--color-warning);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* `:user-invalid` — native, zero-JS validity feedback (matches only after
|
|
347
|
+
the user has interacted with the field and left it invalid, unlike
|
|
348
|
+
`:invalid`, which can match a required-but-untouched field on page
|
|
349
|
+
load). Guarded with `@supports selector(...)`: a browser lacking the
|
|
350
|
+
pseudo-class simply never matches this block and falls back to
|
|
351
|
+
whatever `data-state`/`aria-invalid` already provide — the same
|
|
352
|
+
"degrades to the no-JS/no-support baseline" shape essay.css's `@media`
|
|
353
|
+
gate uses for its own progressive enhancement. This is a bonus layer,
|
|
354
|
+
not a replacement for authoring `data-state="error"` explicitly. */
|
|
355
|
+
@supports selector(:user-invalid) {
|
|
356
|
+
.remarque-input:user-invalid {
|
|
357
|
+
border-color: var(--color-error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ─── Standalone data tables (issue #30) ──────────────────────────────────
|
|
362
|
+
* `.remarque-table` re-scopes prose.css's table rules (mono `th` voice,
|
|
363
|
+
* 2px `border-bottom` under the header row, 1px row rules only, `.num`
|
|
364
|
+
* for tabular-lining right-aligned numeric columns) under a top-level
|
|
365
|
+
* class instead of `.remarque-prose table`. This is intentionally a
|
|
366
|
+
* near-verbatim copy of those declarations, not a shared mixin (CSS has
|
|
367
|
+
* none) and not a requirement to wrap a standalone data table in the full
|
|
368
|
+
* `.remarque-prose` container — `.remarque-prose` also applies oldstyle
|
|
369
|
+
* proportional numerals, `> * + *` paragraph-spacing rhythm, and prose
|
|
370
|
+
* max-width, none of which a reference data grid (e.g. `Table.astro`)
|
|
371
|
+
* wants. Living in forms.css rather than a new subpath: this module is
|
|
372
|
+
* already "the supplementary-UI module" in AGENT_RULES.md's build order
|
|
373
|
+
* (step 7 groups buttons, tags, cards, code blocks, AND tables together),
|
|
374
|
+
* and a table this small does not justify its own subpath the way the
|
|
375
|
+
* Essay/Broadsheet modules' much larger surface area did.
|
|
376
|
+
*
|
|
377
|
+
* Wrap a wide table exactly like prose.css's convention:
|
|
378
|
+
* <div class="remarque-table-wrap"><table class="remarque-table">...</table></div>
|
|
379
|
+
*/
|
|
380
|
+
.remarque-table-wrap {
|
|
381
|
+
overflow-x: auto;
|
|
382
|
+
-webkit-overflow-scrolling: touch;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.remarque-table {
|
|
386
|
+
width: 100%;
|
|
387
|
+
border-collapse: collapse;
|
|
388
|
+
font-size: var(--text-body);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.remarque-table caption {
|
|
392
|
+
font-family: var(--font-mono);
|
|
393
|
+
font-size: var(--text-meta);
|
|
394
|
+
letter-spacing: var(--tracking-meta);
|
|
395
|
+
color: var(--color-muted);
|
|
396
|
+
text-align: left;
|
|
397
|
+
caption-side: top;
|
|
398
|
+
margin-bottom: var(--space-2);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.remarque-table th {
|
|
402
|
+
font-family: var(--font-mono);
|
|
403
|
+
font-size: var(--text-meta);
|
|
404
|
+
letter-spacing: var(--tracking-meta);
|
|
405
|
+
font-weight: var(--weight-semibold);
|
|
406
|
+
text-align: left;
|
|
407
|
+
padding: var(--space-2) var(--space-3);
|
|
408
|
+
border-bottom: 2px solid var(--color-border-bold);
|
|
409
|
+
color: var(--color-fg);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.remarque-table td {
|
|
413
|
+
font-family: var(--font-body);
|
|
414
|
+
font-size: var(--text-body);
|
|
415
|
+
padding: var(--space-2) var(--space-3);
|
|
416
|
+
border-bottom: var(--border-width) var(--border-style) var(--color-border);
|
|
417
|
+
color: var(--color-fg);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.remarque-table th.num,
|
|
421
|
+
.remarque-table td.num {
|
|
422
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
423
|
+
text-align: right;
|
|
424
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remarque-tokens",
|
|
3
|
-
"version": "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.
|
|
35
|
+
"@williamzujkowski/oklch-terminal-themes": "0.4.0",
|
|
36
36
|
"culori": "^4.0.2"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"./essay.css": "./essay.css",
|
|
73
73
|
"./broadsheet": "./broadsheet.css",
|
|
74
74
|
"./broadsheet.css": "./broadsheet.css",
|
|
75
|
+
"./forms": "./forms.css",
|
|
76
|
+
"./forms.css": "./forms.css",
|
|
77
|
+
"./deck": "./deck.js",
|
|
78
|
+
"./deck.js": "./deck.js",
|
|
75
79
|
"./agent-rules": "./AGENT_RULES.md",
|
|
76
80
|
"./spec": "./REMARQUE.md",
|
|
77
81
|
"./package.json": "./package.json"
|
|
@@ -98,6 +102,8 @@
|
|
|
98
102
|
"print.css",
|
|
99
103
|
"essay.css",
|
|
100
104
|
"broadsheet.css",
|
|
105
|
+
"forms.css",
|
|
106
|
+
"deck.js",
|
|
101
107
|
"scripts/lib/css-tokens.mjs"
|
|
102
108
|
]
|
|
103
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:
|
|
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
|
-
|
|
781
|
+
${lightSelector} {
|
|
751
782
|
${decls(light.tokens)}
|
|
752
783
|
--weight-display: 400;
|
|
753
784
|
}
|
|
754
785
|
|
|
755
|
-
|
|
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.
|
|
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.
|
|
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"
|