fix(ui): keep a touch-only affordance reachable, or absent #181

Merged
logan merged 2 commits from fix/137-touch-only-affordances into main 2026-08-21 16:23:43 +00:00
Collaborator

What the issue was

Three controls in the app are revealed by :hover and are the only
route to their action, so on a device without hover the action is gone —
and, being visibility: hidden / opacity: 0 rather than
display: none, they are invisible controls that still take taps. #137
turned them up in #68's sweep and left them out of it, because the fix is
the opposite one.

What changed, and why

Two are always visible where the device has no hover, under
@media not all and (hover: hover):

  • track-details.cover-art-overlay / .cover-art-remove, the only
    route to changing or removing a track's cover art.
  • shortcut-capture.reset-btn, the only way to put a rebound
    shortcut back.

The queue row's remove takes #68's treatment instead, which is what
the issue's own Sequencing paragraph asks for once #60 has landed: the
row's context menu is a bottom sheet now and carries "Remove from
Queue", so the button is redundant on touch rather than missing, and an
always-visible X would spend part of a 424px row on something one
long-press away. It is display: none outside
(hover: hover) and (pointer: fine) — not visibility: hidden, which
would keep the hit area and the accessibility-tree entry for a control
the phone can never see.

Not extracted into styles/. That leaves two call sites of the
always-visible form (three selectors), under the four the issue names as
the threshold.

Verification

  • make ui-test — 988 passed, 6 of them hover-affordance.test.ts
    (three new cases). The three were watched failing on the pre-fix
    build.

  • Measured in a real engine at both media states, against
    make dev-headless, because no tier here can render as a touch device
    (CDP's Emulation.setEmulatedMedia does not reach the component
    tier's iframe, and the e2e projects are Desktop Chrome and Desktop
    Safari):

    control touch context, 424x439 hover context, 1100x760
    .remove-button display: none display: flex; visibility: hidden
    .cover-art-overlay opacity: 1 opacity: 0
    .cover-art-remove opacity: 1 opacity: 0
    .reset-btn opacity: 1 opacity: 0
  • Screenshots of track-details in edit mode, touch and hover, with
    and without art: the pencil overlay and the × are on screen on touch
    and invisible on the desktop.

  • npx tsc --noEmit, make css-check, make skill-check — clean.

Deliberately not done

  • No e2e spec. The suite's two projects have no touch, so a spec
    there could only assert the desktop half, which the component tier
    already pins. Adding a touch project is infrastructure this issue did
    not ask for.
  • No device tier. Chrome 113 has both (hover: hover) and
    not all and (...), so this is nowhere near its documented gaps, but
    it is unverified on the phone.
  • The styles/ extraction, per the threshold above.

Closes #137

