From 209f77ed7d561ceb646b89303619ff598ae61bc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:16:57 +0000 Subject: [PATCH 1/6] Add comprehensive Spotify-inspired UI redesign plan Detailed 7-phase plan covering: bottom tab bar, compact climb list mode with swipe gestures, full-screen play drawer, header simplification, party mode + LED drawer integration, queue polish, and desktop adaptation. https://claude.ai/code/session_01Lqvtq2H16V8yJsQE3jBrFy --- docs/spotify-ui-redesign-plan.md | 692 +++++++++++++++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 docs/spotify-ui-redesign-plan.md diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md new file mode 100644 index 00000000..bcb73118 --- /dev/null +++ b/docs/spotify-ui-redesign-plan.md @@ -0,0 +1,692 @@ +# Spotify-Inspired UI Redesign Plan + +## Overview + +This plan transforms Boardsesh's UI into a Spotify-like experience with a persistent bottom navigation bar, a "now playing" bar that expands into a full-screen view, compact list items with swipe gestures, and drawer-based flows for party mode, creation, and search. The goal is a cohesive, polished mobile-first experience that feels fluid and modern. + +--- + +## Current Architecture Reference + +| Component | File | Role | +|---|---|---| +| Main Layout | `packages/web/app/[board_name]/.../[angle]/layout.tsx` | Header + Content + Affixed QueueControlBar | +| List Layout | `.../[angle]/list/layout-client.tsx` | Main content + desktop sidebar (Queue/Search/Holds tabs) | +| Header | `packages/web/app/components/board-page/header.tsx` | Logo, search, angle, create, party, LED, user menu | +| QueueControlBar | `packages/web/app/components/queue-control/queue-control-bar.tsx` | Now-playing bar with swipe, queue drawer, play button, tick | +| ClimbCard | `packages/web/app/components/climb-card/climb-card.tsx` | Full card view with cover image, title, action footer | +| ClimbTitle | `packages/web/app/components/climb-card/climb-title.tsx` | Name, grade (colorized), quality, setter info | +| QueueListItem | `packages/web/app/components/queue-control/queue-list-item.tsx` | Compact row with thumbnail, swipe to tick/delete | +| PlayView | `.../play/[climb_uuid]/play-view-client.tsx` | Full board renderer with swipe navigation | +| ShareBoardButton | `packages/web/app/components/board-page/share-button.tsx` | Party mode drawer (top placement) | +| SendClimbToBoardButton | `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx` | Bluetooth LED connection | +| SearchButton/Drawer | `packages/web/app/components/search-drawer/` | Right-side drawer with filters | +| ClimbActions | `packages/web/app/components/climb-actions/` | Favorite, queue, tick, share, playlist, open-in-app, mirror | +| BoardRenderer | `packages/web/app/components/board-renderer/board-renderer.tsx` | SVG board visualization | + +--- + +## Design Principles + +1. **Bottom-up navigation** - All primary navigation lives at the bottom of the screen within thumb reach +2. **Progressive disclosure** - Simple surface, rich detail on demand (drawers, menus) +3. **Gestural interaction** - Swipe actions for common operations (heart, queue, tick, delete) +4. **Smooth transitions** - Full-screen drawer transitions for play view, not page navigations +5. **Consistent density** - Two list modes (compact/expanded) instead of full cards vs queue items +6. **Minimal chrome** - Reduce header clutter, move actions closer to content + +--- + +## Wireframes (ASCII) + +### Mobile Layout - List View (Compact Mode) +``` +┌────────────────────────────────────┐ +│ [Logo] [Search Input] [≡ Menu] │ ← Simplified header +├────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ [Thumb] V4 "Problem Name" ⋮│ │ ← Compact climb row +│ │ ★★★ By setter ⋮│ │ Swipe L=♡, R=+Queue +│ └──────────────────────────────┘ │ +│ ┌──────────────────────────────┐ │ +│ │ [Thumb] V7 "Another Climb" ⋮│ │ +│ │ ★★☆ By setter2 ⋮│ │ +│ └──────────────────────────────┘ │ +│ ... │ +│ │ +├────────────────────────────────────┤ +│ [Thumb] "Current Climb" [Q] [✓] │ ← Now Playing bar (tap=expand) +├────────────────────────────────────┤ +│ 🧗 Climbs 🔍 Search ✚ New │ ← Bottom tab bar +└────────────────────────────────────┘ +``` + +### Mobile Layout - List View (Expanded/Card Mode) +``` +┌────────────────────────────────────┐ +│ [Logo] [Search Input] [≡ Menu] │ +├────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ "Problem Name" V4 │ │ ← Standard climb card +│ │ ★★★ By setter │ │ (existing ClimbCard) +│ │ ┌──────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ [Board Renderer] │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────┘ │ │ +│ │ ♡ +Queue 🔗Share ... │ │ +│ └──────────────────────────────┘ │ +│ │ +├────────────────────────────────────┤ +│ [Thumb] "Current Climb" [Q] [✓] │ +├────────────────────────────────────┤ +│ 🧗 Climbs 🔍 Search ✚ New │ +└────────────────────────────────────┘ +``` + +### Now Playing Expanded (Full-screen Drawer) +``` +┌────────────────────────────────────┐ +│ ⌄ (drag handle) │ ← Drag down to dismiss +├────────────────────────────────────┤ +│ │ +│ "Problem Name" V4 │ ← ClimbTitle horizontal +│ ★★★ By setter @ 40° │ +│ │ +│ ┌──────────────────────────────┐ │ +│ │ │ │ +│ │ │ │ +│ │ [Board Renderer] │ │ +│ │ (fills height) │ │ +│ │ │ │ +│ │ │ │ +│ └──────────────────────────────┘ │ +│ │ +│ ← swipe indicators → │ +│ │ +├────────────────────────────────────┤ +│ [Mirror] [♡] [Party] [LED] [✓] │ ← Action bar +├────────────────────────────────────┤ +│ ◀ Prev │ ▮▮ Current │ Next ▶ │ ← Mini transport controls +└────────────────────────────────────┘ +``` + +### Desktop Layout +``` +┌─────────────────────────────────────────────────────────────┐ +│ [Logo] [Search Input] [Angle ▾] [Create] [≡ User Menu] │ +├──────────────────────────────────┬──────────────────────────┤ +│ │ │ +│ Climb list (2-col grid) │ Sidebar (400px) │ +│ - Toggle: Compact / Card │ Tabs: Queue | Search │ +│ │ │ +│ [Card] [Card] │ [Queue items...] │ +│ [Card] [Card] │ │ +│ ... │ │ +│ │ │ +├──────────────────────────────────┴──────────────────────────┤ +│ [Thumb] "Current Climb" V4 [🔄Mirror] [Q] [Party] [✓] │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Phase 1: Bottom Tab Bar + +### What changes +Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Climbs**, **Search**, and **Create**. + +### Files to modify + +1. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.tsx`** + - Three tabs: Climbs (default active), Search, Create + - Icons: Use AntD icons - `UnorderedListOutlined` / `AppstoreOutlined` for Climbs, `SearchOutlined` for Search, `PlusOutlined` for Create + - Active state: Primary color for active tab icon + label + - Inactive state: `neutral[400]` color + - Fixed at the bottom, full width + - Height: ~50px with safe-area-inset-bottom padding for iOS + - Desktop: Hidden (search/queue available in sidebar, create in header) + +2. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`** + - Media query to hide on desktop (>= 768px) + - Safe area inset for iOS home indicator + +3. **Modify: `packages/web/app/[board_name]/.../[angle]/layout.tsx`** + - Add BottomTabBar below the Affix'd QueueControlBar + - Wrap both in a shared bottom container + - Adjust Content height to account for tab bar height + +4. **Modify: `packages/web/app/components/search-drawer/search-drawer.tsx`** + - Search tab triggers the existing search drawer (right placement, 90% width) + - No structural change needed - just wire the tab to open it + +5. **New file: `packages/web/app/components/create-drawer/create-drawer.tsx`** + - Bottom drawer with creation options + - Two items initially: + - "Create Climb" - links to `/create` route + - "Create Playlist" - links to `/playlists` with create action + - Each item: Icon + label, full-width rows + - Simple AntD Drawer with bottom placement, auto height + +### Behavior +- **Climbs tab**: Navigates to the climb list view (if not already there). This is the default/home state. +- **Search tab**: Opens the existing SearchDrawer from the bottom tab rather than from the header. On desktop, activates the sidebar search tab instead. +- **Create tab**: Opens the CreateDrawer with options. +- Active tab state reflects current context (Climbs when on list, etc.) +- On desktop (>= 768px): Tab bar is hidden. Search and create remain in header/sidebar. + +### Integration with header +- Remove the mobile SearchButton from the header center section (it moves to bottom tab bar) +- Keep the desktop SearchButton in the sidebar +- Remove "Create Climb" from the mobile meatball menu (it moves to bottom tab bar) + +--- + +## Phase 2: Compact Climb List Mode + +### What changes +Add a "compact" display mode for the climb list that renders climbs as slim rows (similar to QueueListItem) instead of full cards. + +### Files to modify/create + +1. **New file: `packages/web/app/components/climb-card/climb-list-item.tsx`** + - Compact list item component, similar structure to QueueListItem + - Layout: + ``` + ┌─────────────────────────────────────────────────────┐ + │ [Thumbnail] Name + Setter [V-Grade] [⋮] │ + │ 48×auto ★★★ (colorized) (menu) │ + └─────────────────────────────────────────────────────┘ + ``` + - Left side: `ClimbThumbnail` (48px width, maintains aspect ratio) + - Center: Climb name (single line, ellipsis overflow), quality stars, setter name (secondary text, small) + - Right side: Large colorized V-grade text (use `getVGradeColor()`), ellipsis button + - Total height: ~60-64px per item + - No horizontal padding waste - edge-to-edge content + - The V-grade should be visually prominent with the grade color as the text color, similar to how ClimbTitle renders it in horizontal mode but larger/bolder + +2. **Add swipe actions to `climb-list-item.tsx`** + - Reuse the swipe pattern from `queue-list-item.tsx` (same SWIPE_THRESHOLD=100, MAX_SWIPE=120) + - **Swipe right (reveals left)**: Heart/favorite action + - Background: `colors.error` (red, matching heart color) or a warm pink + - Icon: `HeartOutlined` / `HeartFilled` + - Action: Toggle favorite on the climb via existing favorite API + - **Swipe left (reveals right)**: Add to queue action + - Background: `colors.primary` (cyan) + - Icon: `PlusOutlined` or `OrderedListOutlined` + - Action: Add climb to queue via `addToQueue()` from QueueContext + +3. **Ellipsis menu drawer: `packages/web/app/components/climb-card/climb-actions-drawer.tsx`** + - Bottom drawer triggered by the `⋮` (EllipsisOutlined) button + - Header: ClimbTitle (stacked layout) with thumbnail + - Body: Full list of actions as large tap targets (rows, not icons): + - ♡ Favorite / Unfavorite + - + Add to Queue + - ✓ Log Ascent + - 🔗 Share + - 📋 Add to Playlist + - 📱 Open in Aurora App + - 🔄 Mirror (if board supports mirroring) + - ℹ️ View Details (navigate to /view/ page) + - Each row: Icon (24px) + Label text, full width, ~48px height + - Reuse action handlers from existing `ClimbActions` component + +4. **Modify: `packages/web/app/components/board-page/climbs-list.tsx`** + - Add a view mode toggle: "Grid" (current cards) vs "List" (compact) + - Store preference in localStorage (key: `climbListViewMode`) + - Toggle button in a sticky header area above the list + - Icons: `AppstoreOutlined` for grid, `UnorderedListOutlined` for list + - When in list mode: Render `ClimbListItem` instead of `ClimbCard` + - When in list mode: Single column layout (no grid), full width items + - When in grid mode: Keep existing 2-column card grid + - Default to compact/list mode on mobile, grid mode on desktop + +5. **Modify: `packages/web/app/components/climb-card/climb-thumbnail.tsx`** + - No changes needed - already supports the 48px fixed-width pattern + +### Performance considerations +- ClimbListItem should be `React.memo`'d with custom comparator (compare by climb.uuid) +- Swipe state is local to each item (no parent re-renders) +- Virtual scrolling is not needed yet (existing infinite scroll pagination handles this) + +--- + +## Phase 3: Now Playing Bar Redesign + Full-screen Play Drawer + +### What changes +- Redesign QueueControlBar to be simpler with only essential controls +- Tapping the bar opens a full-screen drawer (the "play view") instead of navigating to `/play/[uuid]` +- Remove the `ExpandOutlined` (play mode) button; the bar itself is the entry point +- Add a queue list button and keep the tick button +- Move party mode button to the bar + +### 3A: QueueControlBar Redesign + +**Modify: `packages/web/app/components/queue-control/queue-control-bar.tsx`** + +Current button cluster: `[Mirror] [Play] [Prev] [Next] [Tick]` + +New button cluster: `[Party] [Queue] [Tick]` + +Changes: +- **Remove**: `ExpandOutlined` play mode link button +- **Remove**: Mirror button (moves to full-screen play view action bar) +- **Remove**: Previous/Next navigation buttons (the bar's swipe gestures and the play view handle this) +- **Add**: Party mode button (the `ShareBoardButton` component, moved from header) +- **Add**: Queue list button (`UnorderedListOutlined`) that opens the queue drawer (replaces tapping the climb info text) +- **Keep**: Tick button stays + +New layout: +``` +┌──────────────────────────────────────────────────────────┐ +│ [Thumbnail] "Climb Name" V4 ★★★ │ [👥Party] [≡Q] [✓] │ +│ @ 40° │ │ +└──────────────────────────────────────────────────────────┘ +``` + +Tap behavior: +- Tapping the **left section** (thumbnail + climb info) opens the full-screen play drawer +- Tapping **Queue button** opens the existing queue drawer +- Tapping **Party button** opens the party mode drawer (see Phase 5) +- Tapping **Tick button** logs ascent (existing behavior) +- Swipe left/right: Navigate to next/previous climb (existing behavior, keep as-is) + +The click handler on the climb info section (currently `toggleQueueDrawer`) should change to open the full-screen play drawer instead. + +### 3B: Full-Screen Play Drawer + +**New file: `packages/web/app/components/play-view/play-view-drawer.tsx`** + +A drawer component that replaces the `/play/` route for mobile interaction: + +- **Placement**: Bottom +- **Height**: 100% viewport (or 95% with rounded top corners) +- **Animation**: Smooth slide-up from the now-playing bar, like Spotify's now-playing expansion +- **Drag to close**: Support dragging down from the top handle to dismiss + +Content (reuse existing PlayView logic): +``` +┌──────────────────────────────────────┐ +│ ── (drag handle bar) ── │ +│ │ +│ "Problem Name" V4 │ ClimbTitle (horizontal) +│ ★★★ By setter @ 40° │ with AscentStatus +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ │ │ +│ │ [Board Renderer] │ │ BoardRenderer fillHeight +│ │ │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ ← swipe left/right indicators → │ Swipe navigation +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ [🔄] [♡] [👥] [💡LED] [✓Tick] │ │ Action bar +│ └──────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ [◀ Prev] Current Name [Next ▶]│ │ Mini transport +│ └──────────────────────────────────┘ │ +└──────────────────────────────────────┘ +``` + +Action bar buttons: +- **Mirror** (`SyncOutlined`): Toggle mirrored state (purple when active) +- **Favorite** (`HeartOutlined`/`HeartFilled`): Toggle favorite +- **Party** (`TeamOutlined`): Open party drawer +- **LED** (`BulbOutlined`/`BulbFilled`): Connect/send to board (only if Bluetooth supported) +- **Tick** (`CheckOutlined`): Log ascent + +Mini transport controls: +- Shows previous/current/next climb names +- Tap previous/next to navigate +- Current climb name centered, truncated with ellipsis + +Key implementation details: +- Extract the swipe navigation logic from `play-view-client.tsx` into a shared hook: `usePlayViewNavigation()` +- The drawer should manage its own URL state - when opened, optionally push a `#playing` hash to enable back-button closing +- When the drawer is open and the user navigates (swipe/buttons), update the queue's currentClimbQueueItem without URL navigation +- The existing `/play/[uuid]` route should remain functional for direct links and desktop, but mobile primarily uses the drawer + +**New file: `packages/web/app/components/play-view/use-play-view-navigation.ts`** +- Shared hook extracting swipe logic from play-view-client.tsx +- Handles: handleNext, handlePrevious, swipeHandlers, swipeOffset, showSwipeHint +- Used by both PlayViewDrawer and PlayViewClient + +### 3C: Impact on existing Play route + +**Modify: `.../play/[climb_uuid]/play-view-client.tsx`** +- Keep the full page play view for desktop users and direct URL access +- Extract navigation logic to the shared hook +- On mobile, consider auto-redirecting to list view with the drawer open (or just keep both paths working) + +**Modify: `.../play/layout-client.tsx`** +- No changes needed for desktop sidebar behavior + +--- + +## Phase 4: Header Simplification + +### What changes +Reduce header clutter by moving elements to the bottom tab bar and queue control bar. + +### Files to modify + +**Modify: `packages/web/app/components/board-page/header.tsx`** + +Removals: +- **Remove**: `ShareBoardButton` (party mode) - moves to QueueControlBar +- **Remove**: `SendClimbToBoardButton` (LED) - moves to play view drawer action bar +- **Remove**: Mobile `SearchButton` - moves to bottom tab bar +- **Remove**: "Create Climb" from mobile meatball menu - moves to bottom tab bar + +The header becomes: +``` +Mobile: [Logo] [Search Input] [Angle ▾] [≡ Menu] +Desktop: [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] [User ▾] +``` + +On mobile the header is now much simpler - just branding, search text input, angle selector, and the user menu. + +On play/view pages (mobile), the header shows: +``` +[← Back] [Logo] [Angle ▾] [≡ Menu] +``` + +**Modify: mobile meatball menu items** +- Remove "Create Climb" (now in bottom tab bar's Create drawer) +- Keep: My Playlists, Classify Holds, Profile, Settings, Help, About, Logout + +### Header height +- Keep at `8dvh` / min 48px +- The reduced content means it could potentially be slimmer, but keep it consistent for now + +--- + +## Phase 5: Party Mode & LED as Drawer + +### What changes +Convert party mode into a bottom drawer (instead of top drawer) and integrate LED connection into it. + +### Files to modify + +**Modify: `packages/web/app/components/board-page/share-button.tsx`** +- Change drawer placement from `"top"` to `"bottom"` +- Rename component to `PartyModeButton` (optional, for clarity) +- Add a "Connect to Board" section at the bottom of the drawer when a session is active or as a standalone tab +- The drawer should have two sections/tabs: + 1. **Party Mode** - existing start/join/active session UI + 2. **Connect to Board (LED)** - Bluetooth connection UI + +**New section within the party drawer: LED Connection** +- "Connect to Board" button (replaces the header LED button on mobile) +- Connection status indicator (connected/disconnected) +- When connected: "Connected to [device name]" with disconnect option +- This reuses the logic from `SendClimbToBoardButton` but presents it differently +- On mobile, this is the only way to access LED. On desktop, the header button remains as a quick shortcut. + +**Modify: `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx`** +- Extract connection logic into a shared hook: `useBluetoothConnection()` +- The button component becomes a thin wrapper around the hook +- The party drawer's LED section also uses the same hook + +**New file: `packages/web/app/components/board-bluetooth-control/use-bluetooth-connection.ts`** +- Extracts: device ref, characteristic ref, connect/disconnect, send climb, connection state +- Returns: `{ isConnected, isLoading, connect, disconnect, sendClimb }` + +### Party mode trigger locations +- **QueueControlBar**: Party mode button (badge shows connected user count) +- **Play view drawer action bar**: Party mode button +- **Desktop header**: Keeps existing position (ShareBoardButton) + +--- + +## Phase 6: Queue List Improvements + +### What changes +Minor refinements to the queue list to match the new design language. + +### Files to modify + +**Modify: `packages/web/app/components/queue-control/queue-list-item.tsx`** +- Ensure visual consistency with the new `ClimbListItem` compact format +- Same height, typography, grade coloring +- Keep existing swipe actions (tick left, delete right) +- Keep drag-and-drop reordering + +**Modify: queue drawer behavior** +- Queue drawer now opens from the dedicated Queue button on the bar (not from tapping climb info) +- Placement stays bottom, height stays 70% +- Add a count badge on the Queue button showing queue length + +--- + +## Phase 7: Desktop Adaptation + +### What changes +Ensure the desktop experience remains cohesive while the mobile experience is transformed. + +### Desktop-specific behavior + +1. **No bottom tab bar** - Hidden via CSS media query (>= 768px) +2. **Sidebar stays** - Queue/Search/Holds tabs in the sidebar (existing) +3. **Header keeps** - Party, LED, Create, User menu buttons in header +4. **Play view** - Desktop users still use the full `/play/` page route +5. **Climb list** - Default to grid (card) mode on desktop, compact (list) mode on mobile +6. **QueueControlBar** - Shows additional prev/next buttons on desktop (restore the `navButtons` span behavior) + +### Files to modify +- **`packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`**: `display: none` for >= 768px +- **`packages/web/app/components/queue-control/queue-control-bar.tsx`**: Conditional rendering of play link on desktop (since desktop doesn't use the drawer) +- **`packages/web/app/components/board-page/header.tsx`**: Desktop keeps party + LED buttons via `.desktopOnly` class + +--- + +## Implementation Order + +The phases are designed to be implemented sequentially, each building on the previous: + +``` +Phase 1: Bottom Tab Bar + └─ Foundation for new navigation structure + └─ Create drawer for "New" tab + +Phase 2: Compact Climb List Mode + └─ ClimbListItem component + └─ Swipe actions (heart/queue) + └─ Ellipsis menu drawer + └─ View mode toggle + +Phase 3: Now Playing Bar + Play Drawer + └─ 3A: QueueControlBar button redesign + └─ 3B: Full-screen play drawer + └─ 3C: Shared navigation hook + +Phase 4: Header Simplification + └─ Remove relocated elements + └─ Verify desktop still complete + +Phase 5: Party Mode & LED Drawer + └─ Bottom drawer conversion + └─ LED integration + └─ Bluetooth hook extraction + +Phase 6: Queue List Polish + └─ Visual alignment with compact list + +Phase 7: Desktop Adaptation + └─ Responsive breakpoint verification + └─ Desktop-specific overrides +``` + +--- + +## Component Dependency Graph + +``` +layout.tsx +├── BoardSeshHeader (simplified) +│ ├── Logo +│ ├── SearchClimbNameInput (mobile) +│ ├── AngleSelector +│ └── UserMenu +│ +├── Content +│ ├── ClimbsList +│ │ ├── ClimbCard (grid mode) +│ │ └── ClimbListItem (compact mode) [NEW] +│ │ ├── ClimbThumbnail +│ │ ├── Colorized V-Grade +│ │ ├── Swipe: Heart / Add to Queue +│ │ └── ClimbActionsDrawer [NEW] +│ │ +│ └── ListLayoutClient (desktop sidebar) +│ └── Tabs: Queue | Search +│ +├── QueueControlBar (redesigned) +│ ├── ClimbThumbnail + ClimbTitle (tap → PlayViewDrawer) +│ ├── PartyModeButton (moved from header) +│ ├── QueueButton [NEW] (opens queue drawer) +│ ├── TickButton +│ ├── Queue Drawer (existing) +│ └── PlayViewDrawer [NEW] +│ ├── ClimbTitle +│ ├── BoardRenderer +│ ├── Swipe Navigation (shared hook) +│ ├── Action Bar [Mirror, Heart, Party, LED, Tick] +│ └── Mini Transport [Prev | Current | Next] +│ +└── BottomTabBar [NEW] + ├── Climbs Tab → Navigate to /list + ├── Search Tab → Open SearchDrawer + └── Create Tab → Open CreateDrawer [NEW] + ├── Create Climb → /create route + └── Create Playlist → /playlists route +``` + +--- + +## Shared Hooks to Extract + +| Hook | Source | Used By | +|---|---|---| +| `usePlayViewNavigation` | `play-view-client.tsx` | PlayViewDrawer, PlayViewClient | +| `useBluetoothConnection` | `send-climb-to-board-button.tsx` | SendClimbToBoardButton, PartyDrawer LED section | + +--- + +## CSS / Styling Approach + +- All new components use **themeTokens** for colors, spacing, radii +- CSS modules for layout-only styles (flex, grid, responsive breakpoints) +- Inline styles for dynamic values (colors from theme, computed values) +- Media queries at 768px breakpoint for mobile/desktop splits +- Use `env(safe-area-inset-bottom)` for iOS safe area on bottom bar +- AntD Drawer component for all drawer UIs (consistent animations) +- Swipe gesture patterns reuse the existing `react-swipeable` + offset/threshold pattern + +--- + +## Transition Animations + +| Transition | Type | Duration | +|---|---|---| +| Play drawer open/close | Drawer slide-up | 300ms (AntD default) | +| Bottom tab active state | Color transition | 150ms ease | +| Swipe action reveal | Transform + opacity | Immediate (follows finger) | +| Swipe snap back | Transform | 150ms ease (existing) | +| List mode toggle | Fade / layout shift | 200ms ease | +| Create drawer open | Drawer slide-up | 300ms | + +--- + +## Accessibility Considerations + +- Bottom tab bar items have `aria-label` and `role="tab"` +- Play drawer has `aria-label="Now playing"` and manages focus on open +- Swipe actions have non-swipe alternatives (ellipsis menu for list items, buttons in play view) +- Color-coded grades maintain sufficient contrast ratios +- All interactive elements maintain minimum 44x44px touch targets + +--- + +## State Management Impact + +- **No new contexts needed** - all new components consume existing QueueContext +- **View mode preference**: localStorage (`climbListViewMode: 'compact' | 'grid'`) +- **Play drawer open state**: Local state in QueueControlBar (lifted up if needed) +- **Bottom tab active state**: Derived from current URL pathname +- **Bluetooth connection state**: Extracted to shared hook, existing context patterns + +--- + +## Risk Areas & Mitigations + +| Risk | Impact | Mitigation | +|---|---|---| +| Play drawer performance | Board renderer in drawer might be slow | Lazy-render drawer content; only mount BoardRenderer when drawer is open | +| Swipe conflict with scroll | Horizontal swipes on list items could interfere with vertical scroll | Existing pattern handles this (preventScrollOnSwipe + delta threshold) | +| Desktop regression | Moving buttons around might break desktop flow | Phase 7 explicitly verifies desktop; use `.desktopOnly`/`.mobileOnly` classes | +| Deep link to /play/ | Existing play URLs must still work | Keep the route, just make mobile default to drawer from list | +| Party mode button discovery | Moving from header to bar might confuse users | Badge with user count draws attention; tooltip on first use | +| Double drawer stacking | Play drawer open + queue drawer open | Close play drawer before opening queue, or stack with z-index | + +--- + +## Testing Checklist + +### Phase 1 +- [ ] Bottom tab bar renders on mobile, hidden on desktop +- [ ] Climbs tab navigates to list +- [ ] Search tab opens search drawer +- [ ] Create tab opens create drawer +- [ ] Create drawer links work (create climb, create playlist) +- [ ] iOS safe area padding works + +### Phase 2 +- [ ] Compact list items render correctly +- [ ] Grade colors match existing ClimbTitle colors +- [ ] Swipe right favorites a climb (visual feedback) +- [ ] Swipe left adds to queue (visual feedback + queue updates) +- [ ] Ellipsis menu opens drawer with all actions +- [ ] All actions in drawer work (favorite, queue, tick, share, playlist, open-in-app, mirror, view) +- [ ] View mode toggle persists across page loads +- [ ] Infinite scroll works in both modes +- [ ] Scroll position restoration works in both modes + +### Phase 3 +- [ ] Tapping now-playing bar opens full-screen drawer +- [ ] Play drawer shows board renderer correctly +- [ ] Swipe navigation works in play drawer +- [ ] Mirror/favorite/tick actions work in play drawer +- [ ] Drag-to-close works smoothly +- [ ] Queue button opens queue drawer +- [ ] Party button opens party drawer +- [ ] Desktop still navigates to /play/ route +- [ ] /play/ URLs still work for direct links + +### Phase 4 +- [ ] Header is simplified on mobile +- [ ] All removed items are available elsewhere +- [ ] Desktop header still has all buttons +- [ ] No broken references to moved components + +### Phase 5 +- [ ] Party drawer opens from bottom +- [ ] LED connection section works within party drawer +- [ ] Bluetooth hook works in both contexts +- [ ] Start/join/leave session flows work +- [ ] LED auto-sends on climb change (existing behavior preserved) + +### Phase 6 +- [ ] Queue list items visually match compact climb list items +- [ ] Drag-and-drop still works +- [ ] Queue badge shows correct count + +### Phase 7 +- [ ] Desktop sidebar works unchanged +- [ ] Desktop header has all expected buttons +- [ ] Desktop play page works as before +- [ ] Responsive breakpoints are clean (no flickering between mobile/desktop) From 64c493a9aee28c1e6694b633d02fc302ae43e911 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:34:06 +0000 Subject: [PATCH 2/6] Review and improve Spotify UI redesign plan with codebase-validated corrections - Fix architecture reference table: accurate file paths, component details, server/client boundaries, provider hierarchy, swipe thresholds, action types - Add Spotify-style card-swipe navigation pattern (replaces arrow indicators in play view and queue control bar) - Address gaps: auth requirements for swipe-to-favorite, direction detection for scroll conflicts, FavoritesProvider dependency, MoonBoard feature gates - Add BluetoothContext singleton concern and mitigation strategies - Document server vs client component boundaries for new components - Add drawer stacking prevention (single activeDrawer state pattern) - Add onboarding tour breakage risk and element ID audit checklist - Add hash-based back-button support for play drawer (#playing) - Expand testing checklist with specific implementation details - Add Architecture Notes section: swipe pattern variants, URL strategy, MoonBoard considerations, provider hierarchy requirements https://claude.ai/code/session_01VjXZirGhfNKxhAji3shrha --- docs/spotify-ui-redesign-plan.md | 526 +++++++++++++++++++++---------- 1 file changed, 365 insertions(+), 161 deletions(-) diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md index bcb73118..75345254 100644 --- a/docs/spotify-ui-redesign-plan.md +++ b/docs/spotify-ui-redesign-plan.md @@ -10,19 +10,20 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis | Component | File | Role | |---|---|---| -| Main Layout | `packages/web/app/[board_name]/.../[angle]/layout.tsx` | Header + Content + Affixed QueueControlBar | -| List Layout | `.../[angle]/list/layout-client.tsx` | Main content + desktop sidebar (Queue/Search/Holds tabs) | -| Header | `packages/web/app/components/board-page/header.tsx` | Logo, search, angle, create, party, LED, user menu | -| QueueControlBar | `packages/web/app/components/queue-control/queue-control-bar.tsx` | Now-playing bar with swipe, queue drawer, play button, tick | -| ClimbCard | `packages/web/app/components/climb-card/climb-card.tsx` | Full card view with cover image, title, action footer | -| ClimbTitle | `packages/web/app/components/climb-card/climb-title.tsx` | Name, grade (colorized), quality, setter info | -| QueueListItem | `packages/web/app/components/queue-control/queue-list-item.tsx` | Compact row with thumbnail, swipe to tick/delete | -| PlayView | `.../play/[climb_uuid]/play-view-client.tsx` | Full board renderer with swipe navigation | -| ShareBoardButton | `packages/web/app/components/board-page/share-button.tsx` | Party mode drawer (top placement) | -| SendClimbToBoardButton | `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx` | Bluetooth LED connection | -| SearchButton/Drawer | `packages/web/app/components/search-drawer/` | Right-side drawer with filters | -| ClimbActions | `packages/web/app/components/climb-actions/` | Favorite, queue, tick, share, playlist, open-in-app, mirror | -| BoardRenderer | `packages/web/app/components/board-renderer/board-renderer.tsx` | SVG board visualization | +| Main Layout | `packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/layout.tsx` | Server component. Header + Content + Affixed QueueControlBar. Wraps children in `BoardSessionBridge > ConnectionSettingsProvider > GraphQLQueueProvider > PartyProvider`. | +| List Layout | `.../[angle]/list/layout-client.tsx` | Client component. Main content + desktop sidebar with 3 tabs (Queue/Search/Search by Hold). Sidebar uses AntD `Sider` at 400px width. | +| Header | `packages/web/app/components/board-page/header.tsx` | Client component. Logo, search input, angle selector, create (desktop), party, LED, user menu. Mobile has a meatball menu dropdown. Uses `usePageMode()` to adapt layout per page type. | +| QueueControlBar | `packages/web/app/components/queue-control/queue-control-bar.tsx` | Now-playing bar with swipe left/right (prev/next), queue drawer (bottom, 70%), play button link, mirror, prev/next buttons (desktop-only via `.navButtons` CSS), tick. Also shows "added by" avatar indicator. | +| ClimbCard | `packages/web/app/components/climb-card/climb-card.tsx` | Card view with cover image, horizontal ClimbTitle header, action footer. Has two render paths: `ClimbCardWithActions` (generates actions dynamically) and `ClimbCardStatic` (memoized with external actions). | +| ClimbTitle | `packages/web/app/components/climb-card/climb-title.tsx` | Name, grade (colorized), quality stars, setter info. Supports `layout="horizontal"` and stacked modes. | +| QueueListItem | `packages/web/app/components/queue-control/queue-list-item.tsx` | Compact row with thumbnail, swipe right=tick, swipe left=delete. Includes drag-and-drop via `@atlaskit/pragmatic-drag-and-drop`. Uses direction detection (horizontal vs vertical) to avoid scroll conflicts. Has ellipsis dropdown menu with View/Tick/Open in App/Remove actions. | +| PlayView | `.../play/[climb_uuid]/play-view-client.tsx` | Full board renderer with swipe navigation. Uses `SWIPE_THRESHOLD=80` (different from queue items' 100). Shows swipe hint that auto-hides after 3 seconds. | +| ShareBoardButton | `packages/web/app/components/board-page/share-button.tsx` | Party mode drawer (top placement, 70vh height). Has Start/Join session tabs. Shows users list, QR code, share URL when connected. | +| SendClimbToBoardButton | `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx` | Bluetooth LED connection. Dynamically imported (`next/dynamic`, SSR disabled). Auto-sends climb on `currentClimbQueueItem` change when connected. Uses `useWakeLock` to prevent sleep while connected. Shows iOS Bluefy browser recommendation modal. | +| SearchButton/Drawer | `packages/web/app/components/search-drawer/` | Desktop sidebar filter column (hidden on mobile via CSS module). Mobile search is `SearchClimbNameInput` in header + `SearchButton` icon for advanced filters. | +| ClimbActions | `packages/web/app/components/climb-actions/` | Modular action system with 10 action types: viewDetails, fork, favorite, queue, tick, openInApp, mirror, share, addToList, playlist. Supports `icon`, `button`, `compact`, and `dropdown` view modes. | +| BoardRenderer | `packages/web/app/components/board-renderer/board-renderer.tsx` | SVG board visualization with `fillHeight` option | +| ClimbsList | `packages/web/app/components/board-page/climbs-list.tsx` | Client component. Paginated climb list with IntersectionObserver infinite scroll. Uses `Row`/`Col` grid layout (xs=24, lg=12). Deduplicates by UUID. Hash-based scroll position restoration. | --- @@ -98,14 +99,12 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis │ ┌──────────────────────────────┐ │ │ │ │ │ │ │ │ │ -│ │ [Board Renderer] │ │ -│ │ (fills height) │ │ -│ │ │ │ +│ │ [Board Renderer] │ │ ← Swipe left/right here +│ │ (card swipe animation) │ │ to navigate between climbs +│ │ │ │ Card slides out, next slides in │ │ │ │ │ └──────────────────────────────┘ │ │ │ -│ ← swipe indicators → │ -│ │ ├────────────────────────────────────┤ │ [Mirror] [♡] [Party] [LED] [✓] │ ← Action bar ├────────────────────────────────────┤ @@ -113,6 +112,8 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis └────────────────────────────────────┘ ``` +**Swipe navigation**: Spotify-style card swiping where the board renderer card physically translates with the user's finger, and the next/previous card slides in from the edge. No static arrow indicators - the motion itself provides feedback. + ### Desktop Layout ``` ┌─────────────────────────────────────────────────────────────┐ @@ -141,46 +142,62 @@ Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Cli ### Files to modify 1. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.tsx`** + - Client component (`'use client'`) - Three tabs: Climbs (default active), Search, Create - Icons: Use AntD icons - `UnorderedListOutlined` / `AppstoreOutlined` for Climbs, `SearchOutlined` for Search, `PlusOutlined` for Create - - Active state: Primary color for active tab icon + label - - Inactive state: `neutral[400]` color + - Active state: Primary color (`themeTokens.colors.primary`) for active tab icon + label + - Inactive state: `themeTokens.neutral[400]` color - Fixed at the bottom, full width - Height: ~50px with safe-area-inset-bottom padding for iOS - Desktop: Hidden (search/queue available in sidebar, create in header) + - **Props needed**: Requires board route parameters to construct navigation URLs (the tab bar needs to know the current board/layout/size/set/angle to build the `/list` URL) + - **Search drawer integration**: Needs a callback prop or ref to open the search drawer, since the drawer state lives elsewhere 2. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`** - Media query to hide on desktop (>= 768px) - - Safe area inset for iOS home indicator + - Safe area inset for iOS home indicator: `padding-bottom: env(safe-area-inset-bottom)` -3. **Modify: `packages/web/app/[board_name]/.../[angle]/layout.tsx`** +3. **Modify: `packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/layout.tsx`** + - **Important**: This is a server component. The BottomTabBar is a client component, so it needs to be placed correctly within the component tree. - Add BottomTabBar below the Affix'd QueueControlBar - Wrap both in a shared bottom container - Adjust Content height to account for tab bar height + - **Note**: The current layout uses `` around QueueControlBar. The BottomTabBar should be placed outside/below the Affix, or both should be wrapped in a new bottom-anchored container. Consider replacing `` with a simple flex layout since both elements are permanently fixed at the bottom. + - **Note**: The Content area currently has `height: '80vh'` which will need adjustment to account for the tab bar. Consider using `flex: 1` with proper `calc()` or letting flexbox handle it naturally. -4. **Modify: `packages/web/app/components/search-drawer/search-drawer.tsx`** - - Search tab triggers the existing search drawer (right placement, 90% width) - - No structural change needed - just wire the tab to open it +4. **Modify: Mobile search integration** + - The current mobile search has two parts: `SearchClimbNameInput` (text input in header) and `SearchButton` (opens advanced filter drawer). + - The Search tab should open the advanced filter drawer (from `SearchButton`), not replace the text input. + - `search-drawer.tsx` is actually a desktop-only sidebar `FilterColumn` wrapper - the mobile search button (`search-button.tsx`) triggers a separate Drawer. The bottom tab Search action should trigger the same drawer that `SearchButton` opens. + - **Keep the header search text input** (`SearchClimbNameInput`) on mobile - it provides quick name search. The tab bar Search opens the advanced filters drawer. 5. **New file: `packages/web/app/components/create-drawer/create-drawer.tsx`** - Bottom drawer with creation options - - Two items initially: + - Items: - "Create Climb" - links to `/create` route - "Create Playlist" - links to `/playlists` with create action + - **Note**: Both URLs require the full board route context (`board_name/layout_id/size_id/set_ids/angle`). The component needs access to `boardDetails` and `angle` props (or use URL params) to construct proper slug-based URLs, matching the pattern used in `header.tsx` with `generateLayoutSlug`/`generateSizeSlug`/`generateSetSlug`. + - **Note**: Hide playlist option for MoonBoard (matching existing header behavior: `isMoonboard` check) - Each item: Icon + label, full-width rows - Simple AntD Drawer with bottom placement, auto height ### Behavior - **Climbs tab**: Navigates to the climb list view (if not already there). This is the default/home state. -- **Search tab**: Opens the existing SearchDrawer from the bottom tab rather than from the header. On desktop, activates the sidebar search tab instead. +- **Search tab**: Opens the advanced search filters drawer (the same one `SearchButton` triggers in the header). On desktop, activates the sidebar search tab instead. - **Create tab**: Opens the CreateDrawer with options. - Active tab state reflects current context (Climbs when on list, etc.) - On desktop (>= 768px): Tab bar is hidden. Search and create remain in header/sidebar. +- **On play/view/create pages**: Consider whether the tab bar should remain visible. On play pages the user may want to quickly return to the list. On create pages it could cause confusion. Recommend: show on play/view, hide on create. ### Integration with header -- Remove the mobile SearchButton from the header center section (it moves to bottom tab bar) -- Keep the desktop SearchButton in the sidebar -- Remove "Create Climb" from the mobile meatball menu (it moves to bottom tab bar) +- Remove the mobile `SearchButton` (advanced filters icon) from the header center section (it moves to bottom tab bar) +- **Keep** the `SearchClimbNameInput` text input in the mobile header for quick name-based search +- Keep the desktop sidebar search +- Remove "Create Climb" from the mobile meatball menu (`mobileMenuItems` in header.tsx, key `create-climb`) + +### Considerations +- **Onboarding tour**: The existing `OnboardingTour` component in `layout-client.tsx` may reference the old header elements. Verify tour steps still point to valid targets after moving buttons. +- **Controller mode**: When in controller mode (URL has `controllerUrl` param), the Create tab may not be relevant. Consider hiding it or adjusting behavior. --- @@ -197,29 +214,34 @@ Add a "compact" display mode for the climb list that renders climbs as slim rows ``` ┌─────────────────────────────────────────────────────┐ │ [Thumbnail] Name + Setter [V-Grade] [⋮] │ - │ 48×auto ★★★ (colorized) (menu) │ + │ 48×auto ★★★ AscentStatus (colorized) (menu) │ └─────────────────────────────────────────────────────┘ ``` - Left side: `ClimbThumbnail` (48px width, maintains aspect ratio) - - Center: Climb name (single line, ellipsis overflow), quality stars, setter name (secondary text, small) - - Right side: Large colorized V-grade text (use `getVGradeColor()`), ellipsis button + - Center: Climb name (single line, ellipsis overflow), quality stars, setter name (secondary text, small), `AscentStatus` icon (reuse from `queue-list-item.tsx`) + - Right side: Large colorized V-grade text, ellipsis button - Total height: ~60-64px per item - No horizontal padding waste - edge-to-edge content - The V-grade should be visually prominent with the grade color as the text color, similar to how ClimbTitle renders it in horizontal mode but larger/bolder + - **Selected state**: Show `themeTokens.semantic.selected` background + left border (matching QueueListItem's `isCurrent` style) when `currentClimb?.uuid === climb.uuid` + - **Tap handler**: Single tap should set as current climb (`setCurrentClimb`), double-tap opens play drawer (Phase 3) or navigates to view. This matches the existing `handleClimbDoubleClick` pattern in `climbs-list.tsx`. 2. **Add swipe actions to `climb-list-item.tsx`** - Reuse the swipe pattern from `queue-list-item.tsx` (same SWIPE_THRESHOLD=100, MAX_SWIPE=120) + - **Important**: Copy the direction-detection logic from `queue-list-item.tsx` (the `isHorizontalSwipe` state pattern), not the simpler swipe from `queue-control-bar.tsx`. The direction detection prevents scroll conflicts by checking `absX > absY` before committing to horizontal swipe. Without this, vertical scrolling through the list will be janky. - **Swipe right (reveals left)**: Heart/favorite action - - Background: `colors.error` (red, matching heart color) or a warm pink + - Background: `themeTokens.colors.error` (red, matching heart color) - Icon: `HeartOutlined` / `HeartFilled` - - Action: Toggle favorite on the climb via existing favorite API + - Action: Toggle favorite on the climb via `useFavorite()` hook from `climb-actions/use-favorite.ts` + - **Auth requirement**: Favorite requires authentication (`AUTH_REQUIRED_ACTIONS` includes 'favorite'). If not authenticated, the swipe action should open the auth modal or show a brief toast prompting sign-in. - **Swipe left (reveals right)**: Add to queue action - - Background: `colors.primary` (cyan) + - Background: `themeTokens.colors.primary` (cyan) - Icon: `PlusOutlined` or `OrderedListOutlined` - - Action: Add climb to queue via `addToQueue()` from QueueContext + - Action: Add climb to queue via `setCurrentClimb()` from QueueContext (the existing pattern adds to queue) + - **Feedback**: Brief visual confirmation (e.g., snap animation or toast) that the climb was added 3. **Ellipsis menu drawer: `packages/web/app/components/climb-card/climb-actions-drawer.tsx`** - - Bottom drawer triggered by the `⋮` (EllipsisOutlined) button + - Bottom drawer triggered by the `⋮` (`MoreOutlined` icon, matching existing patterns) button - Header: ClimbTitle (stacked layout) with thumbnail - Body: Full list of actions as large tap targets (rows, not icons): - ♡ Favorite / Unfavorite @@ -229,9 +251,11 @@ Add a "compact" display mode for the climb list that renders climbs as slim rows - 📋 Add to Playlist - 📱 Open in Aurora App - 🔄 Mirror (if board supports mirroring) + - 🍴 Fork/Edit (if board supports it - matching the `fork` action type) - ℹ️ View Details (navigate to /view/ page) - Each row: Icon (24px) + Label text, full width, ~48px height - - Reuse action handlers from existing `ClimbActions` component + - **Implementation approach**: Use the existing `ClimbActions` component with `viewMode="button"` or create a new `viewMode="list"` that renders full-width rows. The existing action system already handles availability, auth requirements, and board-specific filtering. Adding a new view mode is cleaner than reimplementing action logic. + - **Note**: The `ClimbActions` component uses hooks internally (each action is a component). The drawer must render within the React tree properly to support this. 4. **Modify: `packages/web/app/components/board-page/climbs-list.tsx`** - Add a view mode toggle: "Grid" (current cards) vs "List" (compact) @@ -240,16 +264,20 @@ Add a "compact" display mode for the climb list that renders climbs as slim rows - Icons: `AppstoreOutlined` for grid, `UnorderedListOutlined` for list - When in list mode: Render `ClimbListItem` instead of `ClimbCard` - When in list mode: Single column layout (no grid), full width items - - When in grid mode: Keep existing 2-column card grid + - When in grid mode: Keep existing 2-column card grid (`Col xs={24} lg={12}`) - Default to compact/list mode on mobile, grid mode on desktop + - **Important**: The current `climbs-list.tsx` passes `boardDetails` and `onCoverDoubleClick` to `ClimbCard`. The `ClimbListItem` needs the same props plus the swipe action handlers. Thread `boardDetails` through for thumbnail rendering and action construction. + - **Hash-based scroll restoration**: The existing `restoreScrollFromHash()` and `updateHash()` must work with both modes. Since items use `id={climb.uuid}`, this should work unchanged as long as `ClimbListItem` also renders with the same `id` attribute. + - **Infinite scroll**: The `IntersectionObserver` on `loadMoreRef` works independently of item type, so no changes needed there. 5. **Modify: `packages/web/app/components/climb-card/climb-thumbnail.tsx`** - No changes needed - already supports the 48px fixed-width pattern ### Performance considerations -- ClimbListItem should be `React.memo`'d with custom comparator (compare by climb.uuid) +- ClimbListItem should be `React.memo`'d with custom comparator (compare by `climb.uuid` and `selected` state) - Swipe state is local to each item (no parent re-renders) - Virtual scrolling is not needed yet (existing infinite scroll pagination handles this) +- **FavoritesProvider**: The `useFavorite()` hook requires `FavoritesProvider` in the component tree. Verify that this provider exists above `ClimbsList` in the hierarchy, or add it if missing. The current `ClimbCard` → `ClimbCardWithActions` path may handle this differently. --- @@ -266,19 +294,23 @@ Add a "compact" display mode for the climb list that renders climbs as slim rows **Modify: `packages/web/app/components/queue-control/queue-control-bar.tsx`** -Current button cluster: `[Mirror] [Play] [Prev] [Next] [Tick]` +Current button cluster (mobile): `[Mirror] [Play] [Tick]` (Prev/Next are hidden on mobile via `.navButtons` CSS class) +Current button cluster (desktop): `[Mirror] [Play] [Prev] [Next] [Tick]` -New button cluster: `[Party] [Queue] [Tick]` +New mobile button cluster: `[Party] [Queue] [Tick]` +New desktop button cluster: `[Mirror] [Play] [Prev] [Next] [Party] [Queue] [Tick]` Changes: -- **Remove**: `ExpandOutlined` play mode link button -- **Remove**: Mirror button (moves to full-screen play view action bar) -- **Remove**: Previous/Next navigation buttons (the bar's swipe gestures and the play view handle this) -- **Add**: Party mode button (the `ShareBoardButton` component, moved from header) +- **Remove (mobile only)**: `ExpandOutlined` play mode link button - replaced by tapping the bar itself +- **Move (mobile only)**: Mirror button to full-screen play view action bar +- **Keep (desktop)**: `ExpandOutlined` play link and mirror button - desktop still navigates to `/play/` route +- **Keep (desktop)**: Previous/Next navigation buttons (already hidden on mobile via `.navButtons` CSS class) +- **Add**: Party mode button (the `ShareBoardButton` component, moved from header). **Note**: `ShareBoardButton` currently manages its own drawer state internally - it renders both the button and the drawer. Moving it is straightforward since it's self-contained. - **Add**: Queue list button (`UnorderedListOutlined`) that opens the queue drawer (replaces tapping the climb info text) - **Keep**: Tick button stays +- **Remove**: "Added by" avatar indicator from the bar (move to play drawer or remove - it clutters the simplified bar) -New layout: +New mobile layout: ``` ┌──────────────────────────────────────────────────────────┐ │ [Thumbnail] "Climb Name" V4 ★★★ │ [👥Party] [≡Q] [✓] │ @@ -287,13 +319,24 @@ New layout: ``` Tap behavior: -- Tapping the **left section** (thumbnail + climb info) opens the full-screen play drawer +- Tapping the **left section** (thumbnail + climb info) opens the full-screen play drawer (mobile only) - Tapping **Queue button** opens the existing queue drawer - Tapping **Party button** opens the party mode drawer (see Phase 5) - Tapping **Tick button** logs ascent (existing behavior) -- Swipe left/right: Navigate to next/previous climb (existing behavior, keep as-is) -The click handler on the climb info section (currently `toggleQueueDrawer`) should change to open the full-screen play drawer instead. +Swipe behavior (Spotify-style card swipe): +- Swipe left/right on the bar to navigate between climbs +- **Replace** the current arrow-indicator swipe pattern (FastBackwardOutlined/FastForwardOutlined backgrounds) with a card-swipe animation +- During swipe: the current climb info (thumbnail + ClimbTitle section) translates horizontally with the finger +- The next/previous climb info slides in from the opposite edge +- On swipe completion: current info animates out, next info slides to center, queue advances +- This matches the Spotify "now playing" bar behavior where you can swipe between tracks and see the track info physically move +- The swipe action backgrounds (cyan with arrow icons) should be removed entirely + +The click handler on the climb info section needs conditional behavior: +- **Mobile**: Open the full-screen play drawer (new behavior) +- **Desktop on list page**: Currently does nothing (the `toggleQueueDrawer` already returns early on desktop list page). Change to keep existing no-op on desktop list, open play drawer on desktop non-list pages. +- **Note**: The current `toggleQueueDrawer` uses `window.matchMedia('(min-width: 768px)')` for the desktop check. Continue using this pattern for conditional behavior. ### 3B: Full-Screen Play Drawer @@ -304,7 +347,11 @@ A drawer component that replaces the `/play/` route for mobile interaction: - **Placement**: Bottom - **Height**: 100% viewport (or 95% with rounded top corners) - **Animation**: Smooth slide-up from the now-playing bar, like Spotify's now-playing expansion -- **Drag to close**: Support dragging down from the top handle to dismiss +- **Drag to close**: AntD Drawer doesn't natively support drag-to-dismiss. Options: + 1. Use AntD Drawer's built-in close button + swipe-down gesture via `react-swipeable` on the handle area + 2. Implement a custom bottom sheet component (more complex but true Spotify feel) + 3. Use a library like `react-spring-bottom-sheet` - but adds a dependency + - **Recommended**: Start with AntD Drawer + a custom drag handle using `react-swipeable` (already a dependency). If the UX isn't smooth enough, revisit with a custom solution. Content (reuse existing PlayView logic): ``` @@ -316,12 +363,11 @@ Content (reuse existing PlayView logic): │ │ │ ┌──────────────────────────────────┐ │ │ │ │ │ -│ │ [Board Renderer] │ │ BoardRenderer fillHeight -│ │ │ │ +│ │ [Board Renderer] │ │ Card-swipe container +│ │ (Spotify-style swipe) │ │ Finger drags card, next +│ │ │ │ card slides in from edge │ └──────────────────────────────────┘ │ │ │ -│ ← swipe left/right indicators → │ Swipe navigation -│ │ │ ┌──────────────────────────────────┐ │ │ │ [🔄] [♡] [👥] [💡LED] [✓Tick] │ │ Action bar │ └──────────────────────────────────┘ │ @@ -332,12 +378,14 @@ Content (reuse existing PlayView logic): └──────────────────────────────────────┘ ``` +**Swipe navigation**: Uses a Spotify-style card swipe pattern. The board renderer is contained in a "card" that translates with the user's finger during horizontal swipe. When the swipe exceeds the threshold, the current card animates off-screen and the next/previous card slides in from the opposite edge. No static arrow indicators are shown - the physical card motion provides all navigation feedback. + Action bar buttons: -- **Mirror** (`SyncOutlined`): Toggle mirrored state (purple when active) -- **Favorite** (`HeartOutlined`/`HeartFilled`): Toggle favorite -- **Party** (`TeamOutlined`): Open party drawer -- **LED** (`BulbOutlined`/`BulbFilled`): Connect/send to board (only if Bluetooth supported) -- **Tick** (`CheckOutlined`): Log ascent +- **Mirror** (`SyncOutlined`): Toggle mirrored state (purple when active, using `themeTokens.colors.purple`). Use existing `mirrorClimb()` from QueueContext. +- **Favorite** (`HeartOutlined`/`HeartFilled`): Toggle favorite. Use `useFavorite()` hook. +- **Party** (`TeamOutlined`): Open party drawer. Render `ShareBoardButton` or trigger its drawer. +- **LED** (`BulbOutlined`/`BulbFilled`): Connect/send to board (only if Bluetooth supported). **Note**: `SendClimbToBoardButton` is dynamically imported with `ssr: false` because LED placement data is ~50KB. The play drawer should also use dynamic import or the extracted `useBluetoothConnection` hook (Phase 5). +- **Tick** (`CheckOutlined`): Log ascent. Use `TickButton` component. Mini transport controls: - Shows previous/current/next climb names @@ -346,25 +394,51 @@ Mini transport controls: Key implementation details: - Extract the swipe navigation logic from `play-view-client.tsx` into a shared hook: `usePlayViewNavigation()` -- The drawer should manage its own URL state - when opened, optionally push a `#playing` hash to enable back-button closing -- When the drawer is open and the user navigates (swipe/buttons), update the queue's currentClimbQueueItem without URL navigation +- **Note on swipe thresholds**: `play-view-client.tsx` uses `SWIPE_THRESHOLD=80`, while `queue-control-bar.tsx` and `queue-list-item.tsx` use `SWIPE_THRESHOLD=100`. The shared hook should use 80 (the play view value) since it's for navigating between climbs in the renderer view. +- The drawer should manage its own URL state - when opened, push a `#playing` hash to enable back-button closing via `popstate` event listener +- When the drawer is open and the user navigates (swipe/buttons), update the queue's `currentClimbQueueItem` via `setCurrentClimbQueueItem()` without URL navigation - The existing `/play/[uuid]` route should remain functional for direct links and desktop, but mobile primarily uses the drawer +- **BoardRenderer lazy mounting**: Only mount `BoardRenderer` when the drawer is open (`destroyOnClose` or conditional render) to avoid performance impact. BoardRenderer renders an SVG with potentially hundreds of hold elements. +- **Wake lock**: The play drawer should also use `useWakeLock(true)` when open, matching the behavior of `SendClimbToBoardButton` when Bluetooth is connected. + +**Card-swipe navigation pattern (Spotify-style)**: +- Remove the static arrow indicators (`LeftOutlined`/`RightOutlined` overlays) from the current play view +- Instead, the board renderer area becomes a horizontally swipeable card container +- During a swipe: the current card (board renderer) translates with the finger via `transform: translateX(${swipeOffset}px)` +- The next/previous card is pre-rendered off-screen (e.g., `translateX(100%)` or `translateX(-100%)`) and moves into view as the current card moves out +- On swipe completion (past threshold): animate the current card fully off-screen and the next card to center position using CSS transitions (~300ms ease-out) +- On swipe cancellation (below threshold): snap the current card back to center +- **Pre-rendering consideration**: Pre-rendering the next/previous BoardRenderer could be expensive. Options: + 1. **Lightweight preview**: Show a placeholder (climb thumbnail or miniature board) as the incoming card, then swap to full BoardRenderer after the animation completes + 2. **Cached render**: If the BoardRenderer is pure (same props = same output), consider caching the SVG output + 3. **Defer rendering**: Only render the incoming card's BoardRenderer after the swipe animation settles. During the swipe, show a colored placeholder card with the climb name/grade. + - **Recommended**: Option 3 (defer rendering) for simplicity. The transition is fast enough (~300ms) that a brief placeholder is acceptable. **New file: `packages/web/app/components/play-view/use-play-view-navigation.ts`** - Shared hook extracting swipe logic from play-view-client.tsx -- Handles: handleNext, handlePrevious, swipeHandlers, swipeOffset, showSwipeHint +- Handles: handleNext, handlePrevious, swipeHandlers, swipeOffset, isAnimating, direction +- **No longer provides**: showSwipeHint (removed - the card motion itself is the feedback) +- Parameters: `{ boardDetails, angle, navigateOnChange?: boolean }` - when `navigateOnChange=true` (page route), push URL; when `false` (drawer), just update queue state +- Returns `{ swipeHandlers, swipeOffset, isAnimating, handleNext, handlePrevious, nextClimb, prevClimb }` - Used by both PlayViewDrawer and PlayViewClient ### 3C: Impact on existing Play route **Modify: `.../play/[climb_uuid]/play-view-client.tsx`** - Keep the full page play view for desktop users and direct URL access -- Extract navigation logic to the shared hook -- On mobile, consider auto-redirecting to list view with the drawer open (or just keep both paths working) +- Extract navigation logic to the shared hook (`usePlayViewNavigation` with `navigateOnChange: true`) +- **Replace** the static arrow indicator overlays (`LeftOutlined`/`RightOutlined` with opacity transitions) with the Spotify-style card-swipe animation (board renderer card translates with finger, next card slides in) +- **Remove** the `showSwipeHint` text ("Swipe left/right to navigate") - the card motion is self-explanatory +- On mobile, consider auto-redirecting to list view with the drawer open (or just keep both paths working). **Recommendation**: Keep both working. Direct links to `/play/` should work on mobile without redirect - forcing a redirect would break shared URLs. **Modify: `.../play/layout-client.tsx`** - No changes needed for desktop sidebar behavior +### Drawer state management +- The play drawer state (`isPlayDrawerOpen`) lives in `QueueControlBar` as local state +- **Potential issue**: If the play drawer is open and the user taps the Queue button, both drawers could overlap. Solution: Close the play drawer before opening the queue drawer. Use a single `activeDrawer` state: `'none' | 'play' | 'queue'` +- **Potential issue**: If the play drawer is open and the user triggers a page navigation (e.g., tapping a bottom tab), the drawer should auto-close. Listen for pathname changes via `usePathname()` to close the drawer. + --- ## Phase 4: Header Simplification @@ -376,33 +450,39 @@ Reduce header clutter by moving elements to the bottom tab bar and queue control **Modify: `packages/web/app/components/board-page/header.tsx`** -Removals: -- **Remove**: `ShareBoardButton` (party mode) - moves to QueueControlBar -- **Remove**: `SendClimbToBoardButton` (LED) - moves to play view drawer action bar -- **Remove**: Mobile `SearchButton` - moves to bottom tab bar -- **Remove**: "Create Climb" from mobile meatball menu - moves to bottom tab bar - -The header becomes: -``` -Mobile: [Logo] [Search Input] [Angle ▾] [≡ Menu] -Desktop: [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] [User ▾] -``` +Removals (mobile only): +- **Remove**: `ShareBoardButton` (party mode) from the `onboarding-party-light-buttons` span - moves to QueueControlBar +- **Remove**: `SendClimbToBoardButton` (LED) from the `onboarding-party-light-buttons` span - moves to play view drawer action bar +- **Remove**: Mobile `SearchButton` - moves to bottom tab bar (but **keep** `SearchClimbNameInput` text input) +- **Remove**: "Create Climb" from `mobileMenuItems` array (key: `create-climb`) - moves to bottom tab bar -On mobile the header is now much simpler - just branding, search text input, angle selector, and the user menu. +Desktop retains all buttons unchanged (wrapped in existing `.desktopOnly` CSS class). -On play/view pages (mobile), the header shows: +The header becomes: ``` -[← Back] [Logo] [Angle ▾] [≡ Menu] +Mobile (list page): [Logo] [Search Input] [Angle ▾] [≡ Menu] +Mobile (play/view): [← Back] [Logo] [Angle ▾] [≡ Menu] +Mobile (create): [Logo] [Cancel] [Beta] [Publish] (unchanged) +Desktop: [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] [User ▾] ``` -**Modify: mobile meatball menu items** -- Remove "Create Climb" (now in bottom tab bar's Create drawer) +**Modify: `mobileMenuItems` array in header.tsx** +- Remove "Create Climb" entry (key: `create-climb`) - now in bottom tab bar's Create drawer - Keep: My Playlists, Classify Holds, Profile, Settings, Help, About, Logout +- **Note**: The "My Playlists" item is conditionally shown (`session?.user && playlistsUrl && !isMoonboard`). Since playlists are also accessible via the Create drawer, consider keeping it in the menu for discoverability. + +**Modify: `packages/web/app/components/board-page/header.module.css`** +- The existing `.mobileOnly`, `.desktopOnly`, and `.mobileMenuButton` classes handle responsive visibility. No new CSS classes needed, but verify the `onboarding-party-light-buttons` span is properly hidden on mobile after removing its children. +- If the span becomes empty on mobile, wrap it in a `.desktopOnly` div to avoid empty space. ### Header height - Keep at `8dvh` / min 48px - The reduced content means it could potentially be slimmer, but keep it consistent for now +### Considerations +- **Onboarding tour references**: The tour component uses element IDs like `onboarding-party-light-buttons` to highlight the party/LED buttons. After moving these off the header on mobile, the tour steps may point to elements that don't exist. Update the tour to reference the new locations (QueueControlBar party button, play drawer LED button) or skip those steps on mobile. +- **Dynamic import of SendClimbToBoardButton**: Currently this component is dynamically imported in header.tsx with `ssr: false`. After Phase 4, this import only needs to happen on desktop. The play drawer (Phase 3) handles its own LED button import. Ensure the dynamic import is wrapped in the desktop-only conditional to avoid loading ~50KB of LED placement data on mobile where it's no longer used in the header. + --- ## Phase 5: Party Mode & LED as Drawer @@ -414,33 +494,48 @@ Convert party mode into a bottom drawer (instead of top drawer) and integrate LE **Modify: `packages/web/app/components/board-page/share-button.tsx`** - Change drawer placement from `"top"` to `"bottom"` -- Rename component to `PartyModeButton` (optional, for clarity) -- Add a "Connect to Board" section at the bottom of the drawer when a session is active or as a standalone tab -- The drawer should have two sections/tabs: - 1. **Party Mode** - existing start/join/active session UI - 2. **Connect to Board (LED)** - Bluetooth connection UI - -**New section within the party drawer: LED Connection** +- Change drawer styles from `wrapper: { height: '70vh' }` to appropriate bottom drawer height +- Rename component to `PartyModeButton` (optional, for clarity). **Note**: This requires updating all import sites: `header.tsx`, `queue-control-bar.tsx` (added in Phase 3), and `play-view-drawer.tsx` (added in Phase 3B). If renaming, also update the named export `ShareBoardButton`. +- Add a "Connect to Board" section. The existing drawer already uses AntD `Tabs` for Start/Join session. Add a third tab: + 1. **Start Session** - existing start session UI + 2. **Join Session** - existing join session UI + 3. **Connect to Board (LED)** - Bluetooth connection UI +- When a session is active (connected state), the tab structure collapses to show session info + LED tab + +**New section within the party drawer: LED Connection tab** - "Connect to Board" button (replaces the header LED button on mobile) - Connection status indicator (connected/disconnected) - When connected: "Connected to [device name]" with disconnect option - This reuses the logic from `SendClimbToBoardButton` but presents it differently - On mobile, this is the only way to access LED. On desktop, the header button remains as a quick shortcut. +- **Note**: Show iOS Bluefy recommendation within this tab when `navigator.bluetooth` is not available, reusing the existing modal content from `SendClimbToBoardButton`. +- **Note**: The auto-send on climb change behavior (the `useEffect` watching `currentClimbQueueItem` in `SendClimbToBoardButton`) must be preserved. The extracted `useBluetoothConnection` hook should include this auto-send effect. **Modify: `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx`** - Extract connection logic into a shared hook: `useBluetoothConnection()` - The button component becomes a thin wrapper around the hook - The party drawer's LED section also uses the same hook +- **Keep the dynamic import pattern** - the button still needs `next/dynamic` with `ssr: false` for the header usage on desktop **New file: `packages/web/app/components/board-bluetooth-control/use-bluetooth-connection.ts`** - Extracts: device ref, characteristic ref, connect/disconnect, send climb, connection state -- Returns: `{ isConnected, isLoading, connect, disconnect, sendClimb }` +- Returns: `{ isConnected, isLoading, connect, disconnect, sendClimb, showBluetoothWarning, isBluetoothSupported }` +- **Must include**: The `useWakeLock(isConnected)` call to prevent device sleep while connected +- **Must include**: The disconnection event listener (`gattserverdisconnected`) +- **Must include**: The auto-send effect when `currentClimbQueueItem` changes +- **Must include**: The `convertToMirroredFramesString` logic for mirrored climbs +- **Dependency**: Needs `boardDetails` prop for `getLedPlacements()` and `getBluetoothPacket()` - these require `board_name`, `layout_id`, `size_id` +- **Dependency**: Needs `currentClimbQueueItem` from `useQueueContext()` - decide whether the hook subscribes to context internally or receives it as a parameter. Recommend: receive as parameter for flexibility. ### Party mode trigger locations - **QueueControlBar**: Party mode button (badge shows connected user count) - **Play view drawer action bar**: Party mode button - **Desktop header**: Keeps existing position (ShareBoardButton) +### Considerations +- **Multiple instances of ShareBoardButton**: After Phase 3, `ShareBoardButton` renders in QueueControlBar, the play drawer action bar, and the desktop header. Each instance manages its own drawer state independently, which is fine since only one drawer opens at a time. However, the button's badge (user count) relies on `useQueueContext()` which is shared. +- **Bluetooth singleton**: The `useBluetoothConnection` hook manages refs to a single Bluetooth device/characteristic. If multiple components mount this hook simultaneously, they'll each have independent refs, potentially causing conflicts. Solution: Either use a React context to share Bluetooth state, or ensure only one component mounts the hook at a time (e.g., the party drawer tab is the single source, with only a status indicator elsewhere). + --- ## Phase 6: Queue List Improvements @@ -452,14 +547,18 @@ Minor refinements to the queue list to match the new design language. **Modify: `packages/web/app/components/queue-control/queue-list-item.tsx`** - Ensure visual consistency with the new `ClimbListItem` compact format -- Same height, typography, grade coloring -- Keep existing swipe actions (tick left, delete right) -- Keep drag-and-drop reordering +- Same height (~60-64px), typography, grade coloring +- Keep existing swipe actions (swipe right=tick, swipe left=delete) - these are intentionally different from ClimbListItem's swipe actions (heart/queue) because queue items need tick and remove operations +- Keep drag-and-drop reordering (`@atlaskit/pragmatic-drag-and-drop`) +- Keep the existing ellipsis dropdown menu (View Climb, Tick, Open in App, Remove) +- **Note**: QueueListItem currently uses `Row`/`Col` with `xs`/`sm` breakpoints for layout. Consider switching to flexbox to match ClimbListItem and avoid layout inconsistency at different viewport sizes. -**Modify: queue drawer behavior** +**Modify: queue drawer behavior in `queue-control-bar.tsx`** - Queue drawer now opens from the dedicated Queue button on the bar (not from tapping climb info) - Placement stays bottom, height stays 70% - Add a count badge on the Queue button showing queue length +- **Keep**: The existing `TOUR_DRAWER_EVENT` listener that opens/closes the drawer for the onboarding tour. The tour may need to target the new Queue button instead of the old click area. +- **Keep**: The `handleDrawerOpenChange` callback that scrolls to the current climb when the drawer opens --- @@ -471,16 +570,26 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr ### Desktop-specific behavior 1. **No bottom tab bar** - Hidden via CSS media query (>= 768px) -2. **Sidebar stays** - Queue/Search/Holds tabs in the sidebar (existing) -3. **Header keeps** - Party, LED, Create, User menu buttons in header -4. **Play view** - Desktop users still use the full `/play/` page route -5. **Climb list** - Default to grid (card) mode on desktop, compact (list) mode on mobile -6. **QueueControlBar** - Shows additional prev/next buttons on desktop (restore the `navButtons` span behavior) +2. **Sidebar stays** - Queue/Search/Search by Hold tabs in the sidebar (existing `ListLayoutClient` with 3 tabs, not 2) +3. **Header keeps** - Party, LED, Create, User menu buttons in header (wrapped in `.desktopOnly` class from `header.module.css`) +4. **Play view** - Desktop users still use the full `/play/` page route with the sidebar layout (`play/layout-client.tsx`) +5. **Climb list** - Default to grid (card) mode on desktop, compact (list) mode on mobile. Detect initial preference via `window.matchMedia('(min-width: 768px)')` on first load, but respect any stored localStorage preference. +6. **QueueControlBar** - Shows additional prev/next buttons on desktop (existing `.navButtons` CSS class already handles this), keeps mirror button visible, keeps play link ### Files to modify - **`packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`**: `display: none` for >= 768px -- **`packages/web/app/components/queue-control/queue-control-bar.tsx`**: Conditional rendering of play link on desktop (since desktop doesn't use the drawer) -- **`packages/web/app/components/board-page/header.tsx`**: Desktop keeps party + LED buttons via `.desktopOnly` class +- **`packages/web/app/components/queue-control/queue-control-bar.tsx`**: Conditional rendering of play link and mirror button on desktop (since desktop doesn't use the drawer). Use the existing `.navButtons` CSS pattern or add a new `.desktopOnly` class. +- **`packages/web/app/components/board-page/header.tsx`**: Desktop keeps party + LED buttons via `.desktopOnly` class (already the case after Phase 4 changes) +- **`packages/web/app/components/queue-control/queue-control-bar.module.css`**: May need new CSS classes for desktop-only buttons (mirror, play link) + +### Verification checklist +- The 3-tab sidebar (Queue/Search/Search by Hold) works unchanged +- Desktop header shows: Logo, Search Input, Angle, Create, Party, LED, User dropdown +- Desktop QueueControlBar shows: Mirror, Play link, Prev, Next, Party, Queue, Tick +- Bottom tab bar is invisible on desktop +- Play drawer never opens on desktop (tapping bar navigates to `/play/` route instead) +- Climb list defaults to grid mode on desktop +- All keyboard navigation still works (tab, enter, etc.) --- @@ -526,54 +635,65 @@ Phase 7: Desktop Adaptation ## Component Dependency Graph ``` -layout.tsx +layout.tsx (server component) +├── BoardSessionBridge +│ └── ConnectionSettingsProvider +│ └── GraphQLQueueProvider (provides QueueContext used everywhere) +│ └── PartyProvider +│ ├── BoardSeshHeader (simplified) │ ├── Logo -│ ├── SearchClimbNameInput (mobile) +│ ├── SearchClimbNameInput (mobile, kept for quick name search) │ ├── AngleSelector -│ └── UserMenu +│ ├── UserMenu (mobile meatball + desktop dropdown) +│ ├── CreateModeButtons (only on /create page) +│ └── [Desktop only]: CreateButton, ShareBoardButton, SendClimbToBoardButton, UserDropdown │ ├── Content │ ├── ClimbsList +│ │ ├── ViewModeToggle [NEW] (Grid/List switch, sticky) │ │ ├── ClimbCard (grid mode) │ │ └── ClimbListItem (compact mode) [NEW] │ │ ├── ClimbThumbnail -│ │ ├── Colorized V-Grade +│ │ ├── ClimbTitle (name, grade, stars, AscentStatus) │ │ ├── Swipe: Heart / Add to Queue │ │ └── ClimbActionsDrawer [NEW] +│ │ └── ClimbActions (viewMode="list" or "button") │ │ │ └── ListLayoutClient (desktop sidebar) -│ └── Tabs: Queue | Search +│ └── Tabs: Queue | Search | Search by Hold │ ├── QueueControlBar (redesigned) -│ ├── ClimbThumbnail + ClimbTitle (tap → PlayViewDrawer) -│ ├── PartyModeButton (moved from header) -│ ├── QueueButton [NEW] (opens queue drawer) +│ ├── ClimbThumbnail + ClimbTitle (mobile: tap → PlayViewDrawer) +│ ├── [Desktop only]: MirrorButton, PlayLink, PrevButton, NextButton +│ ├── ShareBoardButton / PartyModeButton (moved from header) +│ ├── QueueButton [NEW] (badge with count, opens queue drawer) │ ├── TickButton -│ ├── Queue Drawer (existing) -│ └── PlayViewDrawer [NEW] -│ ├── ClimbTitle -│ ├── BoardRenderer -│ ├── Swipe Navigation (shared hook) +│ ├── Queue Drawer (existing, bottom, 70%) +│ └── PlayViewDrawer [NEW] (mobile only, bottom, 95-100%) +│ ├── Drag handle (react-swipeable) +│ ├── ClimbTitle (horizontal) + AscentStatus +│ ├── BoardRenderer (lazy-mounted, fillHeight) +│ ├── Swipe Navigation (usePlayViewNavigation hook) │ ├── Action Bar [Mirror, Heart, Party, LED, Tick] │ └── Mini Transport [Prev | Current | Next] │ -└── BottomTabBar [NEW] +└── BottomTabBar [NEW] (mobile only) ├── Climbs Tab → Navigate to /list - ├── Search Tab → Open SearchDrawer + ├── Search Tab → Open advanced SearchDrawer └── Create Tab → Open CreateDrawer [NEW] ├── Create Climb → /create route - └── Create Playlist → /playlists route + └── Create Playlist → /playlists route (hidden for MoonBoard) ``` --- ## Shared Hooks to Extract -| Hook | Source | Used By | -|---|---|---| -| `usePlayViewNavigation` | `play-view-client.tsx` | PlayViewDrawer, PlayViewClient | -| `useBluetoothConnection` | `send-climb-to-board-button.tsx` | SendClimbToBoardButton, PartyDrawer LED section | +| Hook | Source | Used By | Key Dependencies | +|---|---|---|---| +| `usePlayViewNavigation` | `play-view-client.tsx` | PlayViewDrawer, PlayViewClient | `useQueueContext()`, `react-swipeable`, `boardDetails`, `angle`. Parameterized with `navigateOnChange` to control URL push behavior. | +| `useBluetoothConnection` | `send-climb-to-board-button.tsx` | SendClimbToBoardButton, PartyDrawer LED section, PlayViewDrawer LED button | `boardDetails` (for LED placements), `currentClimbQueueItem` (for auto-send). Includes `useWakeLock`, disconnect listener, mirrored frames conversion. **Singleton concern**: Must ensure only one active Bluetooth connection across all consumers - consider a BluetoothContext provider instead of a plain hook. | --- @@ -595,8 +715,11 @@ layout.tsx |---|---|---| | Play drawer open/close | Drawer slide-up | 300ms (AntD default) | | Bottom tab active state | Color transition | 150ms ease | -| Swipe action reveal | Transform + opacity | Immediate (follows finger) | -| Swipe snap back | Transform | 150ms ease (existing) | +| Card swipe (play view) | Transform translateX | Immediate (follows finger), 300ms ease-out on release | +| Card swipe (queue bar) | Transform translateX | Immediate (follows finger), 200ms ease-out on release | +| Card swipe snap-back | Transform translateX | 200ms ease (when below threshold) | +| List item swipe action reveal | Transform + opacity | Immediate (follows finger) | +| List item swipe snap-back | Transform | 150ms ease (existing) | | List mode toggle | Fade / layout shift | 200ms ease | | Create drawer open | Drawer slide-up | 300ms | @@ -614,11 +737,13 @@ layout.tsx ## State Management Impact -- **No new contexts needed** - all new components consume existing QueueContext -- **View mode preference**: localStorage (`climbListViewMode: 'compact' | 'grid'`) -- **Play drawer open state**: Local state in QueueControlBar (lifted up if needed) -- **Bottom tab active state**: Derived from current URL pathname -- **Bluetooth connection state**: Extracted to shared hook, existing context patterns +- **One new context likely needed**: `BluetoothContext` to share Bluetooth connection state across `SendClimbToBoardButton` (desktop header), play drawer LED button, and party drawer LED tab. A plain hook would create independent connection instances. Alternatively, keep the hook-only approach but mount it in exactly one place and pass state down via props. +- **Existing contexts consumed**: `QueueContext` (via `useQueueContext()`), `BoardProvider` (via `useBoardProvider()`), `FavoritesProvider` (via `useFavorite()`) +- **View mode preference**: localStorage (`climbListViewMode: 'compact' | 'grid'`). Initialize from `window.matchMedia` on first load if no stored preference. +- **Play drawer open state**: Local state in QueueControlBar. Use a single `activeDrawer: 'none' | 'play' | 'queue'` state to prevent drawer stacking conflicts. +- **Bottom tab active state**: Derived from current URL pathname via `usePathname()` +- **Bluetooth connection state**: Extracted to shared hook or context (see above) +- **Hash state for play drawer**: `#playing` hash pushed on drawer open, cleared on close. `popstate` listener to close drawer on back button. --- @@ -626,12 +751,17 @@ layout.tsx | Risk | Impact | Mitigation | |---|---|---| -| Play drawer performance | Board renderer in drawer might be slow | Lazy-render drawer content; only mount BoardRenderer when drawer is open | -| Swipe conflict with scroll | Horizontal swipes on list items could interfere with vertical scroll | Existing pattern handles this (preventScrollOnSwipe + delta threshold) | -| Desktop regression | Moving buttons around might break desktop flow | Phase 7 explicitly verifies desktop; use `.desktopOnly`/`.mobileOnly` classes | -| Deep link to /play/ | Existing play URLs must still work | Keep the route, just make mobile default to drawer from list | -| Party mode button discovery | Moving from header to bar might confuse users | Badge with user count draws attention; tooltip on first use | -| Double drawer stacking | Play drawer open + queue drawer open | Close play drawer before opening queue, or stack with z-index | +| Play drawer performance | BoardRenderer in drawer renders complex SVG with hundreds of hold elements | Lazy-render: only mount BoardRenderer when drawer `open=true`. Use AntD Drawer's `destroyOnClose` or conditional render. | +| Swipe conflict with scroll | Horizontal swipes on ClimbListItem could interfere with vertical scroll in the list | Copy the direction-detection pattern from `queue-list-item.tsx` (not `queue-control-bar.tsx`). The `isHorizontalSwipe` state with `absX > absY` check on first 10px of movement is critical. | +| Desktop regression | Moving buttons around might break desktop flow | Phase 7 explicitly verifies desktop. Use existing `.desktopOnly`/`.mobileOnly` CSS classes from `header.module.css`. Add equivalent classes to `queue-control-bar.module.css`. | +| Deep link to /play/ | Existing play URLs must still work on mobile | Keep the `/play/` route fully functional. Don't auto-redirect to list+drawer. Both paths work. | +| Party mode button discovery | Moving from header to bar might confuse users | Badge with user count draws attention. Consider a one-time tooltip on the new party button location using the existing onboarding tour system. | +| Double drawer stacking | Play drawer open + queue drawer open simultaneously | Use single `activeDrawer` state in QueueControlBar: `'none' | 'play' | 'queue'`. Opening one automatically closes the other. | +| Bluetooth singleton conflict | Multiple components mounting `useBluetoothConnection` create independent connections | Use a BluetoothContext provider or ensure hook is mounted in exactly one place with state shared via props/context. | +| AntD Drawer drag-to-dismiss | AntD Drawer doesn't natively support drag-to-close gesture | Implement custom drag handle with `react-swipeable` at the top of the drawer. On downward swipe past threshold, call `onClose`. May feel less smooth than native sheet - test early. | +| LED data bundle size | LED placement data (~50KB) loaded via `getLedPlacements()` | Currently dynamically imported in header. After redesign, also needed in play drawer. Use dynamic import in both locations, or move to BluetoothContext that lazy-loads data on first connection attempt. | +| Onboarding tour breakage | Tour steps reference element IDs that move or disappear on mobile | Audit all tour step selectors (`onboarding-queue-bar`, `onboarding-party-light-buttons`, `onboarding-climb-card`, `onboarding-queue-toggle`) and update targets for the new layout. | +| Create URL construction | BottomTabBar and CreateDrawer need board route context to build `/create` and `/playlists` URLs | Pass `boardDetails` and `angle` as props from `layout.tsx`, or use `useParams()` to read from URL. Prefer props from the server component for accuracy. | --- @@ -640,53 +770,127 @@ layout.tsx ### Phase 1 - [ ] Bottom tab bar renders on mobile, hidden on desktop - [ ] Climbs tab navigates to list -- [ ] Search tab opens search drawer +- [ ] Search tab opens advanced search drawer (same as old header SearchButton) - [ ] Create tab opens create drawer - [ ] Create drawer links work (create climb, create playlist) -- [ ] iOS safe area padding works +- [ ] Create drawer hides playlist option for MoonBoard +- [ ] iOS safe area padding works (`env(safe-area-inset-bottom)`) +- [ ] Tab bar does not overlap QueueControlBar +- [ ] Content area scrolling is not blocked by tab bar +- [ ] Tab bar shows on play/view pages, hides on create page ### Phase 2 -- [ ] Compact list items render correctly +- [ ] Compact list items render correctly with proper layout - [ ] Grade colors match existing ClimbTitle colors -- [ ] Swipe right favorites a climb (visual feedback) +- [ ] AscentStatus icon shows on compact list items +- [ ] Swipe right favorites a climb (visual feedback, auth check) - [ ] Swipe left adds to queue (visual feedback + queue updates) -- [ ] Ellipsis menu opens drawer with all actions -- [ ] All actions in drawer work (favorite, queue, tick, share, playlist, open-in-app, mirror, view) -- [ ] View mode toggle persists across page loads +- [ ] Swipe direction detection works (vertical scroll not blocked) +- [ ] Ellipsis menu opens bottom drawer with all actions +- [ ] All actions in drawer work (favorite, queue, tick, share, playlist, open-in-app, mirror, fork, view) +- [ ] View mode toggle persists across page loads (localStorage) +- [ ] View mode defaults to compact on mobile, grid on desktop - [ ] Infinite scroll works in both modes -- [ ] Scroll position restoration works in both modes +- [ ] Scroll position restoration (hash-based) works in both modes +- [ ] Selected climb highlighting works in compact mode ### Phase 3 -- [ ] Tapping now-playing bar opens full-screen drawer -- [ ] Play drawer shows board renderer correctly -- [ ] Swipe navigation works in play drawer +- [ ] Tapping now-playing bar opens full-screen drawer (mobile only) +- [ ] Tapping now-playing bar navigates to /play/ route (desktop) +- [ ] Play drawer shows board renderer correctly (lazy-mounted) +- [ ] Card-swipe navigation works in play drawer (content translates, next card slides in) +- [ ] Card-swipe navigation works in QueueControlBar (current climb slides out, next slides in) - [ ] Mirror/favorite/tick actions work in play drawer -- [ ] Drag-to-close works smoothly -- [ ] Queue button opens queue drawer +- [ ] Drag-to-close works smoothly (custom react-swipeable handle) +- [ ] Back button closes play drawer (hash-based `#playing`) +- [ ] Queue button opens queue drawer (play drawer closes first) - [ ] Party button opens party drawer -- [ ] Desktop still navigates to /play/ route -- [ ] /play/ URLs still work for direct links +- [ ] Desktop QueueControlBar still has mirror, play link, prev/next buttons +- [ ] /play/ URLs still work for direct links on all devices +- [ ] Wake lock activates when play drawer is open ### Phase 4 -- [ ] Header is simplified on mobile -- [ ] All removed items are available elsewhere -- [ ] Desktop header still has all buttons +- [ ] Header is simplified on mobile (only Logo, SearchInput, Angle, Menu) +- [ ] All removed mobile items are accessible elsewhere (party→bar, LED→play drawer, search→tab bar, create→tab bar) +- [ ] Desktop header still has all buttons (Create, Party, LED, User) - [ ] No broken references to moved components +- [ ] Onboarding tour steps still target valid elements +- [ ] SendClimbToBoardButton dynamic import only loads on desktop ### Phase 5 -- [ ] Party drawer opens from bottom -- [ ] LED connection section works within party drawer -- [ ] Bluetooth hook works in both contexts -- [ ] Start/join/leave session flows work +- [ ] Party drawer opens from bottom (not top) +- [ ] Party drawer has 3 tabs: Start, Join, Connect to Board +- [ ] LED connection tab shows Bluefy recommendation on iOS +- [ ] Bluetooth hook works from party drawer LED tab +- [ ] Start/join/leave session flows work unchanged - [ ] LED auto-sends on climb change (existing behavior preserved) +- [ ] Wake lock activates when Bluetooth connected +- [ ] No duplicate Bluetooth connections from multiple hook instances ### Phase 6 -- [ ] Queue list items visually match compact climb list items -- [ ] Drag-and-drop still works -- [ ] Queue badge shows correct count +- [ ] Queue list items visually match compact climb list items (height, typography) +- [ ] Drag-and-drop reordering still works +- [ ] Queue badge shows correct count on Queue button +- [ ] Onboarding tour can still open/close queue drawer ### Phase 7 -- [ ] Desktop sidebar works unchanged +- [ ] Desktop sidebar works unchanged (3 tabs: Queue, Search, Search by Hold) - [ ] Desktop header has all expected buttons -- [ ] Desktop play page works as before +- [ ] Desktop play page works as before (full route with sidebar) - [ ] Responsive breakpoints are clean (no flickering between mobile/desktop) +- [ ] Bottom tab bar is invisible on desktop +- [ ] Climb list defaults to grid on desktop +- [ ] Desktop QueueControlBar shows all buttons (mirror, play, prev, next, party, queue, tick) + +--- + +## Architecture Notes & Decisions + +### Server vs Client Component Boundary + +The main `layout.tsx` is a **server component** that wraps everything in providers. New client components (BottomTabBar, PlayViewDrawer, CreateDrawer) must be placed within the existing client boundary. Specifically: + +- `BottomTabBar` needs `QueueContext` access (for active state) → must be inside `GraphQLQueueProvider` +- `PlayViewDrawer` needs `QueueContext` + `BoardProvider` → must be inside both providers +- `CreateDrawer` needs route params → can use `useParams()` or receive props from layout + +The current provider stack is: `BoardSessionBridge > ConnectionSettingsProvider > GraphQLQueueProvider > PartyProvider`. All new components go inside this stack. + +### Existing Swipe Pattern Variants + +The codebase has three distinct swipe implementations that will be consolidated: + +1. **`queue-control-bar.tsx`** - Swipe left/right for prev/next climb. Uses `react-swipeable` with `preventScrollOnSwipe: true`. Reveals FastBackward/FastForward icons behind a translating bar. `SWIPE_THRESHOLD=100`. **Changing to**: Spotify card-swipe animation. + +2. **`queue-list-item.tsx`** - Swipe left=delete, right=tick. Uses direction detection (`isHorizontalSwipe`) to avoid scroll conflicts. Has `preventScrollOnSwipe: false` and manual `preventDefault()`. `SWIPE_THRESHOLD=100`. **Keeping as-is** (list item reveal pattern, not card swipe). + +3. **`play-view-client.tsx`** - Swipe left/right for prev/next. Shows static arrow indicators on swipe progress. `SWIPE_THRESHOLD=80`. **Changing to**: Spotify card-swipe animation. Arrow indicators removed. + +After redesign: +- **Card swipe pattern** (Spotify-style): Used in PlayViewDrawer, PlayViewClient, QueueControlBar. Shared via `usePlayViewNavigation` hook. +- **Action reveal pattern**: Used in ClimbListItem (heart/queue), QueueListItem (tick/delete). Each implements locally but shares the same threshold constants and direction-detection logic. + +### URL Strategy + +The redesign adds a drawer-based play view but does **not** replace URL-based navigation: + +- `/list` - Climb list (unchanged) +- `/play/[uuid]` - Full play page (kept for desktop + direct links) +- `/view/[uuid]` - Climb details page (unchanged) +- `/create` - Climb creation (unchanged) +- `/playlists` - Playlists (unchanged) +- `#playing` hash - Transient state indicating play drawer is open (mobile only) + +No new routes are added. The play drawer is purely a client-side UI overlay. + +### MoonBoard Considerations + +Several features have MoonBoard-specific behavior: +- `boardDetails.board_name === 'moonboard'` checks exist in header.tsx +- MoonBoard doesn't support: playlists, hold classification, mirroring +- MoonBoard has its own renderer (`moonboard-renderer/`) +- The redesign should propagate these checks to new components: + - CreateDrawer: hide "Create Playlist" for MoonBoard + - ClimbListItem: hide mirror swipe/action for MoonBoard + - PlayViewDrawer: hide mirror button for MoonBoard + - Use `boardDetails.supportsMirroring` (existing boolean) rather than name checks where possible From 59c25aeb5561109f07ae2c23c85be14bcb72ea23 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:44:07 +0000 Subject: [PATCH 3/6] Add cleanup & consolidation section, default compact list view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive Cleanup & Consolidation section identifying 7 areas: URL construction (20+ duplicates across 9 files), swipe logic (3 implementations → 2 shared hooks), AddedByAvatar component, dead code removal checklist, inline style migration, board feature checks, and ClearQueueButton extraction (~515 LOC reduction estimated) - Change default list view to compact on all devices (not just mobile), grid mode available via toggle but compact is the primary experience - Map each consolidation to the phase where it should happen (not a separate pass) to avoid touching files twice https://claude.ai/code/session_01VjXZirGhfNKxhAji3shrha --- docs/spotify-ui-redesign-plan.md | 176 +++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 6 deletions(-) diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md index 75345254..99cb00b4 100644 --- a/docs/spotify-ui-redesign-plan.md +++ b/docs/spotify-ui-redesign-plan.md @@ -265,7 +265,7 @@ Add a "compact" display mode for the climb list that renders climbs as slim rows - When in list mode: Render `ClimbListItem` instead of `ClimbCard` - When in list mode: Single column layout (no grid), full width items - When in grid mode: Keep existing 2-column card grid (`Col xs={24} lg={12}`) - - Default to compact/list mode on mobile, grid mode on desktop + - Default to compact/list mode on all devices (user can toggle to grid if preferred) - **Important**: The current `climbs-list.tsx` passes `boardDetails` and `onCoverDoubleClick` to `ClimbCard`. The `ClimbListItem` needs the same props plus the swipe action handlers. Thread `boardDetails` through for thumbnail rendering and action construction. - **Hash-based scroll restoration**: The existing `restoreScrollFromHash()` and `updateHash()` must work with both modes. Since items use `id={climb.uuid}`, this should work unchanged as long as `ClimbListItem` also renders with the same `id` attribute. - **Infinite scroll**: The `IntersectionObserver` on `loadMoreRef` works independently of item type, so no changes needed there. @@ -573,7 +573,7 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr 2. **Sidebar stays** - Queue/Search/Search by Hold tabs in the sidebar (existing `ListLayoutClient` with 3 tabs, not 2) 3. **Header keeps** - Party, LED, Create, User menu buttons in header (wrapped in `.desktopOnly` class from `header.module.css`) 4. **Play view** - Desktop users still use the full `/play/` page route with the sidebar layout (`play/layout-client.tsx`) -5. **Climb list** - Default to grid (card) mode on desktop, compact (list) mode on mobile. Detect initial preference via `window.matchMedia('(min-width: 768px)')` on first load, but respect any stored localStorage preference. +5. **Climb list** - Default to compact (list) mode on all devices. Grid (card) mode available via toggle. Respect any stored localStorage preference. 6. **QueueControlBar** - Shows additional prev/next buttons on desktop (existing `.navButtons` CSS class already handles this), keeps mirror button visible, keeps play link ### Files to modify @@ -588,7 +588,7 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr - Desktop QueueControlBar shows: Mirror, Play link, Prev, Next, Party, Queue, Tick - Bottom tab bar is invisible on desktop - Play drawer never opens on desktop (tapping bar navigates to `/play/` route instead) -- Climb list defaults to grid mode on desktop +- Climb list defaults to compact mode on desktop (grid available via toggle) - All keyboard navigation still works (tab, enter, etc.) --- @@ -739,7 +739,7 @@ layout.tsx (server component) - **One new context likely needed**: `BluetoothContext` to share Bluetooth connection state across `SendClimbToBoardButton` (desktop header), play drawer LED button, and party drawer LED tab. A plain hook would create independent connection instances. Alternatively, keep the hook-only approach but mount it in exactly one place and pass state down via props. - **Existing contexts consumed**: `QueueContext` (via `useQueueContext()`), `BoardProvider` (via `useBoardProvider()`), `FavoritesProvider` (via `useFavorite()`) -- **View mode preference**: localStorage (`climbListViewMode: 'compact' | 'grid'`). Initialize from `window.matchMedia` on first load if no stored preference. +- **View mode preference**: localStorage (`climbListViewMode: 'compact' | 'grid'`). Default to `'compact'` on all devices when no stored preference. - **Play drawer open state**: Local state in QueueControlBar. Use a single `activeDrawer: 'none' | 'play' | 'queue'` state to prevent drawer stacking conflicts. - **Bottom tab active state**: Derived from current URL pathname via `usePathname()` - **Bluetooth connection state**: Extracted to shared hook or context (see above) @@ -789,7 +789,7 @@ layout.tsx (server component) - [ ] Ellipsis menu opens bottom drawer with all actions - [ ] All actions in drawer work (favorite, queue, tick, share, playlist, open-in-app, mirror, fork, view) - [ ] View mode toggle persists across page loads (localStorage) -- [ ] View mode defaults to compact on mobile, grid on desktop +- [ ] View mode defaults to compact on all devices - [ ] Infinite scroll works in both modes - [ ] Scroll position restoration (hash-based) works in both modes - [ ] Selected climb highlighting works in compact mode @@ -839,11 +839,175 @@ layout.tsx (server component) - [ ] Desktop play page works as before (full route with sidebar) - [ ] Responsive breakpoints are clean (no flickering between mobile/desktop) - [ ] Bottom tab bar is invisible on desktop -- [ ] Climb list defaults to grid on desktop +- [ ] Climb list defaults to compact on desktop (grid via toggle) - [ ] Desktop QueueControlBar shows all buttons (mirror, play, prev, next, party, queue, tick) --- +## Cleanup & Consolidation + +The UI redesign touches many of the same files where duplicated patterns exist. This is the ideal time to consolidate them rather than adding more duplication. These cleanups should be done **during** the relevant phase, not as a separate pass. + +### 1. URL Construction Consolidation (do in Phase 1) + +**Problem**: The slug-vs-numeric URL fallback pattern is copy-pasted 20+ times across 9+ files: +```tsx +// This pattern repeats everywhere: +const url = boardDetails.layout_name && boardDetails.size_name && boardDetails.set_names + ? constructPlayUrlWithSlugs(board_name, layout_name, size_name, ...) + : `/${board_name}/${layout_id}/${size_id}/${set_ids.join(',')}/${angle}/play/${uuid}`; +``` + +**Files with duplication**: +- `queue-control-bar.tsx` (2 instances: `buildClimbUrl()`, `getPlayUrl()`) +- `next-climb-button.tsx` (1 instance) +- `previous-climb-button.tsx` (1 instance) +- `play-view-client.tsx` (2 instances: `getBackToListUrl()`, `navigateToClimb()`) +- `climb-view-actions.tsx` (1 instance: `getBackToListUrl()`) +- `playlist-view-actions.tsx` (1 instance: `getBackToListUrl()`) +- `playlists-list-content.tsx` (2 instances) +- `discover-playlists-content.tsx` (1 instance) +- `header.tsx` (2 instances: `getBackToListUrl()`, `createClimbUrl`) + +**Consolidation**: Add safe wrapper functions to `url-utils.ts`: +```tsx +// url-utils.ts - new functions +export const buildClimbPlayUrl = (boardDetails: BoardDetails, angle: Angle, climbUuid: string, climbName?: string) => { ... } +export const buildClimbViewUrl = (boardDetails: BoardDetails, angle: Angle, climbUuid: string, climbName?: string) => { ... } +export const buildClimbListUrl = (boardDetails: BoardDetails, angle: Angle) => { ... } +export const buildCreateUrl = (boardDetails: BoardDetails, angle: Angle) => { ... } +export const buildPlaylistsUrl = (boardDetails: BoardDetails, angle: Angle) => { ... } +``` + +Each function internally handles the slug-vs-numeric check. All 20+ call sites become one-liners. + +**Also extract**: A shared hook `useClimbNavigation(boardDetails, angle)` that returns `{ navigateToClimb, navigateToList, buildClimbUrl }` for components that need router integration + search param preservation. + +### 2. Swipe Logic Consolidation (do in Phases 2-3) + +**Problem**: Three swipe implementations with overlapping but inconsistent code: + +| File | Purpose | Threshold | MAX_SWIPE | Direction Detection | preventScrollOnSwipe | +|------|---------|-----------|-----------|--------------------|--------------------| +| `queue-control-bar.tsx` | Prev/next climb | 100 | 120 | None | `true` | +| `queue-list-item.tsx` | Tick/delete reveal | 100 | 120 | State-based (`isHorizontalSwipe`) | `false` + manual | +| `play-view-client.tsx` | Prev/next climb | 80 | Unclamped | Inline check | `false` | + +**Consolidation**: Create two shared hooks in `packages/web/app/hooks/`: + +1. **`use-card-swipe-navigation.ts`** - Spotify-style card swipe (Phase 3) + - Used by: PlayViewDrawer, PlayViewClient, QueueControlBar + - Handles: translateX animation, next/prev card transition, threshold detection + - Parameters: `{ threshold?, onSwipeLeft, onSwipeRight }` + +2. **`use-swipe-to-reveal.ts`** - Action reveal behind list items (Phase 2) + - Used by: ClimbListItem (new), QueueListItem (refactored) + - Handles: direction detection, clamped offset, action opacity, snap-back + - Parameters: `{ threshold?, maxSwipe?, onSwipeLeft, onSwipeRight, leftAction, rightAction }` + - Includes the `isHorizontalSwipe` direction detection from `queue-list-item.tsx` + +**Naming inconsistency to fix**: `queue-list-item.tsx` has opacity variable names swapped (`leftActionOpacity` controls right-swipe opacity). Fix during extraction. + +### 3. "Added By" Avatar Component (do in Phase 6) + +**Problem**: Identical avatar rendering code (~15 lines) in `queue-control-bar.tsx` and `queue-list-item.tsx`: +```tsx +{item.addedByUser ? ( + + } /> + +) : ( + + } /> + +)} +``` + +**Consolidation**: Extract to `packages/web/app/components/queue-control/added-by-avatar.tsx`: +```tsx +export const AddedByAvatar: React.FC<{ addedByUser?: QueueUser }> = ({ addedByUser }) => { ... } +``` + +### 4. Dead Code Removal (do during each phase) + +After moving components around, the following become dead code and should be deleted: + +**After Phase 3**: +- `play-view-client.tsx`: `showSwipeHint` state and the 3-second timer effect +- `play-view-client.tsx`: Static arrow indicator overlays (`swipeIndicator` CSS classes) +- `play-view.module.css`: `.swipeIndicator`, `.swipeIndicatorLeft`, `.swipeIndicatorRight`, `.swipeIndicatorVisible` classes +- `queue-control-bar.tsx`: `FastBackwardOutlined` / `FastForwardOutlined` imports and swipe action backgrounds (`.swipeAction` colored divs) +- `queue-control-bar.module.css`: `.swipeAction` class (if no longer used) + +**After Phase 4**: +- `header.tsx`: Mobile-only `SearchButton` import and rendering +- `header.tsx`: `create-climb` entry from `mobileMenuItems` array +- Verify the `onboarding-party-light-buttons` span doesn't become an empty wrapper on mobile + +**After Phase 5**: +- `send-climb-to-board-button.tsx`: Connection logic that moves to `use-bluetooth-connection.ts` (the component becomes a thin wrapper) + +### 5. Inline Style Cleanup (ongoing) + +**Problem**: `CLAUDE.md` says "Try to avoid use of the style property", but several key files have 20-40+ inline style objects. + +**Worst offenders being modified in the redesign**: +- `queue-control-bar.tsx` - heavy inline styles for the bar layout, swipe backgrounds, drawer +- `share-button.tsx` - inline styles for party mode drawer sections +- `queue-list-item.tsx` - inline styles for swipe backgrounds, item layout + +**Approach**: As each file is modified during a phase, migrate inline styles to its CSS module. Don't do a separate refactor pass - do it incrementally as code is touched. Priority targets: +- Swipe action backgrounds → CSS module classes +- Flex layout containers → CSS module classes +- Theme token references → CSS custom properties (already partially done in `index.css`) + +### 6. Board Feature Checks Consolidation (do in Phase 2) + +**Problem**: `boardDetails.board_name === 'moonboard'` checks scattered across files. The redesign adds more MoonBoard-conditional behavior (CreateDrawer, ClimbListItem mirror action, PlayViewDrawer mirror button). + +**Consolidation**: Create a utility or use existing `boardDetails` properties: +```tsx +// Already exists but underused: +boardDetails.supportsMirroring // Use this instead of name checks for mirror + +// Add to BoardDetails type if missing: +boardDetails.supportsPlaylists +boardDetails.supportsHoldClassification +boardDetails.supportsClimbCreation +``` + +This avoids the new components needing to know about specific board names. + +### 7. Queue Clear Confirmation Pattern (do in Phase 6) + +**Problem**: Queue clear with confirmation dialog duplicated in: +- `queue-control-bar.tsx` (queue drawer header) +- `list/layout-client.tsx` (desktop sidebar queue tab) +- `play/layout-client.tsx` (desktop play sidebar) + +**Consolidation**: Extract to `packages/web/app/components/queue-control/clear-queue-button.tsx`: +```tsx +export const ClearQueueButton: React.FC<{ boardDetails: BoardDetails }> = ({ boardDetails }) => { ... } +``` + +Includes the Popconfirm, analytics tracking, and `setQueue([])` call. + +### Summary + +| Consolidation | Phase | Files Affected | LOC Saved (est.) | +|---|---|---|---| +| URL construction wrappers | 1 | 9+ files | ~200 lines | +| Swipe hooks extraction | 2-3 | 3 files → 2 hooks | ~150 lines | +| AddedByAvatar component | 6 | 2 files | ~25 lines | +| Dead code removal | 3-5 | 4+ files | ~80 lines | +| Inline style migration | Ongoing | 3+ files | Net zero (moves to CSS) | +| Board feature checks | 2 | 5+ files | ~20 lines + maintainability | +| ClearQueueButton | 6 | 3 files | ~40 lines | +| **Total** | | | **~515 lines** | + +--- + ## Architecture Notes & Decisions ### Server vs Client Component Boundary From 61487bac67642bb00164a46d8b8b30a4341e74b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:47:50 +0000 Subject: [PATCH 4/6] Update bottom tab bar: Home screen (feature-flagged) replaces Climbs tab - First tab is now Home (HomeOutlined icon) instead of Climbs - Add /home route with placeholder page, feature-flagged behind NEXT_PUBLIC_ENABLE_HOME_SCREEN (defaults to false) - When flag is off, Home tab navigates to /list (same as old Climbs tab) - When flag is on, Home tab navigates to /home placeholder - Update wireframes, dependency graph, implementation order, and testing checklist to reflect the Home screen addition https://claude.ai/code/session_01VjXZirGhfNKxhAji3shrha --- docs/spotify-ui-redesign-plan.md | 50 +++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md index 99cb00b4..b5d16d87 100644 --- a/docs/spotify-ui-redesign-plan.md +++ b/docs/spotify-ui-redesign-plan.md @@ -59,7 +59,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ├────────────────────────────────────┤ │ [Thumb] "Current Climb" [Q] [✓] │ ← Now Playing bar (tap=expand) ├────────────────────────────────────┤ -│ 🧗 Climbs 🔍 Search ✚ New │ ← Bottom tab bar +│ 🏠 Home 🔍 Search ✚ New │ ← Bottom tab bar └────────────────────────────────────┘ ``` @@ -83,7 +83,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ├────────────────────────────────────┤ │ [Thumb] "Current Climb" [Q] [✓] │ ├────────────────────────────────────┤ -│ 🧗 Climbs 🔍 Search ✚ New │ +│ 🏠 Home 🔍 Search ✚ New │ └────────────────────────────────────┘ ``` @@ -137,21 +137,40 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ## Phase 1: Bottom Tab Bar ### What changes -Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Climbs**, **Search**, and **Create**. +Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Home**, **Search**, and **Create**. + +### Home Screen + +A new landing/home screen accessible via the first tab. This is a placeholder for a future discovery/dashboard experience. + +**New file: `packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/home/page.tsx`** +- Server component, minimal placeholder content +- For now: a centered message like "Home - Coming Soon" or a redirect to `/list` +- This will eventually become a discovery/dashboard screen (recent climbs, suggested climbs, activity feed, etc.) + +**New file: `packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/home/home-placeholder.tsx`** +- Client component with placeholder UI + +**Feature flag**: `NEXT_PUBLIC_ENABLE_HOME_SCREEN` +- When `'true'`: Home tab navigates to the `/home` route +- When falsy (default): Home tab navigates to `/list` instead (acts as the Climbs tab) +- This matches the existing feature flag pattern (e.g., `NEXT_PUBLIC_ENABLE_ONBOARDING_TOUR` in `onboarding-tour.tsx`) +- Add to `.env.local`: `NEXT_PUBLIC_ENABLE_HOME_SCREEN=false` ### Files to modify 1. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.tsx`** - Client component (`'use client'`) - - Three tabs: Climbs (default active), Search, Create - - Icons: Use AntD icons - `UnorderedListOutlined` / `AppstoreOutlined` for Climbs, `SearchOutlined` for Search, `PlusOutlined` for Create + - Three tabs: Home (default active), Search, Create + - Icons: Use AntD icons - `HomeOutlined` for Home, `SearchOutlined` for Search, `PlusOutlined` for Create - Active state: Primary color (`themeTokens.colors.primary`) for active tab icon + label - Inactive state: `themeTokens.neutral[400]` color - Fixed at the bottom, full width - Height: ~50px with safe-area-inset-bottom padding for iOS - Desktop: Hidden (search/queue available in sidebar, create in header) - - **Props needed**: Requires board route parameters to construct navigation URLs (the tab bar needs to know the current board/layout/size/set/angle to build the `/list` URL) + - **Props needed**: Requires board route parameters to construct navigation URLs (the tab bar needs to know the current board/layout/size/set/angle to build the `/home` or `/list` URL) - **Search drawer integration**: Needs a callback prop or ref to open the search drawer, since the drawer state lives elsewhere + - **Feature flag check**: Read `process.env.NEXT_PUBLIC_ENABLE_HOME_SCREEN` to determine Home tab destination 2. **New file: `packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`** - Media query to hide on desktop (>= 768px) @@ -182,10 +201,10 @@ Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Cli - Simple AntD Drawer with bottom placement, auto height ### Behavior -- **Climbs tab**: Navigates to the climb list view (if not already there). This is the default/home state. +- **Home tab**: When feature flag is enabled, navigates to the `/home` route. When disabled, navigates to the `/list` route (the climb list). This is the default/first tab. - **Search tab**: Opens the advanced search filters drawer (the same one `SearchButton` triggers in the header). On desktop, activates the sidebar search tab instead. - **Create tab**: Opens the CreateDrawer with options. -- Active tab state reflects current context (Climbs when on list, etc.) +- Active tab state reflects current context (Home when on /home or /list, etc.) - On desktop (>= 768px): Tab bar is hidden. Search and create remain in header/sidebar. - **On play/view/create pages**: Consider whether the tab bar should remain visible. On play pages the user may want to quickly return to the list. On create pages it could cause confusion. Recommend: show on play/view, hide on create. @@ -195,6 +214,11 @@ Add a persistent bottom tab bar below the QueueControlBar with three tabs: **Cli - Keep the desktop sidebar search - Remove "Create Climb" from the mobile meatball menu (`mobileMenuItems` in header.tsx, key `create-climb`) +### Home screen route +- Add the `/home` route alongside the existing `/list` route under the `[angle]` segment +- The home page reuses the same layout as list (header + content + queue bar + tab bar) +- The home page is purely a placeholder - feature-flagged off by default, so the Home tab acts as a `/list` navigation until enabled + ### Considerations - **Onboarding tour**: The existing `OnboardingTour` component in `layout-client.tsx` may reference the old header elements. Verify tour steps still point to valid targets after moving buttons. - **Controller mode**: When in controller mode (URL has `controllerUrl` param), the Create tab may not be relevant. Consider hiding it or adjusting behavior. @@ -598,8 +622,9 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr The phases are designed to be implemented sequentially, each building on the previous: ``` -Phase 1: Bottom Tab Bar +Phase 1: Bottom Tab Bar + Home Screen └─ Foundation for new navigation structure + └─ Home screen placeholder (feature-flagged) └─ Create drawer for "New" tab Phase 2: Compact Climb List Mode @@ -679,7 +704,7 @@ layout.tsx (server component) │ └── Mini Transport [Prev | Current | Next] │ └── BottomTabBar [NEW] (mobile only) - ├── Climbs Tab → Navigate to /list + ├── Home Tab → Navigate to /home (feature-flagged) or /list (default) ├── Search Tab → Open advanced SearchDrawer └── Create Tab → Open CreateDrawer [NEW] ├── Create Climb → /create route @@ -769,7 +794,9 @@ layout.tsx (server component) ### Phase 1 - [ ] Bottom tab bar renders on mobile, hidden on desktop -- [ ] Climbs tab navigates to list +- [ ] Home tab navigates to /list when feature flag is off (default) +- [ ] Home tab navigates to /home when `NEXT_PUBLIC_ENABLE_HOME_SCREEN=true` +- [ ] Home placeholder page renders without errors - [ ] Search tab opens advanced search drawer (same as old header SearchButton) - [ ] Create tab opens create drawer - [ ] Create drawer links work (create climb, create playlist) @@ -778,6 +805,7 @@ layout.tsx (server component) - [ ] Tab bar does not overlap QueueControlBar - [ ] Content area scrolling is not blocked by tab bar - [ ] Tab bar shows on play/view pages, hides on create page +- [ ] Active tab state highlights correctly on /home, /list, and other pages ### Phase 2 - [ ] Compact list items render correctly with proper layout From b359b1365275e655a9de7830cad9986b0fbf36f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:55:43 +0000 Subject: [PATCH 5/6] Redesign Phase 4: user avatar drawer replaces meatball/user menus, board selector in drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace meatball menu (mobile) and user dropdown (desktop) with a single user avatar button (top-left) that opens a left-side drawer - User drawer contains: avatar/username, View Profile, Change Board, Settings, Classify Holds, Recents, My Playlists, Help, About, Logout - Board selector ("Change Board") lives in the user drawer, not the header bar — navigates to setup wizard or shows quick-switch options - Recents section reuses session-history-panel.tsx data for board config quick-switching - Update wireframes, component dependency graph, implementation order, dead code list, and testing checklists to match - AuthModal and HoldClassificationWizard move from header to user drawer https://claude.ai/code/session_01VjXZirGhfNKxhAji3shrha --- docs/spotify-ui-redesign-plan.md | 201 ++++++++++++++++++++++++------- 1 file changed, 159 insertions(+), 42 deletions(-) diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md index b5d16d87..0c626fb7 100644 --- a/docs/spotify-ui-redesign-plan.md +++ b/docs/spotify-ui-redesign-plan.md @@ -43,7 +43,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Mobile Layout - List View (Compact Mode) ``` ┌────────────────────────────────────┐ -│ [Logo] [Search Input] [≡ Menu] │ ← Simplified header +│ [👤] [Logo] [Search Input] [Angle]│ ← Simplified header ├────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ @@ -66,7 +66,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Mobile Layout - List View (Expanded/Card Mode) ``` ┌────────────────────────────────────┐ -│ [Logo] [Search Input] [≡ Menu] │ +│ [👤] [Logo] [Search Input] [Angle]│ ├────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ @@ -117,7 +117,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Desktop Layout ``` ┌─────────────────────────────────────────────────────────────┐ -│ [Logo] [Search Input] [Angle ▾] [Create] [≡ User Menu] │ +│ [👤] [Logo] [Search Input] [Angle ▾] [Create] [Party] [LED] │ ├──────────────────────────────────┬──────────────────────────┤ │ │ │ │ Climb list (2-col grid) │ Sidebar (400px) │ @@ -465,47 +465,136 @@ Key implementation details: --- -## Phase 4: Header Simplification +## Phase 4: Header Redesign — Board Selector, User Drawer, Simplified Layout ### What changes -Reduce header clutter by moving elements to the bottom tab bar and queue control bar. +- **Remove** the top-right meatball/ellipsis menu entirely +- **Add** a board selector next to the angle selector for switching boards +- **Add** a user avatar button in the top-right that opens a left-side user drawer +- Reduce header clutter by moving Party/LED/Search to bottom tab bar and queue control bar (from earlier phases) -### Files to modify +### 4A: User Avatar & Left Drawer + +**Remove**: The mobile meatball menu (`MoreOutlined` button + `mobileMenuItems` Dropdown) — entirely deleted +**Remove**: The desktop user dropdown (`UserOutlined` button + `userMenuItems` Dropdown) — replaced by the same drawer + +**New file: `packages/web/app/components/user-drawer/user-drawer.tsx`** + +A left-side drawer triggered by tapping the user avatar in the top-right corner of the header. + +Trigger button: +- Logged in: User's avatar image (`session.user.image`) or initials fallback in a small circular AntD `Avatar` +- Not logged in: Generic `UserOutlined` icon in a circular avatar (neutral background) +- Position: Top-right of header (replaces the meatball menu on mobile, replaces the user dropdown on desktop) +- Works on both mobile and desktop (same component, same behavior) + +Drawer: +- **Placement**: `"left"` (slides in from the left edge) +- **Width**: ~300px on desktop, ~85vw on mobile +- **Content**: + +``` +┌──────────────────────────────┐ +│ ← (close) │ +│ │ +│ ┌─────┐ │ +│ │ │ Username │ +│ │ AVA │ user@email.com │ +│ │ │ │ +│ └─────┘ │ +│ │ +│ [View Profile] │ → /crusher/[user_id] +│ │ +├───────────────────────────────┤ +│ │ +│ 🔀 Change Board │ → board switcher (see below) +│ ⚙ Settings │ → /settings +│ 🎯 Classify Holds │ opens HoldClassificationWizard +│ 🕐 Recents │ → recent boards/sessions +│ 📋 My Playlists │ → /playlists (hidden for MoonBoard) +│ │ +├───────────────────────────────┤ +│ │ +│ ❓ Help │ → /help +│ ℹ️ About │ → /about +│ │ +├───────────────────────────────┤ +│ │ +│ 🚪 Logout │ calls signOut() +│ │ +└───────────────────────────────┘ +``` + +- **Not logged in** variant: Show avatar placeholder + "Sign in" button at the top. Below: Change Board, Recents, Help, About. +- **Change Board**: Navigates to the root setup wizard (`/`) or shows an inline list of `SUPPORTED_BOARDS` (kilter, tension, moonboard if enabled). Tapping a board navigates to `/[board_name]` to pick layout/size/sets. If the user has recently visited other board configurations (from `session-history-panel.tsx` data), those can appear as quick-switch options under Recents as well. +- **Recents section**: Shows recently visited board configurations from stored session data. Each entry shows board name + layout + size, tapping navigates directly to that configuration. This provides a quick way to switch between boards without going through the full setup wizard. + +### 4B: Header Layout After Redesign **Modify: `packages/web/app/components/board-page/header.tsx`** -Removals (mobile only): -- **Remove**: `ShareBoardButton` (party mode) from the `onboarding-party-light-buttons` span - moves to QueueControlBar -- **Remove**: `SendClimbToBoardButton` (LED) from the `onboarding-party-light-buttons` span - moves to play view drawer action bar -- **Remove**: Mobile `SearchButton` - moves to bottom tab bar (but **keep** `SearchClimbNameInput` text input) -- **Remove**: "Create Climb" from `mobileMenuItems` array (key: `create-climb`) - moves to bottom tab bar +All pages: +``` +Mobile: [Avatar] [Logo] [Search Input] [Angle ▾] +Desktop: [Avatar] [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] +``` -Desktop retains all buttons unchanged (wrapped in existing `.desktopOnly` CSS class). +Create page (unchanged): +``` +Mobile: [Logo] [Cancel] [Beta] [Publish] +Desktop: [Logo] [Cancel] [Beta] [Publish] +``` -The header becomes: +Play/view pages: ``` -Mobile (list page): [Logo] [Search Input] [Angle ▾] [≡ Menu] -Mobile (play/view): [← Back] [Logo] [Angle ▾] [≡ Menu] -Mobile (create): [Logo] [Cancel] [Beta] [Publish] (unchanged) -Desktop: [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] [User ▾] +Mobile: [Avatar] [← Back] [Logo] [Angle ▾] +Desktop: [Avatar] [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] ``` -**Modify: `mobileMenuItems` array in header.tsx** -- Remove "Create Climb" entry (key: `create-climb`) - now in bottom tab bar's Create drawer -- Keep: My Playlists, Classify Holds, Profile, Settings, Help, About, Logout -- **Note**: The "My Playlists" item is conditionally shown (`session?.user && playlistsUrl && !isMoonboard`). Since playlists are also accessible via the Create drawer, consider keeping it in the menu for discoverability. +Key changes: +- **Avatar** is now the leftmost element (before Logo) — tapping opens the left user drawer +- **Meatball menu** (`MoreOutlined`) is completely removed — all items move to the user drawer +- **Desktop user dropdown** is completely removed — replaced by the same avatar → drawer +- **Board switching** lives in the user drawer (via "Change Board" item), not the header bar +- Desktop still shows Party, LED, Create buttons inline (unchanged from earlier phases) +- **Mobile `SearchButton`** (advanced filters icon) already moved to bottom tab bar (Phase 1) +- **Mobile `SearchClimbNameInput`** stays in header for quick name search + +### Files to modify + +**Modify: `packages/web/app/components/board-page/header.tsx`** + +Removals: +- **Remove**: `ShareBoardButton` (party mode) from `onboarding-party-light-buttons` span (mobile only) — moved to QueueControlBar in Phase 3 +- **Remove**: `SendClimbToBoardButton` (LED) from `onboarding-party-light-buttons` span (mobile only) — moved to play view drawer in Phase 3 +- **Remove**: Mobile `SearchButton` — moved to bottom tab bar in Phase 1 +- **Remove**: `mobileMenuItems` array and the mobile meatball `Dropdown` entirely +- **Remove**: `userMenuItems` array and the desktop user `Dropdown` entirely +- **Remove**: All `signOut`, `setShowAuthModal`, `setShowHoldClassification` handlers from header (they move to user drawer) + +Additions: +- **Add**: `` component (avatar that opens the drawer) — leftmost position + +**New file: `packages/web/app/components/user-drawer/user-drawer.tsx`** +- Left-side drawer with user info, navigation links, and actions + +**New file: `packages/web/app/components/user-drawer/user-drawer.module.css`** +- Styles for drawer sections, avatar area, menu items **Modify: `packages/web/app/components/board-page/header.module.css`** -- The existing `.mobileOnly`, `.desktopOnly`, and `.mobileMenuButton` classes handle responsive visibility. No new CSS classes needed, but verify the `onboarding-party-light-buttons` span is properly hidden on mobile after removing its children. -- If the span becomes empty on mobile, wrap it in a `.desktopOnly` div to avoid empty space. +- Remove `.mobileMenuButton` class (no longer needed) +- May need new positioning for avatar button ### Header height - Keep at `8dvh` / min 48px -- The reduced content means it could potentially be slimmer, but keep it consistent for now +- The added board selector may make the header slightly more crowded — if needed, make the board selector icon-only on mobile with a tooltip ### Considerations -- **Onboarding tour references**: The tour component uses element IDs like `onboarding-party-light-buttons` to highlight the party/LED buttons. After moving these off the header on mobile, the tour steps may point to elements that don't exist. Update the tour to reference the new locations (QueueControlBar party button, play drawer LED button) or skip those steps on mobile. -- **Dynamic import of SendClimbToBoardButton**: Currently this component is dynamically imported in header.tsx with `ssr: false`. After Phase 4, this import only needs to happen on desktop. The play drawer (Phase 3) handles its own LED button import. Ensure the dynamic import is wrapped in the desktop-only conditional to avoid loading ~50KB of LED placement data on mobile where it's no longer used in the header. +- **Onboarding tour references**: Tour steps referencing `onboarding-party-light-buttons` need updating (Party/LED moved in earlier phases). The user avatar is a new stable target for a "profile" tour step. +- **Dynamic import of SendClimbToBoardButton**: After this phase, only loaded on desktop in the header. Play drawer (Phase 3) handles its own LED button. +- **HoldClassificationWizard**: Currently rendered in `header.tsx` and triggered by meatball menu. After redesign, the wizard should be rendered within or triggered from the user drawer. The drawer can manage its own `showHoldClassification` state. +- **AuthModal**: Currently rendered in `header.tsx`. After redesign, the user drawer handles sign-in. Move `AuthModal` rendering to the user drawer component. +- **Session data for Recents**: The `session-history-panel.tsx` already reads stored sessions from localStorage. Reuse this data source for the Recents section in the user drawer. --- @@ -595,7 +684,7 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr 1. **No bottom tab bar** - Hidden via CSS media query (>= 768px) 2. **Sidebar stays** - Queue/Search/Search by Hold tabs in the sidebar (existing `ListLayoutClient` with 3 tabs, not 2) -3. **Header keeps** - Party, LED, Create, User menu buttons in header (wrapped in `.desktopOnly` class from `header.module.css`) +3. **Header keeps** - Party, LED, Create buttons in header (wrapped in `.desktopOnly` class from `header.module.css`). User avatar + left drawer works the same on desktop (no separate user dropdown). 4. **Play view** - Desktop users still use the full `/play/` page route with the sidebar layout (`play/layout-client.tsx`) 5. **Climb list** - Default to compact (list) mode on all devices. Grid (card) mode available via toggle. Respect any stored localStorage preference. 6. **QueueControlBar** - Shows additional prev/next buttons on desktop (existing `.navButtons` CSS class already handles this), keeps mirror button visible, keeps play link @@ -603,12 +692,12 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr ### Files to modify - **`packages/web/app/components/bottom-tab-bar/bottom-tab-bar.module.css`**: `display: none` for >= 768px - **`packages/web/app/components/queue-control/queue-control-bar.tsx`**: Conditional rendering of play link and mirror button on desktop (since desktop doesn't use the drawer). Use the existing `.navButtons` CSS pattern or add a new `.desktopOnly` class. -- **`packages/web/app/components/board-page/header.tsx`**: Desktop keeps party + LED buttons via `.desktopOnly` class (already the case after Phase 4 changes) +- **`packages/web/app/components/board-page/header.tsx`**: Desktop keeps party + LED + create buttons via `.desktopOnly` class. Avatar + user drawer works the same on both breakpoints. - **`packages/web/app/components/queue-control/queue-control-bar.module.css`**: May need new CSS classes for desktop-only buttons (mirror, play link) ### Verification checklist - The 3-tab sidebar (Queue/Search/Search by Hold) works unchanged -- Desktop header shows: Logo, Search Input, Angle, Create, Party, LED, User dropdown +- Desktop header shows: Avatar, Logo, Search Input, Angle, Create, Party, LED - Desktop QueueControlBar shows: Mirror, Play link, Prev, Next, Party, Queue, Tick - Bottom tab bar is invisible on desktop - Play drawer never opens on desktop (tapping bar navigates to `/play/` route instead) @@ -638,9 +727,11 @@ Phase 3: Now Playing Bar + Play Drawer └─ 3B: Full-screen play drawer └─ 3C: Shared navigation hook -Phase 4: Header Simplification - └─ Remove relocated elements - └─ Verify desktop still complete +Phase 4: Header Redesign — User Drawer + Board Selector + └─ User avatar button + left drawer + └─ Change Board in drawer + └─ Recents from session history + └─ Remove meatball menu + desktop user dropdown Phase 5: Party Mode & LED Drawer └─ Bottom drawer conversion @@ -666,13 +757,24 @@ layout.tsx (server component) │ └── GraphQLQueueProvider (provides QueueContext used everywhere) │ └── PartyProvider │ -├── BoardSeshHeader (simplified) +├── BoardSeshHeader (redesigned) +│ ├── UserDrawerButton [NEW] (avatar → opens left drawer) +│ │ └── UserDrawer [NEW] (left-side) +│ │ ├── Avatar + Username + Email +│ │ ├── View Profile → /crusher/[user_id] +│ │ ├── Change Board → / (setup wizard) or quick-switch +│ │ ├── Settings → /settings +│ │ ├── Classify Holds → HoldClassificationWizard +│ │ ├── Recents (from session-history data) +│ │ ├── My Playlists → /playlists (hidden for MoonBoard) +│ │ ├── Help → /help +│ │ ├── About → /about +│ │ └── Logout / Sign In │ ├── Logo │ ├── SearchClimbNameInput (mobile, kept for quick name search) │ ├── AngleSelector -│ ├── UserMenu (mobile meatball + desktop dropdown) │ ├── CreateModeButtons (only on /create page) -│ └── [Desktop only]: CreateButton, ShareBoardButton, SendClimbToBoardButton, UserDropdown +│ └── [Desktop only]: CreateButton, ShareBoardButton, SendClimbToBoardButton │ ├── Content │ ├── ClimbsList @@ -838,10 +940,19 @@ layout.tsx (server component) - [ ] Wake lock activates when play drawer is open ### Phase 4 -- [ ] Header is simplified on mobile (only Logo, SearchInput, Angle, Menu) -- [ ] All removed mobile items are accessible elsewhere (party→bar, LED→play drawer, search→tab bar, create→tab bar) -- [ ] Desktop header still has all buttons (Create, Party, LED, User) -- [ ] No broken references to moved components +- [ ] Meatball menu is completely removed (mobile and desktop) +- [ ] Desktop user dropdown is completely removed +- [ ] User avatar button appears top-left on all pages +- [ ] Tapping avatar opens left-side user drawer +- [ ] User drawer shows correct content when logged in (avatar, username, email, all menu items) +- [ ] User drawer shows sign-in prompt when logged out +- [ ] "Change Board" navigates to setup wizard or shows board list +- [ ] "Recents" shows recently visited board configurations +- [ ] "Classify Holds" opens HoldClassificationWizard from the drawer +- [ ] Logout works from the drawer +- [ ] Desktop header still has Create, Party, LED buttons inline +- [ ] Mobile header is simplified (Avatar, Logo, SearchInput, Angle) +- [ ] All removed items are accessible via user drawer or other new locations - [ ] Onboarding tour steps still target valid elements - [ ] SendClimbToBoardButton dynamic import only loads on desktop @@ -970,7 +1081,12 @@ After moving components around, the following become dead code and should be del **After Phase 4**: - `header.tsx`: Mobile-only `SearchButton` import and rendering -- `header.tsx`: `create-climb` entry from `mobileMenuItems` array +- `header.tsx`: Entire `mobileMenuItems` array and the meatball `Dropdown` component +- `header.tsx`: Entire `userMenuItems` array and the desktop user `Dropdown` component +- `header.tsx`: `signOut` import and `handleSignOut` handler (moves to user drawer) +- `header.tsx`: `showAuthModal` state and `AuthModal` render (moves to user drawer) +- `header.tsx`: `showHoldClassification` state and `HoldClassificationWizard` render (moves to user drawer) +- `header.module.css`: `.mobileMenuButton` class (no longer needed) - Verify the `onboarding-party-light-buttons` span doesn't become an empty wrapper on mobile **After Phase 5**: @@ -1064,8 +1180,9 @@ After redesign: ### URL Strategy -The redesign adds a drawer-based play view but does **not** replace URL-based navigation: +The redesign adds a drawer-based play view and a new home route: +- `/home` - Home/dashboard placeholder (feature-flagged via `NEXT_PUBLIC_ENABLE_HOME_SCREEN`) - `/list` - Climb list (unchanged) - `/play/[uuid]` - Full play page (kept for desktop + direct links) - `/view/[uuid]` - Climb details page (unchanged) @@ -1073,7 +1190,7 @@ The redesign adds a drawer-based play view but does **not** replace URL-based na - `/playlists` - Playlists (unchanged) - `#playing` hash - Transient state indicating play drawer is open (mobile only) -No new routes are added. The play drawer is purely a client-side UI overlay. +One new route (`/home`) is added. The play drawer is purely a client-side UI overlay. ### MoonBoard Considerations From c44f5a9bbf98b0d7748c1e0dab06773ca6aad234 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 12:05:22 +0000 Subject: [PATCH 6/6] =?UTF-8?q?Remove=20search=20input=20from=20header=20?= =?UTF-8?q?=E2=80=94=20search=20moves=20to=20bottom=20tab=20bar=20Search?= =?UTF-8?q?=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove SearchClimbNameInput and SearchButton from header entirely - Search tab in bottom tab bar opens a full-screen search drawer with name input + BasicSearchForm filters + SearchResultsFooter - Update all wireframes: header now shows [Avatar] [Logo] [Angle] - Update dead code list: SearchClimbNameInput, SearchButton, UISearchParamsProvider all removed from header.tsx - Update component dependency graph, testing checklists, desktop verification sections https://claude.ai/code/session_01VjXZirGhfNKxhAji3shrha --- docs/spotify-ui-redesign-plan.md | 41 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/docs/spotify-ui-redesign-plan.md b/docs/spotify-ui-redesign-plan.md index 0c626fb7..48b14996 100644 --- a/docs/spotify-ui-redesign-plan.md +++ b/docs/spotify-ui-redesign-plan.md @@ -12,7 +12,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis |---|---|---| | Main Layout | `packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/layout.tsx` | Server component. Header + Content + Affixed QueueControlBar. Wraps children in `BoardSessionBridge > ConnectionSettingsProvider > GraphQLQueueProvider > PartyProvider`. | | List Layout | `.../[angle]/list/layout-client.tsx` | Client component. Main content + desktop sidebar with 3 tabs (Queue/Search/Search by Hold). Sidebar uses AntD `Sider` at 400px width. | -| Header | `packages/web/app/components/board-page/header.tsx` | Client component. Logo, search input, angle selector, create (desktop), party, LED, user menu. Mobile has a meatball menu dropdown. Uses `usePageMode()` to adapt layout per page type. | +| Header | `packages/web/app/components/board-page/header.tsx` | Client component. Logo, angle selector, create (desktop), party, LED, user menu. Mobile has a meatball menu dropdown. Uses `usePageMode()` to adapt layout per page type. | | QueueControlBar | `packages/web/app/components/queue-control/queue-control-bar.tsx` | Now-playing bar with swipe left/right (prev/next), queue drawer (bottom, 70%), play button link, mirror, prev/next buttons (desktop-only via `.navButtons` CSS), tick. Also shows "added by" avatar indicator. | | ClimbCard | `packages/web/app/components/climb-card/climb-card.tsx` | Card view with cover image, horizontal ClimbTitle header, action footer. Has two render paths: `ClimbCardWithActions` (generates actions dynamically) and `ClimbCardStatic` (memoized with external actions). | | ClimbTitle | `packages/web/app/components/climb-card/climb-title.tsx` | Name, grade (colorized), quality stars, setter info. Supports `layout="horizontal"` and stacked modes. | @@ -20,7 +20,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis | PlayView | `.../play/[climb_uuid]/play-view-client.tsx` | Full board renderer with swipe navigation. Uses `SWIPE_THRESHOLD=80` (different from queue items' 100). Shows swipe hint that auto-hides after 3 seconds. | | ShareBoardButton | `packages/web/app/components/board-page/share-button.tsx` | Party mode drawer (top placement, 70vh height). Has Start/Join session tabs. Shows users list, QR code, share URL when connected. | | SendClimbToBoardButton | `packages/web/app/components/board-bluetooth-control/send-climb-to-board-button.tsx` | Bluetooth LED connection. Dynamically imported (`next/dynamic`, SSR disabled). Auto-sends climb on `currentClimbQueueItem` change when connected. Uses `useWakeLock` to prevent sleep while connected. Shows iOS Bluefy browser recommendation modal. | -| SearchButton/Drawer | `packages/web/app/components/search-drawer/` | Desktop sidebar filter column (hidden on mobile via CSS module). Mobile search is `SearchClimbNameInput` in header + `SearchButton` icon for advanced filters. | +| SearchButton/Drawer | `packages/web/app/components/search-drawer/` | Desktop sidebar filter column (hidden on mobile via CSS module). Mobile search is `SearchClimbNameInput` in header + `SearchButton` icon for advanced filters. After redesign, search moves entirely to the Search tab/page (bottom tab bar) and desktop sidebar. | | ClimbActions | `packages/web/app/components/climb-actions/` | Modular action system with 10 action types: viewDetails, fork, favorite, queue, tick, openInApp, mirror, share, addToList, playlist. Supports `icon`, `button`, `compact`, and `dropdown` view modes. | | BoardRenderer | `packages/web/app/components/board-renderer/board-renderer.tsx` | SVG board visualization with `fillHeight` option | | ClimbsList | `packages/web/app/components/board-page/climbs-list.tsx` | Client component. Paginated climb list with IntersectionObserver infinite scroll. Uses `Row`/`Col` grid layout (xs=24, lg=12). Deduplicates by UUID. Hash-based scroll position restoration. | @@ -43,7 +43,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Mobile Layout - List View (Compact Mode) ``` ┌────────────────────────────────────┐ -│ [👤] [Logo] [Search Input] [Angle]│ ← Simplified header +│ [👤] [Logo] [Angle ▾] │ ← Simplified header ├────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ @@ -66,7 +66,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Mobile Layout - List View (Expanded/Card Mode) ``` ┌────────────────────────────────────┐ -│ [👤] [Logo] [Search Input] [Angle]│ +│ [👤] [Logo] [Angle ▾] │ ├────────────────────────────────────┤ │ │ │ ┌──────────────────────────────┐ │ @@ -117,7 +117,7 @@ This plan transforms Boardsesh's UI into a Spotify-like experience with a persis ### Desktop Layout ``` ┌─────────────────────────────────────────────────────────────┐ -│ [👤] [Logo] [Search Input] [Angle ▾] [Create] [Party] [LED] │ +│ [👤] [Logo] [Angle ▾] [Create] [Party] [LED] │ ├──────────────────────────────────┬──────────────────────────┤ │ │ │ │ Climb list (2-col grid) │ Sidebar (400px) │ @@ -185,10 +185,10 @@ A new landing/home screen accessible via the first tab. This is a placeholder fo - **Note**: The Content area currently has `height: '80vh'` which will need adjustment to account for the tab bar. Consider using `flex: 1` with proper `calc()` or letting flexbox handle it naturally. 4. **Modify: Mobile search integration** - - The current mobile search has two parts: `SearchClimbNameInput` (text input in header) and `SearchButton` (opens advanced filter drawer). - - The Search tab should open the advanced filter drawer (from `SearchButton`), not replace the text input. - - `search-drawer.tsx` is actually a desktop-only sidebar `FilterColumn` wrapper - the mobile search button (`search-button.tsx`) triggers a separate Drawer. The bottom tab Search action should trigger the same drawer that `SearchButton` opens. - - **Keep the header search text input** (`SearchClimbNameInput`) on mobile - it provides quick name search. The tab bar Search opens the advanced filters drawer. + - The `SearchClimbNameInput` (text input) and `SearchButton` (advanced filters icon) both move out of the header entirely. + - The Search tab in the bottom tab bar navigates to a dedicated search page/view that combines both the name input and the advanced filters in one place. + - `search-drawer.tsx` is actually a desktop-only sidebar `FilterColumn` wrapper - the mobile search button (`search-button.tsx`) triggers a separate Drawer. The bottom tab Search action should either navigate to a search-focused view or open the same drawer that `SearchButton` opens. + - **Recommended approach**: The Search tab opens the search drawer (bottom placement, full-height or near-full) containing `SearchClimbNameInput` at the top + `BasicSearchForm` filters below + `SearchResultsFooter` at the bottom. This gives search its own full-screen experience on mobile while keeping the header clean. 5. **New file: `packages/web/app/components/create-drawer/create-drawer.tsx`** - Bottom drawer with creation options @@ -209,8 +209,8 @@ A new landing/home screen accessible via the first tab. This is a placeholder fo - **On play/view/create pages**: Consider whether the tab bar should remain visible. On play pages the user may want to quickly return to the list. On create pages it could cause confusion. Recommend: show on play/view, hide on create. ### Integration with header -- Remove the mobile `SearchButton` (advanced filters icon) from the header center section (it moves to bottom tab bar) -- **Keep** the `SearchClimbNameInput` text input in the mobile header for quick name-based search +- Remove the mobile `SearchButton` (advanced filters icon) from the header (it moves to bottom tab bar) +- Remove the `SearchClimbNameInput` text input from the header (search now lives in the Search tab's full-screen experience) - Keep the desktop sidebar search - Remove "Create Climb" from the mobile meatball menu (`mobileMenuItems` in header.tsx, key `create-climb`) @@ -535,8 +535,8 @@ Drawer: All pages: ``` -Mobile: [Avatar] [Logo] [Search Input] [Angle ▾] -Desktop: [Avatar] [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] +Mobile: [Avatar] [Logo] [Angle ▾] +Desktop: [Avatar] [Logo] [Angle ▾] [+ Create] [👥 Party] [💡 LED] ``` Create page (unchanged): @@ -548,7 +548,7 @@ Desktop: [Logo] [Cancel] [Beta] [Publish] Play/view pages: ``` Mobile: [Avatar] [← Back] [Logo] [Angle ▾] -Desktop: [Avatar] [Logo] [Search Input] [Angle ▾] [+ Create] [👥 Party] [💡 LED] +Desktop: [Avatar] [Logo] [Angle ▾] [+ Create] [👥 Party] [💡 LED] ``` Key changes: @@ -558,7 +558,7 @@ Key changes: - **Board switching** lives in the user drawer (via "Change Board" item), not the header bar - Desktop still shows Party, LED, Create buttons inline (unchanged from earlier phases) - **Mobile `SearchButton`** (advanced filters icon) already moved to bottom tab bar (Phase 1) -- **Mobile `SearchClimbNameInput`** stays in header for quick name search +- **Mobile `SearchClimbNameInput`** removed from header — search now lives entirely in the Search tab's full-screen experience (Phase 1) ### Files to modify @@ -568,6 +568,8 @@ Removals: - **Remove**: `ShareBoardButton` (party mode) from `onboarding-party-light-buttons` span (mobile only) — moved to QueueControlBar in Phase 3 - **Remove**: `SendClimbToBoardButton` (LED) from `onboarding-party-light-buttons` span (mobile only) — moved to play view drawer in Phase 3 - **Remove**: Mobile `SearchButton` — moved to bottom tab bar in Phase 1 +- **Remove**: `SearchClimbNameInput` — search now lives in the Search tab experience (Phase 1) +- **Remove**: `UISearchParamsProvider` wrapper in header (was there for search components, no longer needed) - **Remove**: `mobileMenuItems` array and the mobile meatball `Dropdown` entirely - **Remove**: `userMenuItems` array and the desktop user `Dropdown` entirely - **Remove**: All `signOut`, `setShowAuthModal`, `setShowHoldClassification` handlers from header (they move to user drawer) @@ -697,7 +699,7 @@ Ensure the desktop experience remains cohesive while the mobile experience is tr ### Verification checklist - The 3-tab sidebar (Queue/Search/Search by Hold) works unchanged -- Desktop header shows: Avatar, Logo, Search Input, Angle, Create, Party, LED +- Desktop header shows: Avatar, Logo, Angle, Create, Party, LED - Desktop QueueControlBar shows: Mirror, Play link, Prev, Next, Party, Queue, Tick - Bottom tab bar is invisible on desktop - Play drawer never opens on desktop (tapping bar navigates to `/play/` route instead) @@ -771,7 +773,6 @@ layout.tsx (server component) │ │ ├── About → /about │ │ └── Logout / Sign In │ ├── Logo -│ ├── SearchClimbNameInput (mobile, kept for quick name search) │ ├── AngleSelector │ ├── CreateModeButtons (only on /create page) │ └── [Desktop only]: CreateButton, ShareBoardButton, SendClimbToBoardButton @@ -951,7 +952,7 @@ layout.tsx (server component) - [ ] "Classify Holds" opens HoldClassificationWizard from the drawer - [ ] Logout works from the drawer - [ ] Desktop header still has Create, Party, LED buttons inline -- [ ] Mobile header is simplified (Avatar, Logo, SearchInput, Angle) +- [ ] Mobile header is simplified (Avatar, Logo, Angle) - [ ] All removed items are accessible via user drawer or other new locations - [ ] Onboarding tour steps still target valid elements - [ ] SendClimbToBoardButton dynamic import only loads on desktop @@ -1080,7 +1081,9 @@ After moving components around, the following become dead code and should be del - `queue-control-bar.module.css`: `.swipeAction` class (if no longer used) **After Phase 4**: -- `header.tsx`: Mobile-only `SearchButton` import and rendering +- `header.tsx`: `SearchButton` import and rendering +- `header.tsx`: `SearchClimbNameInput` import and rendering +- `header.tsx`: `UISearchParamsProvider` wrapper (was there for search components) - `header.tsx`: Entire `mobileMenuItems` array and the meatball `Dropdown` component - `header.tsx`: Entire `userMenuItems` array and the desktop user `Dropdown` component - `header.tsx`: `signOut` import and `handleSignOut` handler (moves to user drawer)