Skip to content

🐛 fix(theme): one two-tone focus ring, 3:1 on every background (#135) - #147

Merged
Cemoche merged 7 commits into
mainfrom
issue-135-focus-ring
Oct 21, 2026
Merged

Cemoche merged 7 commits into
mainfrom
issue-135-focus-ring

Conversation

@Cemoche

@Cemoche Cemoche commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Closes #135

Summary

The app now has one focus ring, in two tones, chosen by the owner. It reaches at least 3:1 against every background a focusable sits on, in LIGHT, DARK_MEDIA and DARK_FORCED.

  • Ring: a 2 px --accent outline and a 1 px band in a new token, --focus-ring-inner: black in light, white in dark. Two utilities in src/index.css draw it:
    • focus-ring sits outside the control: the outline at offset 1 px, the band as a box-shadow in the gap.
    • focus-ring-inset sits inside: the outline at −2 px, the band as an inset box-shadow. It is used for rows, tabs, toolbar and menu items, and fields inside clipping or scrolling parents.
  • Behaviour kept:
    • A control's own shadows are still drawn under the band.
    • aria-invalid:outline-danger still wins.
  • Contrast:
    • The band alone reaches 3:1 on every background: 15 page backgrounds, the swatch tints, the tag badges, and 7 grid fills including selected rows and the change fills.
    • Band against the accent: 5.23 in light, 3.93 in dark.
  • Grid: Glide's red focused-cell ring was under 3:1 on --sel, the change fills and selected rows. It now gets the same inner band, drawn in drawCell 1 px inside Glide's ring. The geometry was checked against the Glide 6.0.3 source, and unfocused cells pay nothing extra.
  • Checkboxes: a new --accent-control token (#0062CC in light, #4A88FA in dark) replaces accent-accent. It reaches at least 3.30 on dark --pill.
  • Row-detail textarea on an edited field: the 1.52 blue border is replaced by the inset ring.
  • Menu items and the resize edge: their focus was only a fill under 3:1. They keep the fill and gain the inset ring.
  • Guards in src/styles/focusRing.test.ts:
    • No outline, ring or border colour under a focus variant. No outline-none without the shared ring (8 entries in ALLOWED, each with its reason). No accent-accent.
    • focus_ring_everywhere scans every .tsx file for buttons, inputs, selects, textareas, links, tabIndex ≥ 0 elements and MenuButton call sites. It follows class strings through constants, functions, style objects and imports. When a name has several definitions, every one must draw the ring.
    • Before the fix it flagged 39 elements that still used the browser's default ring: dialogs, confirm buttons, prompts, the import dialog, the pending changes panel, the breadcrumb, the Structure tab and the row detail actions. All 39 now use the shared ring. 5 exceptions are allowed, each with its reason.
  • Docs: amendment Contrast: focus rings and accent controls under 3:1 on some backgrounds #135 in spec §5.1, and the focus-ring rule in src/CLAUDE.md.

The swatch label uses has-[:focus-visible], which needs WebKitGTK 2.42 or later, as TabBar already does. No Rust change.

How it was tested

$ pnpm exec vitest run --maxWorkers=1     # 109 files, 1420 tests passed (then focusRing.test.ts after the last round: 9/9)
$ pnpm exec tsc --noEmit                  # clean

I didn't run the app, so the owner needs to make one keyboard pass, in light and then in dark.

Manual check list (light, then dark, at 100 %)

Do each step in the light theme, then the dark theme (Settings, or data-theme), at 100 % zoom.

  1. Connections screen. Tab through:
    • the Welcome actions;
    • the search field;
    • the row's "…" button, a group header and connection rows (ring inside the row);
    • the selected row.
  2. Connection form. Tab through:
    • the fields, buttons and the primary Save (blue fill: there should be a band between the fill and the ring);
    • the colour swatches: the ring goes around the 24 px swatch, outside the checked ring;
    • the SSH checkbox: ring, and a check fill visible on --content.
  3. Toolbar. Tab through:
    • the round buttons (ring inside);
    • the status pill (ring outside, not cut);
    • the safe mode menu: open it, arrow to an item, the ring sits inside the highlighted item;
    • the window controls (GNOME circles outside; Windows caption buttons inside).
  4. Open connections column. Tab through the tiles and the close "×".
  5. Sidebar.
    • Tab through the Items/Queries switch and the search field: the ring is inside the field.
    • Open the search menu and arrow through it: the ring is inside the blue item.
    • Tab through tree rows, including a selected one (--seg-sel) and a hovered one.
    • Footer: + and ⌄ (ring inside the joined group) and the schema select.
    • Resize edge: Tab to it; the ring shows inside the 8 px strip.
  6. Tab bar. Tab through:
    • the inactive tabs (--tab) and the active tab (dark --tab-sel: the white band should stand out);
    • the rename field (double click). Type an invalid name: the outline turns red.
    • the "…" list button.
  7. Table tab.
    • Filter bar: checkbox (on --side, and on an applied row's --filter-applied), fields, buttons, SQL preview.
    • Bottom bar: Data, Structure and Index segments; the selected segment (blue fill) shows the band. Also the Columns popover (checkboxes, hovered row) and the Page popover.
  8. Grid.
    • Click cells on a plain row, a zebra row, a selected row (blue), an edited cell (amber), an inserted row (green), a deleted row (pink), and a selected column (--sel).
    • In each case: the red ring with the inner black (light) or white (dark) line, on all four sides, including the last column and the last row.
    • Context menu: arrow through it; the ring is inside the highlighted item.
  9. Row detail panel.
    • Search field (ring inside), close button.
    • The value field on a plain cell and on an edited cell (amber): the inset ring replaces the old blue border.
  10. SQL tab.
    • Toolbar buttons, result tabs, history rows and Clear, saved queries.
    • Quick Look: Copy and the value region.
  11. Settings. Section list (ring inside, selected --sel), fields (ring inside, over the border), buttons.
  12. Structure / Index. The Index button.

In each case, check:

  • the ring is never cut by its container;
  • it is not hidden under the next row;
  • an element with a shadow keeps it.
  1. Outside ring next to a filled sibling. A later sibling's background can cover the band.
    • In the connection list, Tab to a group header right above a selected row: the bottom band and ring should stay visible.
    • In the FilterBar, Tab across the buttons next to the joined MenuButton halves and an applied row's fill.
    • In the bottom bar, Tab across the Add row / Columns buttons.
  2. Dialogs. Tab through every button and field:
    • confirm, rename and save-query dialogs;
    • Confirm run (safe mode), the secret prompt, the host key prompt, the URL import, the TablePlus import (including its checkboxes and password field).
  3. Panels. Tab through:
    • the pending changes panel (header, Undo);
    • the row detail panel (FK links, Retry, Set NULL / Empty / Default);
    • the breadcrumb links;
    • the table tab notices (Reload, Dismiss), the disconnected workspace's Connect;
    • the Structure tab (section tabs, FK links, Copy, Reload);
    • the tab list's filter field.

Checklist

  • pnpm test green
  • pnpm lint green (tsc; no Rust change, clippy in CI)
  • pnpm test:rust green (not applicable: no src-tauri/ change)
  • No secret, token, real password or private key in the diff
  • User-visible strings in src/lib/strings.ts
  • Commits follow <emoji> <type>(<scope>): <description>

🤖 Generated with Claude Code

Cemoche and others added 7 commits October 21, 2026 09:20
…n every background (#135)

--focus-ring-inner (black / white) is the second band of the two-tone focus
ring and of the grid's focused cell; --accent-control the accent-color of
native checkboxes. tokens.test.ts checks both per background and theme.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… background (#135)

The focus-ring / focus-ring-inset utilities (src/index.css) draw a 2 px
--accent outline plus a --focus-ring-inner band, outside the control or
inside it (rows, tabs, toolbar and menu items, fields in clipping parents).
Every focus-visible / focus-within outline moves to them; the selected
segment drops its white ring, the colour swatches ring the whole swatch,
the row detail's value field and the menus' highlighted items get the ring,
the resize edges too (their accent tint was under 3:1). Native checkboxes
take --accent-control.

focusRing.test.ts refuses a single-tone focus outline, ring or border and an
outline-none without the shared ring (ALLOWED lists the reasons).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… fill (#135)

Glide strokes the active cell in --cell-focus, under 3:1 on --sel, the
change fills and the selected rows. DataGrid's drawCell (now in read-only
grids too) adds a 1 px --focus-ring-inner band just inside it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uctural guard (#135)

Dialogs, prompts, the import dialogs, panel buttons, breadcrumb and FK links,
Structure tabs, the tab list filter and the workspace notices fell back to
the browser's ring. focus_ring_everywhere scans each <button>, <input>,
<select>, <textarea>, <a href> and tab stop (and MenuButton call sites) and
resolves its className through literals, constants and imported constants;
exceptions go to UNRINGED_ALLOWED with their reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s; notes on clipping and :has() (#135)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ition (#135)

The structural guard kept one value per name (the last const, the first
style key), so a duplicate could hide an unringed class. It now keeps every
definition and passes only when each one draws the ring; notes on what it
leaves out (ternary branches, dotted components, contentEditable, summary).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Cemoche
Cemoche merged commit 2aed6d0 into main Oct 21, 2026
8 of 9 checks passed
@Cemoche
Cemoche deleted the issue-135-focus-ring branch October 21, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contrast: focus rings and accent controls under 3:1 on some backgrounds

1 participant