12 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 26 | pwa-performance | draft | true | new-york / neutral / cssVariables | 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 aspb-[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
100dvhviewport height (dynamic viewport unit) rather than100vh.
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:
- "Add to Home Screen" / "Install app" CTA button in the install prompt banner
- "Allow notifications" accept button in the push notification permission prompt
- 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 byenv(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
--primarycolor - 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
beforeinstallpromptfires AND user has visited at least one conversation (not shown on first-ever load before any engagement) - Dismissed: stored in
localStoragekeynexus.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.onLinereturns 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:
localStoragekeynexus.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 existingpaperclip-v2to 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