feat(web): PWA install bottom sheet on phone when not running installed #142
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#142
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On a phone browser that is not running as an installed PWA, show a bottom-sheet dialog offering to install the app to the Home Screen. Designed in a grill session 2026-07-11; every decision below is settled.
Behavior
Eligibility — the sheet shows only when ALL hold:
matchMedia('(display-mode: standalone)')is false ANDnavigator.standalone(iOS legacy) is falsy.matchMedia('(pointer: coarse)')— phones and tablets, never desktop. No viewport-width or UA gate. (iPadOS reports a Mac UA but coarse pointer, so it correctly qualifies.)Two variants, adaptive:
beforeinstallpromptat module init withpreventDefault()(this also suppresses Chrome's mini-infobar so only our sheet appears) and stash the event in a signal. The sheet renders an Install button that calls.prompt()on the stashed event, plus Not now. Reactive timing: the sheet appears whenever the event arrives, even seconds after shell mount; if the event never fires (already installed, unsupported browser), Android shows nothing — no dead Install button, no fallback instructions.Dismissal: "Not now" (or scrim tap / close) sets a localStorage flag — the sheet never auto-shows again in that browser.
Settings re-entry: an Install app row in Settings reopens the same sheet on demand and clears the dismissed flag. Row visibility: hidden when running standalone; on iOS always shown otherwise; on Android shown only while a captured
beforeinstallpromptevent is in hand.appinstalled: on this event, close the sheet, drop the stashed event, and let the Settings row disappear.Form
Bottom sheet: card slides up from the bottom edge with a scrim behind it — native idiom for phone install prompts, thumb-reachable, and on iOS it sits visually adjacent to Safari's bottom-toolbar Share button that the instructions point at. This is the repo's first modal primitive (only
Toastexists today); keep it self-contained in the component.Code shape
web/src/lib/install.ts— headless logic: event capture, standalone/coarse-pointer/iOS detection, dismissed flag,appinstalledcleanup; exposed as Solid signals plus aneligible()derivation. Follows the existinglib/tested-logic convention.web/src/components/InstallSheet.tsx— scrim + card, both variants.AppShell.tsx(post-auth);Settings.tsxgets the Install app row flipping the same open-signal.Tests
Vitest unit tests for the
lib/install.tseligibility truth table (standalone / pointer / dismissed / event / iOS) with mockedmatchMedia, plus component tests for both sheet variants (jsdom, matching sibling.test.ts(x)convention). No Playwright e2e — real browsers can't fakebeforeinstallpromptordisplay-mode: standalonewithout testing a mock.Out of scope
Landing audit — PR #144 (
afk/142) → PASS (pending merge)ci / nativegreen (success, 5m10s) on the pre-merge head.feat(web): …is Conventional Commits ✓.Closes #142✓.web/. Addslib/install.ts(headless eligibility signals),components/InstallSheet.tsx(modal primitive), wiring in AppShell/Settings, plus base.css. Matches the settled #142 design; no divergence from intent.AppShell.tsximports — both branches added a sibling import at the same line. Auto-resolved as a union in alphabetical-by-path order (drawerGesture→install→liveResource); deterministic and behaviour-preserving, no semantic choice.base.cssauto-merged. The<InstallSheet>element merged cleanly into the rewritten return.tsc --noEmitclean,vitest run678/678 green,eslintclean,prettier --checkclean,vite buildclean.Resolution pushed to
afk/142. Awaiting free-text merge go-ahead.