nexus/.planning/phases/26-pwa-performance/26-02-PLAN.md

13 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
26-pwa-performance 02 execute 2
26-00
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
true
PWA-03
PWA-04
PWA-05
truths artifacts key_links
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
path provides min_lines
ui/src/components/MobileChatView.tsx Full-screen mobile chat layout 40
path provides min_lines
ui/src/components/MobileNavBar.tsx Bottom navigation bar for mobile 30
path provides min_lines
ui/src/components/PullToRefresh.tsx Touch gesture wrapper for conversation list refresh 40
path provides min_lines
ui/src/hooks/usePullToRefresh.ts Touch gesture logic (touchstart/touchmove/touchend) 30
path provides min_lines
ui/src/hooks/useMediaQuery.ts Responsive breakpoint hook 10
from to via pattern
ui/src/components/ChatPanel.tsx ui/src/components/MobileChatView.tsx Conditional render based on useMediaQuery('(min-width: 768px)') MobileChatView
from to via pattern
ui/src/components/ChatConversationList.tsx ui/src/components/PullToRefresh.tsx PullToRefresh wrapper around conversation list on mobile 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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):

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
  1. 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
  2. Create ui/src/components/PullToRefresh.tsx:

    • Props: { children: ReactNode; onRefresh: () => Promise<void> | 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)
  3. 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" <acceptance_criteria>
    • 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 </acceptance_criteria> 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: `