Skip to content

Replace focus rings with a painted token shift (closes #2718) - #2726

Merged
malibio merged 3 commits into
mainfrom
issue-2718-focus-token-shift
Sep 17, 2026
Merged

malibio merged 3 commits into
mainfrom
issue-2718-focus-token-shift

Conversation

@malibio

@malibio malibio commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #2718.

A focused control now adopts its own hover appearance instead of gaining a ring. Focus and hover mean the same thing to the person using the app — "this is the control you are about to act on" — so they look the same, and a treatment built only from background-color cannot move the control or its siblings.

The hard constraint

A focus treatment may change painted properties only. Three families are banned, for three different reasons, and only the first is literally a reflow:

  • border-width / padding genuinely reflow. border: 0 → border: 2px adds 4px to the box and shoves every sibling after it. This is what makes a form twitch line by line as you tab through it.
  • outline with a negative offset does not reflow, but paints inside the element's edge, so the control reads as having suddenly grown a thick inner border.
  • ring-offset-* does not reflow either, but paints a gap plus a ring around the control, visually enlarging its footprint — indistinguishable from movement.

All three are banned together so nobody has to re-derive which kind of "doesn't move" a given property offers. border-color stays allowed: it repaints an existing border without touching its width.

What changed

Component Before After
Button ×6 variants outline-none, no indicator at all each variant's own hover appearance
select-trigger, 2 schema triggers no indicator accent fill
tabs-trigger ring-2 ring-offset-2 accent fill
tabs-content ring on a scroll container nothing — not a control
checkbox ring-ring/50 at 1.84:1 nothing — fill already means checked
switch, radio-group-item ring idioms, bare :focus nothing
dialog-content close bare :focus + ring-2 accent, focus-visible
relationship-viewer-modal ×5 ring-1 accent
app-shell outline: 2px on :focus-within nothing
chat-input, model-manager ×2 alpha box-shadow ring border-color only
input ×2 dead ring-offset-background removed

Two defects the issue had not catalogued

Its survey covered Tailwind utilities; these are hand-written CSS, so nothing in the original table reached them.

  • app-shell carried outline: 2px solid; outline-offset: 2px on :focus-within — which drew a ring around the entire application window whenever anything inside it held focus. This is the idiom removed app-wide in Split-Pane Layout System #300; it survived because :focus-within was in nobody's grep.
  • chat-input and model-manager (×2) paired a border-color change with box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2) — the same theme-blind alpha compositing the checkbox had.

The trap a geometry ban cannot catch

A fill that moves nothing but is indistinguishable from the page satisfies every rule above and still shows the user nothing. This bit twice during implementation:

  1. The neutral treatment was first written as bg-muted, matching how neutral hovers are done elsewhere. --muted is 1.10:1 against --background in light mode.
  2. Then secondary was given focus-visible:bg-secondary/80 to mirror its hover, which composites to 1.08:1.

Measured across both themes, --accent is the only non-brand token clearing the 3:1 non-text floor (5.19:1 light, 3.75:1 dark), so the chrome surface uses it. Hover can afford a subtle shift because the pointer is already on the control; focus cannot, because it is the only thing telling a keyboard user where they are. Both traps now have assertions rather than depending on someone remembering to measure.

Controls that get nothing

Checkbox, switch and radio each show their state with their fill, so borrowing that fill for focus would make a focused-unchecked box read as checked. The only surface left is a border — the geometry idiom this change exists to prevent. A tab panel is likewise a scroll container rather than a control; there is nothing in it for Enter to activate.

The editor surface is untouched. Tab is bound to indent/outdent there rather than focus traversal, and the four sites carrying /* Removed :focus-visible border */ keep that decision.

Test

focus-treatment.test.ts (10 tests) sweeps every source file for focus utilities and hand-written :focus blocks. It is a text scan rather than a render because Happy-DOM applies no Tailwind stylesheet — a computed style reports an empty background for a correct class and a forbidden one alike, and would pass either way.

Mutation-tested against four regressions, each caught by the intended assertion:

Mutation Caught by
ring-2 ring-offset-2 restored on switch geometry-utility sweep
border-width: 2px in a CSS :focus rule hand-written-CSS sweep
focus-visible: reverted to focus: bare-:focus sweep
secondary/80 invisible alpha fill restored alpha-focus-fill assertion

Also verified through PostCSS that all 11 focus classes compile and resolve to their intended tokens — the --*-hover ones bare and opaque, as the config intends.

Known gap, stated rather than hidden

--accent-foreground on --accent is 3.40:1 in light mode, below the 4.5:1 text floor. It is a pre-existing property of the accent pair, already shipped on seven hover:bg-accent sites, which this change inherits rather than introduces. The test pins the figure so it cannot silently worsen, and #2724 tracks the fix (light --accent-foreground is near-black on a dark fill — almost certainly copied from --secondary-foreground).

Verification

  • Frontend: 252 files / 5164 tests passing (baseline 251 / 5154 — +10 tests, no regressions)
  • Full pre-push gate green: test:all, cargo build --bin nodespaced, test:e2e
  • quality:fix clean: eslint, design tokens (134 known, unchanged), svelte-check 0 errors across 2543 files, cargo fmt + clippy
  • DESIGN.md updated in nodespace-docs (bfe6b7f)

🤖 Generated with Claude Code

A focused control now adopts its own hover appearance instead of gaining a
ring. Focus and hover mean the same thing to the person using the app —
"this is the control you are about to act on" — so they look the same, and a
treatment built only from background-color cannot move the control or its
siblings.

The hard constraint is that focus must never change geometry. Three families
are banned for three different reasons: border-width/padding genuinely reflow
and shove every sibling after the control; outline with a negative offset
paints inside the edge and reads as a sudden inner border; ring-offset-* paints
a gap plus a ring and visually enlarges the footprint. Only the first is
literally a reflow, which is why all three are banned together rather than
leaving a reader to work out which kind of "doesn't move" each one offers.

Two defects surfaced that the issue had not catalogued, because its survey
covered Tailwind utilities and these are hand-written CSS:

- app-shell carried `outline: 2px solid; outline-offset: 2px` on :focus-within,
  which drew a ring around the ENTIRE application window whenever anything
  inside it held focus. This is the idiom removed app-wide in #300; it survived
  because :focus-within was in nobody's grep.
- chat-input and model-manager (x2) paired a border-color change with
  `box-shadow: 0 0 0 2px hsl(var(--ring) / 0.2)` — the same theme-blind alpha
  compositing the checkbox had. The border-color change is kept: it repaints an
  existing border without touching its width, so it moves nothing.

