From 4dfd2f6e8d6a43aa1a431c884601a9302d460ecc Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Thu, 2 Apr 2026 01:25:22 +0000 Subject: [PATCH] docs(26): UI design contract for PWA & Performance phase Co-Authored-By: Claude Sonnet 4.6 --- .../phases/26-pwa-performance/26-UI-SPEC.md | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 .planning/phases/26-pwa-performance/26-UI-SPEC.md diff --git a/.planning/phases/26-pwa-performance/26-UI-SPEC.md b/.planning/phases/26-pwa-performance/26-UI-SPEC.md new file mode 100644 index 00000000..de8da5d1 --- /dev/null +++ b/.planning/phases/26-pwa-performance/26-UI-SPEC.md @@ -0,0 +1,260 @@ +--- +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 | 500 | 1.3 | +| Heading | 16px | 600 | 1.2 | +| Display | 20px | 700 | 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 (←), 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