Shipped in v0.10.0 (PR #29). See /docs/pwa-install for the user-facing install guide, .kb/decisions/D15-pwa-install.md for the architecture, and /help/pwa-diagnostics for the live health check.
What it does
A header install button that opens a static, per-platform install guide. Click behavior adapts to the detected browser + OS:
- Chromium desktop (Chrome / Edge / Opera / Brave / Vivaldi / Arc) — modal with per-brand menu path (Chrome’s is
⋮ → Cast, save, and share → Install Loft Tools) plus an “already tried installing? reset first” section walking through chrome://apps → Remove from Chrome.
- Chromium Android (Chrome / Edge / Samsung) — modal with
⋮ → Install app and uninstall flow (long-press home icon → Uninstall).
- iOS Safari (iPhone / iPad) — Share sheet → Add to Home Screen + uninstall flow.
- macOS Safari 17+ — File menu → Add to Dock + uninstall flow.
- Firefox / unknown — button hidden (no PWA install support).
- Running as installed app — button hidden.
The button does not try to capture beforeinstallprompt or fire a native prompt. That approach was prototyped and reverted — it produced more support-ticket traffic than it prevented (sticky install state, invisible dismiss cooldowns, platform-specific edge cases). The static-modal approach matches production PWAs like Twitter/X, Squoosh, Excalidraw, and Starbucks.
Architecture
src/
lib/
pwa-install.ts — synchronous platform + brand detection
use-pwa-install.ts — tiny React hook: { platform, isInstalled }
components/
InstallButton.tsx — header trigger
InstallButton.css — styling
InstallGuideModal.tsx — per-platform static guide, portal'd to document.body
InstallGuideModal.css — styling
PwaDiagnosticsPanel.tsx — live health check UI
PwaDiagnosticsPanel.css — styling
pages/
docs/pwa-install.astro — user-facing install help page
help/pwa-diagnostics.astro — diagnostics route (unauthed; for bug-report triage)
Diagnostics page
/help/pwa-diagnostics is a live capability report: platform classification, SW + manifest + storage state, IDB pin count. A Copy Report (markdown) button produces a paste-ready snippet for /bug-report submissions. No auth — everything it exposes is client-observable.
Accessibility
- Modal uses
role="dialog" + aria-modal + aria-labelledby; Escape closes, click-outside closes.
- Rendered via
createPortal(document.body) so it escapes the Navbar’s backdrop-filter stacking context.
- All interactive elements carry
data-sid for observability.
Prerequisites (all met)
- ✅ Brand assets (192/512 px app icons + maskable variant) in
public/brand/
- ✅ HTTPS (Cloudflare Pages deploy)
- ✅ Service worker (Workbox via vite-plugin-pwa, 5-tier caching)
- ✅ Web App Manifest at
/manifest.webmanifest
- ✅
InstallButton.tsx — full implementation, slotted into the header