Skip to content

fix(a11y): pin SPRAWL's chip back to its corner, and guard the sp-* scale - #291

Merged
andraderaul merged 1 commit into
fix/vendor-snapshot-formatfrom
fix/touch-target-position-and-sp-guard
Aug 1, 2026
Merged

fix(a11y): pin SPRAWL's chip back to its corner, and guard the sp-* scale#291
andraderaul merged 1 commit into
fix/vendor-snapshot-formatfrom
fix/touch-target-position-and-sp-guard

Conversation

@andraderaul

@andraderaul andraderaul commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Stacked on #290 (the CI fix), so the diff here stays the a11y work alone. GitHub retargets this to main when that merges.

#288 describes this review round in its own body — the sp-* guard, TOUCH_TARGET_ICON, the position find. None of it is in main. The description shipped; the code did not. This lands it.

The one that is a real defect

TOUCH_TARGET_HEIGHT opens with relative, because an ::after overlay needs something to anchor to. SPRAWL's basemap chip named its own absolute before the constant:

'absolute bottom-xs left-xs …',   // ← main
TOUCH_TARGET_HEIGHT,              // ← opens with `relative`, and wins

cn is tailwind-merge, which resolves a position conflict in favour of the last name. So the chip lost absolute and fell out of its corner back into the flow — with every target class present and correct, which is exactly why the className assertion sailed past it. Order swapped, and the new test asserts the position (absolute present, relative absent) rather than the target classes, because that is the axis that broke.

Never shipped: sprawl-touch-targets.md is still unreleased, so the bug lived only between #288 and here. Both constants now carry the ordering rule, and the changeset spells it out for anyone reaching for them next.

Every other caller positions via a parent — I checked all seven — so this was the only exposed site.

The sp-* hole in the scale guard

sp-* is a second scale under the same utilities, and its ends do not line up with the base one:

runs
base spacing 2xs3xl
sp-* macro xs2xl

So gap-3xl is real, p-sp-3xl renders nothing, and sharing the utilities is precisely what makes the second easy to reach for. The deny list now carries the sp- spellings step by step rather than by a prefix rule — the step is the only thing telling the two apart, so a prefix rule would either miss p-sp-3xl or falsely reject p-sp-2xl.

The list is composed from two named halves now (EXTRAPOLATED_STEPS × SECTION_ONLY_GAPS) instead of one hand-written array, and the completeness test holds the new entries to the preset exactly as it holds the old ones — define sp-3xl for real and the list has to give it up. That test also stopped defaulting to {}: a scale whose shape moved now throws by name rather than quietly comparing against nothing, which passes while proving nothing.

Dropped 2sm / 2md / 2lg while here — the multiplier prefix only ever attaches to xs / xl, nobody writes those, and a deny list is read less the more of it is theatre.

TOUCH_TARGET_ICON

Both canvases had independently composed TOUCH_TARGET_HEIGHT + min-w-[44px] inline-flex items-center justify-center. Two real callers is ADR 0014's bar, so it becomes one export. Its sibling TOUCH_TARGET_OVERLAY stays module-local — still one caller.

The asymmetry is the point and is documented on it: height is an overlay, width is real. A centred overlay wide enough for a ~27px icon reaches into its neighbour's across a gap-xs, and two overlapping targets are worse than one small target, because the press that lands stops being the one the user aimed at.

Tests assert the constant, not its expansion

Five target tests compared against string literals like after:h-[44px]. They now match against the exported constant, so respelling a target cannot red them without an actual regression behind it. SPRAWL's is the deliberate exception — it filters relative out, since the chip's own absolute displaces it and either one anchors the overlay.

Two ASCII/GLITCH tests that asserted the same element twice collapsed into one each.

Changeset bumps corrected

deck-kit-a11y-blockers and deck-kit-tablist-and-alerts were marked minor for what are pure fixes — no API moved. Both are now patch. deck-kit-touch-targets stays minor and absorbs TOUCH_TARGET_ICON, so deck-kit's net bump is unchanged; only the changelog reads honestly now.

Recorded rather than silently done

  • The byte-identical input[type=range] stylesheets in ASCII and GLITCH. Extracting only the newest rule would split one control's styles across two files — worse than either end. tokens.css is :root-only by its own decision, so element rules need a second stylesheet export and every app's import order re-checked. Its own change.
  • CLAUDE.md's scale table split sp-* onto its own row, since the shared-utilities-different-ends trap is the whole reason the guard needed widening. deck-kit's CONTEXT.md gets the matching note.
  • TabStrip's roving-focus JSDoc → //. It sat above a const arrow inside a component body, so it was surfacing as that local's hover text.

Verification

  • npm run check — clean, 291 files
  • npm run typecheck — clean across all 5 workspaces
  • npm run test:run2120 passed (286 + 363 + 436 + 92 + 943)
  • npm run build — all three apps ship min-width:44px and height:44px in their built CSS, which is what proves the new classes survive the purge

