--- phase: 26 slug: pwa-performance status: draft shadcn_initialized: true preset: new-york / neutral / cssVariables created: 2026-04-01 --- # Phase 26 — UI Design Contract > Visual and interaction contract for Phase 26: PWA & Performance. > Generated by gsd-ui-researcher, verified by gsd-ui-checker. --- ## Design System | Property | Value | |----------|-------| | Tool | shadcn (new-york style) | | Preset | new-york, neutral base color, cssVariables: true | | Component library | Radix UI (via shadcn) | | Icon library | lucide-react | | Font | System default (no custom web font — performance constraint) | Source: `ui/components.json`, `ui/vite.config.ts` --- ## Existing PWA Infrastructure (Pre-Phase) The following already exists and must NOT be re-implemented — Phase 26 extends it: | Asset | Path | Current State | |-------|------|---------------| | Service worker | `ui/public/sw.js` | Network-first, cache-as-fallback, skips `/api` | | Web manifest | `ui/public/site.webmanifest` | Standalone display, Nexus name, two PNG icon sizes | | Viewport meta | `ui/index.html` | `viewport-fit=cover`, `user-scalable=no`, `maximum-scale=1` | | Apple PWA meta | `ui/index.html` | `apple-mobile-web-app-capable`, status bar style, title | | Theme-color meta | `ui/index.html` | Dynamic per-theme via inline script | | SW registration | `ui/src/main.tsx` | Registered on `load` event | | Icons (existing) | `ui/public/` | `android-chrome-192x192.png`, `android-chrome-512x512.png`, `apple-touch-icon.png`, favicons | Phase 26 upgrades the SW to a proper cache-first strategy for static assets, adds offline message queuing, adds the install prompt UI, adds push notification wiring, and completes the responsive mobile layout. --- ## Spacing Scale Declared values (multiples of 4): | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon gaps, inline chip padding | | sm | 8px | Compact element spacing, icon button padding | | md | 16px | Default element spacing, input padding | | lg | 24px | Section padding, card padding | | xl | 32px | Layout gaps | | 2xl | 48px | Major section breaks | | 3xl | 64px | Page-level spacing | Exceptions: - Touch targets: minimum 44px × 44px on all interactive elements (mobile). Applies to: conversation list items, message action buttons, pull-to-refresh drag zone, install prompt dismiss button, notification permission button. - iOS safe area insets: input bar bottom padding must use `env(safe-area-inset-bottom)` on mobile to avoid the home indicator. Apply as `pb-[env(safe-area-inset-bottom)]` or equivalent. - Keyboard-aware resize: when the virtual keyboard appears, the sticky input bar must remain visible above it. This is handled via `100dvh` viewport height (dynamic viewport unit) rather than `100vh`. --- ## Typography | Role | Size | Weight | Line Height | |------|------|--------|-------------| | Body | 14px | 400 | 1.5 | | Label | 12px | 400 | 1.3 | | Heading | 16px | 600 | 1.2 | | Display | 20px | 600 | 1.2 | Source: Matches existing chat component patterns (ChatPanel, ChatInput, ChatConversationList use `text-sm`/`text-xs`/`text-base`/`font-semibold`). No new font sizes are introduced in Phase 26. All new UI elements (install prompt, offline banner, notification permission prompt) use the existing Body/Label scale. --- ## Color | Role | Value | Usage | |------|-------|-------| | Dominant (60%) | `var(--background)` | App shell, chat background | | Secondary (30%) | `var(--card)` / `var(--sidebar)` | Sidebar, cards, input surface, overlays | | Accent (10%) | `var(--primary)` | Install prompt CTA button, notification permission accept button | | Destructive | `var(--destructive)` | Offline banner error state, notification permission deny action | Theme-aware values: - Catppuccin Mocha (dark default): background `#1e1e2e`, card `#181825`, primary `#89b4fa` - Tokyo Night (dark): background `#1a1b26`, card `#16161e`, primary `#7aa2f7` - Catppuccin Latte (light): background `#eff1f5`, card `#e6e9ef`, primary `#1e66f5` Accent reserved for: 1. "Add to Home Screen" / "Install app" CTA button in the install prompt banner 2. "Allow notifications" accept button in the push notification permission prompt 3. Offline queue flush indicator (spinner/dot when messages are queued and reconnection is in progress) Offline banner uses a dedicated amber/warning surface: - Dark themes: `bg-amber-900/40 text-amber-200 border border-amber-800` - Light theme: `bg-amber-50 text-amber-800 border border-amber-200` (These are utility classes, not new CSS variables — keeps the token count stable.) --- ## Layout Breakpoints | Breakpoint | Viewport | Chat Layout | |------------|----------|-------------| | Mobile | < 768px (below Tailwind `md`) | Full-screen chat replaces desktop slide-in panel; sidebar becomes a bottom sheet or full-screen overlay; input bar sticky at bottom | | Tablet | 768px–1024px | Split view: sidebar at 280px + chat panel fills remainder | | Desktop | > 1024px | Existing layout: `hidden md:flex` chat panel at fixed width, sidebar pinned | The existing `hidden md:flex` class on ChatPanel means the panel is invisible on mobile. Phase 26 adds a mobile-first full-screen chat view accessible via a bottom navigation tap or a floating action button. Mobile chat layout: - Header: 48px tall, contains back button (← with `aria-label="Back to conversations"`), conversation title (truncated), agent selector icon - Message list: fills remaining viewport height using `h-[calc(100dvh-48px-56px-env(safe-area-inset-bottom))]` - Input bar: 56px minimum, sticky `bottom-0`, padded by `env(safe-area-inset-bottom)` Conversation list on mobile: - Full-screen overlay or bottom sheet (Sheet component from shadcn) - Pull-to-refresh drag zone: 60px from top, visual indicator using a spinner at `--primary` color - Each conversation list item: minimum 48px tall touch target --- ## Component Inventory (New for Phase 26) | Component | Type | Description | |-----------|------|-------------| | `InstallPromptBanner` | New | Top or bottom banner shown after `beforeinstallprompt`; contains "Add to Home Screen" CTA + dismiss | | `OfflineBanner` | New | Amber banner shown when `navigator.onLine` is false; shows queued message count | | `NotificationPermissionPrompt` | New | Modal or inline prompt requesting push notification permission; two buttons: Allow + Not now | | `PullToRefresh` | New | Wraps conversation list; detects swipe-down gesture and triggers refetch | | `MobileChatView` | New | Full-screen mobile chat layout (header + message list + sticky input) | | `MobileNavBar` | New | Bottom navigation bar for mobile: Dashboard, Chat, Inbox tabs (minimum 44px tap height) | | `OfflineMessageQueue` | Logic only | Hook `useOfflineQueue` — no visual output; stores unsent messages in IndexedDB, flushes on reconnect | Existing components modified in Phase 26: | Component | Change | |-----------|--------| | `ChatPanel` | Wrap with responsive logic; show `MobileChatView` on mobile, existing panel on desktop | | `ChatConversationList` | Wrap with `PullToRefresh` | | `ChatInput` | Add `pb-[env(safe-area-inset-bottom)]` safe area padding; ensure 44px minimum tap height on Send button | --- ## Copywriting Contract | Element | Copy | |---------|------| | Install prompt heading | "Add Nexus to your home screen" | | Install prompt body | "Get the full experience — launch instantly, works offline." | | Install prompt CTA | "Add to Home Screen" | | Install prompt dismiss | "Not now" | | Offline banner | "You're offline — messages will send when you reconnect" | | Offline banner with queue | "You're offline — {n} message{s} queued" | | Notification permission heading | "Stay in the loop" | | Notification permission body | "Get notified when your agents complete tasks or need input." | | Notification permission accept | "Allow notifications" | | Notification permission deny | "Not now" | | Pull-to-refresh loading | (spinner only, no text) | | Pull-to-refresh release | "Release to refresh" | | Pull-to-refresh pulling | "Pull to refresh" | | Empty offline state | "No conversations yet" / "Start a conversation to get going." | | Cached load label | (no user-visible label — load is silent) | | Push notification title (mention) | "Nexus — {AgentName} mentioned you" | | Push notification body (mention) | "{snippet of message, max 80 chars}" | | Push notification title (task complete) | "Nexus — Task completed" | | Push notification body (task complete) | "{AgentName} completed "{TaskTitle}"" | | Push notification title (handoff) | "Nexus — Handoff request" | | Push notification body (handoff) | "{AgentName} is ready with a spec for you." | | Destructive: revoke notifications | "Turn off notifications": "You can re-enable them any time in your browser settings." | Destructive actions in Phase 26: - None requiring a confirmation dialog. Notification permission denial is non-destructive (browser handles revoking via settings). No data deletion in this phase. --- ## Interaction States ### Install Prompt Banner - Shown: once per session, after `beforeinstallprompt` fires AND user has visited at least one conversation (not shown on first-ever load before any engagement) - Dismissed: stored in `localStorage` key `nexus.installPromptDismissed`; not shown again for 7 days - Already installed: banner never shown (detect via `window.matchMedia('(display-mode: standalone)')`) - Position: fixed bottom, above the input bar on mobile; fixed top on desktop ### Offline Banner - Shown: when `navigator.onLine === false` - Hidden: auto-dismisses 3 seconds after `navigator.onLine` returns true and the queue is empty - Persistent while offline: does not auto-dismiss while still offline ### Notification Permission Prompt - Shown: after user's third agent response in any conversation (engagement gate) - Never shown if `Notification.permission === 'granted'` or `=== 'denied'` - Stored dismissed state: `localStorage` key `nexus.notifPromptDismissed` ### Pull-to-Refresh - Trigger threshold: 64px downward drag from top of conversation list - Visual: spinner using `var(--primary)` color, 24px, appears at the pull origin point - Max overdrag: 96px before triggering (prevents accidental trigger) - Haptic feedback: `navigator.vibrate(10)` on trigger if available ### Mobile Chat Navigation - Bottom nav is always visible on mobile (< 768px) when the app is installed (standalone) or on a touch device - Active tab uses accent color (`var(--primary)`) for icon tint - Inactive tabs use `var(--muted-foreground)` for icon tint --- ## Performance Targets (Visual-Side Contracts) | Metric | Target | Implementation Contract | |--------|--------|------------------------| | Initial load (broadband) | < 2s | Vite code-split per route; no blocking scripts; no web fonts | | Initial load (3G) | < 5s | Vite code-split; compress assets; inline critical CSS | | PWA cached load | < 1s | SW cache-first for all static assets (JS/CSS/HTML); stale-while-revalidate for fonts/images | | Shell paint | < 200ms | App shell (HTML/CSS) cached by SW; no API calls blocking shell render | | Offline UI load | < 1s | All static assets served from SW cache; API failures handled gracefully | SW cache strategy: - Cache name: `nexus-v1` (rename from existing `paperclip-v2` to bust stale cache) - Static assets (JS, CSS, fonts, images): cache-first with network fallback - Navigation requests (`/`): cache-first (app shell) - API requests (`/api/*`): network-only; failures trigger offline queue for POST/mutations --- ## Registry Safety | Registry | Blocks Used | Safety Gate | |----------|-------------|-------------| | shadcn official | button, sheet, dialog, badge, skeleton, tooltip | not required | | none (third-party) | — | not applicable | No third-party registries are declared for Phase 26. --- ## Checker Sign-Off - [ ] Dimension 1 Copywriting: PASS - [ ] Dimension 2 Visuals: PASS - [ ] Dimension 3 Color: PASS - [ ] Dimension 4 Typography: PASS - [ ] Dimension 5 Spacing: PASS - [ ] Dimension 6 Registry Safety: PASS **Approval:** pending