Android: raise every remaining control to the 44px touch floor #198

Merged
logan merged 2 commits from 186-touch-targets-settings into main 2026-08-22 03:22:16 +00:00
Collaborator

Second pass on #186: Settings. #195 did the page header; this is the
rest of the page a user configures the app from.

commit issue
2100f00 fix(settings): raise every Settings control to the touch floor #186
7b09d46 fix(ui): raise the last controls below the touch floor #186

What it found

The issue's table implies 93 Settings controls with config-field as
the offender. Swept on the device with all eleven config-sections
expanded, it is 120 controls under the floor and config-field is
eight of them — the density is behind the disclosures, which is also
why a sweep of a freshly-opened Settings page reports seven.

control before count
.column-arrow-btn 16x14 36
.column-toggle 16x16 29
shortcut-capture button 80x25 26
download format checkbox 16x16 8
config-field select 335x30 7
wa-input / wa-button 204x20, 185x21 6

The 16x14 arrows are the smallest controls in the app — smaller than
autotag's 15px section toggle, and there are 36 of them.

The density argument

Measured rather than guessed, and it is the reason the shape is what
it is. The rows were already near the floor (.column-item 335x36,
.shortcut-row 335x37) — it is the controls inside them that were
14–25px. So each control grows into the row it already occupies and
the row goes 36 → 44: the two column lists went 373→447 and 690→850,
+234px over the whole page. Half a screen of extra scroll on a
page that already scrolls, against 36 targets of 16x14.

Growing each control to its own 44px row instead would have cost four
screens.

Three shapes, not one rule

  • A native checkbox is targeted through its label, because it
    cannot grow its hit area without growing its paint. .column-label
    is a real <label for> now, so the column's name is the target —
    70x44 rather than 16x16, and a behaviour improvement rather than an
    annotation.
  • The arrows take padding, which is invisible: they have
    background: none and a transparent border.
  • Web Awesome's controls come from --wa-form-control-height, the
    library's own theming variable, in styles/wa-touch-floor.css.ts
    adopted by the component rather than declared at :root, so the
    component tier can measure it.

Two controls no sweep can see

Both are absent from the issue's tables and neither is an oversight:

  • config-field's toggle has an <input> that is
    opacity: 0; width: 0; height: 0, so a walk of every input skips
    it as a zero-sized node. What a finger hits is the <label>, which
    measured 34x19 — smaller than anything in either table.
  • shortcut-capture's reset button renders only for a shortcut
    somebody has already rebound.

Verification

  • Device (TLP301, 424x439, index build stopped and jobs empty),
    same sweep that filed the issue: 120 under the floor before, 42
    after.
    All 42 accounted for — 37 checkboxes whose labels measure
    70x44 and 57x44, four wa-input inner inputs at 204x42 which is
    the control measured inside its own 1px border (part=base is
    238x44), and the skip link, which #186 already ruled out as
    keyboard-only. Screenshot of the column list checked by eye for the
    density.
  • make ui-test — 1041 pass. 26 new tests across two files in
    settings-touch-targets.test.ts, and all 14 fail on main with
    the device's own numbers (34x19, config-field select 1280x34,
    130 controls under the floor in this tier).
  • make e2e — 236 pass locally, top-bar-fit and
    header-action-overflow included. That is chromium and half an
    answer; CI ran both, and E2E — webkit passed rather than being
    skipped, which is the signal #195 needed and could not get on this
    machine.
  • CI green on 4f628b1 (run 17515), check and e2e both.
  • make lint, make test, make bindings-check, make skill-check,
    make css-check, tsc --noEmit in frontend/ — all clean, and all
    formalities: no Go changed and no binding moved.
  • make ui-visual — two failures left, both pre-existing on
    main and both filed as #196.

A note on the screenshots

Two page-header references are refreshed. They are this issue's own
debris — #195's taller sort control, merged last session, references
never re-recorded — and each diff was read before blessing.

app-sidebar's and now-playing's are deliberately left failing.
They are unrelated drift, and blessing an unrelated screenshot is
exactly how the sidebar reference came to still list Jobs, a
destination #27 retired. That is #196.

