Add the Install the app bottom sheet for mobile browsers

- New InstallSheet mounted in the root layout: appears ~1s after load on
  iOS/Android phones and tablets, over any screen including the key gate,
  with a dimmed backdrop. Never on desktop, never in standalone mode, and
  at most once per 7 days after a dismissal (localStorage).
- Platform/browser detection (lib/install.ts): iPhone/iPad incl. iPadOS
  reporting as Mac with touch, Android, Safari vs Chrome vs other vs
  in-app browsers, with step 1 adapted accordingly.
- iOS: numbered steps with chips (Safari, Share, ... , More, Add to Home
  Screen) and the accent Add pill. Android: captured beforeinstallprompt
  turns into a native Install button; fallback steps use Install app /
  Add to Home screen chips and an accent Install pill.
- Close via X, backdrop, Escape or swipe-down on the handle; the site is
  untouched afterwards. Anchored to 100dvh so browser toolbars never hide
  the last step; pb-safe respects the home indicator.
- Dark sheet by default; light only when the app theme was explicitly set
  to light. Settings gains a mobile-only Install app row that reopens it.
- The inline head script captures beforeinstallprompt before hydration and
  records appinstalled. Old InstallHint banner removed.
- Tests: new install-sheet suite (45 checks across iPhone Safari/Chrome/
  in-app, Android Chrome/Samsung, desktop, prompt path, dismissal, 7-day
  expiry, standalone, Settings row, both themes); pwa suite updated.
This commit is contained in:
Martin Dvořák 2026-10-07 08:27:43 +00:00
commit 26e36bc0c0
8 changed files with 632 additions and 79 deletions

View file

@ -285,3 +285,54 @@ export function ExternalLinkIcon({ className }: IconProps) {
</Svg>
);
}
/* --- Install-sheet chips ------------------------------------------------ */
export function CompassIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m15.6 8.4-2.3 5.3-5.3 2.3 2.3-5.3 5.3-2.3Z" />
</Svg>
);
}
export function EllipsisIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="5.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="18.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
</Svg>
);
}
export function MoreIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="3.5" y="3.5" width="17" height="17" rx="5" />
<circle cx="8.4" cy="12" r="1.2" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none" />
<circle cx="15.6" cy="12" r="1.2" fill="currentColor" stroke="none" />
</Svg>
);
}
export function AddToHomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="4" y="4" width="16" height="16" rx="4.5" />
<path d="M12 8.8v6.4M8.8 12h6.4" />
</Svg>
);
}
export function ChromeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="3.4" />
<path d="M12 8.6V3M9.05 13.7 4.2 16.5M14.95 13.7l4.85 2.8" />
</Svg>
);
}