Skip to content

PWA foundation: manifest, service worker, install prompt - #16

Merged
unforced merged 1 commit into
mainfrom
pwa-foundation
Apr 19, 2026
Merged

PWA foundation: manifest, service worker, install prompt#16
unforced merged 1 commit into
mainfrom
pwa-foundation

Conversation

@unforced

Copy link
Copy Markdown
Contributor

Summary

First PR of the v0.2 offline-capable PWA arc. Makes Parachute Lens installable and sets up the service worker foundation that later PRs build on.

  • Manifestid, name, short_name, description, theme_color (#4a7c59), background_color (#faf8f4), display: standalone, start_url: /, scope: /
  • Icons — single public/icon.svg (sage rounded-square + lens-aperture glyph) rasterized via @vite-pwa/assets-generator into 64/192/512, apple-touch-icon-180, maskable 512, favicon. Maskable padding 0.2 + sage fill so Android adaptive crops don't eat the glyph
  • Service workervite-plugin-pwa with Workbox. registerType: "prompt" so users choose when to reload. navigateFallback: /index.html gives the SPA an offline shell. navigateFallbackDenylist: [/^\/api\//, /^\/oauth\//, /^\/\.well-known\//] keeps vault + OAuth + discovery requests passing through cleanly so they error loudly offline rather than getting a stale HTML fallback. Google Fonts use StaleWhileRevalidate
  • UpdateBanner — bottom-center <output role="status"> with Later / Reload when a new SW is waiting; also checks for updates hourly while open
  • InstallPrompt — captures beforeinstallprompt, shows an Install app button in the header when the browser offers installability; hidden once installed. iOS fallback: <dialog> with Share → Add to Home Screen instructions
  • README — new "Install Parachute Lens" section at the top with Chrome/Edge, Android, and iOS instructions plus iOS storage caveats

iOS quirks named in this PR

  • PWA storage quota is ~50 MB per app on iOS
  • Apple may clear data from PWAs that haven't been opened in a while
  • There is no beforeinstallprompt event on iOS — install is manual via the Share sheet, so Lens surfaces instructions instead of a button

Out of scope (later in v0.2)

Gates

  • bun run lint — biome clean
  • bun run typecheck — tsc clean
  • bun run test — 189 passed (30 files)
  • bun run builddist/sw.js, dist/workbox-*.js, manifest.webmanifest, 21 precache entries (1411 KiB)

New test surface:

  • src/lib/pwa.test.ts — isIOS (iPhone, iPad, iPadOS-on-Mac-UA, desktop Chrome, Android); isStandalone (display-mode media, navigator.standalone, regular tab)
  • src/components/InstallPrompt.test.tsx — hidden when standalone; hidden before event; shows button + calls prompt(); iOS dialog fallback
  • src/components/UpdateBanner.test.tsx — smoke test for the default needRefresh=false path
  • src/pwa-manifest.test.ts — required installability fields; 192/512 icons; maskable present; JSON-serializable

To keep the manifest testable, it's extracted to src/pwa-manifest.ts and imported by both vite.config.ts and the test. The virtual virtual:pwa-register/react module is aliased to a vitest stub so components using it can render under jsdom.

Surprises / Calls

  • jsdom + iOS detection — jsdom 25 exposes ontouchend on document, so a naive "Mac UA + has touch" check flagged desktop Chrome as iOS. isIOS now takes an explicit hasTouch parameter (defaulting to navigator.maxTouchPoints > 1), which is both easier to test and more accurate than the ontouchend probe
  • NetworkOnly via denylist — rather than add an explicit Workbox NetworkOnly rule matching unknown cross-origin vault URLs, I denylisted /api/, /oauth/, and /.well-known/ from the nav fallback and omitted runtimeCaching for them. Workbox's default passthrough is NetworkOnly, so this yields the same behavior with less config
  • Bundle size warning — main chunk crossed 500 KB (1.2 MB gzipped to 388 KB). Pre-existing condition from react-force-graph-2d + CodeMirror, not introduced here. Logged as a follow-up concern for a later code-splitting pass

Test plan

  • Build + serve dist/, visit in desktop Chrome, confirm Install button appears and installs the app
  • Open installed Lens, confirm it runs standalone and loads
  • In installed app: take the network offline, reload — shell loads, data views show clear error state
  • Ship a new SW (any follow-up commit), confirm the UpdateBanner appears with Later / Reload
  • On iOS Safari, confirm the Install button is hidden and the Add-to-Home-Screen hint dialog opens
  • Lighthouse PWA audit passes installability checks

🤖 Generated with Claude Code

Sets up Parachute Lens as an installable Progressive Web App:

- manifest.json with sage-palette theme + background colors, standalone
  display, 192/512/maskable icons generated from a single icon.svg via
  @vite-pwa/assets-generator
- Service worker via vite-plugin-pwa (Workbox): precaches the app shell,
  falls back to /index.html for SPA navigations, and denies that fallback
  for /api/, /oauth/, /.well-known/ so vault + OAuth requests error
  cleanly offline. Google Fonts use StaleWhileRevalidate
- registerType "prompt": subtle UpdateBanner lets the user choose when
  to reload instead of force-reloading
- InstallPrompt captures beforeinstallprompt, shows an Install button in
  the header when available, and falls back to an iOS Add to Home Screen
  hint dialog for Safari
- Icons: clean sage rounded-square with a lens-aperture glyph (circle
  in circle), no font dependency, readable at 64px
- README: Install Parachute Lens section at the top with desktop, Android,
  and iOS instructions plus iOS storage-quota caveats

Out of scope for this PR (coming in v0.2): offline data, voice capture,
Scribe integration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@unforced
unforced merged commit f982fa8 into main Apr 19, 2026
@unforced
unforced deleted the pwa-foundation branch April 19, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant