mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Quiet fields: base.css field baseline, --field tokens, 3:1 border-strong, app-wide migration
This commit is contained in:
@@ -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) 12–20%, 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, 11–12px, 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 1–2px). 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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user