Filed on the way

  • #196make ui-visual has been failing on main and nothing
    runs it. CI's check runs make ui-test, the same suite with
    comparisons off, so the visual tier is the only one here never
    executed by anything but a person. Four stale references spanning
    three separate pieces of work.
  • #197 — Settings lists two columns both called "Track Name"
    (titleArtist and trackName), with colliding accessible names,
    and one of them may not be selectable at all. Made more visible by
    this PR rather than caused by it: the column's name is now the thing
    you tap.

The second commit: the rest of the table

explore-view's search row and the second table's one-offs.

back-button was six controls, not one. The issue names it in
artist-details because that is the view the sweep opened; the same
declaration is byte-identical in six components, 32px in all of them.
It is styles/back-button.css.ts now, and a source sweep fails on a
seventh copy
— because the failure this invites is not a size
changing, it is somebody adding a detail view and writing
.back-button out again, which no device sweep catches for exactly
the reason this one did not.

The Explore search box was two faults. The row was 36px and the
input inside it was 18 — so eight pixels at each edge were not a
target at all, and a tap near the top of the box landed on the
container and did nothing.

The Downloads tabs take padding rather than a min-size, because
the mark for the selected tab is its bottom border: a min-size
centres the label and leaves the underline 10px beneath it.

page-action-check-now (113x29) is in that table and is deliberately
not here — it is a PageAction, so #195 raised it with the rest
of the header's actions.

Verified on the device by sweeping each view the way the issue was
filed: explore, downloads, autotag and artist-details now report
one control under the floor apiece, and it is the skip link.
.search-mode-tab 89x44 / 79x44, search input 325x44, Downloads tabs
85x44 / 96x44, .section-toggle 174x44, .folders-menu-trigger
44x44, .back-button 44x44. All 12 new tests fail on main, the
source sweep naming all six copies.

So #186 is complete rather than half-done, and the footer is on the
second commit.

Closes #186

CI found one, and it is the third instance of the same trap

The first push failed check. The Downloads tabs had been given their
height as padding — so the active tab's underline would stay against
its label rather than sitting 10px below a centred one — which makes
the total 13 + 13 + 2 + whatever line box the font gives 13px text.

That is 44px on Arch and 43px in ubuntu:24.04.

AssertionError: Requests: expected 43 to be greater than or equal to 44

A height computed from a font's line box is not a height you control,
which is #195's "stated as a property on the strength of one engine"
one layer down — in the PR that records it, having just written the
lesson down. The padding stays, with a min-block-size under it.

Two things about it are worth keeping:

  • The new test caught it, in the tier that could see it, before a
    person did. That is the half of this that worked.
  • Every other rule in both commits is an explicit 44px min-size or
    an exact size
    — swept the diff for this specifically after the
    failure. The tab was the only one deriving a dimension from
    something the renderer decides.
