Web frontend: touch layout for phones

Phones got the kiosk's keyboard-first layout squeezed down: shelves cut off
by the tab rail and a header taking a fifth of the screen. They now get
their own layout, chosen by a media query (narrow, or short landscape on a
touch screen); tablets and desktop are unchanged.

- Pick a group (Musik/Hörbücher/Podcasts), then a category, then scroll
  through a grid of smaller albums.
- A floating glass dock: mini player, Musik/Zimmer tabs (no typing tab - a
  phone has no keyboard for it) and a search button that turns into a real
  search field, raised above the on-screen keyboard.
- Album sheet and room page get compact phone variants.

Browse state stays in the shared UiState, so resizing or rotating keeps
your place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 21:33:26 +02:00
parent 61104d1e25
commit 0f08e1d16c
11 changed files with 1232 additions and 28 deletions

View File

@@ -0,0 +1,26 @@
/** How much of the bottom of the layout viewport the on-screen keyboard covers, in px.
*
* iOS Safari slides the keyboard over the page without resizing it - `100dvh` does not
* follow it - so a bar pinned to the bottom ends up hidden behind the very keyboard it
* is typing with. The visual viewport does shrink, and the difference is the inset. */
import { useEffect, useState } from "react";
export function useKeyboardInset(): number {
const [inset, setInset] = useState(0);
useEffect(() => {
const viewport = window.visualViewport;
if (!viewport) return;
const measure = () =>
setInset(Math.max(0, Math.round(window.innerHeight - viewport.height - viewport.offsetTop)));
viewport.addEventListener("resize", measure);
viewport.addEventListener("scroll", measure);
return () => {
viewport.removeEventListener("resize", measure);
viewport.removeEventListener("scroll", measure);
};
}, []);
return inset;
}

View File

@@ -0,0 +1,23 @@
/** Whether the screen is a phone, which gets its own touch-first layout (see
* `components/mobile/`) instead of the kiosk's keyboard-first one.
*
* Narrow portrait, or short landscape on a touch screen - a phone turned sideways is
* wide enough for the kiosk layout's columns but nowhere near tall enough for its
* header, shelves and player bar. A tablet is neither and keeps the kiosk layout. */
import { useEffect, useState } from "react";
const PHONE_QUERY = "(max-width: 600px), (max-height: 500px) and (pointer: coarse)";
export function usePhoneLayout(): boolean {
const [phone, setPhone] = useState(() => matchMedia(PHONE_QUERY).matches);
useEffect(() => {
const media = matchMedia(PHONE_QUERY);
const onChange = () => setPhone(media.matches);
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
return phone;
}