Aero music: the button works on the podcast player

"The music thing button with the Frutiger Aero doesn't work on the podcasts."
The podcast page is a full-screen overlay at z-45 and the control sat at z-40:
visible through the transparent page, but every tap landed on the overlay.

The control now takes a data-raised="player" position on /podcast/[id] -
z-index 46, docked to the header's corner (top under the header pill, right
mirroring the max-w-3xl geometry) where neither the transport bar nor the
voice panel lives - and the transcript gained pt-32 so the episode card clears
it. Study/review/exam feeds keep their "steps aside" behaviour.

aero-music-test: a podcast section asserts the control exists, that
elementFromPoint at its centre hits the button, and that a tap expands the
credit bar. 31 checks. Version 0.7.9, SW v101.
This commit is contained in:
Martin Dvořák 2026-10-16 21:04:39 +00:00
commit 724a068bec
7 changed files with 74 additions and 4 deletions

View file

@ -1492,6 +1492,17 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
107. **The Aero soundtrack button rides above the podcast player.** "The music thing button with
the Frutiger Aero doesn't work on the podcasts": the podcast page is a full-screen overlay at
`z-45` and the music control sat at `z-40` — visible through the transparent page, but every
tap landed on the overlay. The control now takes a `data-raised="player"` position on
`/podcast/[id]`: `z-index: 46`, docked to the header's corner (`top` under the header pill,
`right` mirroring the `max-w-3xl` geometry) where neither the transport bar nor the voice panel
lives, and the transcript gained `pt-32` so the episode card clears it. The study/review/exam
feeds keep the "steps aside" behaviour — their bottom corners are spoken for. aero-music-test
grew a podcast section that asserts the control is there, that `elementFromPoint` at its centre
hits the button (nothing covers it) and that a tap expands the credit bar (31 checks).
106. **When the browser has no speech engine at all.** "The podcasts don't even work on my Linux
setup on Firefox": Firefox on Linux speaks through speech-dispatcher, and without it
`speechSynthesis` is *present* (so `speechAvailable()` says yes) but empty and silent —

View file

@ -933,6 +933,15 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
.aero-music[data-raised="wizard"] {
bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
}
/* The podcast player is a full-screen overlay (z-45): the music control has
to ride above it, and it moves to the header's corner — the transport and
the voice panel own the bottom of that screen. */
.aero-music[data-raised="player"] {
top: calc(max(env(safe-area-inset-top), 0.75rem) + 3.9rem);
right: max(0.75rem, calc((100vw - 48rem) / 2 + 0.75rem));
bottom: auto;
z-index: 46;
}
body:has(.overlay-open) .aero-music,
/* The one-time notice shares this corner; it wins while it is up. */
body:has(.cookie-notice) .aero-music {

View file

@ -205,7 +205,14 @@ export function AeroMusic() {
/* The composer sits at the bottom of the chat screen and the wizard has its
own action bar; everywhere else the phone tab bar is the only neighbour. */
const raised = pathname === "/chat" ? "chat" : pathname === "/create" || pathname === "/homework" ? "wizard" : undefined;
const raised =
pathname.startsWith("/podcast/")
? "player"
: pathname === "/chat"
? "chat"
: pathname === "/create" || pathname === "/homework"
? "wizard"
: undefined;
return (
<div className="aero-music" data-testid="aero-music" data-raised={raised}>

View file

@ -585,7 +585,9 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) {
const index = Number(target.dataset.word);
if (Number.isInteger(index)) jumpWord(index);
}}
className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-24"
/* pt-32 clears the Aero music control, which rides above this overlay
in the header's corner while the theme is on. */
className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-32"
>
<div className="mx-auto w-full max-w-3xl space-y-6">
{naturalError ? (

View file

@ -1,6 +1,6 @@
{
"name": "freezmo",
"version": "0.7.8",
"version": "0.7.9",
"private": true,
"license": "GPL-3.0-only",
"scripts": {

View file

@ -9,7 +9,7 @@
*
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/
const CACHE_VERSION = "freezmo-v100";
const CACHE_VERSION = "freezmo-v101";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -208,6 +208,47 @@ async function stubSoundCloud(page) {
await context.close();
}
/* 4b. the podcast player: the control rides above that overlay -------- */
{
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
await stubSoundCloud(page);
await seed(page, {
theme: "aero",
decks: [],
extra: {
"freezmo.podcasts.v1": JSON.stringify([
{
id: "pod_music",
title: "While you listen",
summary: "",
language: "English",
sourceText: "x",
createdAt: 1,
updatedAt: 1,
sections: [{ heading: "One", text: "Look up on a clear day." }],
},
]),
},
});
await page.goto(`${BASE}/podcast/pod_music`, { waitUntil: "networkidle" });
await page.waitForTimeout(1500);
const circle = page.getByRole("button", { name: "Frutiger Aero music" });
check("podcast: the music control is there while listening", (await circle.count()) === 1);
const reachable = await page.evaluate(() => {
const button = document.querySelector("[data-testid=aero-music] button");
if (!button) return "missing";
const box = button.getBoundingClientRect();
const top = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
return button.contains(top) || top === button ? "on-top" : `covered by ${top?.className?.toString().slice(0, 40)}`;
});
check("podcast: nothing covers it (it rides above the overlay)", reachable === "on-top", reachable);
await circle.click();
await page.waitForTimeout(400);
check("podcast: tapping it expands the credit bar", (await page.getByText("SoundCloud ·", { exact: false }).count()) >= 1);
await context.close();
}
/* 5. immersive overlays step aside, the music keeps playing ----------- */
{
const { context, page } = await makePage(browser, { width: 1280, height: 731 });