fix(a11y): give interactive controls a boundary users can actually see (#14684)
* fix(a11y): give interactive controls a boundary users can actually see
WCAG 1.4.11 wants 3:1 on the boundary that identifies a control. Ours
measured 1.27:1 in light and 1.43-2.72:1 in dark, so every text field,
dropdown trigger, outlined button and radio was effectively unbounded.
The obvious fix -- darken --border or --input -- is wrong here. Bare
`border` resolves to --border via the `* { @apply border-border }` base
rule, so darkening it would repaint every divider, card edge, table rule
and AG-Grid line in the product; 1.4.11 does not cover those. And --input
is overloaded as a fill (five panel backgrounds, four hover states, the
switch off-track), so darkening it would turn those panels grey. Neither
token means "control boundary", so this adds one that does.
--control-boundary is computed against the surfaces controls actually sit
on, not against an assumed white/black: 3.36:1 on white and 3.06:1 on
--muted in light; in dark it is sized against --muted (#27272a), the worst
real surface, for 3.37:1 there and 4.01:1 on --background. Hue and
saturation stay in the existing family so this is a value shift, not a
recolor.
Only boundaries move. --border and --input keep their current values, so
hover fills, panel backgrounds, dividers and card edges are untouched.
Checkboxes already passed (border-muted-foreground, 5.28:1) and are left
alone. The switch off-state is a fill rather than a boundary, so it needs
its own design call and is not included.
The guard reads the real token values out of index.css and re-derives the
ratios, so the token cannot be lightened back without failing.
Ref LE-2270
* fix(a11y): close the review findings — three missed control boundaries, a hover-state dip, and a guard that punished improvement
Review turned up that "all in-scope boundaries fixed" was over-claimed, plus
two states the token-level audit could not see:
- Three in-scope controls still drew --border: the Action Picker's label
field and its outlined add-button, and the file input — the subtle one,
where a focus:border-border utility outranked .primary-input's
components-layer focus boundary (utilities beat @layer components
regardless of class order), so the control met 3:1 at rest and dropped to
1.27:1 exactly when focused. All three now use border-control; the file
input simply drops the override so the existing focus:border-foreground
applies.
- A residual sweep found two more real misses the review predicted would
exist: the app-header language <select> and the assistant composer (the
exact parallel of the two chat composers already migrated). Also removed
an order-ambiguous duplicate border class from the list-selection
checkbox so its measured 5.28:1 colour no longer depends on stylesheet
order.
- The outline button kept hover:bg-input, and the new boundary measures
2.65:1 (light) / 1.75:1 (dark) against that fill — a state regression the
token change itself introduced. hover:bg-muted keeps the pair at
3.06:1 / 3.37:1.
- The contrast guard asserted --border MUST fail 3:1, which would fail CI on
a future change that improves --border — a valid improvement. The case now
documents the split without the upper bound.
- The read-only embedding display in the KB upload modal is back on
border-input: its edge is decorative, not a control boundary.
- The control-boundary convention (and its two traps: focus-utility
overrides, hover-fill pairs) is written into the frontend-code-review
skill so the next control regresses in review, not in an audit.
* fix(frontend): clear pre-existing biome errors in files this PR touches
Biome lints whole changed files, so two long-standing errors in files
this PR only restyled were failing CI: an untyped `item` prop in
ComboBoxItem and unsorted imports in LanguageSelector.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> V
Viktor Avelino committed
97ced626d8e29a43f9590b8b17777d1f262f9b26
Parent: 2a29054
Committed by GitHub <noreply@github.com>
on 8/20/2026, 8:15:15 PM