Quiet fields: base.css field baseline, --field tokens, 3:1 border-strong, app-wide migration

This commit is contained in:
2026-06-13 00:06:33 +03:00
parent ebdeac7747
commit e0806bf3db
19 changed files with 218 additions and 232 deletions
+55 -26
View File
@@ -54,6 +54,7 @@ were choices, not drift:
| Color | "Blue at the core"; other hues only for purpose | **Dropped.** Color/theming is free and expressive; accent is a token, many themes welcome |
| Neutrals | Carbon gray ramp | **Borrowed** — accessible, well-tuned, a good legible base |
| Motion | Productive vs. expressive | **Productive only** — subtle, purposeful, reduced-motion-aware |
| Secondary button | A dark gray **fill** (`$button-secondary`) | **Outlined** (Carbon's _tertiary_ shape) — one filled button per region stays the rule |
---
@@ -98,7 +99,8 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
| ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `--bg` | App canvas (lowest layer) |
| `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow |
| `--border` / `--border-strong` | Subtle and prominent separators |
| `--border` / `--border-strong` | Subtle separators / component boundaries (3:1 non-text contrast) |
| `--field-01` / `--field-02` (+ `--field-hover-*`) | Field fills: one step off the canvas / off a `--layer-01` surface |
| `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy |
| `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue |
| `--accent-soft` / `--accent-soft-hover` | Low-emphasis accent **wash** (accent mixed into `--bg`) for a tinted-but-quiet surface |
@@ -113,18 +115,26 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
- **Status palette** (borrowed, stable): error `#da1e28`, success `#198038`,
warning `#f1c21b`, info `#0043ce` — tuned per theme for contrast.
- **Contrast:** target WCAG AA (4.5:1 text, 3:1 large/UI). Accent-on-`--bg` and
text-on-`--accent` must both pass for any shipped theme.
text-on-`--accent` must both pass for any shipped theme. **`--border-strong` is a
component boundary, not decoration — it must hold 3:1 against the surface it
bounds (WCAG 1.4.11)**: gray-50 `#8d8d8d` light (3.32:1 on `--bg`, 3.02:1 on
`--layer-01`), gray-60 `#6f6f6f` dark (3.60:1 on `--bg`). Carbon's gray-30/gray-70
"strong" values fail this; don't drift back to them.
- **Soft accent is derived, not hardcoded.** `--accent-soft` /
`--accent-soft-hover` are `color-mix(in srgb, var(--accent) 1220%, var(--bg))`,
so the wash follows whatever accent + theme is active rather than carrying a
per-accent value. Use it for a surface that should be _noticed_ without competing
with a primary action (the header's **Donate** button).
- **Field-on-layer (Carbon layering).** A field is a layer _on top of_ the surface
it sits on — so it steps **toward `--bg`**, never to a darker gray. On the white
canvas and on a `--layer-01` panel alike, an input reads as **`--bg` fill +
`--border-strong` outline**, not a `--layer-02` fill. Stacking a darker-gray field
on a gray panel (canvas → `--layer-01` panel → `--layer-02` field) muddies into
three indistinct grays — the bug this rule prevents (the snippet metadata panel).
- **Field-on-layer (Carbon layering).** A field's fill is **one step off the
surface it sits on**, alternating like Carbon's field set: `--field-01` on the
canvas (gray on white / near-black on black), `--field-02` on a `--layer-01`
surface (white on gray / a step lighter on dark). The alternation is what keeps
a field visible without a box and prevents three indistinct grays from stacking
(canvas → panel → field — the snippet-metadata-panel bug). Mechanism: components
consume the contextual **`--field`** / `--field-hover` tokens only; an elevated
surface sets `--field: var(--field-02)` (and the hover variant) once on its
container — the same pattern as `--control-hover-fill`. Setters today: the modal
chrome and body (ModalShell), the library metadata panel, the settings popover.
### 3.4 Shape & elevation
@@ -149,7 +159,15 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
## 4. Component conventions
- **The control scale (two heights, enforced by tokens + primitives).** Every
- **How a shared look travels.** Exactly four mechanisms, in escalating order:
**design tokens** (`styles/tokens.css`) for values; **contextual custom
properties** set once by a surface and consumed by everything on it
(`--control-hover-fill`, `--field`); **`base.css` element baselines** at zero
specificity for looks every instance of an element shares (the focus ring, the
field recipe); **React primitives** (`Button`, `IconButton`) when shared
_behavior_ or enforced variants justify a component. Nothing else — no
CSS-module `composes`, no utility classes, no mixin layer. A fifth mechanism is
drift, even when it's locally cleaner. Every
interactive control is `--control-height` (32px) or `--control-height-lg`
(40px) — tokens.css. 32px is THE control height: buttons, fields, selects,
segmented controls, icon buttons, anything in a toolbar, form row, or dialog
@@ -162,9 +180,8 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
_inside_ a 32px control — a search field's clear, a toast's dismiss); writing
`height:` on a new ad-hoc button is the code smell.
- **Buttons:** square, via the `Button` primitive. Variants: **primary** (filled
`--accent`), **secondary** (1px `--border-strong`, `--bg` fill — the same
field-on-layer treatment as inputs, so a bordered control on a gray panel goes
white, never a darker gray), **ghost** (text-only, borderless; a transparent
`--accent`), **secondary** (1px `--border-strong`, `--bg` fill —
so a bordered control on a gray panel goes white, never a darker gray), **ghost** (text-only, borderless; a transparent
border holds the box size), **soft-accent** (ghost on an `--accent-soft` wash —
a low-emphasis solicitation, e.g. Donate), **danger** (filled
`--support-error` — the confirm step), **danger-outline** (secondary geometry,
@@ -176,16 +193,16 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
control scale — content-sized, 1112px, no box — so they read as part of the
prose/panel they act on, not as toolbar controls. Don't "promote" them to
Buttons; their smallness is the emphasis level.
- **Borders mark function, not decoration.** A 1px `--border-strong` box belongs
to things that _hold or receive a value_: fields, selects/disclosure triggers,
segmented controls, secondary buttons, drop targets (dashed). Plain actions are
ghost or filled — never outlined boxes. List rows are flat (hairline dividers +
hover fill), not stacked boxes. With square chrome, every box makes alignment
errors visible, so each border must earn its place; when a region looks
"busy", remove boxes before shrinking anything. (Carbon goes further — fields
are bottom-border-only and its secondary button is a gray fill; our fully-boxed
fields are a recorded divergence, §2, which is exactly why box discipline
matters more here, not less.)
- **Borders mark function, not decoration.** Fields and select-like triggers are
**not boxes** — they're the quiet-field recipe below (fill + bottom border). A
1px `--border-strong` **box** is reserved for the few value-holding controls
that need full enclosure: segmented controls, secondary buttons, drop targets
(dashed), the color-swatch input. Plain actions are ghost or filled — never
outlined boxes; passive chrome (tags, badges, type glyphs) takes `--border`,
never `--border-strong`. List rows are flat (hairline dividers + hover fill),
not stacked boxes. With square chrome, every box makes alignment errors
visible, so each border must earn its place; when a region looks "busy",
remove boxes before shrinking anything.
- **Emphasis hierarchy (Carbon button/usage).** A region carries **one**
high-emphasis (primary) button at most; everything else is lower emphasis. In
toolbars/headers full of utilities, the utilities go **ghost** so they recede
@@ -207,10 +224,22 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
inherit the right step instead of each re-encoding it.
- **Focus ring:** a 2px `--focus` outline (offset 12px). Always visible on
keyboard focus — accessibility is non-negotiable (principle 4).
- **Fields** (text, textarea, select, search): square, 1px `--border-strong`,
**`--bg` fill** (field-on-layer, §3.3 — white on the canvas _and_ on a
`--layer-01` panel; never a darker-gray fill), accent border + focus ring on
focus. Mono font for spec/JSON inputs.
- **Fields** (text, textarea, select-trigger, search): **the quiet field** —
square, `--field` fill (one step off the surface, §3.3), **bottom border only**
in `--border-strong`, no box; 2px `--focus` ring hugging the box (offset 2px)
on focus. Mono font for spec/JSON inputs. The recipe is declared **once** in
`styles/base.css` as a zero-specificity element baseline (text-like `input`
types + `textarea`, excluding Monaco's internal widgets); component modules add
only idiosyncrasies — width, padding, font size — never a competing border or
fill. Select-like triggers are `<button>`s the baseline can't reach, so
SelectControl/SortControl restate it (they are fields, not buttons: a trigger
holds a value); their hover/open fill is `--field-hover`. Writing `border:` on
an input is the code smell — the field look has exactly one home. The quiet
treatment is Carbon's; the boxed alternative (GOV.UK's canon — 2px solid
enclosure) is equally legitimate a11y-wise but spends a box on every field,
and in a square-chrome editor UI boxes are reserved for the few controls that
need full enclosure. A field's boundary is carried by its fill step plus the
3:1 underline.
- **List rows** (snippet library): compact, full-row hover (`--layer-01`),
active row marked by an accent left-border + `--layer-01` fill, secondary
metadata in `--text-secondary`. Row-level actions reveal on hover.