Second pass on #186: Settings. #195 did the page header; this is the rest of the page a user configures the app from. | commit | issue | |---|---| | `2100f00` fix(settings): raise every Settings control to the touch floor | #186 | | `7b09d46` fix(ui): raise the last controls below the touch floor | #186 | ## What it found The issue's table implies 93 Settings controls with `config-field` as the offender. Swept on the device with all eleven `config-section`s expanded, it is **120 controls under the floor** and `config-field` is eight of them — the density is behind the disclosures, which is also why a sweep of a freshly-opened Settings page reports seven. | control | before | count | |---|---|---| | `.column-arrow-btn` | **16x14** | 36 | | `.column-toggle` | 16x16 | 29 | | `shortcut-capture` button | 80x**25** | 26 | | download format checkbox | 16x16 | 8 | | `config-field` select | 335x**30** | 7 | | `wa-input` / `wa-button` | 204x**20**, 185x**21** | 6 | The 16x14 arrows are the smallest controls in the app — smaller than autotag's 15px section toggle, and there are 36 of them. ## The density argument Measured rather than guessed, and it is the reason the shape is what it is. The rows were already near the floor (`.column-item` 335x36, `.shortcut-row` 335x37) — it is the controls *inside* them that were 14–25px. So each control grows into the row it already occupies and the row goes 36 → 44: the two column lists went 373→447 and 690→850, **+234px** over the whole page. Half a screen of extra scroll on a page that already scrolls, against 36 targets of 16x14. Growing each control to its own 44px row instead would have cost four screens. ## Three shapes, not one rule - **A native checkbox is targeted through its label**, because it cannot grow its hit area without growing its paint. `.column-label` is a real `<label for>` now, so the column's *name* is the target — 70x44 rather than 16x16, and a behaviour improvement rather than an annotation. - **The arrows take padding**, which is invisible: they have `background: none` and a transparent border. - **Web Awesome's controls come from `--wa-form-control-height`**, the library's own theming variable, in `styles/wa-touch-floor.css.ts` — adopted by the component rather than declared at `:root`, so the component tier can measure it. ## Two controls no sweep can see Both are absent from the issue's tables and neither is an oversight: - `config-field`'s toggle has an `<input>` that is `opacity: 0; width: 0; height: 0`, so a walk of every `input` skips it as a zero-sized node. What a finger hits is the `<label>`, which measured **34x19** — smaller than anything in either table. - `shortcut-capture`'s reset button renders only for a shortcut somebody has already rebound. ## Verification - **Device** (TLP301, 424x439, index build stopped and jobs empty), same sweep that filed the issue: **120 under the floor before, 42 after.** All 42 accounted for — 37 checkboxes whose labels measure 70x44 and 57x44, four `wa-input` inner inputs at 204x**42** which is the control measured *inside* its own 1px border (`part=base` is 238x44), and the skip link, which #186 already ruled out as keyboard-only. Screenshot of the column list checked by eye for the density. - **`make ui-test`** — 1041 pass. 26 new tests across two files in `settings-touch-targets.test.ts`, and **all 14 fail on `main`** with the device's own numbers (`34x19`, `config-field select 1280x34`, `130` controls under the floor in this tier). - **`make e2e`** — 236 pass locally, `top-bar-fit` and `header-action-overflow` included. That is chromium and half an answer; **CI ran both, and `E2E — webkit` passed** rather than being skipped, which is the signal #195 needed and could not get on this machine. - **CI green** on `4f628b1` (run 17515), `check` and `e2e` both. - `make lint`, `make test`, `make bindings-check`, `make skill-check`, `make css-check`, `tsc --noEmit` in `frontend/` — all clean, and all formalities: no Go changed and no binding moved. - **`make ui-visual`** — two failures left, both pre-existing on `main` and both filed as #196. ## A note on the screenshots Two `page-header` references are refreshed. They are this issue's own debris — #195's taller sort control, merged last session, references never re-recorded — and each diff was read before blessing. `app-sidebar`'s and `now-playing`'s are deliberately **left failing**. They are unrelated drift, and blessing an unrelated screenshot is exactly how the sidebar reference came to still list Jobs, a destination #27 retired. That is #196. ## Filed on the way - **#196** — `make ui-visual` has been failing on `main` and nothing runs it. CI's `check` runs `make ui-test`, the same suite with comparisons off, so the visual tier is the only one here never executed by anything but a person. Four stale references spanning three separate pieces of work. - **#197** — Settings lists two columns both called "Track Name" (`titleArtist` and `trackName`), with colliding accessible names, and one of them may not be selectable at all. Made more visible by this PR rather than caused by it: the column's name is now the thing you tap. ## The second commit: the rest of the table `explore-view`'s search row and the second table's one-offs. **`back-button` was six controls, not one.** The issue names it in `artist-details` because that is the view the sweep opened; the same declaration is byte-identical in six components, 32px in all of them. It is `styles/back-button.css.ts` now, and a **source sweep fails on a seventh copy** — because the failure this invites is not a size changing, it is somebody adding a detail view and writing `.back-button` out again, which no device sweep catches for exactly the reason this one did not. **The Explore search box was two faults.** The row was 36px *and* the input inside it was 18 — so eight pixels at each edge were not a target at all, and a tap near the top of the box landed on the container and did nothing. **The Downloads tabs take padding rather than a `min-size`**, because the mark for the selected tab is its bottom border: a `min-size` centres the label and leaves the underline 10px beneath it. `page-action-check-now` (113x29) is in that table and is deliberately **not** here — it is a `PageAction`, so #195 raised it with the rest of the header's actions. Verified on the device by sweeping each view the way the issue was filed: explore, downloads, autotag and artist-details now report **one** control under the floor apiece, and it is the skip link. `.search-mode-tab` 89x44 / 79x44, search input 325x44, Downloads tabs 85x44 / 96x44, `.section-toggle` 174x44, `.folders-menu-trigger` 44x44, `.back-button` 44x44. All 12 new tests fail on `main`, the source sweep naming all six copies. So #186 is complete rather than half-done, and the footer is on the second commit. Closes #186 ## CI found one, and it is the third instance of the same trap The first push failed `check`. The Downloads tabs had been given their height as *padding* — so the active tab's underline would stay against its label rather than sitting 10px below a centred one — which makes the total `13 + 13 + 2 +` whatever line box the font gives 13px text. That is 44px on Arch and **43px in `ubuntu:24.04`**. ``` AssertionError: Requests: expected 43 to be greater than or equal to 44 ``` A height computed from a font's line box is not a height you control, which is #195's "stated as a property on the strength of one engine" one layer down — in the PR that records it, having just written the lesson down. The padding stays, with a `min-block-size` under it. Two things about it are worth keeping: - **The new test caught it**, in the tier that could see it, before a person did. That is the half of this that worked. - **Every other rule in both commits is an explicit 44px min-size or an exact size** — swept the diff for this specifically after the failure. The tab was the only one deriving a dimension from something the renderer decides.
logan added 1 commit 2026-08-22 03:03:55 +00:00
#56 named 44px and #195 took the page header there. Settings is the
other half of #186 and much the larger one: swept on the reference
device (TLP301, 424x439) with all eleven config-sections expanded,
**120 controls** were under the floor -- not the 93 the issue's table
implies, and config-field is eight of them.

The bulk is behind the disclosures, which is why nobody had counted it:

    36  .column-arrow-btn          16x14   <- smallest in the app
    29  .column-toggle             16x16
    26  shortcut-capture button    80x25
     8  download format checkbox   16x16
     7  config-field select        335x30
     6  wa-input / wa-button       204x20, 185x21

**The density argument, measured rather than guessed, and it is
smaller than it looks.** The rows were already near the floor --
.column-item is 335x36 and .shortcut-row 335x37; it is the controls
*inside* them that were 14-25px. So a control grows into the row it
already occupies and the row goes 36 to 44. Measured after: the two
column lists went 373->447 and 690->850, +234px over the whole page.
Half a screen of extra scroll on a page that already scrolls, against
36 targets of 16x14.

**Settings is cheaper than the header was, and for a stated reason.**
There is no overflow fit on this page, so the header's "only width is
contested" rule does not bind at all and nothing here needs padding
with a negative margin. Height is a min-size, and the two square
controls can simply be square.

Three shapes, because one rule does not fit three kinds of control:

**A native checkbox is targeted through its label.** It cannot grow
its hit area without growing its paint, and a 44px checkbox is not
what anyone wants -- so .column-label is a real <label for> now and
the column's *name* is the target, 70x44 rather than 16x16. That is
the argument config-field already makes one file over ("a real label
association also makes the label text a click target, which is
behaviour, not annotation"), and here it is the whole fix. The
download formats already had the label; they only needed the height.

**The arrows take padding, which is invisible.** They carry
background: none and a transparent border, so 16x14 -> 44x44 changes
nothing anyone can see until hover -- #186's Direction exactly.

**Web Awesome's controls come from the library's own API.** Their
height is decided inside somebody else's shadow root, and
--wa-form-control-height is the variable that decides it. A custom
property inherits through a shadow boundary, so a :host declaration
reaches them; styles/wa-touch-floor.css.ts is that, once, adopted
rather than written at :root in index.css -- a :root rule would be
invisible to the component tier, which renders a component and no page
stylesheet.

**Two controls no sweep can see are fixed by name**, and they are the
trap this issue keeps setting. config-field's toggle has an <input>
that is opacity: 0; width: 0; height: 0, so a walk of every input
skips it as a zero-sized node -- what a finger hits is the <label>,
which measured **34x19**, smaller than anything in either of #186's
tables and absent from both. It is 44x44 with the pill still painted
at 2.5em x 1.4em and negative inline margins keeping it flush with the
inputs above. And shortcut-capture's reset button renders only for a
shortcut somebody has rebound, so a sweep of a fresh install never
meets it.

Verified on the device, same method as the sweep that filed it:
120 controls under the floor before, 42 after. All 42 are accounted
for -- 37 are checkboxes whose labels measure 70x44 and 57x44, four
are wa-input's inner input at 204x**42**, which is the control
measured *inside* its own 1px border (part=base is 238x44), and one is
the skip link, which #186 already ruled out as keyboard-only.

The e2e suite passes, top-bar-fit and header-action-overflow included
-- but that is **chromium**, which is half an answer, and saying so is
the whole of what #195's second commit was about. What can be argued
rather than run: library-filter is the only thing here in a container
that measures itself, and its width did not change. The fit measures
inline size.

Two page-header screenshots are refreshed because they are this
issue's own debris -- #195's taller sort control, merged last session,
with its references never re-recorded. app-sidebar's and
now-playing's are deliberately left: they are unrelated drift, and
blessing an unrelated screenshot is how the sidebar reference came to
still list a destination #27 retired. That is #196.
logan added 1 commit 2026-08-22 03:08:44 +00:00
fix(ui): raise the last controls below the touch floor
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m32s
CI / e2e (pull_request) Successful in 9m36s
4f628b1f52
The rest of #186's second table, and one thing it could not have said.

    .section-toggle          187x15  autotag
    .folders-menu-trigger     32x18  autotag
    .back-button              32x32  artist-details
    Requests / Downloads tabs 85x34, 96x34
    .search-mode-tab          89x26, 79x26  explore
    explore search input     325x18  in a 36px box

**back-button was six controls, not one.** The issue names it in
artist-details because that is the view the sweep opened; the same
declaration is byte-identical in artist-details, genre-details,
playlist-details, smart-playlist-details, explore-artist-details and
explore-album-details, 32px in all six. So it is styles/back-button.
css.ts now, adopted by each, and a source sweep fails on a seventh
copy -- because the failure this invites is not a size changing, it is
somebody adding a detail view and writing `.back-button` out again,
which no device sweep would catch for the same reason this one did
not. That is icon-language.test.ts's shape, and the argument for it
here is the inverse of the column arrows': one declaration covering
36 controls is cheap to fix, and six declarations of one control are
six chances to miss five.

It is a real 44px box rather than padding with the width handed back:
a detail header runs no fit pass, and this button has a visible
background, so a hit area larger than the circle would be a control
bigger than it looks. The size is #55's, reached there for the same
reason -- "the way out is 44px on a phone".

**The explore search box was two faults.** The row was 36px *and* the
input inside it was 18, so eight pixels at each edge were not a target
at all: a tap near the top of the box landed on the container and did
nothing. The container is 44 and the input stretches to it.

**The Downloads tabs take padding rather than a min-size**, because
the mark for the selected tab is its bottom border -- a min-size
centres the label and leaves the underline 10px beneath it.

page-action-check-now (113x29) is in that table and is not here: it is
a PageAction, so #195 raised it with the rest of the header's actions
and touch-targets.test.ts already covers it.

**The Downloads tabs needed a min-size as well as the padding, and CI
is what said so.** Padding alone made them 44px on this machine and
**43px in the container**: the total is 13 + 13 + 2 + whatever line box
the font gives 13px text, and ubuntu:24.04's is a pixel shorter than
Arch's. A height computed from a font's line box is not a height you
control -- which is #195's "stated as a property on the strength of one
engine" one layer down, in the same PR that recorded it. The padding
stays, because it is what keeps the underline against the label; the
min-size is the floor.

Caught by the new test rather than by a person, which is the half of
this that worked.

Verified on the device, sweeping each view the way the issue was
filed: explore, downloads, autotag and artist-details now report
**one** control under the floor apiece, and it is the skip link, which
#186 already ruled out as keyboard-only. .search-mode-tab 89x44 and
79x44, the search input 325x44, the Downloads tabs 85x44 and 96x44,
.section-toggle 174x44, .folders-menu-trigger 44x44, .back-button
44x44.

All 12 new tests fail on main, the source sweep naming all six copies.
make ui-test 1041 pass; make e2e 236 pass on chromium, which is half
an answer -- CI had the other half, and used it.

Closes #186
logan force-pushed 186-touch-targets-settings from 7b09d467dd to 4f628b1f52 2026-08-22 03:08:44 +00:00 Compare
logan merged commit 4f32d4e13c into main 2026-08-22 03:22:16 +00:00
Sign in to join this conversation.