catacomb/docs/superpowers/specs/2026-07-05-mobile-scaling-and-pwa-design.md
Luna 891b1e0d3c
feat(web): fix mobile scaling regressions and ship installable PWA
Mobile fixes (all in the embedded SPA):
- Move the mobile media queries to the bottom of the stylesheet — the
  cinematheque design layer was appended after them, so its base rules
  (header padding, video heights) silently overrode the phone layout by
  source order. A comment now pins the ordering invariant.
- The design layer's uiDrop entry animation (fill-mode:both) permanently
  overrode the sidebar's translateX(-100%), leaving the drawer stuck open
  over the content on phones; the mobile block now disables it.
- Lower the single-column breakpoint 380px -> 350px: it was catching
  360/375px phones and blowing every card up to full width.
- viewport-fit=cover so the env(safe-area-inset-*) paddings actually
  resolve on iPhones; notch-safe padding on header/content/modals.
- 16px inputs on small screens (kills iOS Safari's focus auto-zoom),
  same for the login page's password field.
- Mobile rules for the design-layer components that had none: full-height
  command-palette sheet, clamped stats numerals, coarse-pointer touch
  targets for the custom player (taller scrub track, always-visible
  thumb, no volume slider).
- CSP font-src now allows data: — the embedded base64 woff2 fonts were
  being blocked and silently fell back to system faces.

PWA:
- manifest.webmanifest, minimal service worker, and a new Catacomb arch
  icon set (SVG source + rendered PNGs incl. maskable + apple-touch),
  all include_str!/include_bytes!-embedded like the HTML.
- sw.js only intercepts GET navigations to "/" (network-first, cached
  offline fallback) and the static assets; /api, /ws, /files,
  /music-files, /feed are never touched, and it's served no-store so
  binary upgrades keep propagating.
- Routes + auth_middleware allowlist so the browser can fetch the
  statics pre-login; theme-color meta tracks the active theme's panel.
- tests/api.rs covers the new endpoints incl. the ungated-when-password
  invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:04:17 -07:00

4 KiB
Raw Blame History

Mobile scaling fixes + PWA support — design

Date: 2026-07-05. Scope: src/web_ui/ + src/web.rs + tests/api.rs.

Problem

  1. Mobile scaling regressions. The "private cinematheque" design layer was appended after the @media(max-width:640px) / @media(max-width:380px) blocks in src/web_ui/index.html. Media queries add no specificity, so the design layer's base rules (header padding, .vid-wrap video max-height, …) override the mobile rules by source order. The design layer also introduced whole components with no mobile rules at all: the custom player (.pl-*), command palette (.cp-*), stats dashboard (.sx-*) and maintenance console (.mx-*).
  2. The viewport meta lacks viewport-fit=cover, so every env(safe-area-inset-*) the CSS relies on evaluates to 0 on iPhones.
  3. Text inputs are 13px, which triggers iOS Safari's automatic focus-zoom — the classic "the page scales itself when I tap search" bug.
  4. No PWA support: no manifest, no service worker, no installability, and the bundled icon.png is a YouTube-logo lookalike unsuitable for an installed app icon.

Approach chosen

Mobile fixes (index.html only, no JS-visible id/class changes):

  • Add viewport-fit=cover to the viewport meta.
  • Move both mobile media-query blocks to the end of the stylesheet, after the design layer, with a comment stating the ordering invariant. This is the structural fix; alternatives (raising specificity, !important) were rejected as fragile.
  • Inside the ≤640px block: bump text inputs/selects to 16px (kills iOS focus-zoom), and add rules for the new components — full-bleed command palette with dvh sizing, clamped stats-hero numerals, safe-area padding.
  • New @media(pointer:coarse) block for touch ergonomics independent of width: taller scrub track, always-visible seek thumb, ≥40px player buttons, hide the hover-oriented volume slider (phones use hardware volume).

PWA (additive, all static assets embedded like the existing HTML):

  • src/web_ui/manifest.webmanifest — name/short_name Catacomb, display: standalone, start_url: /, id: /, theme/background #1a1a2e, icons 192/512 + maskable 512.
  • New Catacomb-branded icon: src/web_ui/icon.svg (crimson catacomb arch on the app's dark palette) rendered to icon-192.png, icon-512.png, icon-maskable-512.png, apple-touch-icon.png (180px, opaque). SVG source checked in; PNGs regenerated via rsvg-convert.
  • src/web_ui/sw.js — deliberately minimal service worker. Intercepts only GET navigations to / (network-first, falling back to a cached copy when offline) and the static icon/manifest paths (cache-first). It never touches /api/*, /ws/*, /files/*, /music-files/*, or /feed* — no interference with auth, streaming ranges, or WebSockets. Served no-store so upgrades propagate on next load, matching the existing "no stale UI" policy.
  • src/web.rsinclude_str!/include_bytes! consts + routes for /manifest.webmanifest, /sw.js, /icons/*, /apple-touch-icon.png; these paths are allowlisted (GET, static, nothing sensitive) in auth_middleware so the browser can fetch them pre-login.
  • index.html head: manifest link, theme-color meta (kept in sync with the active theme's --panel by applyTheme), apple-touch-icon link, iOS standalone metas; SW registration guarded by 'serviceWorker' in navigator.

Testing

  • tests/api.rs: new test asserting /manifest.webmanifest, /sw.js, and an icon route return 200 with sane content types, and that they are reachable without auth when a password is set (follows the existing curl harness).
  • node --check on the extracted inline script and on sw.js.
  • Headless Chromium screenshots at phone viewports (375×812, 360×740) before and after, against a real --web instance.

Out of scope

Offline caching of library data/media, push notifications, background sync, replacing icon.png used by the desktop .desktop entry (flagged for a follow-up since it's a YouTube trademark lookalike).