Skip to content

Commit 5daff48

Browse files
committed
Extend the token contract past colour, invert the button base (v2.10.0)
A design audit read all three repos and found the same thing everywhere: the guards check colour values and token names, and nothing else. Not numbers, not fonts, not shadows, not transitions, not breakpoints — and that is exactly where the three repositories had come apart. Drift here has never been a discipline problem; the annotated breakpoints and careful fallback comments are evidence of real care. It is a coverage problem. So publish more, and assert what is published. tokens.json gains two keys. `values.light` / `values.dark` resolve EVERY non-colour token to a literal CSS value — type steps, spacing, radii, control sizes, font stacks, transitions, and shadows collapsed through their oklab mixes to rgba(). `breakpoints` publishes the media-query widths, which media queries cannot read as custom properties and so must restate as literals. Both modules now compare against them; between them they found ~290 drifted fallbacks that no guard could previously see. check-token-usage.js also fails on a font-size set to an absolute literal. The theme was reaching around its own scale twice (_buttons.scss, _language-tag) while the modules did it ninety times — when the source of truth does it, the consumers will do it more. --text-2xs (11px) is the new floor and extends the UI tier into a 11/13/15/17/19 progression; there is deliberately no 14px step. Scale collapse, since a system that avoids elevation published nine elevation tokens and four non-pill radii: --shadow-xl and --radius-xl are removed (both were referenced by nothing in any of the three repos), each remaining shadow and radius is documented with the one job it has, and the glow ramp is labelled as what it is — the three states of one button, not a scale. Focus was one decision with four tokens, two idioms and no rule for choosing. There is now --focus-outline for the composed default outline (its absence is why one declaration got hand-copied 44 times downstream), --ring-focus for when an outline would be clipped, and the tint is renamed --focus-ring-color so no two focus tokens are one transposition apart. Finally, the button base is inverted. A bare <button> is now quiet — outlined, flat, ink on transparent — and the filled-primary treatment opts in via .btn--primary or by being a submit control, which is the one button in a form that IS the loud action. It was backwards: sixteen component files reset border-radius / box-shadow / transform purely to escape a default that a system whose stated register is restraint should never have had. Those resets are now redundant rather than load-bearing, and the gotcha runs the other way: a control that needs to shout must say so. Also fixes a real bug in the published vocabulary — the declaration regex was matching BEM modifiers in selectors, so `.cell--type::before` published `--type` and `--date` as theme tokens. Fictional entries in `names` are precisely what the name check exists to catch.
1 parent f6e8120 commit 5daff48

21 files changed

Lines changed: 876 additions & 124 deletions

CLAUDE.md

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,43 @@ that renders it — and "fixing" it by lowercasing `theme.ini` just moves the fa
3737

3838
### Design tokens are machine-checked — never hand-maintain a list
3939

40-
`scripts/build-tokens.js` resolves every OKLCH token in `_colors.scss` to sRGB and writes
41-
`tokens.json` — the light/dark values **plus** `names`, the full token vocabulary — then
40+
`scripts/build-tokens.js` reads the four variable files and writes `tokens.json`, then
4241
syncs it into IwacSearch and IwacVisualizations, whose `check-theme-tokens` guards fail
43-
their builds on a drifted fallback or an unknown token name.
42+
their builds on anything that disagrees with it. It publishes four things:
43+
44+
| Key | What |
45+
|---|---|
46+
| `light` / `dark` | every OKLCH colour token resolved to sRGB hex |
47+
| `values.light` / `values.dark` | every **other** token resolved to a literal CSS value — type steps, spacing, radii, control sizes, font stacks, shadows (collapsed to `rgba()`), transitions |
48+
| `names` | the full custom-property vocabulary |
49+
| `breakpoints` | the six media-query widths |
4450

4551
- A wrong or invented token name is caught by `npm run check:tokens`. Run it; don't
4652
reason about it from memory.
47-
- **Adding a token is a cross-repo change**: `npm run build:tokens`, then rebuild both modules.
53+
- **Adding a token is a cross-repo change**: `npm run sync:tokens`, then rebuild both modules.
4854
- Never hand-edit `tokens.json` or the `<!-- BEGIN GENERATED -->` tables in
4955
[docs/DESIGN-SYSTEM.md](docs/DESIGN-SYSTEM.md).
5056

57+
`values` and `breakpoints` exist because the guards used to check colour and nothing
58+
else: the fallback assertion was a regex matching a hex literal in the fallback slot, so
59+
every non-colour fallback in three repos was unchecked, and roughly 290 of them had
60+
drifted — line-heights, control sizes, type steps, font stacks (one still naming the
61+
removed *Noto Serif*), shadows, transitions. **Drift here has never been a discipline
62+
problem; it is a coverage problem.** Every value the generator publishes and a guard
63+
compares has stayed correct across a major redesign. Every value left to prose moved.
64+
So: when you add a design decision, publish it and assert it — a comment saying
65+
`/* sm */` beside a `640px` media query is what "documented" looked like right up until
66+
it was wrong.
67+
68+
### Type sizes and media widths are asserted too
69+
70+
`npm run check:tokens` also fails on a `font-size` set to an absolute literal (px/rem/pt)
71+
anywhere in `asset/sass` — use a `--text-*` token; `--text-2xs` (11px) is the floor, and
72+
there is deliberately no 14px step. Relative units (`em`, `%`) stay legal. Both modules'
73+
guards enforce the same rule plus the breakpoint contract: `min-width` sits **on** a
74+
published breakpoint, `max-width` at **breakpoint − 1**, so the halves of a pair never
75+
both match.
76+
5177
### `asset/css/` is generated
5278

5379
Edit `asset/sass/`. Anything written to `asset/css/` is overwritten by the next build.
@@ -68,12 +94,21 @@ keep the mixing perceptual.
6894
Use `> dl > .property > dd`. Value-annotation tooltips nest their own `<dl>` inside a
6995
`<dd>`, and a descendant selector leaks the 168px label-column layout into them.
7096

71-
### Every `<button>` inherits the global base style
72-
73-
`_buttons.scss` gives all buttons a radius, `box-shadow: var(--glow-sm)`, and a hover
74-
lift + glow. A component that overrides only `background`/`color` keeps the rounded
75-
floating shape and the halo — reset `border-radius`, `box-shadow` and `transform`
76-
explicitly.
97+
### The `<button>` default is QUIET — the loud one opts in
98+
99+
Inverted in 2.10. A bare `<button>` is now an outlined flat control (ink text, hairline
100+
border, no shadow, no lift). The filled-primary treatment — brand fill, `--glow-sm`
101+
halo, hover lift — comes from `.btn--primary` or from being a **submit** control
102+
(`input[type=submit]` / `button[type=submit]`), which Omeka core and module forms render
103+
without any theme class to hook.
104+
105+
Before this, the base selector painted *every* button filled-and-glowing, so sixteen
106+
component files reset `border-radius` / `box-shadow` / `transform` purely to escape the
107+
default, and a component overriding only `background`/`color` silently kept a rounded
108+
floating halo. Those resets are now redundant rather than load-bearing — harmless where
109+
they remain, and safe to drop when you're already editing the file. The thing to watch
110+
now is the reverse: **a control that needs to shout must say so**, or it will render
111+
quiet.
77112

78113
### Read a module's rendered HTML before styling it
79114

asset/css/style.css

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

asset/sass/abstracts/mixins/_mixins.scss

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,37 @@
5252
box-shadow var(--transition-fast);
5353
}
5454

