docs(26): UI design contract for PWA & Performance phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd217cfa48
commit
4dfd2f6e8d
1 changed files with 260 additions and 0 deletions
260
.planning/phases/26-pwa-performance/26-UI-SPEC.md
Normal file
260
.planning/phases/26-pwa-performance/26-UI-SPEC.md
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue