Install prompt banner appears after beforeinstallprompt fires and user has visited a conversation
Install prompt is dismissable and respects 7-day localStorage cooldown
On iOS (no beforeinstallprompt), banner shows Share menu instructions
Offline banner appears when navigator.onLine is false, showing queued message count
Offline banner auto-dismisses 3 seconds after reconnection when queue is empty
Unsent messages are stored in IndexedDB and flushed when online event fires
path
provides
min_lines
ui/src/components/InstallPromptBanner.tsx
PWA install prompt UI
40
path
provides
min_lines
ui/src/components/OfflineBanner.tsx
Offline status banner with queue count
20
path
provides
min_lines
ui/src/hooks/useInstallPrompt.ts
Captures beforeinstallprompt event
20
path
provides
min_lines
ui/src/hooks/useOfflineQueue.ts
IndexedDB message queue with flush on reconnect
40
path
provides
min_lines
ui/src/hooks/useOnlineStatus.ts
navigator.onLine reactive state
10
from
to
via
pattern
ui/src/hooks/useOfflineQueue.ts
idb
openDB for IndexedDB access
openDB
from
to
via
pattern
ui/src/components/InstallPromptBanner.tsx
ui/src/hooks/useInstallPrompt.ts
canInstall + promptInstall from hook
useInstallPrompt
from
to
via
pattern
ui/src/components/OfflineBanner.tsx
ui/src/hooks/useOnlineStatus.ts
isOnline state for show/hide
useOnlineStatus
Create the PWA install prompt banner, offline status banner, and offline message queue. Users see an install prompt after engagement, an amber banner when offline with queued message count, and messages auto-send when back online.
Purpose: Delivers PWA-01 (offline capability with message queuing), PWA-02 (installable manifest — already done, this plan adds the install UI), and PWA-08 (Add to Home Screen prompt).
Output: 2 new components, 3 new hooks, ChatPanel integration for offline queue.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/26-pwa-performance/26-RESEARCH.md
@.planning/phases/26-pwa-performance/26-UI-SPEC.md
@.planning/phases/26-pwa-performance/26-02-SUMMARY.md
@ui/src/components/ChatPanel.tsx
@ui/src/api/chat.ts
From ui/src/api/chat.ts (chatApi used by offline queue):
```typescript
// chatApi.postMessage or equivalent POST method for sending messages
// The offline queue needs to know the POST shape to replay messages
```
flush callback: opens db, gets all entries and keys, iterates sequentially:
a. For each entry, call chatApi.sendMessage(entry.conversationId, { content: entry.content }) (read chatApi to find the correct method name)
b. On success, delete the key from the store, decrement queuedCount
c. On failure, break — stop flushing, retry next time
useEffect listening for online event on window — calls flush()
useEffect on mount — reads current queue count from IndexedDB and sets queuedCount
useOnlineStatus.ts returns boolean based on navigator.onLine
useInstallPrompt.ts captures beforeinstallprompt event and returns canInstall, promptInstall, isIOS
useOfflineQueue.ts uses idb library's openDB, stores to nexus-offline DB, message_queue store
useOfflineQueue.ts flushes on online event, stops on first failure
pnpm --filter @paperclipai/ui build succeeds
</acceptance_criteria>
Three hooks created: useOnlineStatus (reactive online/offline state), useInstallPrompt (beforeinstallprompt capture with iOS detection), useOfflineQueue (IndexedDB queue with auto-flush).
Task 2: Create InstallPromptBanner, OfflineBanner, and wire into ChatPanel
ui/src/components/InstallPromptBanner.tsx, ui/src/components/OfflineBanner.tsx, ui/src/components/ChatPanel.tsx
- ui/src/hooks/useInstallPrompt.ts
- ui/src/hooks/useOnlineStatus.ts
- ui/src/hooks/useOfflineQueue.ts
- ui/src/components/ChatPanel.tsx
- .planning/phases/26-pwa-performance/26-UI-SPEC.md
1. Create `ui/src/components/InstallPromptBanner.tsx`:
- Import `useInstallPrompt` hook
- Props: none (self-contained)
- Internal state: `dismissed` from `localStorage.getItem("nexus.installPromptDismissed")`
- Show conditions (ALL must be true):
a. `canInstall === true` OR `isIOS === true` (iOS gets instruction text)
b. Not already installed (`display-mode: standalone` check is in the hook)
c. Not dismissed within last 7 days (check `nexus.installPromptDismissed` timestamp in localStorage)
d. User has visited at least one conversation (pass `hasEngaged` as prop or check localStorage)
- Layout (per UI-SPEC):
a. Fixed position: `fixed bottom-16 left-4 right-4 md:bottom-auto md:top-4 md:left-auto md:right-4 md:max-w-sm` (bottom on mobile above MobileBottomNav, top-right on desktop)
b. Background: `bg-card border border-border rounded-lg shadow-lg p-4`
c. Heading: "Add Nexus to your home screen" in `text-sm font-semibold`
d. Body: "Get the full experience — launch instantly, works offline." in `text-xs text-muted-foreground`
e. For iOS: body text changes to "Open the Share menu and tap 'Add to Home Screen'"
f. CTA button: `` — calls `promptInstall()` (or no-op on iOS)
g. Dismiss button: "Not now" as `