## What the issue was Three controls in the app are revealed by `:hover` and are the **only** route to their action, so on a device without hover the action is gone — and, being `visibility: hidden` / `opacity: 0` rather than `display: none`, they are invisible controls that still take taps. #137 turned them up in #68's sweep and left them out of it, because the fix is the opposite one. ## What changed, and why **Two are always visible where the device has no hover**, under `@media not all and (hover: hover)`: - `track-details` — `.cover-art-overlay` / `.cover-art-remove`, the only route to changing or removing a track's cover art. - `shortcut-capture` — `.reset-btn`, the only way to put a rebound shortcut back. **The queue row's remove takes #68's treatment instead**, which is what the issue's own Sequencing paragraph asks for once #60 has landed: the row's context menu is a bottom sheet now and carries "Remove from Queue", so the button is redundant on touch rather than missing, and an always-visible X would spend part of a 424px row on something one long-press away. It is `display: none` outside `(hover: hover) and (pointer: fine)` — not `visibility: hidden`, which would keep the hit area and the accessibility-tree entry for a control the phone can never see. **Not extracted into `styles/`.** That leaves two call sites of the always-visible form (three selectors), under the four the issue names as the threshold. ## Verification - **`make ui-test`** — 988 passed, 6 of them `hover-affordance.test.ts` (three new cases). The three were **watched failing** on the pre-fix build. - **Measured in a real engine at both media states**, against `make dev-headless`, because no tier here can render as a touch device (CDP's `Emulation.setEmulatedMedia` does not reach the component tier's iframe, and the e2e projects are Desktop Chrome and Desktop Safari): | control | touch context, 424x439 | hover context, 1100x760 | |---|---|---| | `.remove-button` | `display: none` | `display: flex; visibility: hidden` | | `.cover-art-overlay` | `opacity: 1` | `opacity: 0` | | `.cover-art-remove` | `opacity: 1` | `opacity: 0` | | `.reset-btn` | `opacity: 1` | `opacity: 0` | - **Screenshots** of `track-details` in edit mode, touch and hover, with and without art: the pencil overlay and the × are on screen on touch and invisible on the desktop. - `npx tsc --noEmit`, `make css-check`, `make skill-check` — clean. ## Deliberately not done - **No e2e spec.** The suite's two projects have no touch, so a spec there could only assert the desktop half, which the component tier already pins. Adding a touch project is infrastructure this issue did not ask for. - **No device tier.** Chrome 113 has both `(hover: hover)` and `not all and (...)`, so this is nowhere near its documented gaps, but it is unverified on the phone. - **The `styles/` extraction**, per the threshold above. Closes #137
Author
Collaborator

CI is green on 5fbe1d9 (run 17395): check success, e2e success — the latter covering both Chromium and WebKit.

Not merging; leaving Status/In Progress on #137.

CI is green on `5fbe1d9` (run 17395): `check` **success**, `e2e` **success** — the latter covering both Chromium and WebKit. Not merging; leaving `Status/In Progress` on #137.
logan force-pushed fix/137-touch-only-affordances from 5fbe1d9923 to 3a47070d3f 2026-08-21 14:38:24 +00:00 Compare
logan added 2 commits 2026-08-21 15:59:46 +00:00
Three controls are revealed by :hover and are the only route to their
action on a device that has none. #68 hid the home card's play button on
touch, which was right because tapping the card does the same thing;
these are the opposite case, so hiding them removes the action outright
and leaving them costs the same long-press flash #68 was filed for --
they are visibility:hidden / opacity:0, so on touch they are invisible
controls that still take taps.

track-details' cover-art overlay and remove, and shortcut-capture's
reset, are always visible under `@media not all and (hover: hover)`.

The queue row's remove is the third case the report names and takes the
other treatment, because #60 has since landed: the row's context menu is
a bottom sheet carrying "Remove from Queue", so the action is one
long-press away and an always-visible X would spend part of a 424px row
on something already reachable. It is display:none outside
`(hover: hover) and (pointer: fine)` rather than visibility:hidden,
which would leave a button holding its hit area and its place in the
accessibility tree -- the trap this issue is about.

The rule is not extracted into styles/ yet: that leaves two call sites
of the always-visible form, under the four the report names.

No tier here can render as a touch device, so the tests read the parsed
stylesheet the way #68's does and say so; the touch and hover renderings
were measured against the running app in a hasTouch context instead.

Closes #137
fix(ui): make the touch pen a corner chip, not a scrim over the art
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m28s
CI / e2e (pull_request) Successful in 9m19s
d78830aa52
Always-visible is not the same as always-in-the-way: the overlay is inset:0 at 50% black, so gating it on hover left every touch device with the artwork it is editing permanently darkened. It is only a hint — .cover-art-edit carries the click, so tapping the art always worked — while the × really is the only route to its action and stays. The chip borrows the remove button's size, disc and alpha.

Also corrects the claim that no tier can render as a touch device: no committed one does, which is a choice about projects rather than a limit.
logan force-pushed fix/137-touch-only-affordances from 003610b7a3 to d78830aa52 2026-08-21 15:59:46 +00:00 Compare
logan merged commit 510d3470f9 into main 2026-08-21 16:23:43 +00:00
Sign in to join this conversation.