55-
@mixin primary-button {
56-
@include button;
55+
// ==========================================================================
56+
// The LOUD button — filled primary, glow, hover lift.
57+
//
58+
// Appearance only (no padding / case / layout), so it can be laid over the
59+
// quiet base in _buttons.scss without re-deciding geometry. This is the
60+
// opt-in half of the base-style inversion: until v2.10 every <button> in the
61+
// document got this treatment by default and any quiet control had to reset
62+
// three properties to escape it. Restraint is the register; the shout opts in.
63+
// ==========================================================================
64+
@mixin button-filled {
5765
background: var(--primary);
58-
border: none;
66+
border: 1px solid transparent;
5967
color: var(--white);
6068
box-shadow: var(--glow-sm);
6169

62-
&:hover:not(.disabled) {
70+
&:hover:not(:disabled):not(.disabled) {
6371
background: var(--primary-hover);
72+
border-color: transparent;
6473
color: var(--white);
6574
transform: translateY(var(--lift-xxs));
6675
box-shadow: var(--glow-md);
6776
}
6877

69-
&:active:not(.disabled) {
78+
&:active:not(:disabled):not(.disabled) {
7079
background: var(--primary-active);
7180
transform: translateY(0);
7281
box-shadow: var(--glow-xs);
7382
}
7483

7584
&:focus-visible {
7685
color: var(--white);
77-
outline: 2px solid var(--primary);
78-
outline-offset: 2px;
7986
}
8087

8188
&:disabled,
@@ -92,6 +99,16 @@
9299
}
93100
}
94101

102+
@mixin primary-button {
103+
@include button;
104+
@include button-filled;
105+
106+
&:focus-visible {
107+
outline: 2px solid var(--primary);
108+
outline-offset: 2px;
109+
}
110+
}
111+
95112
// ==========================================================================
96113
// Card hover effect
97114
// Consistent shadow + quiet border emphasis. Calmer than v1's brand-tinted
@@ -112,9 +129,11 @@
112129
// Focus ring
113130
// Consistent focus-visible outline for interactive elements
114131
// ==========================================================================
132+
// The default focus idiom. Use --ring-focus instead when an outline would be
133+
// clipped (inside overflow:hidden or a scroll container) — see _tokens.scss.
115134
@mixin focus-ring {
116135
&:focus-visible {
117-
outline: 2px solid var(--focus-color);
136+
outline: var(--focus-outline);
118137
outline-offset: 2px;
119138
}
120139
}

asset/sass/abstracts/variables/_breakpoints.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
// The responsive contract. Published into tokens.json by build-tokens.js and
2+
// asserted by both modules' check-theme-tokens.js, because media queries
3+
// cannot read custom properties: the modules necessarily restate these as
4+
// literals, and until 2026-08 the only thing keeping them honest was a
5+
// `/* sm */` comment beside each one. That failed twice — IwacVisualizations'
6+
// laicite block reflowed at 640px labelled "sm" while every other block on
7+
// the same page reflowed at 600px.
8+
//
9+
// Six steps. Use `max-width: #{$x - 1px}` (or `- 0.02px`) for the "below"
10+
// half of a pair so the two never overlap.
11+
$xs: 400px; // narrow phones — hero headline step only
112
$sm: 600px;
213
$md: 768px;
314
$lg: 1024px;

asset/sass/abstracts/variables/_colors.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363

6464
// Semantic / state
6565
--focus-color: var(--primary);
66-
--focus-ring: color-mix(in oklab, var(--primary) 30%, transparent);
66+
--focus-ring-color: color-mix(in oklab, var(--primary) 30%, transparent);
6767
--selection-bg: color-mix(in oklab, var(--primary) 14%, transparent);
6868

6969
// Status colors — OKLCH, calibrated for AA on light surfaces
@@ -157,7 +157,7 @@
157157
--border-strong: oklch(42% 0.016 70);
158158

159159
--focus-color: var(--primary);
160-
--focus-ring: color-mix(in oklab, var(--primary) 35%, transparent);
160+
--focus-ring-color: color-mix(in oklab, var(--primary) 35%, transparent);
161161
--selection-bg: color-mix(in oklab, var(--primary) 22%, transparent);
162162

163163
--success: oklch(72% 0.14 152);

asset/sass/abstracts/variables/_tokens.scss

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@
2020
--shadow-color-strong: color-mix(in oklab, oklch(15% 0.01 264) 18%, transparent);
2121
--shadow-color-subtle: color-mix(in oklab, oklch(20% 0.01 264) 6%, transparent);
2222

23+
// Four steps, deliberately not five: shadows are reserved for true panels
24+
// and overlays here — rows and tags are divided by rules instead — so an
25+
// elevation scale long enough for a card-based grammar is a scale that
26+
// gets used like one. --shadow-xl (the fifth) was removed in 2.10: it was
27+
// referenced by nothing in any of the three repos.
28+
// xs — hairline lift on quiet chrome sm — panels (--panel-shadow)
29+
// md — hover / raised panels lg — overlays: drawer, modal
2330
--shadow-xs: 0 1px 2px 0 var(--shadow-color-subtle);
2431
--shadow-sm: 0 1px 3px 0 var(--shadow-color), 0 1px 2px -1px var(--shadow-color-subtle);
2532
--shadow-md: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color-subtle);
2633
--shadow-lg: 0 10px 15px -3px var(--shadow-color), 0 4px 6px -4px var(--shadow-color-subtle);
27-
--shadow-xl: 0 20px 25px -5px var(--shadow-color), 0 10px 10px -5px var(--shadow-color-subtle);
2834

2935
--panel-bg: var(--surface);
3036
// See the note in :root — re-declare so the inner var() resolves against
@@ -62,12 +68,20 @@
6268
// ========================================================================
6369
// Border Radius Scale
6470
// ========================================================================
65-
--radius-sm: 0.375rem; // 6px
66-
--radius-md: 0.5rem; // 8px — tightened from 12px to feel more
67-
// institutional/instrument-like, less consumer
68-
--radius-lg: 0.75rem; // 12px
69-
--radius-xl: 1rem; // 16px
70-
--radius-full: 9999px;
71+
// Three non-pill steps, each with ONE job. A rules-not-boxes system that
72+
// publishes four interchangeable radii invites four different answers to
73+
// the same question — which is how maps ended up at 6px, cards at 8px and
74+
// the compact search panel at 12px on the same page.
75+
--radius-sm: 0.375rem; // 6px — inline marks: chips, tags, map controls
76+
--radius-md: 0.5rem; // 8px — DEFAULT for panels, controls, inputs.
77+
// Tightened from 12px to feel more
78+
// institutional/instrument-like, less consumer.
79+
--radius-lg: 0.75rem; // 12px — media only: figures, thumbnails, carousel
80+
// plates. A photograph reads as matted, not as a
81+
// card; nothing that is chrome may use this.
82+
--radius-full: 9999px; // pills
83+
// (--radius-xl, 16px, removed in 2.10 — declared, referenced by nothing in
84+
// any of the three repos, and one more way to disagree about a corner.)
7185

7286
// ========================================================================
7387
// Accent Lines & Lift
@@ -103,9 +117,29 @@
103117
--measure-wide: 72.5rem; // ~1160px — search results, wide lists
104118

105119
// ========================================================================
106-
// Focus Rings
107-
// ========================================================================
108-
--ring-focus: 0 0 0 3px var(--focus-ring);
120+
// Focus — ONE decision, two idioms, and a rule for picking between them.
121+
//
122+
// --focus-outline DEFAULT. `outline` + `outline-offset: 2px`. Use it
123+
// unless the element is clipped.
124+
// --ring-focus The box-shadow ring, for when an outline would be
125+
// clipped: inside `overflow: hidden`, inside a scroll
126+
// container, or on an element flush against a panel
127+
// edge. --ring-focus-sm is the tighter 2px version for
128+
// dense controls.
129+
// --focus-color the colour. --focus-ring-color is the translucent
130+
// tint --ring-focus is built from; consuming it
131+
// directly is almost always a mistake.
132+
//
133+
// The four names used to be --focus-color / --focus-ring / --ring-focus /
134+
// --ring-focus-sm with nothing saying which applied when, and --focus-ring
135+
// vs --ring-focus one transposition apart. The result downstream was
136+
// `outline: 2px solid var(--focus-color, var(--primary, #ce4115))`
137+
// hand-copied 44 times across IwacVisualizations' block stylesheets —
138+
// because there was no token for the composed outline, only for its
139+
// colour. --focus-outline is that token.
140+
// ========================================================================
141+
--focus-outline: 2px solid var(--focus-color);
142+
--ring-focus: 0 0 0 3px var(--focus-ring-color);
109143
--ring-focus-sm: 0 0 0 2px color-mix(in oklab, var(--primary) 22%, transparent);
110144

111145
// ========================================================================
@@ -123,7 +157,11 @@
123157
--tracking-wider: 0.08em;
124158

125159
// ========================================================================
126-
// Glow Effects (primary-tinted shadows for buttons/accents)
160+
// Glow (primary-tinted shadow) — ONE effect, three states.
161+
//
162+
// Not an elevation scale: xs/sm/md are the active / rest / hover states of
163+
// the single filled button (see base/elements/_buttons.scss, which is the
164+
// only sanctioned consumer). Nothing else in the system glows.
127165
// Derived from --primary so admin color overrides cascade through.
128166
// ========================================================================
129167
--glow-xs: 0 1px 3px 0 color-mix(in oklab, var(--primary) 20%, transparent);
@@ -184,7 +222,6 @@
184222
--shadow-sm: 0 2px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color-subtle);
185223
--shadow-md: 0 6px 16px -4px var(--shadow-color-strong), 0 4px 8px -4px var(--shadow-color-strong);
186224
--shadow-lg: 0 12px 28px -8px color-mix(in oklab, black 35%, transparent), 0 8px 16px -8px color-mix(in oklab, black 20%, transparent);
187-
--shadow-xl: 0 20px 40px -12px color-mix(in oklab, black 45%, transparent), 0 12px 24px -8px var(--shadow-color-strong);
188225