A painted-only rule creates a trap a geometry ban cannot catch: a fill that
moves nothing but is indistinguishable from the page satisfies every rule and
still shows the user nothing. This bit twice during implementation. The neutral
treatment was first written as bg-muted, matching how neutral hovers are done
elsewhere — 1.10:1 against the page in light mode. Then `secondary` was given
focus-visible:bg-secondary/80 to mirror its hover, which composites to 1.08:1.
Measured against both themes, --accent is the only non-brand token clearing the
3:1 non-text floor (5.19:1 light, 3.75:1 dark), so chrome focus uses it. Both
traps now have assertions rather than relying on someone re-measuring.

Checkbox, switch and radio get no focus treatment at all. Each shows its state
WITH its fill, so borrowing that fill for focus would make a focused-unchecked
box read as checked; the only surface left is a border, which is the geometry
idiom this change exists to prevent. A tab panel is likewise a scroll container
rather than a control — nothing in it for Enter to activate.

The editor surface is untouched. Tab is bound to indent/outdent there rather
than focus traversal, and the four sites carrying "Removed :focus-visible
border" keep that decision.

focus-treatment.test.ts enforces all of it, sweeping every source file for
focus utilities and hand-written :focus blocks. It is a text scan rather than a
render because Happy-DOM applies no Tailwind stylesheet, so a computed style
reports an empty background for a correct class and a forbidden one alike.
Mutation-tested against four regressions: a reintroduced ring, a border-width
change in CSS, a focus-visible->focus revert, and the invisible alpha fill.

One known gap is stated rather than hidden: --accent-foreground on --accent is
3.40:1 in light mode, below the 4.5:1 text floor. It is a pre-existing property
of the accent pair, already shipped on seven hover:bg-accent sites, which the
focus rule inherits rather than introduces. The test pins the figure so it
cannot silently worsen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@malibio

malibio commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Review Type

Initial review — PR #2726, single commit 9688912c, 16 files, +350/−31. No prior reviews or comments.

Verification performed independently of the PR body: WCAG ratios recomputed from app.css tokens with my own sRGB/luminance implementation; FORBIDDEN_UTILITY and the CSS brace state machine exercised against 36 and 15 synthetic cases respectively; buttonVariants({variant}) evaluated at runtime through the real vitest config to observe post-tw-merge output; the codebase swept for surviving focus idioms; the editor surface diffed for accidental changes.

Headline: the core design work is sound and the contrast reasoning is correct — every figure in the PR body reproduces exactly. Two things block: a WCAG 2.4.7 regression where outline-none remains on controls that now have no replacement indicator, and a set of false negatives in the guard test that leave the rule enforceable only against the exact idioms that happened to exist today.


Requirements Check

# Acceptance criterion Status Evidence
1 A single painted-only focus idiom replaces all five current ones ✅ Met One idiom (focus-visible:bg-accent / each variant's *-hover) across button, select-trigger, tabs-trigger, dialog-content, the 2 schema triggers and the 5 modal rails.
2 Button, select-trigger, schema-field-leaf, nested-field-trigger have a visible focus treatment ✅ Met All four verified. buttonVariants evaluated at runtime: all 6 variants emit a surviving focus class.
3 Zero ring-*, ring-offset-*, outline (≠outline-none), border-width or padding in any focus declaration, app-wide ✅ Met in fact, 🟡 partially guarded Grep sweep confirms zero survivors. Remaining ring-* are all aria-invalid: (not a focus state — correctly out of scope). But the guard has false negatives (F1, F2 below).
4 No alpha-composited focus values; every focus treatment clears 3:1 against its surface in both themes 🟡 Partial No-alpha holds. The 3:1 claim does not hold for the filled variants: --primary-hover vs --primary is 1.38:1 light / 1.30:1 dark, and --destructive-hover likewise. The test only measures --accent vs --background and never measures the filled pair. See F3.
5 Bare :focus replaced by :focus-visible on radio-group-item and dialog-content ✅ Met Both converted; guarded by the bare-focus: sweep with a documented dropdown-menu/ exception (correct — bits-ui roving focus).
6 Checkbox, switch and radio have no focus treatment rather than a border-based one ✅ Met as written, 🔴 but the spec is wrong Implemented exactly as specced. The spec's consequence is a 2.4.7 failure — see F4.
7 Editor surface untouched — the four "Removed :focus-visible border" sites still have no treatment ✅ Met git diff --name-only over node-tree/tab-system/navigation-sidebar returns empty. All 7 marker comments (4 named + 2 in noderef.css + 1) intact.
8 A test enumerates every focus utility and fails on any that changes geometry 🟡 Partial 10 tests, all passing, no vacuous assertion (the FILES.length > 100 guard is a good touch). But "every" overstates it — see F1/F2.
9 DESIGN.md records the painted-only rule and the editor/chrome distinction ✅ Met nodespace-docs@bfe6b7f, new ### Focus section. Out of this diff by design (separate repo).

Independent contrast verification — every figure the PR asserts reproduces to the stated precision:

Claim PR says I measured
--accent vs --background, light 5.19:1 5.19 ✅
--accent vs --background, dark 3.75:1 3.75 ✅
--accent-foreground on --accent, light 3.40:1 3.40 ✅
--muted vs --background, light 1.10:1 1.10 ✅
--secondary/80 composited vs --background 1.08:1 1.08 ✅

Credit where due: the "invisible fill" trap is real, the diagnosis is right, and --accent genuinely is the only non-brand token clearing 3:1 in both themes. That reasoning survives scrutiny.


Code Review Findings

🔴 Critical

F4 — outline-none survives on the controls that lost their indicator: checkbox, switch and radio now have no focus indication at all, in any form.

📁 packages/desktop-app/src/lib/components/ui/checkbox/checkbox.svelte:21
📁 packages/desktop-app/src/lib/components/ui/switch/switch.svelte:20
📁 packages/desktop-app/src/lib/components/ui/radio-group/radio-group-item.svelte:20

This is the finding I'd most want the author to push back on, so let me be precise about why it isn't the same as "the spec said so."

The spec said don't give them a border treatment. What shipped is stronger: each of these keeps outline-none (unconditional on checkbox's base string; focus-visible:outline-none on switch and radio), which suppresses the user agent's own focus ring. Before this PR they had a bad indicator (1.84:1). After it they have zero — not a weak one, not a native fallback, nothing. The browser default was the safety net and this removes it while removing the thing that replaced it.

The concrete failure is live in the tree today:

📁 packages/desktop-app/src/lib/components/settings/import-options-modal.svelte:183-196

<div class="grid gap-3">
  {#each optionRows as option (option.id)}
    <div class="flex items-start gap-3">
      <Checkbox id={option.id} bind:checked={modalState[option.key]} ... />

A vertical stack of checkboxes and nothing else interactive between them. A keyboard user tabbing into this modal cannot tell which checkbox Space will toggle. That is WCAG 2.4.7 (Focus Visible, Level AA) failing outright, and 2.1.1 in practice — the control is reachable but not operable with confidence.

I'd also challenge the premise in the source comment. checkbox.svelte:22-26 argues the only surfaces are "its border and its fill," and that border means geometry. But the component already carries border border-primary at rest — a 1px border that always exists. Changing its color changes no geometry at all, and the PR itself makes exactly this argument three times for hand-written CSS:

📁 chat-input.svelte:98 — "Border COLOR only: the width is unchanged, so nothing reflows."

The rule the PR wrote permits focus-visible:border-accent on all three of these. The change applied a stricter reading of its own rule than the rule requires, and paid for it with an accessibility regression.

Suggested fix — one of, in order of preference:

  1. focus-visible:border-accent (or a dedicated --focus-border token) on all three. Zero geometry change, consistent with the PR's own stated principle, restores 2.4.7. --accent vs --input gives ample separation.
  2. Drop outline-none from these three specifically and let the UA ring stand. Ugly and inconsistent across platforms, but correct, and defensible as an explicit "native fallback" carve-out.
  3. If the user insists on literally nothing: that is a conscious, documented AA exception and needs to be stated to them in those terms — "checkbox, switch and radio will have no focus indicator and keyboard users cannot operate the import-options modal" — not left implied by "as specced."

Principle: removing a fallback and its replacement in the same change is not a neutral refactor. The spec authorized declining to add a treatment; it did not authorize suppressing the one the platform provides for free.


🟡 Improvements

F1 — FORBIDDEN_UTILITY has false negatives that let genuine geometry changes through.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:75-76

I ran the regex against 36 cases. It correctly catches everything the PR mutation-tested, and correctly spares outline-none, bg-accent, border-primary, underline — the discrimination between a border color and a border width works, which is the hard part and is done well. The misses:

Utility Caught? Why it matters
focus-visible:border ❌ Bare border = border-width: 1px. On a control with border-0 at rest this adds 2px to the box — the exact reflow this PR exists to prevent, and the single most natural way to write the regression.
focus-visible:border-[3px] ❌ border-\d requires a digit immediately after the hyphen; [ is not a digit.
focus-visible:p-[4px], px-[2px] ❌ Same arbitrary-value hole.
focus-visible:outline ❌ outline-(?!none) requires a trailing hyphen; bare outline = outline-width: 1px.
focus-visible:shadow-lg / shadow-md ❌ Tailwind's shadow-* is box-shadow — the property the PR bans in hand-written CSS (FORBIDDEN_CSS_PROPERTY catches it there) but not in utility form. Inconsistent between the two sweeps.
focus-visible:tracking-wide ❌ letter-spacing — banned in the CSS regex and in the issue's table, absent here.
focus-visible:font-extrabold ❌ Alternation lists bold|semibold|medium|light only.

Every one of these would ship a geometry-changing focus state past a green suite. Suggested tightening:

/(?:focus|focus-visible|focus-within)(?::[a-z-]+)*:(?:ring(?:-offset)?(?:-|\b)|border(?:-[xytrbl])?(?:-(?:\d|\[))?\b|p[xytrbl]?-(?:\d|\[)|m[xytrbl]?-(?:\d|\[)|outline(?!-none)|shadow(?:-|\b)|scale-|translate-|tracking-|font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black))/

Principle: a guard test's value is bounded by what it doesn't let through, not by what it caught during authoring. Mutation-testing against four regressions you chose yourself confirms the assertions fire; it doesn't establish coverage. These seven are what an unfamiliar contributor would plausibly write next.


F2 — the hand-written-CSS sweep misses several real block shapes; the {-on-next-line case is the one you flagged and it is a real gap.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:141-170

I ran the state machine against 15 CSS shapes. Confirmed misses:

Shape Result
.btn:focus-visible
{
border: 2px solid red;
❌ missed — the line.includes('{') gate requires the brace on the selector line.
.btn:focus-visible { border: 2px solid red; } (single line) ❌ missed — FORBIDDEN_CSS_PROPERTY is anchored ^\s*, but the declaration follows { on the same line, so the anchor never reaches it. Also depth returns to 0 on that line, closing the block before any lookahead.
{ outline: none; box-shadow: 0 0 0 2px red; } (multiple decls, one line) ❌ missed — same ^\s* anchor; only the first declaration is ever a candidate.
.a:focus-visible,
.b { (comma list, brace on line 2)
❌ missed
@apply ring-2; inside a :focus-visible block ❌ missed — @apply is a real pattern in this repo's stack.
border-block-width: 2px ❌ missed — logical properties absent from the alternation.
Multi-line value (box-shadow:
0 0 0 2px red;)
✅ caught (on the property line)
:focus inside @media ✅ caught
Nested &:focus-visible ✅ caught
border-radius ✅ correctly not flagged

The single-line-rule and multiple-declarations-per-line misses are the most likely in practice — prettier will collapse a short rule onto one line, and .x:focus { outline: none; box-shadow: ... } is precisely the shape of the three defects this PR just fixed in chat-input and model-manager. Reintroduce one in collapsed form and the suite stays green.

Suggested fix: rather than patching the line-oriented scanner case by case, flatten each file's CSS before scanning — strip comments, normalize whitespace, then split declarations on ; within a matched :focus[...]{...} region. A @apply check is a two-line addition once the region is identified. That removes all six misses at once instead of six more special cases.

Principle: line-oriented parsing of a brace language is a category error; each fix buys one shape and leaves the next. Normalizing the input first makes the scanner's correctness independent of formatting.


F3 — AC #4's "every focus treatment clears 3:1 against its surface" is asserted for --accent and silently not asserted for the filled variants, which do not clear it.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:211-218
📁 packages/desktop-app/src/lib/components/ui/button/types.ts:45,47

The test measures --accent vs --background in both themes — correct, and it passes. But default and destructive don't use --accent; they use focus-visible:bg-primary-hover / bg-destructive-hover. The relevant question for those is how different is the focused button from the unfocused button, and:

  • --primary-hover vs --primary: 1.38:1 light, 1.30:1 dark
  • --destructive-hover vs --destructive: same order of magnitude

That is well under the 3:1 the AC claims for "every focus treatment," and it is the same class of defect as the bg-muted and secondary/80 traps the PR correctly caught and documented. The reasoning that rejected secondary/80 at 1.08:1 applies with almost equal force at 1.30:1.

To be fair about what this is and isn't: 3:1 in WCAG 1.4.11 governs a component against adjacent colors, and a filled button against the page still clears it easily. There's a defensible reading where a hover-token shift on an already-high-contrast filled button is acceptable. But the PR asserts the stronger property and doesn't measure it, and the whole thesis of the "invisible fill" section is don't trust a shift you haven't measured. Two options:

  1. Add the assertion and accept whatever threshold you actually want to defend (e.g. >= 1.25 as an explicitly-lower "filled variants shift within an already-distinct surface" floor), documented the way the 3.40 pin is.
  2. Route the filled variants to --accent too, as secondary already does.

Either way the gap between what the AC says and what the test checks should close. Right now a reader of AC #4 plus a green suite concludes something untrue.

Principle: an assertion that covers one of three cases and is named as if it covers all three is worse than no assertion — it converts an open question into a false answer.


F5 — sourceFiles() excludes tests/ wholesale, which also excludes fixtures and any future component-under-test.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:54-64

The stated reason is sound (this suite's own prose names the forbidden utilities). But the exclusion is by directory name at any depth, so any src/**/tests/ directory anywhere in the tree is skipped, not just src/tests/. Narrower and equally effective: exclude only this file, or only src/tests/design/. As written, a contributor who puts a component beside its test in a tests/ folder gets silent non-coverage.

Also worth noting: the sweep is src-only, so packages/desktop-app/src-tauri and any other package's frontend assets are out of scope. That's almost certainly right today; a one-line comment saying so would stop the next reader wondering.

Nit: FILES is computed once at module scope and each of the four sweeps re-reads every file from disk (fs.readFileSync per assertion × 4). At ~250 files that's 1000 reads for a 61ms suite — fine now, but reading once into a Map is two lines and removes the question.


F6 — the link variant's focus-visible:underline is a no-op in the one situation it most needs to work.

📁 packages/desktop-app/src/lib/components/ui/button/types.ts:65

link is hover:underline focus-visible:underline. Two problems:

  1. When a link is hovered and focused — a mouse user who has tabbed, or a hover that lands on the focused control — the focus state is visually identical to the resting hover state. The indicator conveys nothing at the moment it's most likely to be needed.
  2. An underline alone is a weak focus indicator for text that may already be underlined by surrounding context, and it fails the "focus and hover mean the same thing so they look the same" argument in a way the fills don't: for the fills, hover and focus genuinely coincide only when both are true; here link's resting state in a prose context can already be underlined.

Mitigating: variant="link" has no usages in the app today (I grepped — zero call sites), so this is latent rather than live. That's why it's 🟡 and not 🔴.

Suggestion: focus-visible:bg-accent focus-visible:text-accent-foreground would contradict the comment's reasoning about painting a block behind inline text — fair. But focus-visible:underline focus-visible:decoration-2 still changes only text-decoration-thickness (painted, no reflow — it doesn't affect line box height), and differs from plain hover. Or focus-visible:text-accent, a color shift.


🟢 Nitpicks / Observations

N1 — 📁 app-shell.svelte:832-839: verified independently — the removed rule was .app-shell:focus-within { outline: 2px solid; outline-offset: 2px } and .app-shell is the root wrapper, so it genuinely drew a 2px ring around the entire application window whenever anything inside held focus. Nothing else references it; no test covered it. Good catch, and the replacement comment explaining why there is nothing here is exactly the right artifact to leave behind — a future reader would otherwise "fix" the missing focus style.

N2 — 📁 input.svelte:27,41: ring-offset-background removed. Confirmed dead: with no ring-* on the element it set a CSS variable nothing consumed. Note transition-[color,box-shadow] survives on both and now transitions a box-shadow that no state changes. Harmless (and the repo's global transition: none !important neutralizes it anyway per #2719), but it's the same species of dead declaration you just removed one line up.

N3 — 📁 dialog-content.svelte:36: this one gets focus-visible:opacity-100 in addition to the accent fill — a second, independent signal on the close button. Good instinct. Worth noting it's the only control that got belt-and-braces treatment; the reasoning isn't recorded.

N4 — issue numbers in comments: I checked app-shell.svelte, checkbox.svelte, button/types.ts and focus-treatment.test.ts for #NNN references. Clean — zero matches. The #300 and #2724 references live in the commit message and PR body only, which CLAUDE.md explicitly exempts as process mechanics. The source comments correctly describe the constraint directly ("the one idiom this codebase has removed twice") and point at DESIGN.md rather than an issue. This standard was followed carefully.

N5 — no lint suppression, no any: verified across the diff. Clean.

N6 — tw-merge: I evaluated buttonVariants({variant}) at runtime through the real vitest config for all six variants. No collisions. outline keeps focus-visible:bg-accent alongside dark:bg-input/30 and dark:hover:bg-input/50 (different variant groups, no merge); secondary keeps focus-visible:bg-accent against bg-secondary (different modifier, no merge); ghost keeps it against dark:hover:bg-accent/50. The concern was worth checking and the answer is clean.

N7 — comment density. The source comments here are unusually long — checkbox.svelte carries 9 lines of comment for 0 lines of code. I think this is correct in this instance: each one explains a deliberate absence, which is the case where a future reader is most likely to "fix" the thing on purpose. Recording why nothing is here is high-value. Flagging only so it's a conscious pattern rather than drift.


Recommendation

🔴 REQUEST CHANGES

This is close, and most of it is genuinely good work — the contrast analysis is correct in every particular I could check, the app-shell and alpha-ring defects are real finds the issue missed, the tw-merge interaction is clean, the editor surface is properly untouched, and the decision to leave comments explaining deliberate absences is the right call. The net direction is a clear improvement on five inconsistent idioms.

Blocking — one item:

  • F4: checkbox, switch and radio keep outline-none while losing their only indicator. This suppresses the UA fallback and leaves import-options-modal — a checkbox-only form — with no visible focus at all. WCAG 2.4.7 AA. The PR's own border-color reasoning (used three times in hand-written CSS) permits focus-visible:border-accent here at zero geometry cost. If the user genuinely wants literally nothing, that's their call, but it needs to be surfaced to them in those words rather than inherited from a spec line that only said "no border treatment."

Strongly recommended before merge (the test is the artifact that has to outlive the change):

  • F1 — seven false negatives in FORBIDDEN_UTILITY, including bare focus-visible:border, which is the most natural way to write the exact regression this PR exists to prevent.
  • F2 — six missed block shapes in the CSS sweep, including the single-line rule, which is the collapsed form of the three defects this PR just fixed.
  • F3 — AC Enhanced BaseNode with Hierarchical Circle Indicators + TextNode Implementation #4 claims 3:1 for every focus treatment; the filled variants are 1.38:1/1.30:1 and are never measured.

Non-blocking: F5, F6, N1–N7.

@malibio malibio left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated pragmatic-code-review: REQUEST CHANGES (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.

Review of #2726 returned REQUEST CHANGES on one blocking finding plus three
recommended. All four addressed.

F4 (blocking) — checkbox, switch and radio keep `outline-none`, so they
suppress the user agent's own focus ring AND have no replacement. That is
strictly worse than before this branch: they previously had a bad indicator
(1.84:1), now they have none, and `import-options-modal` is a stack of
checkboxes with nothing else interactive in it.

Resolved as a deliberate, documented WCAG 2.4.7 exception rather than a fix,
per the product decision. The reviewer's suggested `focus-visible:border-accent`
does not survive measurement — accent against primary is 1.41:1, which is the
invisible-fill trap this branch exists to avoid. Measured alternatives, all
recorded in DESIGN.md and in the Checkbox source:

  bg-accent    unchecked 5.19:1 light / 3.75:1 dark, CHECKED 1.41:1
  border-accent 1.41:1 in both states
  border-width  geometry, banned by the rule itself

No painted treatment serves both states, so a fill-based indicator would be
visible on empty boxes and invisible on ticked ones. Restoring the UA ring
would close the gap and is named in DESIGN.md as the option to reach for if the
decision is revisited; it was declined because a native ring on three controls
and a painted shift on the rest is two idioms, which is what this rule replaced.

F1 — FORBIDDEN_UTILITY had ten false negatives. Verified against 32 cases.
Now caught: bare `focus-visible:border` (= border-width:1px, the most natural
way to write the exact regression this file prevents), arbitrary values
(`border-[3px]`, `p-[4px]`), bare `outline`/`ring`/`shadow`, and the aliases
that do not name their property — `shadow-*` IS box-shadow and `tracking-*` IS
letter-spacing, both already banned in the CSS sweep, so omitting them here made
the two halves of one rule disagree. Still correctly spares `border-primary`
and `border-accent`, which are border COLORS and move nothing.

F2 — the line-based CSS state machine missed four block shapes, including the
single-line rule (`.x:focus { border-width: 2px; }`) that a formatter produces
from a short rule and that is the collapsed form of the three defects this
branch just fixed. Replaced with a whole-source extractor that strips comments,
walks brace depth and flattens each rule body, so layout stops mattering
instead of needing a special case per shape. Verified against 23 cases.

Also closed a real gap the reviewer flagged and I nearly dismissed: CSS logical
properties (`border-block-width`, `padding-inline-start`). They are used at 8
sites in this codebase, so the hole was live rather than theoretical.

F3 — AC #4 claims every focus treatment clears 3:1, which is true for the
neutral fill and NOT true for the filled variants: primary-hover against primary
is 1.38:1 light / 1.30:1 dark. Recorded rather than asserted, with the reason
the case differs — a neutral control has to be told apart from an unbounded
page, whereas a filled button only has to be told apart from itself a moment
earlier, by the same shift it already makes on hover. The figures are pinned so
a token edit that flattens the shift fails loudly.

F5 — narrowed the `tests/` exclusion to `src/tests` by full path (a bare name
check would have dropped any `tests/` nested under `lib/` from every sweep), and
read each file once instead of four times.

Mutation-tested the two rewritten scanners against the exact holes reported:
a single-line CSS rule with border-width, and a bare focus-visible:border
utility. Both now fail the suite; both passed it before this commit.

Not adopted: F6 (`link`'s focus underline) — the variant has zero call sites,
so the change would be unverifiable in situ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@malibio

malibio commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Address Review — e836dd7a

Four of six findings addressed, one adopted with a different fix than suggested, one skipped. Review was thorough and caught two things I had verified insufficiently.

✅ F4 (🔴 blocking) — resolved as a documented exception

The finding is correct and it was my error: I described these controls as getting "no custom indicator," but outline-none means they suppress the UA's own ring too. Net effect is worse than before this branch — 1.84:1 became nothing — and import-options-modal.svelte:183-196 is exactly the pathological case, a checkbox stack with nothing else interactive.

The suggested fix does not survive measurement. focus-visible:border-accent is 1.41:1 against the resting --primary border — the invisible-fill trap this PR exists to catch. I measured every alternative:

Candidate Unchecked Checked
bg-accent 5.19:1 light / 3.75:1 dark ✓ 1.41:1 ✗
border-accent 1.41:1 ✗ 1.41:1 ✗
border width — geometry, banned by the rule

No painted treatment serves both states: the fill already carries "checked," so any fill-based indicator is visible on empty boxes and invisible on ticked ones — inconsistent, which is worse than absent.

Taken to the product owner with the three options (restore UA ring / accent-fill-when-unchecked / accept and document). Decision: accept and document. So this is now a deliberate WCAG 2.4.7 exception, recorded in three places rather than left implicit:

  • DESIGN.md — a table of what was measured, why each option fails, and restoring the UA ring named as the option to reach for if revisited (it draws outside the element, so it changes no geometry; declined because a native ring on three controls plus a painted shift on the rest is two idioms, which is what this rule replaced)
  • checkbox.svelte — the measured reasoning, with an explicit "read DESIGN.md before changing this line"
  • switch.svelte / radio-group-item.svelte — pointer to both

Your framing — "removing a fallback and its replacement in the same change is not a neutral refactor" — is the right principle, and it's why this went back to the user rather than being settled in the PR.

✅ F1 — ten false negatives closed, verified against 32 cases

All confirmed real. Now caught: bare focus-visible:border (= border-width: 1px, and the most natural way to write the very regression this file prevents), arbitrary values (border-[3px], p-[4px]), bare outline/ring/shadow, tracking-*, font-extrabold.

The sharpest one was the inconsistency you named: shadow-* is box-shadow and tracking-* is letter-spacing, both already banned in the CSS sweep — so omitting them from the utility sweep made the two halves of one rule disagree with each other.

Still correctly spares border-primary / border-accent (border colors, which move nothing).

✅ F2 — scanner rewritten, verified against 23 cases

Took the recommendation to normalize rather than special-case. The line-based state machine is replaced with a whole-source extractor: strip comments, walk brace depth, flatten each rule body. Layout stops mattering instead of needing a case per shape.

All four reported misses now caught, including the single-line rule — which, as you noted, is what a formatter produces from a short rule and is the collapsed form of the three defects this PR just fixed.

Also closed a gap I nearly dismissed: CSS logical properties (border-block-width, padding-inline-start). I checked before waving it off — they're used at 8 sites in this codebase, so the hole was live, not theoretical. Good catch.

✅ F3 — claim corrected rather than quietly dropped

You're right that the PR asserted a stronger property than it measured. --primary-hover vs --primary is 1.38:1 light / 1.30:1 dark; --destructive-hover vs --destructive is 1.24:1 / 1.21:1.

Now recorded with the reason the case genuinely differs from the traps: a neutral control focusing to muted had to be distinguished from the page — an unbounded surface with nothing marking where the control ends. A filled button is already a saturated shape against that page, so its shift only has to be distinguishable from the same button a moment earlier, which is the identical shift it already makes on hover. The figures are pinned (1.15–1.6) so a token edit flattening the shift fails loudly.

✅ F5 — both parts fixed

tests/ exclusion narrowed to src/tests by full path — the bare name check would have silently dropped any tests/ nested under lib/ from every sweep, which is a worse bug than the one it prevented. Files now read once into a Map instead of four times.

⏭️ F6 — skipped (link focus underline)

variant="link" has zero call sites in the app today, which you noted. Changing it to decoration-2 would be an unverifiable change to dead code; if a link button ever ships, the focus treatment gets designed against a real use.

Mutation testing

Both rewritten scanners were re-mutated against the exact holes reported. Each passed the suite before this commit and fails it now:

Mutation Result
.form-select:focus { border-color: …; border-width: 2px; } (single line) ✗ caught
focus-visible:border on select-trigger (bare utility) ✗ caught

Verification

  • Design suite: 173 passing (was 171)
  • Frontend: 252 files / 5166 tests passing
  • quality:fix clean — 0 errors/warnings across 2543 files, design-token baseline unchanged at 134
  • DESIGN.md updated in nodespace-docs (5176d76)

Summary

  • ✅ Addressed: 5 — F4 🔴, F1 🟡, F2 🟡, F3 🟡, F5 🟡
  • ⏭️ Skipped: 1 — F6 🟢 (variant="link" has zero call sites)
  • 📝 Commits: 1 — e836dd7a, plus 5176d76 in nodespace-docs
  • 🧪 Tests: passing — 252 files / 5166 tests; design suite 173 (was 171)

Re-Review Decision

Decision: RE-REVIEW NEEDED

Rationale: Three reasons, in order of weight:

  1. The blocking finding was resolved by a product decision plus documentation rather than the suggested code change. That resolution deserves a second look — particularly whether the DESIGN.md exception states the cost plainly enough to be honest about a knowingly-shipped AA gap.
  2. Both guard-test scanners were substantially rewritten — a new brace-walking CSS extractor replacing the line-based state machine, and a restructured utility regex. A guard test that is wrong in a new way is worse than one wrong in a known way, and this file is the artifact meant to outlive the change.
  3. The contrast arithmetic behind rejecting border-accent (1.41:1) is new and load-bearing, since it is why the suggested fix was not taken. It is worth independent verification.

Process note

This summary should have been posted immediately after e836dd7a and was not — the fixes were implemented by hand without invoking /address-review, so its final steps (post the summary, own the re-review call) were skipped. Caught on a direct question from the repo owner, then re-run properly. The code changes above were unaffected; what was missing was this comment, which is what a re-review reads to check whether prior feedback was addressed.

@malibio

malibio commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Review Type

Re-review — PR #2726, new commit e836dd7a on top of the reviewed 9688912c. 4 files, +192/−66. Scope of this pass: the delta only, plus nodespace-docs@5176d76 (DESIGN.md → ### Focus), which lives outside this diff.

Verification performed independently rather than read off the address-review comment: every contrast figure recomputed from app.css with my own sRGB/relative-luminance implementation (including the dark-mode --input alpha composite); FORBIDDEN_UTILITY exercised against 57 cases (35 must-match / 22 must-not-match); focusRules() + FORBIDDEN_CSS_PROPERTY exercised against 44 CSS and Svelte-markup shapes; the variant="link" call-site claim grepped; the suite run.

Headline: the blocking finding F4 is genuinely resolved-by-decision, and the documentation is honest rather than defensive — it states the cost in the first sentence rather than after the justification. F1 is fully fixed with no false positives I could construct. F2 is fixed for all four reported shapes and the rewrite is the right call, but I found two residual holes the rewrite introduced or left, one of which is a silent false negative. Neither blocks.


Previous Review Summary

Finding Severity then Status now
F4 — outline-none on checkbox/switch/radio with no replacement indicator (WCAG 2.4.7) 🔴 Blocking ✅ Resolved by decision, documented honestly. Verified below.
F1 — 7 false negatives in FORBIDDEN_UTILITY 🟡 ✅ Fixed. All 7 caught; 0 false positives in 22 allowed cases.
F2 — 6 missed block shapes in the CSS sweep 🟡 🟡 Mostly fixed. All 4 reported layout shapes + logical properties now caught. 2 residual holes found (F7, F8 below).
F3 — AC #4 claims 3:1 for every focus treatment; filled variants never measured 🟡 ✅ Fixed. Figures verified exact; assertion added, non-vacuous.
F5 — tests/ excluded by name at any depth; files re-read 4× 🟡 ✅ Fixed. Path-based, read-once, all four sweeps converted.
F6 — link variant's focus-visible:underline is a no-op when hovered 🟡 ⏭️ Skipped, correctly. Zero call sites confirmed.
N1–N7 🟢 Informational; N4/N5 re-verified clean on the new comments.

Addressed: 5. Correctly declined: 1 (F6). New issues: 2 (both 🟡).


Requirements Check

# Item Status Evidence
1 Scope is the delta only ✅ git diff --name-only returns exactly the 4 expected files. No non-test source changed beyond the three comment blocks.
2 2.4.7 exception documented, not buried ✅ DESIGN.md leads with the cost. See N10.
3 Component comments consistent with DESIGN.md ✅ All three point at the Focus section; figures match.
4 Escape hatch recorded actionably ✅ "Restoring the UA ring would fix the accessibility gap, and it is the option to reach for if this decision is ever revisited."
5 Contrast arithmetic behind the rejection ✅ Reproduces exactly — but see N8.
6 Suite green, nothing vacuous ✅ focus-treatment.test.ts 12 passed (was 10); full design dir 173 passed.
7 No lint suppression, no any, no issue numbers ✅ git diff | grep '^\+.*#[0-9]{2,}' → empty. DESIGN.md Focus section → zero #NNN.

Independent Verification

Contrast arithmetic — every figure reproduces exactly

Claim Stated I measured
--accent vs --background light 5.19:1 5.19 ✅
--accent vs --background dark 3.75:1 3.75 ✅
--accent vs --primary (checked box) 1.41:1 1.41 light, 1.45 dark ✅
--primary-hover vs --primary light 1.38:1 1.38 ✅
--primary-hover vs --primary dark 1.30:1 1.30 ✅
--destructive-hover vs --destructive light 1.24:1 1.24 ✅
--destructive-hover vs --destructive dark 1.21:1 1.21 ✅

The rejection of my border-accent suggestion is sound. The decisive number is --accent vs --primary = 1.41:1, and the checkbox's rest border is border-primary — so on a checked box, swapping the border to accent is a 1.41:1 change against both the border it replaces and the fill behind it. My original suggestion cited "--accent vs --input gives ample separation," which measures 4.09:1 light — but that is the radio/switch rest border, not the checkbox's, and it only holds unchecked. The author checked the case I didn't. The "visible on empty boxes, invisible on ticked ones" objection is arithmetically correct and is the right reason to decline.

F1 — FORBIDDEN_UTILITY: clean

All 8 named false negatives now caught: bare focus-visible:border, border-[3px], p-[4px], bare outline, shadow-lg, bare shadow, tracking-wide, font-extrabold. Also verified caught: shadow-2xl, shadow-inner, shadow-[…], outline-[3px], border-x-[2px], border-b-4, rotate-3, mx-[2px], and the variant-stacked forms dark:focus-visible:border-2 / md:focus-visible:p-2.

Zero false positives across 22 allowed cases, including every one flagged as at-risk: focus-visible:border-primary, border-accent, border-input, border-transparent, border-b-primary, border-t-accent, outline-none, bg-accent, underline, decoration-2, shadow-none, font-[inherit], opacity-100, rounded-sm. The (?![\w-]) boundary is what makes bare-vs-color discrimination work, and it is applied consistently to ring, border, outline and shadow. The restructure into a concatenated string is a readability win that costs nothing — the alternation is now commented branch by branch.

F2 — focusRules(): right approach, two residual holes

All four reported shapes caught: { on next line, single-line rule, two declarations on one line, comma selector list with brace on line 2. Also caught: multi-line values, @media nesting, &:focus-visible nesting, the first rule in a file (lastIndexOf returning -1 → slice(0) is correct), logical properties (border-block-width, padding-inline-start, margin-inline-end, border-inline-start-width). Correctly not flagged: border-color, border-radius, border-block-color, border-inline-color, border-top-color, outline: none, box-shadow: none, transform: none.

The Svelte false-positive concern is unfounded — I probed six markup shapes ({#if}, {@render}, {...restProps}, {#each}, TS object literals and function bodies following a focus-class string). None produce an offender. The reason is structural rather than lucky: the selector regex requires :focus followed by a non-word boundary in the text immediately preceding a brace, and Svelte expressions never place a bare :focus pseudo-selector there. The function does produce junk selectors on a .svelte file (e.g. "<style> .a:focus-visible"), but junk selectors are harmless — only the body is scanned for declarations, and markup bodies contain none.


Findings

🟡 Improvements (non-blocking)

F7 — a declaration that FOLLOWS a nested block inside a :focus rule is silently missed.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:203

rules.push({ selector, body: css.slice(i + 1, end).replace(/\{[^{}]*\}/g, ' ') });

The nested-block strip removes the {...} but leaves the nested block's selector text in the body. split(';') then glues that orphaned selector onto the next declaration, and FORBIDDEN_CSS_PROPERTY is anchored ^\s*, so the anchor never reaches the property name:

.a:focus-visible {
  .x { color: red; }
  padding: 4px;      /* ← NOT flagged */
}

Body after strip: "\n .x \n padding: 4px;\n" → first split(';') chunk is ".x \n padding: 4px" → ^\s*padding fails. Order-dependent: the same declaration before the nested block is caught correctly.

Two-character fix — strip the nested selector along with its block: .replace(/[^;{}]*\{[^{}]*\}/g, ' '). I verified this makes the case pass and regresses none of the other 43 shapes.

Principle: when you normalize by deletion, delete the whole syntactic unit — a half-removed construct leaves a token that changes how the next one parses.


F8 — @apply inside a :focus block is still not checked, and the rewrite made it cheap to add.

📁 packages/desktop-app/src/tests/design/focus-treatment.test.ts:226-238

@apply ring-2 / @apply border-2 p-2 inside a :focus-visible rule passes both sweeps: the utility sweep needs a focus*: prefix (absent — the variant is the enclosing selector), and the CSS sweep only recognizes property: value. I flagged this in F2 and it wasn't picked up; the address-review comment doesn't mention it either way.

Mitigating, and why this is 🟡 not higher: grep -rn "@apply" packages/desktop-app/src returns 0. The repo doesn't use @apply anywhere, so this is latent, not live. But the rewrite is what makes the fix a one-liner — inside the existing for (const declaration of body.split(';')) loop, a /^\s*@apply\b.*\b(ring|border|outline|shadow|p[xytrbl]?|m[xytrbl]?)-/ test and a push. Worth taking while the loop is open.


🟢 Observations

N8 — 📁 DESIGN.md ### Focus, candidate table. The border-accent row reads 1.41 ✗ in both the Unchecked and Checked columns. The checked figure is right (accent vs the border-primary rest border, and vs the primary fill). The unchecked figure is where it gets loose: an unchecked checkbox's border is border-primary against a --background page, so swapping it to accent is 1.41:1 against the border it replaces but 5.19:1 against the page. Whether that reads as visible depends on which comparison a 1px stroke actually invites — a judgment call, but the table presents one number as if the comparison were unambiguous. The decision does not change (the checked column alone settles it), so this is a precision note, not a correction. A parenthetical — "1.41:1 against the border it replaces" — would close it.

N9 — the three component comments are accurate and consistent. Checkbox carries the measured reasoning; switch and radio defer to it and to DESIGN.md rather than duplicating figures that could drift apart. That is the right factoring. All three now name outline-none / focus-visible:outline-none explicitly as the suppressing mechanism, which was the specific thing F4 said was implied rather than stated. The "it is not a treatment waiting to be filled in; adding one here would contradict a decision" framing is the correct artifact — it addresses the actual failure mode, which is a future contributor "fixing" the gap.

N10 — DESIGN.md states the cost before the justification. The heading is bolded as an exception, and the very next paragraph is "a keyboard user tabbing through a group of them cannot see which one Space will toggle. A pane containing only checkboxes — the import options modal is one — has no visible focus position." It names the concrete affected surface I cited. Only then does the justification follow. That ordering is what makes it honest documentation rather than rationalization, and the escape hatch records both the action and the reason it was declined. A future maintainer can act on this. F4 is properly discharged.

N11 — F3's new test is well-judged. It asserts > 1.15 and < 1.6 on both tokens in both themes — a band, not a floor, so it catches a token edit that flattens the shift toward zero and one that overshoots. Non-vacuous: measured values 1.21–1.38 sit inside with real margin on both sides. The comment is explicit that this is recorded, not asserted against 3:1, and the distinction it draws (a neutral fill must be told apart from an unbounded page; a filled button need only be told apart from itself a moment earlier, and it is the identical shift hover already ships) is sound reasoning, not rationalization. The honest thing was to write the number down where someone will find it, and that is what happened.

N12 — F5 verified. Only one tests directory exists under src (src/tests), so the path-based exclusion is presently equivalent to the name-based one — but it is correct for the case it was written for, and the comment says why. Post-exclusion file count is 421, comfortably above the > 100 vacuity guard. All four sweeps consistently destructure { path, text }; no readFileSync survives in an assertion body. The dropdown-menu exception filter still works after the path switch — path.relative(srcRoot, …) yields lib/components/ui/dropdown-menu/…, which startsWith matches — and I confirmed there are no bare focus: utilities outside that directory.

N13 — F6 skip verified. grep -rn "variant=.link" packages/desktop-app/src → 0 matches. The only buttonVariants consumers are button.svelte, the two calendar nav buttons, and two test files. The claim holds; skipping was right under YAGNI.


Recommendation

🟢 APPROVE

The blocking finding is discharged. F4 was a decision to make, not a bug to fix, and the owner made it — what I asked for was that it be surfaced in those words rather than inherited from a spec line, and DESIGN.md does exactly that: cost first, named affected surface, reasoning second, escape hatch recorded. The arithmetic that rejected my suggested fix is correct, and correct for a reason I had missed (the checkbox's rest border is border-primary, not border-input). That is the right outcome of a review disagreement.

F1 is fully closed with no false positives I could construct in 57 attempts. F3 and F5 are closed. F2 is closed for every shape I reported, and the decision to rewrite rather than patch was right — the residual F7 is a new, narrower bug in a strictly better parser, not a survival of the old one.

Not blocking, worth a follow-up commit or issue (author's call):

  • F7 — declaration after a nested block inside a :focus rule is missed. One-line fix; no instance in the repo today.
  • F8 — @apply still unguarded. Zero @apply in the repo; the loop is now open and the check is two lines.
  • N8 — the border-accent unchecked figure would benefit from naming its comparison surface.

None of these changes what ships to a user; all three are guard-test reach, and the guard is already substantially stronger than at 9688912c. Merging is the user's call.

@malibio malibio left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated pragmatic-code-review (re-review): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.

Re-review returned APPROVE with two non-blocking findings, both in the CSS
scanner rewritten in the previous commit. Both fixed, since the whole point of
that file is catching what nobody thought to look for.

F7 — a declaration FOLLOWING a nested block inside a `:focus` rule was silently
missed. Stripping only `{...}` left the nested block's SELECTOR text behind,
and because declarations are split on `;` that orphan glued onto the front of
the next one, defeating the `^\s*` anchor:

  .a:focus-visible { .x { color: red } padding: 4px }
                                       ^ passed, chunk read ".x  padding: 4px"

A bug I introduced with the rewrite, and an order-dependent one — the same
declaration placed BEFORE the nested block was caught, which is exactly the
shape that survives casual inspection. The strip now takes each nested block
together with its selector (`[^;{}]*\{[^{}]*\}`). Reproduced against 3 variants
first, then verified the fix against all 28 CSS cases with no regressions.

F8 — `@apply ring-2` inside a `:focus` block fell between the two sweeps: the
utility sweep wants a `focus-visible:` prefix that is not there (the focus state
is in the selector), and the property sweep wants `property: value`, which
`@apply` is not. Closed by extracting the geometry list into GEOMETRY_UTILITY
and building both the prefixed and the bare form from it, so the two spellings
of one rule cannot drift apart. No `@apply` exists in the codebase today; this
closes the seam rather than waiting for the first one to land in a focus block.

N8 — DESIGN.md's rejected-candidates table showed `border-accent` as 1.41:1 in
both columns without saying against what. Unchecked it is 1.41:1 against the
border it replaces and 5.19:1 against the page; the first is the number that
matters, because a focus treatment has to be told apart from what was there a
moment ago. Added the comparison surface as a column, since the surface is what
decides each row.

Mutation-tested all three scanners again:

  padding after a nested block   -> caught (passed before this commit)
  @apply ring-2 in a :focus block -> caught (passed before this commit)
  @apply bg-accent (painted)      -> correctly NOT caught, no false positive

Verification: design suite 173 passing, frontend 252 files / 5166 tests,
quality:fix clean, 32 utility and 28 CSS regex cases green after the refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@malibio

malibio commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Address Review (re-review) — 2e491f7c

Verdict was APPROVE with two non-blocking findings. Both fixed anyway, plus the docs nit — this file exists to catch what nobody thought to look for, so a known hole in it is not a good thing to ship.

✅ F7 — real bug, and one I introduced

Reproduced before fixing, across three variants. All three passed silently on e836dd7a:

.a:focus-visible {
  .x { color: red; }
  padding: 4px;      /* ← missed */
}

Your diagnosis is exactly right: stripping only {...} leaves the nested block's selector text behind, and since declarations split on ;, that orphan glues onto the front of the next one — so ^\s* never reaches padding. The chunk reads ".x \n padding: 4px".

What makes it worth the comment I left in the source: it was order-dependent. The same declaration placed before the nested block was caught. That is the shape that survives casual inspection, because the obvious test case passes.

Applied your fix ([^;{}]*\{[^{}]*\}) — verified against all 28 CSS cases, no regressions.

✅ F8 — @apply seam closed

Fixed rather than deferred. Rather than duplicating the utility list, I extracted it into a single GEOMETRY_UTILITY string and build both spellings from it:

  • FORBIDDEN_UTILITY — the prefixed form (focus-visible:ring-2), how markup writes it
  • FORBIDDEN_APPLIED_UTILITY — the bare form (@apply ring-2), how @apply writes it inside a :focus block

Two spellings of one rule, one definition, so they cannot drift. That drift is the same failure mode you flagged in the first review, when shadow-*/tracking-* were banned in the CSS sweep and absent from the utility sweep.

Confirmed no false positive: @apply bg-accent text-accent-foreground (painted) passes clean.

✅ N8 — comparison surface named

You were right that the table asserted 1.41 ✗ in both columns without saying against what. Unchecked, border-accent is 1.41:1 against the border it replaces and 5.19:1 against the page — healthy-looking by the wrong measure.

Added a "Compared against" column plus a sentence on why the surface is what decides each row: a focus treatment has to be distinguishable from what was there a moment ago, not from the page. nodespace-docs@7b4e871.

Mutation testing

Mutation Before 2e491f7c After
padding: 4px after a nested block in :focus ✅ passed (hole) ✗ caught
@apply ring-2 ring-offset-2 in a :focus block ✅ passed (hole) ✗ caught
@apply bg-accent (painted control) passes passes — no false positive

On the F4 arithmetic

Noted that you re-derived the numbers and found your own original border-accent suggestion was measured against --input (the radio/switch rest border) rather than the checkbox's border-primary. That is the kind of correction that makes a review worth running twice — thank you for checking it rather than taking the rejection at face value.

Summary

  • ✅ Addressed: 3 — F7 🟡, F8 🟡, N8 🟢
  • ⏭️ Skipped: 0
  • 📝 Commits: 1 — 2e491f7c, plus 7b4e871 in nodespace-docs
  • 🧪 Tests: passing — design 173, frontend 252 files / 5166 tests, quality:fix clean, 32 utility + 28 CSS regex cases green after the GEOMETRY_UTILITY refactor

Re-Review Decision

Decision: NO RE-REVIEW NEEDED

Rationale: The two code changes are narrow and each is pinned by a mutation test that demonstrably failed before the fix and passes after. F7 is a one-token change to a regex whose behaviour is verified against 28 cases. F8 adds a check built from an existing verified list rather than a new one, and the refactor that enabled it was re-verified against the same 32 utility cases as before. N8 is documentation only. No behaviour visible to a user changed in this commit — nothing shipped here alters what a control looks like.

The substantive work (the focus idiom, the 2.4.7 exception, the scanner rewrite) was reviewed in the two prior rounds and is unchanged.

@malibio
malibio merged commit d99b38e into main Sep 17, 2026
@malibio
malibio deleted the issue-2718-focus-token-shift branch September 17, 2026 12:07
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.

Replace focus rings with a token shift in the chrome surface

1 participant