docs(player): record who owns the volume, and what it gave back
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m34s
CI / e2e (pull_request) Successful in 8m47s

CLAUDE.md's volume paragraph ended "#64 asks for it to be gone on
Android outright, which is a platform question the frontend cannot
currently ask", which is no longer true -- it can, and the paragraph
now says why the answer is a capability rather than a viewport and what
that costs.  The mediacontrols entry gains the corollary: on that
platform "the user's level" is a constant, and the duck is the one
thing that may still move the output.

NOTES.md carries the measurements: the per-element budget at 424x439
before and after, the :host([hidden]) specificity trap, the fact that
the bar's centring survives the control going away, and what no tier
here could check.
This commit is contained in:
2026-08-21 00:53:44 -04:00
parent 2be6fb3066
commit b43172a60c
2 changed files with 133 additions and 7 deletions
+76
View File
@@ -4573,3 +4573,79 @@ fault in it, and it is filed as #172 with the per-element budget. #64
umbrella; folding shuffle and repeat back onto the primary row was
considered and rejected — it buys 52px, leaves the art at 91px, and
costs a third arrangement of the same five buttons.
## The volume is not ours on Android, and the predicate could not be a width (measured 2026-08-21)
#64 asked for the in-app volume control to be absent on Android. Its
first Finding said `volume-control` "already stands down at narrow
widths", which was true of one of its two copies and is why the issue
had been read as nearly done. The bar's copy goes by width; the
full-screen view's copy was deliberately kept, with a comment saying a
slider does belong there.
**The crux was platform versus width, and three options were on the
issue.** What settled it is that the *backend* half of the same issue —
pin the level at 1.0 — makes a width rule wrong on the platform the
issue is about: an Android tablet at >=600px gets the bottom bar, and
the bar's slider would then move a level that is pinned. That is a
control that cannot act, which `library-status-indicator` already
settled is worse than none. The same rule is wrong the other way below
600px, where a narrow desktop window has no hardware keys.
So the frontend asks the player — `SystemOwnsVolume` — and the answer
is right at every width in both mount points. **The predicate is named
after the capability rather than the platform**, which is what makes it
testable: only `platformOwnsVolume` is behind a build tag, in two files
that declare nothing else, and everything else is decided against a
field a Go test sets either way. `frontend/test/components/
volume-ownership.test.ts` stubs the binding and so exercises the
*Android* rendering on an ordinary Linux runner; both of its tests were
confirmed to fail on the build before the change.
**Measured at 424x439, by flipping `platformOwnsVolume` to true in the
`!android` file and rebuilding** — the real binding, the real store, the
real component, everything except the tag:
| element | before | after |
|---|---|---|
| header | 48 | 48 |
| **album art** | **39** | **68** |
| title / artist / album | 63 | 63 |
| transport (seek + controls + volume) | **172** | **143** |
| — seek bar | 19 | 19 |
| — player-controls | 116 | 116 |
| — volume-control | 21 | **0** |
29px, which is the 21px control plus the 8px flex gap it stops drawing:
a gap is only painted between boxes, so `:host([hidden])` costs the
transport nothing rather than leaving a hole. That is #172's "~30px of
pure gain" confirmed, and the art is 74% larger. It is still the
second-smallest thing on the screen, which is #51's evidence.
Three smaller things worth keeping.
**`:host([hidden])` has to be written down.** The UA's `[hidden]`
rule is `display: none`, but `volume-control`'s own `:host` sets
`display: inline-flex` and outranks it — so setting `hidden` alone
hides nothing. Same family as the nested-`#queue-button` specificity
trap from the session before.
**Rendering `nothing` and hiding the host are two different
assertions**, and the component test makes both: an empty shadow root
is what stops a by-role or positional query finding a button that
cannot act, and `hidden` is what stops the host occupying space. Either
alone passes on a build that gets the other wrong.
**The bar's centring survives the control going away.** #23's outer
columns are the same `min()` expression rather than content-sized, so
at 900px with the volume gone the bar's centre, `audio-player`'s centre
and `player-controls`' centre are all 450 — checked, because "the
transport is centred with a slider bolted to one side" is the fault
that rule exists for and removing the slider is the obvious way to
re-break it.
**What no tier here can check**: the constant itself, and ducking
against a real audio-focus change. The first is a source sweep
(`TestPlatformVolumeOwnershipIsDeclaredOncePerPlatform`), the second is
`TestSystemVolumeStillDucks` against the arithmetic. Neither is a
device, and no device was attached.