189226
--panel-bg: var(--surface-raised);
190227
// See comment in :root block — re-declare for dark cascade.

asset/sass/abstracts/variables/_typography.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ $font__pre: $font__code;
3737
// where the visual gain at large viewports is real and the drift on
3838
// mid-sizes isn't distracting. Body & UI sizes hold steady so a 14px
3939
// facet label doesn't quietly become 14.6px between breakpoints.
40+
// The UI tier is a 2px arithmetic progression: 11 · 13 · 15 · 17 · 19.
41+
// --text-2xs is the FLOOR — nothing in the stack may set type smaller,
42+
// and there is deliberately no 14px step. Both halves of that sentence
43+
// are load-bearing:
44+
//
45+
// * Before the floor existed, the scale stopped at 13px while dense
46+
// axis / chip / legend labels genuinely need less, so ~12 of them
47+
// were written as bare 0.6875rem literals here and in
48+
// IwacVisualizations, plus a 9px one. A step nothing can reach is
49+
// a step everyone reaches around.
50+
// * A 14px step would put three sizes inside 2px, which is a puddle,
51+
// not a scale. IwacVisualizations' 12/14/18px literals came from a
52+
// generic utility framework, not from a gap here: they migrate to
53+
// 13 / 15 / 19 rather than the theme growing a step to match them.
54+
--text-2xs: 0.6875rem; // 11px — FLOOR: dense axis/legend/chip labels
4055
--text-xs: 0.8125rem; // 13px — eyebrow labels, metadata dt
4156
--text-sm: 0.9375rem; // 15px — secondary UI, captions
4257
--text-base: 1.0625rem; // 17px — body copy

0 commit comments

Comments
 (0)