🤖 Generated with Claude Code

Why this PR shows no CI

ci.yml gates pull_request into main and feat/** only. Its comment already anticipates stacks — "without it the sub-PRs of a stack land entirely unchecked" — but the allow-list covers feat/**, and this stack is fix/**. So Build and Test will not report here until #290 merges and GitHub retargets this to main.

Not a reason to merge it unchecked — the same commands CI runs are green locally on this exact branch, listed under Verification above. Worth deciding separately whether that allow-list should be [main, 'feat/**', 'fix/**'], or just the base-branch wildcard.

…cale

PR #288 described this review round in its body but merged without it. This
lands the code the description already promised.

SPRAWL's basemap chip named `absolute` before `TOUCH_TARGET_HEIGHT`, and the
constant opens with `relative` to anchor its overlay. `cn` is tailwind-merge,
which resolves a position conflict in favour of the last name — so the chip
lost its corner and fell back into the flow with every target class present
and correct, which is exactly why the className assertion sailed past it. The
constants now document the ordering, and a test pins the position rather than
the classes.

The scale guard gains the `sp-*` spellings, step by step rather than by a
prefix rule: it is a second scale under the same utilities and its ends do not
match the base one, so `gap-3xl` is real while `p-sp-3xl` renders nothing, and
only the step tells the two apart. The completeness test holds the new entries
to the preset the same way it holds the old ones.

`TOUCH_TARGET_ICON` joins the public surface — the height overlay plus the real
width it tells you to pair with — after both canvases composed the pairing
identically. Two callers, which is ADR 0014's bar.

The target tests now assert against the constants instead of the classes they
happen to expand to, so respelling a target cannot red them without a
regression behind it.

Also recorded rather than silently done: the byte-identical range-input
stylesheets, and the `sp-*` row in CLAUDE.md's scale table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ascii-art-converter Ready Ready Preview Aug 1, 2026 3:18pm
atlas-sprawl Ready Ready Preview Aug 1, 2026 3:18pm
cyberdeck-golem Ready Ready Preview Aug 1, 2026 3:18pm
glitch-studio Building Building Preview Aug 1, 2026 3:18pm

@andraderaul
andraderaul merged commit 1658553 into fix/vendor-snapshot-format Aug 1, 2026
5 checks passed
@andraderaul
andraderaul deleted the fix/touch-target-position-and-sp-guard branch August 1, 2026 15:19
andraderaul added a commit that referenced this pull request Aug 1, 2026
…the formatter (#290)

* fix(ci): let the vendored snapshot stay compact instead of losing to the formatter

CI has been red on main since #289. The vendor bot serializes the dataset with
a bare `JSON.stringify` — compact by an explicit decision recorded on
`serialize`, since pretty-printing triples the committed size of a 1961-point
array for a diff git shows either way — and biome's formatter rejects the
single line that produces. The bot opened its PR, the PR merged, and CI went
red on a diff no human wrote.

Two rules disagreeing, so one has to give. The formatter yields: a vendored,
machine-generated artifact is not source, and `dataset-*.json` leaves
`files.includes`. The alternative — indenting the generator — would have
overturned a decision the script documents, and reformatted 2700 lines to do
it.

Nothing about the data moves. `dataset-2026-07.json` is pretty-printed because
a human reformatted it by hand once; both spellings are now legal and the bot's
own output no longer needs that step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(a11y): pin SPRAWL's chip back to its corner, and guard the sp-* scale (#291)

PR #288 described this review round in its body but merged without it. This
lands the code the description already promised.

SPRAWL's basemap chip named `absolute` before `TOUCH_TARGET_HEIGHT`, and the
constant opens with `relative` to anchor its overlay. `cn` is tailwind-merge,
which resolves a position conflict in favour of the last name — so the chip
lost its corner and fell back into the flow with every target class present
and correct, which is exactly why the className assertion sailed past it. The
constants now document the ordering, and a test pins the position rather than
the classes.

The scale guard gains the `sp-*` spellings, step by step rather than by a
prefix rule: it is a second scale under the same utilities and its ends do not
match the base one, so `gap-3xl` is real while `p-sp-3xl` renders nothing, and
only the step tells the two apart. The completeness test holds the new entries
to the preset the same way it holds the old ones.

`TOUCH_TARGET_ICON` joins the public surface — the height overlay plus the real
width it tells you to pair with — after both canvases composed the pairing
identically. Two callers, which is ADR 0014's bar.

The target tests now assert against the constants instead of the classes they
happen to expand to, so respelling a target cannot red them without a
regression behind it.

Also recorded rather than silently done: the byte-identical range-input
stylesheets, and the `sp-*` row in CLAUDE.md's scale table.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant