Fix the broken input focus outline, unify input styling, tokenize scales

The root cause of 'some inputs show a focus outline, most don't': the
global input:focus/textarea:focus/select:focus rule (outline:none,
border-color: accent) has specificity (0,0,1,1). Every per-component
rule shaped '.wrapper input { border: 1px solid var(--border) }' (.form
input, .field-row input, .kanban__new input, .sidebar__select select,
.modal__field select) ties it exactly, and -- being unconditional --
silently won that tie by simply appearing later in the file, so the
input's border stayed var(--border) forever regardless of focus. Only
two controls escaped: .project-card__name-input (a class applied
directly to the element, (0,0,1,0), too low to ever win the tie) and
.card-row__text (has its own .card-row__text:focus, (0,0,2,0),
genuinely higher). Confirmed with a live computed-style test against
the real stylesheet before and after.

Fix: one canonical .field class (plus .field--compact for inline 'add'
rows and the sidebar, .field--autosize for the dashboard's JS-grown
textarea), applied directly to the <input>/<textarea>/<select> itself
in every view -- structurally immune to the same tie, since a bare
class can never out-specificity input:focus. This also collapses five
near-duplicate rules (each with its own padding/radius/font-size) into
one definition, and fixes .kanban__new input's stray
background: var(--surface) (every other field uses var(--bg); this one
nearly matched its own var(--surface) sidebar panel).

Also: .btn-danger hardcoded #b3261e/#fff instead of the already-
existing var(--error)/var(--accent-text) tokens, so danger buttons
didn't adapt in dark mode like every other error-coloured element;
now they do. Added .field:disabled styling (opacity/cursor), matching
buttons -- latent until now since no input bound :disabled yet.

Second pass, promoting repeated-but-consistent raw values to tokens:
- border-radius: 6/7/8/10/12/999px -- the stray 7px (.sidebar__link)
  folded into the 6px tier -- become --radius-sm/md/lg/xl/pill.
- disabled/ghost opacity: 0.6 was used for every disabled button
  except .status-row__delete's 0.4 (now unified) and drag-ghost states'
  0.5 (semantically different, kept separate) -- --opacity-disabled/
  --opacity-ghost.
Font-size and spacing values also repeat but don't reduce to a clean
scale without arbitrary judgement calls either way -- left alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 22:07:35 +01:00
co-authored by Claude Sonnet 5
parent a85bb182c7
commit a3462af005
8 changed files with 145 additions and 113 deletions
+38
View File
@@ -74,6 +74,44 @@ dashboard; the grid and the sidebar dropdown both pick it up via the shared
`projects` store). Signed-out routes
(`/login`, `/verify-email`) render without the sidebar.
## Styling
`style.css` is one global stylesheet (no scoped/component styles) with a
handful of conventions worth knowing before adding to it:
- **Tokens** (`:root` custom properties): colours (`--bg`, `--surface`,
`--border`, `--text`, `--muted`, `--accent`(-text), `--error`, `--warn-bg`/
`-border`), a border-radius scale (`--radius-sm` 6px compact controls,
`--radius-md` 8px buttons/inputs, `--radius-lg` 10px tiles, `--radius-xl`
12px panels, `--radius-pill`), and two opacity values (`--opacity-disabled`
0.6, `--opacity-ghost` 0.5 for a dragged item's placeholder). Reach for one
of these before hand-writing a value that's really just "the same grey
border again" or "the same rounding as every other button."
- **`.field`** is the one themed `<input>`/`<textarea>`/`<select>` look, used
everywhere from the login form to the sidebar's project switcher.
`.field--compact` is the same thing smaller, for a control that's a flex
child beside a button (an inline "add" row) or squeezed into the sidebar.
`.field--autosize` adds the `resize:none; overflow:hidden` the dashboard's
JS-driven auto-growing textarea needs. Crucially, **`.field` is applied
directly to the control**, not to a wrapper (`.form input` no longer
exists) -- see the next point for why that's load-bearing, not just style.
- **Put component classes on the control itself, not a wrapping element**,
for anything that needs a `:focus` state. `input:focus`/`textarea:focus`/
`select:focus` (global, removes the default outline and colours the border
with `--accent` instead) has specificity `(0,0,1,1)` -- one pseudo-class,
one element. A rule shaped `.wrapper input { border-color: var(--border) }`
ties it exactly, and being unconditional, silently wins that tie by simply
appearing later in the file -- the input keeps `var(--border)` forever,
focused or not, no matter what `:focus` says. `.field`, applied straight to
the element, is `(0,0,1,0)` -- strictly lower, so it can never win that
tie regardless of source order. (This bit a real shipped version of the
app: half the inputs had a working focus ring and half silently didn't,
purely from which pattern each one happened to use.) A component that
needs its own more elaborate `:focus` state (`.card-row__text`, background
swap included) writes `.card-row__text:focus` explicitly -- specificity
`(0,0,2,0)`, genuinely higher, wins outright rather than by luck of
ordering.
## Inbox
A card with no project lives in the caller's inbox (`useInboxStore`), rendered