diff --git a/README.md b/README.md index 577c48c..83b5332 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ is a Tabata whether or not anything is labelled "Tabata". - **Accurate by construction** — state is derived from a wall-clock timestamp with a pure `phaseAt(elapsed, routine)`; nothing accumulates ticks, so nothing drifts. - **Offline-first** — precached service worker, no backend, no accounts, no analytics. Installable as - **Timer**; updates are picked up on page reload, not via a modal prompt. + **Timer**; waiting updates apply immediately when no Routine is active. +- **Automatic updates** — an active or paused Routine defers a waiting worker until Done or Reset. The + new build reloads automatically, then the bottom version shows green **NEW vx.x.x** for that reload only. - **Progressive enhancement** — audio session, Wake Lock and vibration are feature-detected and no-ops where absent. The app is fully correct without any of them. - **Semantic and accessible** — [Pico CSS](https://picocss.com/) classless, WCAG 2.2 AA in every slice. diff --git a/astro.config.mjs b/astro.config.mjs index 2035987..fd86400 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -6,7 +6,7 @@ import { defineConfig } from "astro/config"; export default defineConfig({ integrations: [ AstroPWA({ - registerType: "autoUpdate", + registerType: "prompt", manifest: { name: "Timer", short_name: "Timer", diff --git a/docs/spec.md b/docs/spec.md index 2c278c9..5705bf3 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -328,7 +328,7 @@ No theme override — `prefers-color-scheme` only, and nothing is stored for it. The whole prototype is one `.astro` file: plain DOM, CSS custom properties, `rAF`, native ``. Nothing in the UI justifies an island, and nothing in the PWA layer -does either — registration, update prompt, Wake Lock, audio session and haptics are all plain DOM APIs. +does either — registration, update activation, Wake Lock, audio session and haptics are all plain DOM APIs. --- @@ -416,11 +416,26 @@ layer below is feature-detected, adds capability where present, and is a no-op w `virtual:pwa-register` by hand; (2) **`workbox-window` must be an explicit devDependency** under pnpm, or the build fails with `Rolldown failed to resolve import "workbox-window"`. - Add `/// ` to `src/env.d.ts` for the virtual-module types. -- **`registerType: 'prompt'`, never `autoUpdate`** — an auto-reload mid-Routine would destroy the - running clock. The update prompt is a plain `.astro` component with a ` diff --git a/src/pages/down.astro b/src/pages/down.astro index d93c520..f96e076 100644 --- a/src/pages/down.astro +++ b/src/pages/down.astro @@ -111,6 +111,7 @@ import AppLayout from "../layouts/AppLayout.astro"; configuredDuration.textContent = formatElapsed(durationMs); configuredDuration.dateTime = `PT${durationMs / 1000}S`; const status = isFinished ? "Done" : isRunning(clock) ? "Running" : clock.startedAt === null ? "" : "Paused"; + document.body.dataset.routineActive = String(!isFinished && clock.startedAt !== null); const nextTitle = status ? `${status} · ${visible} - Down Timer` : `${visible} - Down Timer`; if (nextTitle !== pageTitle) { pageTitle = nextTitle; @@ -134,6 +135,7 @@ import AppLayout from "../layouts/AppLayout.astro"; setUrlState("done"); announce(`Done count down: ${announceElapsed(durationMs)}.`); paint(); + window.dispatchEvent(new Event("timer:counter-inactive")); }; const paint = () => { @@ -201,6 +203,7 @@ import AppLayout from "../layouts/AppLayout.astro"; stopTicking(); announce("Reset."); paint(); + window.dispatchEvent(new Event("timer:counter-inactive")); }); const configurationUrl = new URL("/configuration", location.origin); @@ -226,8 +229,8 @@ import AppLayout from "../layouts/AppLayout.astro"; let confirmedNavigation = false; document.querySelectorAll("a[href]").forEach((link) => { link.addEventListener("click", (event) => { - if (!isRunning(clock) || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; - if (!window.confirm("Leave the running Routine?")) { + if (document.body.dataset.routineActive !== "true" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + if (!window.confirm("Leave the active Routine?")) { event.preventDefault(); return; } @@ -237,7 +240,7 @@ import AppLayout from "../layouts/AppLayout.astro"; }); window.addEventListener("beforeunload", (event) => { - if (!isRunning(clock) || confirmedNavigation) return; + if (document.body.dataset.routineActive !== "true" || confirmedNavigation) return; event.preventDefault(); }); diff --git a/src/pages/index.astro b/src/pages/index.astro index 42d4f84..2a8e8ef 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -99,6 +99,7 @@ import AppLayout from "../layouts/AppLayout.astro"; display.dateTime = `PT${state.displayMs / 1000}S`; display.setAttribute("aria-label", announceElapsed(state.displayMs)); const status = isFinished ? "Done" : isRunning(clock) ? "Running" : clock.startedAt === null ? "" : "Paused"; + document.body.dataset.routineActive = String(!isFinished && clock.startedAt !== null); const nextTitle = status ? `${status} · ${visible} - Timer` : `${visible} - Timer`; if (nextTitle !== pageTitle) { pageTitle = nextTitle; @@ -124,6 +125,7 @@ import AppLayout from "../layouts/AppLayout.astro"; setUrlState("done"); announce(`Done: ${announceElapsed(MAX_ELAPSED_MS)}.`); paint(); + window.dispatchEvent(new Event("timer:counter-inactive")); }; const paint = () => { @@ -191,6 +193,7 @@ import AppLayout from "../layouts/AppLayout.astro"; stopTicking(); announce("Reset."); paint(); + window.dispatchEvent(new Event("timer:counter-inactive")); }); document.addEventListener("visibilitychange", () => { @@ -209,8 +212,8 @@ import AppLayout from "../layouts/AppLayout.astro"; let confirmedNavigation = false; document.querySelectorAll("a[href]").forEach((link) => { link.addEventListener("click", (event) => { - if (!isRunning(clock) || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; - if (!window.confirm("Leave the running Routine?")) { + if (document.body.dataset.routineActive !== "true" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + if (!window.confirm("Leave the active Routine?")) { event.preventDefault(); return; } @@ -220,7 +223,7 @@ import AppLayout from "../layouts/AppLayout.astro"; }); window.addEventListener("beforeunload", (event) => { - if (!isRunning(clock) || confirmedNavigation) return; + if (document.body.dataset.routineActive !== "true" || confirmedNavigation) return; event.preventDefault(); }); diff --git a/src/styles/app.css b/src/styles/app.css index a8cd3b3..e05cc56 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -23,6 +23,7 @@ body > main { width: 100%; max-width: none; padding: 1rem max(0.5rem, env(safe-area-inset-left)) 1rem max(0.5rem, env(safe-area-inset-right)); + padding-bottom: calc(3rem + env(safe-area-inset-bottom)); overflow-x: hidden; text-align: center; } @@ -249,6 +250,12 @@ body > main { user-select: none; } +.app-version[data-updated="true"] { + color: #39d353; + font-weight: 700; + opacity: 1; +} + .deploy-clue { letter-spacing: 0.08em; text-transform: uppercase;