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
+20 -5
View File
@@ -59,13 +59,28 @@ Review all changes in scope. If changes span multiple patterns below, apply all
`Button` component, icon-only buttons are `IconButton` — never a freshly styled
`<button>`. Interactive controls are `var(--control-height)` (32px) or
`var(--control-height-lg)` (40px); a hardcoded control height (28px, 36px, …) in a
diff is a finding. A freshly written bordered-field recipe is the same finding —
fields share one look (1px `--border-strong`, `--bg` fill, `--control-height`); a
new input should copy an existing field class, not re-derive it. Call-site classes composed onto a primitive may only do layout
diff is a finding. **The field look has exactly one home** — the element baseline
in `styles/base.css` (fill `var(--field)` + bottom border `--border-strong`, no
box; arch 09 §4): a `border:` or `background:` on an input/textarea in a component
module is a finding (module classes add only width/padding/font-size); the sole
sanctioned restatements are the select-like triggers (SelectControl, SortControl).
A surface that elevates to `--layer-01` sets `--field: var(--field-02)` /
`--field-hover: var(--field-hover-02)` on its container, mirroring
`--control-hover-fill`. Call-site classes composed onto a primitive may only do layout
(flex, margins, reveal) or a documented state accent (outlined-danger, pressed) —
restyling the primitive's box from a call site is a finding. Borders mark function:
value-holding controls get `--border-strong` boxes; plain actions are ghost or
filled; list rows are flat with dividers, not stacked boxes.
full `--border-strong` boxes are reserved for segmented controls, secondary
buttons, drop targets (dashed), and the color-swatch input; fields and triggers
are underlined, not boxed;
passive chrome (tags, badges, glyphs) takes `--border`; plain actions are ghost or
filled; list rows are flat with dividers, not stacked boxes. A shared look travels
through one of exactly four mechanisms — design tokens, contextual custom
properties set by surfaces, `base.css` element baselines, React primitives
(Button/IconButton); introducing a fifth (CSS-module `composes`, utility classes,
a mixin layer) is a finding. When a recipe migrates to a shared baseline, grep
for every selector that restated any of its fragments (focus, placeholder,
border) — a partially deleted restatement is worse than an undeleted one,
because its higher specificity silently overrides the baseline.
6. **Code Comments**: Comments should not duplicate what the code already says. Remove
parroting comments. Ensure comments capture non-obvious _why_ — design decisions,
+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.
+25 -10
View File
@@ -70,7 +70,12 @@
--layer-01: #f4f4f4;
--layer-02: #e0e0e0;
--border: #e0e0e0;
--border-strong: #c6c6c6;
/* 3:1 non-text contrast on canvas and --layer-01 (WCAG 1.4.11). */
--border-strong: #8d8d8d;
--field-01: #f4f4f4;
--field-02: #ffffff;
--field-hover-01: #e8e8e8;
--field-hover-02: #e8e8e8;
--text: #161616;
--text-secondary: #525252;
--text-placeholder: #a8a8a8;
@@ -82,7 +87,11 @@
--layer-01: #262626;
--layer-02: #393939;
--border: #393939;
--border-strong: #525252;
--border-strong: #6f6f6f;
--field-01: #262626;
--field-02: #393939;
--field-hover-01: #333333;
--field-hover-02: #474747;
--text: #f4f4f4;
--text-secondary: #a8a8a8;
--text-placeholder: #6f6f6f;
@@ -494,17 +503,18 @@
font-size: 12px;
color: var(--text-secondary);
}
/* Field-on-layer (arch 09 §3.3): a field is a layer above its surface, so it
steps toward --bg (white) with a --border-strong outline — on the canvas
and on a gray panel alike — never a darker-gray fill. */
/* The quiet field (arch 09 §3.3 + §4): a fill one step off its surface plus
a bottom border only — --field-01 on the canvas, --field-02 on a
--layer-01 panel (panels set --field: var(--field-02)). */
.input,
.select,
.textarea {
font: inherit;
font-size: 14px;
color: var(--text);
background: var(--bg);
border: var(--border-width) solid var(--border-strong);
background: var(--field, var(--field-01));
border: none;
border-block-end: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
}
@@ -516,8 +526,7 @@
.select:focus,
.textarea:focus {
outline: 2px solid var(--focus);
outline-offset: -1px;
border-color: var(--accent);
outline-offset: -2px;
}
.textarea {
font-family: var(--font-mono);
@@ -597,7 +606,8 @@
gap: var(--space-2);
font-size: 12px;
padding: var(--space-1) var(--space-3);
border: var(--border-width) solid var(--border-strong);
/* A passive tag, not a control — subtle border (borders mark function). */
border: var(--border-width) solid var(--border);
}
.dot {
width: 8px;
@@ -693,6 +703,9 @@
border: var(--border-width) solid var(--border);
padding: var(--space-6);
max-width: 320px;
/* Elevated surface: fields on it step their fill off --layer-01 (arch 09 §3.3). */
--field: var(--field-02);
--field-hover: var(--field-hover-02);
}
.panel h4 {
margin-bottom: var(--space-4);
@@ -1667,6 +1680,8 @@
'--layer-02',
'--border',
'--border-strong',
'--field-01',
'--field-02',
'--text',
'--text-secondary',
'--text-placeholder',
-16
View File
@@ -8,22 +8,6 @@ record the resolution into the contract (`docs/architecture/09`+`10` and the rel
## Open
- **Border weight as identity — boxed fields vs Carbon's quieter defaults**
(**priority: pick this up first, next session** — user-flagged 2026-06-12).
Carbon's own defaults use far fewer boxes than we do: fields are filled with a
bottom border only (`reference/carbon` → text-input `_text-input.scss`,
`border-block-end` only), secondary buttons are a gray _fill_, tertiary is the
only outlined button (`_button.scss``button-theme(transparent,
$button-tertiary, …)`), and tiles separate by layer steps with full borders
only behind an accessibility feature flag. Our fully-boxed fields/secondaries
are a recorded divergence (arch 09 §2 + §4) and the box-discipline rules now
contain the noise — the open question is whether to go further toward Carbon
(bottom-border-only fields, filled secondary) or keep the boxed look as
deliberate identity. An identity decision, not cleanup: it would repaint every
field in the app. Council seats: Carbon (canon above), NN/g (affordance of
boxed vs underlined inputs), a11y (3:1 non-text contrast for field boundaries
either way).
- **Two visible "Export" buttons** (`App.tsx` header — workspace export;
`ChartExport.tsx` in the preview header — per-chart export). Same label,
different scope, both visible at once. Candidate fix: rename one ("Export
+6 -1
View File
@@ -55,7 +55,8 @@
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
border: var(--border-width) solid var(--border-strong);
/* A passive badge, not a control — subtle border (borders mark function). */
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
padding: var(--space-1) var(--space-3);
}
@@ -67,6 +68,10 @@
/* Vertical rule separating the utilities from Donate + the theme toggle.
(The header buttons themselves are the shared Button primitive — ghost
utilities, soft-accent Donate; arch 09 §4.) */
/* TODO: --border-strong is the 3:1 component-boundary token (arch 09 §3.3), which
makes this passive divider fairly dark; --border would be the semantic choice but
is near-invisible on the --layer-01 header in light theme. A separator may want
its own subtler value if this reads too heavy. */
.headerDivider {
flex: 0 0 auto;
width: var(--border-width);
+2 -1
View File
@@ -58,7 +58,8 @@
display: inline-block;
padding: var(--space-1) var(--space-3);
background: var(--layer-01);
border: var(--border-width) solid var(--border-strong);
/* A passive key-cap badge, not a control — subtle border (borders mark function). */
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: 12px;
@@ -133,11 +133,6 @@
min-width: 0;
height: var(--control-height);
padding: 0 var(--space-2);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -152,10 +147,6 @@
min-width: 0;
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
}
@@ -165,10 +156,6 @@
min-width: 0;
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
}
@@ -179,13 +166,6 @@
font-family: var(--font-mono);
}
.calcName:focus-visible,
.valueInput:focus-visible,
.exprInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.exprInput[aria-invalid='true'] {
border-color: var(--support-error);
}
@@ -423,20 +403,10 @@
gap: var(--space-5);
}
.dimInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.dimInput {
width: 100px;
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -484,27 +454,29 @@
min-width: 0;
}
/* Actionable-hint remedies: each fix is an offer, never forced, so they're
low-emphasis ghost buttons — suggestions beside the advice, not commands. */
/* Actionable-hint remedies: each fix is an offer, never forced — low-emphasis
suggestions beside the advice, not commands. */
.warningFixes {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
/* Inline link-style actions (arch 09 §4): content-sized accent text, no box —
the smallness is the emphasis level. */
.warningFix {
font: inherit;
font-size: 12px;
cursor: pointer;
padding: var(--space-1) var(--space-2);
border: var(--border-width) solid var(--border-strong);
border: none;
border-radius: var(--radius);
background: var(--bg);
background: transparent;
color: var(--accent);
}
.warningFix:hover {
background: var(--layer-01);
background: var(--control-hover-fill, var(--layer-01));
}
.warningFix:focus-visible {
@@ -727,7 +699,8 @@
.shelfGlyph {
min-width: 22px;
padding: 1px var(--space-2);
border: var(--border-width) solid var(--border-strong);
/* A passive type tag, not a control — subtle border (borders mark function). */
border: var(--border-width) solid var(--border);
background: var(--layer-01);
color: var(--text-secondary);
font-family: var(--font-mono);
@@ -877,11 +850,6 @@
width: 64px;
margin: 0 var(--space-2);
padding: var(--space-1) var(--space-2);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 12px;
}
@@ -1032,11 +1000,6 @@
width: 200px;
height: var(--control-height);
padding: 0 var(--space-2);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -1045,11 +1008,6 @@
cursor: not-allowed;
}
.propInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
/* Reserved faceting placeholder (Phase 4) — non-interactive, signals where it'll live. */
.facetSlot {
display: flex;
+1 -1
View File
@@ -18,7 +18,7 @@
max-width: min(320px, 92vw);
padding: var(--space-3);
background: var(--layer-01);
border: var(--border-width) solid var(--border-strong);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
@@ -388,11 +388,6 @@
.textarea {
width: 100%;
padding: var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -402,17 +397,6 @@
min-height: 160px;
}
.input:focus-visible,
.textarea:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.input::placeholder,
.textarea::placeholder {
color: var(--text-placeholder);
}
.detected {
display: flex;
align-items: center;
@@ -37,23 +37,9 @@
.input {
width: 100%;
padding: var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
.input:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.input::placeholder {
color: var(--text-placeholder);
}
.preview {
margin: 0;
max-height: 240px;
+6 -1
View File
@@ -21,8 +21,11 @@
border-radius: var(--radius);
box-shadow: 0 2px 12px rgb(0 0 0 / 0.3);
/* Modal chrome sits on --layer-01, so ghost/secondary controls in the header
step their hover fill up (arch 09 §4). The body resets to --bg below. */
step their hover fill up, and fields step their fill off this surface
(arch 09 §4). The body resets to --bg below. */
--control-hover-fill: var(--layer-02);
--field: var(--field-02);
--field-hover: var(--field-hover-02);
}
/* Large: the two-pane managers (Datasets). Definite height so inner panes scroll. */
@@ -79,4 +82,6 @@
overflow: auto;
background: var(--bg);
--control-hover-fill: var(--layer-01);
--field: var(--field-01);
--field-hover: var(--field-hover-01);
}
+10 -9
View File
@@ -8,9 +8,13 @@
gap: var(--space-2);
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
/* A trigger is a field, not a button (arch 09 §4): fill one step off the
surface, bottom border only. The baseline recipe in base.css reaches only
input/textarea elements, so this <button> restates it. */
border: none;
border-block-end: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
background: var(--field);
color: var(--text);
font: inherit;
font-size: 13px;
@@ -21,12 +25,9 @@
color var(--dur-fast) var(--ease);
}
.trigger:hover:not(:disabled) {
background: var(--layer-01);
}
.trigger:hover:not(:disabled),
.trigger[aria-expanded='true'] {
background: var(--layer-02);
background: var(--field-hover);
}
.trigger:disabled {
@@ -36,7 +37,7 @@
.trigger:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
outline-offset: -2px;
}
.triggerLabel {
@@ -58,7 +59,7 @@
max-width: 320px;
padding: var(--space-2);
background: var(--layer-01);
border: var(--border-width) solid var(--border-strong);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgb(0 0 0 / 0.4);
}
+4 -16
View File
@@ -16,9 +16,12 @@
max-width: min(320px, 90vw);
padding: var(--space-4);
background: var(--layer-01);
border: var(--border-width) solid var(--border-strong);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
/* The popover surface is --layer-01: fields step their fill off it. */
--field: var(--field-02);
--field-hover: var(--field-hover-02);
}
.title {
@@ -81,11 +84,6 @@
.text {
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 12px;
}
@@ -99,16 +97,6 @@
font-family: var(--font-mono);
}
.number:focus-visible,
.text:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.text::placeholder {
color: var(--text-placeholder);
}
.reset {
border: none;
background: none;
+4 -32
View File
@@ -97,24 +97,9 @@
height: var(--control-height);
/* Room for the leading magnifier and the trailing clear button. */
padding: 0 32px 0 32px;
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
.searchInput::placeholder {
color: var(--text-placeholder);
}
.searchInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
border-color: var(--accent);
}
/* Hide the browser's native search clear so we control the affordance. */
.searchInput::-webkit-search-cancel-button {
appearance: none;
@@ -316,8 +301,11 @@
padding: var(--space-4);
border-top: var(--border-width) solid var(--border);
background: var(--layer-01);
/* The panel sits on --layer-01, so its controls' hover fill steps up. */
/* The panel sits on --layer-01, so its controls' hover fill and its fields'
fill both step off this surface, not the canvas. */
--control-hover-fill: var(--layer-02);
--field: var(--field-02);
--field-hover: var(--field-hover-02);
}
.metaField {
@@ -338,15 +326,6 @@
.metaComment {
width: 100%;
padding: var(--space-2) var(--space-3);
/* A field is a layer *on top of* the panel it sits on (Carbon layering,
arch 09 §3.2): on the gray `--layer-01` panel the field goes white with a
strong border — matching the search input above — rather than a darker gray,
which stacked three muddy grays. */
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
@@ -362,13 +341,6 @@
line-height: 1.4;
}
.metaName:focus-visible,
.metaComment:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
border-color: var(--accent);
}
/* Read-only Created / Modified, as compact label→value rows. */
.metaTimes {
margin: 0;
+9 -10
View File
@@ -17,9 +17,12 @@
gap: var(--space-1);
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
/* A trigger is a field, not a button (arch 09 §4) — same recipe as the
SelectControl trigger and the search input it sits beside. */
border: none;
border-block-end: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: transparent;
background: var(--field);
color: var(--text-secondary);
font: inherit;
font-size: 12px;
@@ -30,19 +33,15 @@
color var(--dur-fast) var(--ease);
}
.trigger:hover {
background: var(--layer-01);
color: var(--text);
}
.trigger:hover,
.trigger[aria-expanded='true'] {
background: var(--layer-02);
background: var(--field-hover);
color: var(--text);
}
.trigger:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
outline-offset: -2px;
}
/* The disclosed panel — portaled to <body>, positioned fixed (mirrors the
@@ -53,7 +52,7 @@
min-width: 160px;
padding: var(--space-2);
background: var(--layer-01);
border: var(--border-width) solid var(--border-strong);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
@@ -121,19 +121,9 @@
width: 100%;
height: var(--control-height);
padding: 0 var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
.input:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.toolbarEnd {
margin-left: auto;
display: flex;
@@ -177,21 +167,12 @@
width: 100%;
min-height: 0;
padding: var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
resize: none;
}
.configText:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
/* The gallery wraps fixed-size swatch cards; scrolls when they overflow. */
.gallery {
display: flex;
+2 -2
View File
@@ -74,7 +74,7 @@ export const lightBaseConfig: Config = {
background: 'transparent',
title: { color: '#161616' }, // --text (light)
axis: {
domainColor: '#c6c6c6', // --border-strong (light)
domainColor: '#c6c6c6', // axis line — deliberately lighter than UI --border-strong; chart guides stay subtle
gridColor: '#e0e0e0', // --border (light)
labelColor: '#525252', // --text-secondary (light)
titleColor: '#161616', // --text (light)
@@ -86,7 +86,7 @@ export const darkBaseConfig: Config = {
background: 'transparent',
title: { color: '#f4f4f4' }, // --text (dark)
axis: {
domainColor: '#525252', // --border-strong (dark)
domainColor: '#525252', // axis line — deliberately lighter than UI --border-strong; chart guides stay subtle
gridColor: '#393939', // --border (dark)
labelColor: '#a8a8a8', // --text-secondary (dark)
titleColor: '#f4f4f4', // --text (dark)
+43
View File
@@ -55,6 +55,49 @@ body {
outline-offset: 2px;
}
/*
* Field baseline (arch 09 §4) — the quiet field: a fill one step off its surface
* plus a bottom border only, no box. The fill comes from the contextual --field
* token (elevated surfaces set `--field: var(--field-02)` on their container,
* the same mechanism as --control-hover-fill); the underline is --border-strong,
* which meets 3:1 non-text contrast in both themes. Declared once here at zero
* specificity (:where) so modules add only their idiosyncrasies — width, mono
* font, padding — never a competing border recipe. Monaco's internal widgets
* (find/replace inputs) keep their own styling and are excluded.
*/
:where(
input:not([type]),
input[type='text'],
input[type='search'],
input[type='number'],
input[type='url'],
textarea
):where(:not(.monaco-editor *)) {
font: inherit;
color: var(--text);
background: var(--field);
border: none;
border-block-end: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
}
:where(input, textarea)::placeholder {
color: var(--text-placeholder);
}
/* On a field the ring hugs the box — an offset ring reads as a second border
where there is no longer a box to anchor it. */
:where(
input:not([type]),
input[type='text'],
input[type='search'],
input[type='number'],
input[type='url'],
textarea
):where(:not(.monaco-editor *)):focus-visible {
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
+22 -2
View File
@@ -65,6 +65,12 @@
/* Focus ring defaults to the accent. */
--focus: var(--accent);
/* Contextual field fill — elevated surfaces set `--field: var(--field-02)`
(and `--field-hover: var(--field-hover-02)`) once on their container, the
same mechanism as --control-hover-fill. */
--field: var(--field-01);
--field-hover: var(--field-hover-01);
}
/* --- Neutral roles: LIGHT theme (Carbon gray ramp) --- */
@@ -73,7 +79,16 @@
--layer-01: #f4f4f4;
--layer-02: #e0e0e0;
--border: #e0e0e0;
--border-strong: #c6c6c6;
/* Component boundaries need 3:1 against the surface (WCAG 1.4.11); gray-50 is
3.32:1 on the canvas and 3.02:1 on --layer-01 (Carbon's $border-strong-01). */
--border-strong: #8d8d8d;
/* Field fills (arch 09 §4): a field is a fill one step off its surface plus a
bottom border — --field-01 on the canvas, --field-02 on an elevated surface.
Surfaces flip the contextual --field token; components consume --field only. */
--field-01: #f4f4f4;
--field-02: #ffffff;
--field-hover-01: #e8e8e8;
--field-hover-02: #e8e8e8;
--text: #161616;
--text-secondary: #525252;
--text-placeholder: #a8a8a8;
@@ -85,7 +100,12 @@
--layer-01: #262626;
--layer-02: #393939;
--border: #393939;
--border-strong: #525252;
/* 3.60:1 on the dark canvas (Carbon g100 $border-strong-01). */
--border-strong: #6f6f6f;
--field-01: #262626;
--field-02: #393939;
--field-hover-01: #333333;
--field-hover-02: #474747;
--text: #f4f4f4;
--text-secondary: #a8a8a8;
--text-placeholder: #6f6f6f;