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:
parent
969f7a82a0
commit
26e36bc0c0
8 changed files with 632 additions and 79 deletions
31
README.md
31
README.md
|
|
@ -395,13 +395,18 @@ Decks are organised into folders — the same way you'd file paper notes.
|
|||
|
||||
Freezmo ships as an installable Progressive Web App with iOS in mind.
|
||||
|
||||
**iOS / iPadOS (Safari):** open the app → **Share** → **Add to Home Screen**. The app shows a
|
||||
one-time hint explaining exactly this. It launches full-screen in standalone mode (no browser
|
||||
chrome), respects the notch/home-indicator safe areas, and has an iOS-style bottom tab bar
|
||||
(Library · New deck · Settings) with sheet-style dialogs.
|
||||
Phones and tablets get an **"Install the app" bottom sheet** about a second after load (never on
|
||||
desktop, never when already installed, and at most once per 7 days after a dismissal). It is a
|
||||
native-feeling sheet with the Freezmo card, numbered steps and chips for the exact buttons to tap,
|
||||
plus an accent confirmation pill. Close it with the X, the backdrop, Escape, or a swipe down — the
|
||||
site keeps working normally. On iOS the steps adapt to the browser (Safari, Chrome, in-app), and
|
||||
on Android Chrome a captured `beforeinstallprompt` turns into a real **Install** button. The sheet
|
||||
can be reopened any time from **Settings → Install app** (mobile only).
|
||||
|
||||
**Android (Chrome):** Chrome offers "Install app" from the ⋮ menu; Freezmo also works offline for
|
||||
the parts that don't need a model.
|
||||
It launches full-screen in standalone mode (no browser chrome), respects the notch/home-indicator
|
||||
safe areas, and has an iOS-style bottom tab bar (Library · New deck · Settings) with sheet-style
|
||||
dialogs. Android Chrome also offers "Install app" from the ⋮ menu; Freezmo works offline for the
|
||||
parts that don't need a model.
|
||||
|
||||
What's included:
|
||||
|
||||
|
|
@ -430,8 +435,9 @@ self-hosted study app with no accounts, no database and no server-side state.
|
|||
production `.next` output for a test key, and by restarting the server (which remembers nothing).
|
||||
- **No server-side injection surface.** The Ollama endpoint and model names come only from the
|
||||
server's `.env`; the client cannot point the server at another host. Model output is validated with
|
||||
zod and rendered as text — there is no `dangerouslySetInnerHTML` except one static theme script,
|
||||
so a deck cannot inject markup or script (React escapes everything).
|
||||
zod and rendered as text — there is no `dangerouslySetInnerHTML` except two static inline scripts
|
||||
(the pre-paint theme choice and the `beforeinstallprompt` capture), so a deck cannot inject markup
|
||||
or script (React escapes everything).
|
||||
- **Key-header sanitisation.** Keys from the client are stripped of control characters/whitespace
|
||||
and capped at 512 chars before they are used in an outbound header, so a crafted key cannot inject
|
||||
headers. Verified with a raw-socket CRLF attempt.
|
||||
|
|
@ -540,7 +546,7 @@ components/
|
|||
Flashcard.tsx MultipleChoice.tsx TrueFalse.tsx FillBlank.tsx
|
||||
Matching.tsx Ordering.tsx ShortAnswer.tsx shared.tsx index.tsx
|
||||
UploadScreen.tsx StudyFeed.tsx ResultsScreen.tsx DeckCard.tsx ApiKeyGate.tsx
|
||||
TabBar.tsx InstallHint.tsx ServiceWorkerRegistrar.tsx FolderBar.tsx
|
||||
InstallSheet.tsx TabBar.tsx ServiceWorkerRegistrar.tsx FolderBar.tsx
|
||||
LibraryScreen.tsx SettingsDialog.tsx AppHeader.tsx ThemeToggle.tsx icons.tsx ui.tsx
|
||||
lib/
|
||||
ollama.ts server-only HTTP client (timeouts, auth, error mapping)
|
||||
|
|
@ -550,6 +556,7 @@ lib/
|
|||
config.ts env config, cloud/direct model-name normalisation
|
||||
documents.ts client-side PDF/DOCX/PPTX/ODT/text extraction
|
||||
folders.ts folder library (create/rename/delete) in localStorage
|
||||
install.ts install-prompt platform/browser detection + 7-day dismissal
|
||||
settings.ts browser-side settings (the user's API key) + auth headers
|
||||
use-health.ts client hook for /api/health, re-runs when the key changes
|
||||
rate-limit.ts in-memory sliding-window limiter for the API routes
|
||||
|
|
@ -760,6 +767,12 @@ Things I chose while building, with the reasoning:
|
|||
`/api/health`, so an invalid key and an unreachable Ollama are worded differently. The old
|
||||
dashboard banner is gone; key management stays in Settings, and removing the key brings the
|
||||
gate back. The gate covers `/`, `/create` and deck study, but never `/offline`.
|
||||
29. **The install prompt is a teaching sheet, not a nag.** It appears once, phones/tablets only,
|
||||
a second after load, and never in standalone mode; dismissing it silences it for 7 days. It
|
||||
detects iOS vs Android and Safari vs Chrome vs an in-app browser so step 1 always matches the
|
||||
device, uses Chrome's deferred `beforeinstallprompt` for a one-tap install where available, and
|
||||
stays reachable from Settings. It is mounted in the root layout, so it can appear over any
|
||||
screen — including the key gate — without blocking the page underneath.
|
||||
14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not
|
||||
support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`).
|
||||
Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue