|
| 1 | +--- |
| 2 | +phase: 06-carousel-navigation |
| 3 | +plan: 01 |
| 4 | +type: execute |
| 5 | +wave: 1 |
| 6 | +depends_on: [] |
| 7 | +files_modified: |
| 8 | + - package.json |
| 9 | + - package-lock.json |
| 10 | + - src/ui/components/mobile/MobileCarousel.tsx |
| 11 | +autonomous: true |
| 12 | + |
| 13 | +must_haves: |
| 14 | + truths: |
| 15 | + - "User can swipe horizontally between heatmap and muscle list (NAV-01)" |
| 16 | + - "Dot indicators visually track current slide position (NAV-02)" |
| 17 | + - "Swipe gesture feels smooth with momentum and snap behavior" |
| 18 | + artifacts: |
| 19 | + - path: "src/ui/components/mobile/MobileCarousel.tsx" |
| 20 | + provides: "Swipeable carousel container with dot indicators" |
| 21 | + exports: ["MobileCarousel"] |
| 22 | + min_lines: 60 |
| 23 | + key_links: |
| 24 | + - from: "src/ui/components/mobile/MobileCarousel.tsx" |
| 25 | + to: "embla-carousel-react" |
| 26 | + via: "useEmblaCarousel hook" |
| 27 | + pattern: "useEmblaCarousel" |
| 28 | + - from: "MobileCarousel.tsx" |
| 29 | + to: "MobileHeatmap.tsx" |
| 30 | + via: "component import" |
| 31 | + pattern: "import.*MobileHeatmap" |
| 32 | + - from: "MobileCarousel.tsx" |
| 33 | + to: "MobileMuscleList.tsx" |
| 34 | + via: "component import" |
| 35 | + pattern: "import.*MobileMuscleList" |
| 36 | +--- |
| 37 | + |
| 38 | +<objective> |
| 39 | +Create a swipeable carousel container that holds MobileHeatmap and MobileMuscleList as two slides with dot indicators. |
| 40 | + |
| 41 | +Purpose: Enable Instagram-style horizontal swipe navigation between the two mobile views (NAV-01, NAV-02) |
| 42 | +Output: MobileCarousel component using embla-carousel-react with physics-based swipe and dot navigation |
| 43 | +</objective> |
| 44 | + |
| 45 | +<execution_context> |
| 46 | +@./.claude/get-shit-done/workflows/execute-plan.md |
| 47 | +@./.claude/get-shit-done/templates/summary.md |
| 48 | +</execution_context> |
| 49 | + |
| 50 | +<context> |
| 51 | +@.planning/PROJECT.md |
| 52 | +@.planning/ROADMAP.md |
| 53 | +@.planning/STATE.md |
| 54 | +@.planning/phases/06-carousel-navigation/06-RESEARCH.md |
| 55 | + |
| 56 | +@src/ui/components/mobile/MobileHeatmap.tsx |
| 57 | +@src/ui/components/mobile/MobileMuscleList.tsx |
| 58 | +</context> |
| 59 | + |
| 60 | +<tasks> |
| 61 | + |
| 62 | +<task type="auto"> |
| 63 | + <name>Task 1: Install embla-carousel-react and create MobileCarousel component</name> |
| 64 | + <files> |
| 65 | + package.json |
| 66 | + package-lock.json |
| 67 | + src/ui/components/mobile/MobileCarousel.tsx |
| 68 | + </files> |
| 69 | + <action> |
| 70 | +1. Install embla-carousel-react: |
| 71 | + ```bash |
| 72 | + npm install embla-carousel-react |
| 73 | + ``` |
| 74 | + |
| 75 | +2. Create `src/ui/components/mobile/MobileCarousel.tsx` with: |
| 76 | + - Props: `{ profileId: string | null; daysBack?: number }` |
| 77 | + - Use `useEmblaCarousel` with options: |
| 78 | + - `loop: false` (critical: loop breaks with only 2 slides) |
| 79 | + - `dragFree: false` (snap to slides) |
| 80 | + - `startIndex: 0` (NAV-03: default to heatmap) |
| 81 | + - Track selected slide index with `useState` + emblaApi `select` event |
| 82 | + - Structure: |
| 83 | + ```tsx |
| 84 | + <div> |
| 85 | + {/* Carousel viewport */} |
| 86 | + <div className="overflow-hidden" ref={emblaRef}> |
| 87 | + <div className="flex touch-pan-y"> |
| 88 | + {/* Slide 1: Heatmap */} |
| 89 | + <div className="flex-[0_0_100%] min-w-0"> |
| 90 | + <MobileHeatmap profileId={profileId} daysBack={daysBack} /> |
| 91 | + </div> |
| 92 | + {/* Slide 2: Muscle List */} |
| 93 | + <div className="flex-[0_0_100%] min-w-0"> |
| 94 | + <MobileMuscleList profileId={profileId} daysBack={daysBack} /> |
| 95 | + </div> |
| 96 | + </div> |
| 97 | + </div> |
| 98 | + |
| 99 | + {/* Dot indicators */} |
| 100 | + <div className="flex justify-center gap-2 py-4" role="tablist"> |
| 101 | + {[0, 1].map((index) => ( |
| 102 | + <button |
| 103 | + key={index} |
| 104 | + onClick={() => emblaApi?.scrollTo(index)} |
| 105 | + className={/* active: w-4 bg-amber-500, inactive: w-2 bg-primary-600 */} |
| 106 | + role="tab" |
| 107 | + aria-selected={index === selectedIndex} |
| 108 | + aria-label={index === 0 ? 'Body heatmap view' : 'Muscle list view'} |
| 109 | + /> |
| 110 | + ))} |
| 111 | + </div> |
| 112 | + </div> |
| 113 | + ``` |
| 114 | + |
| 115 | +3. Critical implementation details (from RESEARCH.md): |
| 116 | + - Use `touch-pan-y` class on flex container (allows vertical scroll, prevents horizontal browser scroll) |
| 117 | + - Use `min-w-0` on slides (prevents flex overflow) |
| 118 | + - Subscribe to `emblaApi.on('select', callback)` in useEffect with cleanup |
| 119 | + - Update selectedIndex from `emblaApi.selectedScrollSnap()` in callback |
| 120 | + - Dot button styling: active dot is `w-4 bg-amber-500`, inactive is `w-2 bg-primary-600 active:bg-primary-500` |
| 121 | + - Dot transition: `transition-all duration-200 h-2 rounded-full` |
| 122 | + |
| 123 | +4. Export the component as named export. |
| 124 | + </action> |
| 125 | + <verify> |
| 126 | + - `npm run build` succeeds (no TypeScript errors) |
| 127 | + - `npm run lint` passes |
| 128 | + - embla-carousel-react appears in package.json dependencies |
| 129 | + </verify> |
| 130 | + <done> |
| 131 | + - MobileCarousel.tsx exists with useEmblaCarousel setup |
| 132 | + - Component renders MobileHeatmap and MobileMuscleList as slides |
| 133 | + - Dot indicators track selected slide via emblaApi select event |
| 134 | + - Swipe gesture has smooth physics (loop: false, dragFree: false) |
| 135 | + </done> |
| 136 | +</task> |
| 137 | + |
| 138 | +</tasks> |
| 139 | + |
| 140 | +<verification> |
| 141 | +```bash |
| 142 | +# Build passes |
| 143 | +npm run build |
| 144 | + |
| 145 | +# Lint passes |
| 146 | +npm run lint |
| 147 | + |
| 148 | +# Component exists with correct structure |
| 149 | +grep -l "useEmblaCarousel" src/ui/components/mobile/MobileCarousel.tsx |
| 150 | + |
| 151 | +# Dependency installed |
| 152 | +grep "embla-carousel-react" package.json |
| 153 | +``` |
| 154 | +</verification> |
| 155 | + |
| 156 | +<success_criteria> |
| 157 | +- embla-carousel-react is installed in package.json |
| 158 | +- MobileCarousel component created with TypeScript types |
| 159 | +- Component uses useEmblaCarousel with correct options (loop: false) |
| 160 | +- Dot indicators respond to slide changes via select event |
| 161 | +- Build and lint pass |
| 162 | +</success_criteria> |
| 163 | + |
| 164 | +<output> |
| 165 | +After completion, create `.planning/phases/06-carousel-navigation/06-01-SUMMARY.md` |
| 166 | +</output> |
0 commit comments