--- phase: 26-pwa-performance plan: 02 type: execute wave: 2 depends_on: - 26-00 files_modified: - ui/src/components/MobileChatView.tsx - ui/src/components/MobileNavBar.tsx - ui/src/components/PullToRefresh.tsx - ui/src/components/ChatPanel.tsx - ui/src/components/ChatConversationList.tsx - ui/src/components/ChatInput.tsx - ui/src/hooks/usePullToRefresh.ts - ui/src/hooks/useMediaQuery.ts autonomous: true requirements: - PWA-03 - PWA-04 - PWA-05 must_haves: truths: - "On screens < 768px, chat renders as a full-screen view, not a slide-in panel" - "Mobile chat has a 48px header with back button, a sticky input bar at the bottom, and safe area padding" - "Bottom navigation bar on mobile shows Dashboard, Chat, and Inbox tabs with 44px min height" - "Pulling down on the conversation list on mobile triggers a refresh after 64px threshold" - "Chat input has minimum 44px touch targets on mobile and env(safe-area-inset-bottom) padding" artifacts: - path: "ui/src/components/MobileChatView.tsx" provides: "Full-screen mobile chat layout" min_lines: 40 - path: "ui/src/components/MobileNavBar.tsx" provides: "Bottom navigation bar for mobile" min_lines: 30 - path: "ui/src/components/PullToRefresh.tsx" provides: "Touch gesture wrapper for conversation list refresh" min_lines: 40 - path: "ui/src/hooks/usePullToRefresh.ts" provides: "Touch gesture logic (touchstart/touchmove/touchend)" min_lines: 30 - path: "ui/src/hooks/useMediaQuery.ts" provides: "Responsive breakpoint hook" min_lines: 10 key_links: - from: "ui/src/components/ChatPanel.tsx" to: "ui/src/components/MobileChatView.tsx" via: "Conditional render based on useMediaQuery('(min-width: 768px)')" pattern: "MobileChatView" - from: "ui/src/components/ChatConversationList.tsx" to: "ui/src/components/PullToRefresh.tsx" via: "PullToRefresh wrapper around conversation list on mobile" pattern: "PullToRefresh" --- Create the responsive mobile layout: MobileChatView (full-screen chat on phones), MobileNavBar (bottom tabs), PullToRefresh (conversation list gesture), and update ChatPanel/ChatInput for mobile-safe rendering. Purpose: Makes Nexus usable on phones and tablets with proper touch targets, safe area insets, and keyboard-aware layout (PWA-03, PWA-04, PWA-05). Output: 3 new components, 2 new hooks, 3 updated components. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 @ui/src/components/ChatPanel.tsx @ui/src/components/ChatConversationList.tsx @ui/src/components/ChatInput.tsx @ui/src/components/SwipeToArchive.tsx From ui/src/components/ChatPanel.tsx: ```typescript export function ChatPanel() // Uses: useChatPanel() for { activeConversationId, setActiveConversationId, ... } // Uses: useChatMessages(), useStreamingChat(), useBrainstormerDefault(), useChatBookmarks() // Uses: useChatFileUpload() // Renders: ChatConversationList, ChatMessageList, ChatInput, ChatAgentSelector, etc. // Layout class: "hidden md:flex overflow-hidden transition-[width] duration-100 ease-out flex-shrink-0 border-l border-border flex-col bg-background" ``` From ui/src/components/SwipeToArchive.tsx (touch gesture reference): ```typescript interface SwipeToArchiveProps { // Uses useRef for startX, useState for offset, native touch events } ``` Task 1: Create useMediaQuery hook, MobileNavBar, and PullToRefresh components ui/src/hooks/useMediaQuery.ts, ui/src/hooks/usePullToRefresh.ts, ui/src/components/MobileNavBar.tsx, ui/src/components/PullToRefresh.tsx - ui/src/components/SwipeToArchive.tsx - ui/src/components/SwipeToArchive.test.tsx - .planning/phases/26-pwa-performance/26-UI-SPEC.md - .planning/phases/26-pwa-performance/26-RESEARCH.md 1. Create `ui/src/hooks/useMediaQuery.ts`: - Export `function useMediaQuery(query: string): boolean` - Use `window.matchMedia(query)` with `addEventListener("change", ...)` for live updates - Return `matches` state - SSR-safe: default to `false` if `window` is undefined 2. Create `ui/src/hooks/usePullToRefresh.ts`: - Export `function usePullToRefresh({ onRefresh, threshold = 64, maxPull = 96, enabled = true }: { onRefresh: () => void; threshold?: number; maxPull?: number; enabled?: boolean })` - Use `useRef` for `startYRef` (touch start Y coordinate) and `containerRef` (scroll container) - Use `useState` for `pullDistance` (current pull offset) and `isRefreshing` (loading state) - Touch handlers (mirror SwipeToArchive.tsx convention — use native DOM events, not React synthetic): a. `handleTouchStart`: only capture if `containerRef.current?.scrollTop === 0`; store `e.touches[0].clientY` in `startYRef` b. `handleTouchMove`: calculate `dy = e.touches[0].clientY - startYRef.current`; if `dy > 0`, set `pullDistance` to `Math.min(dy, maxPull)` c. `handleTouchEnd`: if `pullDistance >= threshold`, call `navigator.vibrate?.(10)` for haptic feedback, set `isRefreshing = true`, call `onRefresh()`, then reset; else reset `pullDistance` to 0 - Return `{ containerRef, pullDistance, isRefreshing, setIsRefreshing }` - `useEffect` to attach/detach touch listeners on `containerRef.current` 3. Create `ui/src/components/PullToRefresh.tsx`: - Props: `{ children: ReactNode; onRefresh: () => Promise | void; enabled?: boolean }` - Use `usePullToRefresh` hook internally - Render a wrapper `div` with `ref={containerRef}` that contains: a. A pull indicator div at the top: spinner (using a `Loader2` icon from lucide-react with `animate-spin`) that appears when `pullDistance > 0`, opacity scales with `pullDistance / threshold` b. Text: show "Pull to refresh" when pulling, "Release to refresh" when `pullDistance >= 64`, spinner-only when `isRefreshing` c. Children rendered below the indicator - Spinner color: `text-primary` (uses `var(--primary)` per UI-SPEC) - Spinner size: 24px (`w-6 h-6`) 4. Create `ui/src/components/MobileNavBar.tsx`: - Props: `{ activeTab: "dashboard" | "chat" | "inbox" }` - Render a `nav` element with classes: `fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t border-border bg-background pb-[env(safe-area-inset-bottom)]` - Minimum height: `min-h-[44px]` (44px touch target per UI-SPEC) - Three tab buttons, each with: a. Icon from lucide-react: `LayoutDashboard` (Dashboard), `MessageSquare` (Chat), `Inbox` (Inbox) b. Label text below icon: "Dashboard", "Chat", "Inbox" in `text-xs` c. Active state: `text-primary` for icon and label (uses `var(--primary)`) d. Inactive state: `text-muted-foreground` e. Each button: `min-h-[44px] min-w-[44px]` touch target, `flex flex-col items-center justify-center gap-0.5` - Use `@/lib/router` Link component for navigation (Dashboard -> `/{prefix}/dashboard`, Inbox -> `/{prefix}/inbox/mine`) - Chat tab calls a callback prop `onChatTap` instead of navigating (opens chat view in-place) test -f ui/src/hooks/useMediaQuery.ts && test -f ui/src/hooks/usePullToRefresh.ts && test -f ui/src/components/PullToRefresh.tsx && test -f ui/src/components/MobileNavBar.tsx && echo "PASS" - All 4 files exist - `grep "useMediaQuery" ui/src/hooks/useMediaQuery.ts` shows the hook export - `grep "threshold" ui/src/hooks/usePullToRefresh.ts` shows 64px default - `grep "min-h-\[44px\]" ui/src/components/MobileNavBar.tsx` shows touch target - `grep "text-primary" ui/src/components/MobileNavBar.tsx` shows active tab color - `grep "safe-area-inset-bottom" ui/src/components/MobileNavBar.tsx` shows safe area padding - `grep "navigator.vibrate" ui/src/hooks/usePullToRefresh.ts` shows haptic feedback - `pnpm --filter @paperclipai/ui build` succeeds useMediaQuery hook, usePullToRefresh hook, PullToRefresh component, and MobileNavBar component created with proper touch targets, safe area insets, and haptic feedback. Task 2: Create MobileChatView and wire ChatPanel for responsive layout ui/src/components/MobileChatView.tsx, ui/src/components/ChatPanel.tsx, ui/src/components/ChatConversationList.tsx, ui/src/components/ChatInput.tsx - ui/src/components/ChatPanel.tsx - ui/src/components/ChatConversationList.tsx - ui/src/components/ChatInput.tsx - ui/src/components/MobileNavBar.tsx - ui/src/hooks/useMediaQuery.ts - .planning/phases/26-pwa-performance/26-UI-SPEC.md 1. Create `ui/src/components/MobileChatView.tsx`: - Full-screen mobile chat layout component - Props: same data props that ChatPanel passes to its children (activeConversationId, messages, streaming state, etc.) — extract what's needed by reading ChatPanel.tsx - Layout structure: a. Outer container: `fixed inset-0 z-40 flex flex-col bg-background` b. Header: `h-12 flex items-center px-3 border-b border-border gap-2` containing: - Back button: `