feat(ui): a shell a phone can be held in
Plan 016 B2, phase 1. Below 600px the grid drops its sidebar column, `bottom-nav` becomes the primary navigation, and the shell fits the viewport instead of scrolling sideways out of it. 600 rather than the sidebar's own 900, because 900 is a laptop and the answer there is a narrower sidebar, which is still a sidebar. Under 600 there is no room for one at all: 360px of viewport over a 200px nav is not a layout. **The tab bar is four destinations and a way to everything else.** Three to five is where touch targets stop being thumb-sized -- eleven over 360px is 32px each -- so the four are the ones plan 016's subset says a phone is for, and "More" opens the *existing* `app-sidebar` in a drawer rather than listing the destinations a second time. Two lists is two places to add the next view to. That reuse has a cost this found the hard way: a shared component brings its `data-testid`s with it, so rendering the drawer's sidebar unconditionally put a second `nav-home` (and ten siblings) in the DOM and **failed 30 existing specs** with "resolved to 2 elements" -- on a desktop viewport, where this element is `display: none` and the drawer can never open. It renders only while the drawer is open, and the component test asserts the absence, because the failure is invisible from inside the component and lands in files nobody touched. **What made the shell overflow was minimums, not padding.** Measured at 360px: the body was 652px wide, because a `min-width` in a flex row is a hard floor and a grid item's implicit minimum is its content. So `min-width: 0` on the boxes between the viewport and the content, and each component stands its own non-essential parts down in its *own* stylesheet -- search-bar's 200px floor, job-indicator's label (the visible one; the live region that announces it is untouched), audio-player's seek bar and volume. A media query inside a shadow root is answered by the viewport, so this is the component saying what it drops rather than the shell reaching in. Volume goes because the hardware keys own it on a phone, which is the same reason mediacontrols' Android handler implements no volume callback. Seeking goes because 4px is not a thumb target; it belongs to the full-screen now-playing view, which is the next phase. An existing spec therefore asserts the opposite of what it did: layout-overflow's 320px case used to require that the 464px behind `overflow: hidden` could be *scrolled to*, which was the remedy available while the shell had one layout. It reflows now -- 320px in a 320px viewport, exactly -- and reflow is what WCAG 1.4.10 asked for.
This commit is contained in:
@@ -2828,3 +2828,77 @@ for boot ok". `pick_device` now resolves `ANDROID_SERIAL` from
|
||||
`ro.boot.qemu.avd_name`, since serials are assigned in boot order and
|
||||
the AVD name is the stable identity. Verified with both emulators
|
||||
running: it selects `yj-test` and installs.
|
||||
|
||||
## The phone shell fits, and what it cost to make it fit (2026-08-16)
|
||||
|
||||
Plan 016 B2, phase 1: the shell below 600px. Measured at 360×780 and
|
||||
390×844 against the real app (`make dev-headless` + Playwright, which
|
||||
is the tier that can answer this — server mode serves the same document
|
||||
an Android WebView renders).
|
||||
|
||||
**What overflowed, and by how much.** The body was 652px wide in a
|
||||
360px viewport before any of this. Walking every element and its shadow
|
||||
roots for a `right` past the viewport named the causes in order:
|
||||
|
||||
| element | width | why |
|
||||
|---|---|---|
|
||||
| `header.top-bar` | 580 | its children's minimums, summed |
|
||||
| `search-bar` | 320 | `.search-container { min-width: 200px }` |
|
||||
| `job-indicator` | 157 | the label, "3 background jobs" |
|
||||
|
||||
A `min-width` in a flex row is a *hard* floor — it does not shrink — and
|
||||
a grid item's implicit minimum is `auto`, i.e. its content. So the
|
||||
header could not get smaller than the sum of what it held, the body grew
|
||||
to the header, and `overflow-x: hidden` would then have hidden a third
|
||||
of the app rather than fitting it. `min-width: 0` on the boxes between
|
||||
the viewport and the content, plus each component standing its own
|
||||
non-essential parts down in its own stylesheet, takes 360 → 360 exactly.
|
||||
At 320px (400% zoom, the width WCAG 1.4.10 names) it is also exact.
|
||||
|
||||
**So an existing spec now asserts the opposite of what it did**, and
|
||||
that is the fix landing rather than the test being weakened.
|
||||
`layout-overflow.spec.ts` used to assert that the 464px of app behind
|
||||
`overflow: hidden` *could be scrolled to* with a wheel gesture, which
|
||||
was the remedy available when the shell had one layout. It reflows now,
|
||||
which is what 1.4.10 asks for; scrolling to the overflow was the
|
||||
concession.
|
||||
|
||||
**And a shared component brings its test handles with it.**
|
||||
`bottom-nav`'s "More" opens the *existing* `<app-sidebar>` in a drawer —
|
||||
the whole point being not to write a second list of destinations — but
|
||||
rendering it unconditionally put a second `data-testid="nav-home"` (and
|
||||
ten siblings) in the DOM. **30 existing specs failed** with "strict mode
|
||||
violation: resolved to 2 elements", on a *desktop* viewport where
|
||||
`bottom-nav` is `display: none` and the drawer can never open. Lazy
|
||||
rendering fixes it; the component test asserts the absence, because the
|
||||
failure is invisible from inside the component and appears in files
|
||||
nobody touched.
|
||||
|
||||
Three smaller things worth keeping:
|
||||
|
||||
- **A new icon name is a runtime failure, not a build one.** `bars` was
|
||||
not in `src/icons/names.txt`, so `offline-icons.spec.ts` caught it —
|
||||
the sweep asserts `window.__yjIconMisses` is empty. `node
|
||||
frontend/scripts/fetch-icons.mjs` re-vendors after adding a line.
|
||||
- **A `wa-drawer` animates, so a test asserts its events**, not its
|
||||
`open` property: setting `open = false` starts a hide that has not
|
||||
finished on the next microtask, and a test reading the property in
|
||||
between sees the state it is leaving.
|
||||
- **`update(el)` in the component tier takes two arguments**
|
||||
(`update(el, {})`), which is only visible from `tsc`, not from a
|
||||
failing test.
|
||||
|
||||
### A pre-existing failure this uncovered but did not cause
|
||||
|
||||
`requested-badge.spec.ts` fails two of its three tests, **on clean
|
||||
`main` as well** (verified by stashing every change and re-running).
|
||||
The symptom is `download.Service.AddRequest` not settling in 10s.
|
||||
|
||||
What is now known, and narrows it for whoever picks it up: the same
|
||||
method with the same arguments, called straight at the runtime endpoint
|
||||
with `curl`, **returns in 4ms** (it inserted, and answered `2`). So it
|
||||
is not the backend and not the documented read-pool trap — `Queries` is
|
||||
built over the writer, and `Reconciler.Trigger` is a non-blocking
|
||||
select. It is the page-side path: `__yjEvents.call` → the `fetch` hook
|
||||
→ `/wails/runtime`. A third test in that file fails only *after* those
|
||||
two, so it is state, not a third bug.
|
||||
|
||||
@@ -246,6 +246,13 @@ view's template is two templates to fix every bug in. Where a view
|
||||
cannot serve both, the split belongs at the chunk boundary that already
|
||||
exists.
|
||||
|
||||
Phase 1 followed that rule and found its cost: reusing `<app-sidebar>`
|
||||
inside the drawer means reusing its `data-testid`s too, and a second
|
||||
copy standing by in the DOM broke 30 specs that had nothing to do with
|
||||
the phone. The rule holds — a second list of destinations would be
|
||||
worse — but a shared component must be rendered only when it is wanted,
|
||||
and the guard belongs in a test that names the reason.
|
||||
|
||||
## What is worth doing regardless of that decision
|
||||
|
||||
Cheap, independently useful, and each unblocks measurement:
|
||||
@@ -308,8 +315,14 @@ places had to agree — `abiFilters`, the Makefile's `android:package`
|
||||
anchor is what stops it also matching the fat APK's line. Adding the
|
||||
ABI back, if modernc ever fixes `Xlstat64`, is those same three edits.
|
||||
|
||||
**B2, the desktop shell.** The largest remaining piece, and the scope
|
||||
is now decided — see "The phone gets a subset" below.
|
||||
**B2, the desktop shell.** Scope decided (below) and **phase 1 is
|
||||
done**: the shell itself. Below 600px the sidebar column is gone,
|
||||
`<bottom-nav>` is the primary navigation, and the shell fits 320px
|
||||
exactly — measured, from 652px in a 360px viewport before. What is left
|
||||
is the *views*: a full-screen now-playing (which is where seeking and
|
||||
volume went), long-press for the context menus that are right-click
|
||||
today, and the track list's resizable columns, which are a pointer
|
||||
feature with no touch equivalent.
|
||||
|
||||
**B3/B4** are unchanged, and B3 is now *possible* where it was not:
|
||||
with all-files access, `tagwriter` can write in place.
|
||||
|
||||
Reference in New Issue
Block a user