Skip to content

feat: Phase 06 - Carousel Navigation#46

Merged
pineapplestrikesback merged 95 commits into
mainfrom
feature/phase-06-carousel-navigation
Jan 23, 2026
Merged

feat: Phase 06 - Carousel Navigation#46
pineapplestrikesback merged 95 commits into
mainfrom
feature/phase-06-carousel-navigation

Conversation

@pineapplestrikesback

@pineapplestrikesback pineapplestrikesback commented Jan 22, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Phase 6: Carousel Navigation - Instagram-style swipeable interface between heatmap and muscle list views on mobile.

Changes

Wave 1: Carousel Component (06-01)

  • ✅ Installed embla-carousel-react v8.6.0 for physics-based swipe handling
  • ✅ Created MobileCarousel.tsx component with:
    • Two slides: MobileHeatmap and MobileMuscleList
    • Dot indicators synced via emblaApi select event
    • Smooth swipe gestures with momentum and snap behavior
    • Critical configs: loop: false, dragFree: false, startIndex: 0

Wave 2: Dashboard Integration + UX Improvements (06-02)

  • ✅ Integrated MobileCarousel into Dashboard (replaces standalone MobileHeatmap)
  • ✅ Implemented 5 UX improvements based on user testing:
    1. Two-line muscle layout - Full muscle names with "current/target" ratio, 4px progress bar below
    2. Clean group headers - Removed set totals, show only name + chevron
    3. Merged Forearms → Arms - Reduced from 7 to 6 UI muscle groups
    4. Bilateral muscle highlighting - Tapping one side highlights both left and right
    5. Persistent highlights - Selection stays visible until user changes it

Requirements Completed

  • NAV-01: Horizontal swipe gesture between heatmap and list slides
  • NAV-02: Dot indicators show current slide position
  • NAV-03: Default view is heatmap (slide 1)

Commits

  • 3b2e7b5: feat(06-01): add swipeable carousel with embla-carousel-react
  • b821b3d: docs(06-01): complete carousel navigation plan
  • 3f0dc3f: feat(06-02): integrate MobileCarousel into Dashboard
  • a18b834: fix(06-02): improve mobile muscle list layout and grouping
  • c19adc3: feat(06-02): add bilateral persistent muscle highlighting
  • b04efbb: docs(06-02): complete dashboard integration and UX improvements plan
  • f069041: docs(06): complete carousel navigation phase

Verification

All automated checks passed:

  • ✅ Build passes (no TypeScript errors)
  • ✅ Lint passes
  • ✅ embla-carousel-react properly installed
  • ✅ Carousel implements all required features
  • ✅ Dashboard integration complete
  • ✅ UX improvements verified in codebase

Human testing confirmed smooth swipe UX on mobile devices.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Mobile-first UI: two-slide swipe carousel (heatmap ↔ muscle list), 3D front/back flip, tappable regions opening a muscle detail panel, collapsible muscle groups, per-muscle progress and single-muscle drill-in.
  • Visual System

    • Centralized perceptually-uniform volume color scale and dark-theme semantic tokens for consistent heatmap and status coloring.
  • Refactor

    • Simplified heatmap to a color-first body visualization (removed floating cards/leader lines).
  • Settings

    • Per-profile custom muscle group editor with drag-and-drop, presets, and save/reset.
  • Tests & Docs

    • New color-scale tests and extensive planning, roadmap, and architecture docs.

✏️ Tip: You can customize this high-level summary in your review settings.

jtomaszewski-gx and others added 30 commits January 18, 2026 11:28
- STACK.md - Technologies and dependencies
- ARCHITECTURE.md - System design and patterns
- STRUCTURE.md - Directory layout
- CONVENTIONS.md - Code style and patterns
- TESTING.md - Test structure
- INTEGRATIONS.md - External services
- CONCERNS.md - Technical debt and issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Mobile muscle heatmap refactor - pattern-driven design replacing label-driven UI
Mode: yolo
Depth: comprehensive
Parallelization: enabled
28 requirements across 8 categories
4 requirements deferred to v2
Phases:
1. Component Foundation: ARCH-01, ARCH-02
2. Visual System: VIS-01, VIS-02, VIS-03
3. Heatmap Core: HEAT-01, HEAT-02, HEAT-03
4. Front/Back Toggle: TOGGLE-01, TOGGLE-02, TOGGLE-03
5. Muscle List: LIST-01 through LIST-04
6. Carousel Navigation: NAV-01, NAV-02, NAV-03
7. Detail Pop-up: DETAIL-01, DETAIL-02, DETAIL-03
8. Tap Interactions: HEAT-04, LIST-05
9. Custom Grouping: GROUP-01 through GROUP-05

All 28 v1 requirements mapped to phases.
Phase 01: Component Foundation
- Implementation decisions documented
- Phase boundary established
Phase 1: Component Foundation
- Standard stack identified (existing hooks, react-body-highlighter)
- Architecture patterns documented (separate mobile component)
- Pitfalls catalogued (data duplication, detection timing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 01: Component Foundation
- 1 plan in 1 wave
- 3 tasks (all autonomous)
- Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add useIsMobileDevice hook with user-agent based detection
- Pattern targets mobile phones (excludes tablets for desktop view)
- Uses useMemo for stable value across renders
- Handles SSR case where navigator is undefined
- Add MobileHeatmap in src/ui/components/mobile/ (ARCH-01)
- Import useScientificMuscleVolume from @db/hooks (ARCH-02)
- Implement loading, error, and success placeholder states
- Tailwind styling consistent with existing dashboard patterns
- Import useIsMobileDevice and MobileHeatmap
- Conditionally render MobileHeatmap on mobile devices
- Render existing MuscleHeatmap on desktop/tablet
- [Rule 1 - Bug] Remove unused view prop (MuscleHeatmap shows both views simultaneously)
- [Rule 1 - Bug] Remove dead toggle buttons that had no effect
Tasks completed: 3/3
- Create device detection hook
- Create mobile heatmap component shell
- Update Dashboard for conditional rendering

SUMMARY: .planning/phases/01-component-foundation/01-01-SUMMARY.md
- Phase 1 verified: 4/4 must-haves confirmed
- ARCH-01, ARCH-02 requirements marked Complete
- STATE.md updated to Phase 2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 02: Visual System
- Implementation decisions documented
- Phase boundary established
Phase 2: Visual System
- Standard stack identified (oklch, Tailwind 4 @theme)
- Architecture patterns documented (centralized color-scale.ts)
- Pitfalls catalogued (HSL interpolation, array mismatch)
- Codebase analysis with files to modify
Phase 02: Visual System
- 2 plans in 2 waves
- Wave 1: 02-01 (color scale utility + CSS tokens)
- Wave 2: 02-02 (component refactoring, depends on 02-01)
- Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create getVolumeColor() for perceptually uniform volume-to-color mapping
- Implement oklch color gradient: purple (0%) -> green (100%) -> red (150%)
- Add getVolumeOpacity() for transparency based on volume percentage
- Add getNoTargetColor() for distinct no-target state visualization
- Include comprehensive unit tests (20 tests, all passing)
- Add surface hierarchy tokens (surface-base, surface-raised, surface-overlay)
- Add text hierarchy tokens (text-primary, text-secondary, text-muted)
- Add semantic status colors (success, warning, danger) aligned with color-scale.ts
- Use oklch() format for perceptual uniformity
- Preserve existing primary and accent color definitions
- Fix TypeScript strict mode issues in color-scale utility
Tasks completed: 2/2
- Create color scale utility with Oklab interpolation
- Add CSS design tokens for dark theme

SUMMARY: .planning/phases/02-visual-system/02-01-SUMMARY.md
- Remove local getHeatColor() function, import from @core/color-scale
- Update highlightedColors to use getVolumeColor for body regions
- Update MuscleCard border colors to use getVolumeColor
- Fix VIS-01: text colors use cool-to-warm progression (no red below 100%)
- Use getNoTargetColor() for body model base color

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add dynamic getProgressBarClass for status-based colors
- Progress bar: teal (in progress) -> amber (75%+) -> green (100%+ goal met)
- Use bg-surface-raised semantic token for card background
- Follows VIS-01/VIS-02: green at goal, consistent semantic colors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tasks completed: 2/2
- Refactor MuscleHeatmap to use centralized colors
- Refactor TotalVolumeCard to use CSS tokens

Phase 02 (Visual System) complete.
SUMMARY: .planning/phases/02-visual-system/02-02-SUMMARY.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- VIS-01, VIS-02, VIS-03 requirements marked complete
- Phase verified: all must-haves satisfied
- Ready for Phase 3 (Heatmap Core)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 3: Heatmap Core
- Analyzed react-body-highlighter v2.0.5 API and limitations
- Documented existing infrastructure from Phases 1-2
- Identified simplification approach (remove floating cards)
- Mapped requirements HEAT-01, HEAT-02, HEAT-03 to implementation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 03: Heatmap Core
- 2 plans in 1 wave (parallel execution)
- 03-01: Simplify desktop MuscleHeatmap by removing floating cards
- 03-02: Implement mobile body visualization with volume coloring

Requirements addressed:
- HEAT-01: Body fills screen without floating labels
- HEAT-02: Each region displays color based on volume
- HEAT-03: Warm color progression (already done in Phase 2)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add react-body-highlighter Model component for split body view
- Import getVolumeColor/getNoTargetColor from @core/color-scale
- Add REGION_TO_MUSCLES and REGION_TO_LIBRARY_MUSCLES mappings
- Calculate regional stats from muscle volume data
- Render anterior/posterior halves with overflow clipping technique
- Add "no data" state for empty workout weeks
…atmap

- Remove MuscleCard component, CARD_POSITIONS, LEADER_LINE_OFFSETS
- Remove visibleMuscles state and toggle functions
- Remove SVG leader lines rendering
- Remove Show All/Hide All toggle button
- Remove orange divider line between body halves
- Keep SplitBodyHighlighter, region mappings, and color integration
- Color becomes the primary signal for training distribution (HEAT-01)
- Use calc(100vh-220px) for viewport-filling body diagram
- Increase maxWidth from 20rem to 24rem for larger body display
- Add items-center for vertical centering
- Body diagram is now more prominent as primary visual signal
- Increase min-height to 420px for better vertical presence
- Add max-w-md and mx-auto for centered mobile layout
- Increase body model maxWidth to 18rem for better screen fill
- Add subtle drop shadow for depth (orange tint)
- Add :active state for mobile touch feedback
- Add hover states with @media (hover: hover) for non-touch devices
- Faster transition (0.15s) for snappy mobile feel
Tasks completed: 2/2
- Remove floating cards and leader lines
- Optimize layout for screen fill

SUMMARY: .planning/phases/03-heatmap-core/03-01-SUMMARY.md
pineapplestrikesback and others added 21 commits January 23, 2026 16:48
- Add selectedMuscle state for tracking tapped muscle
- Import MuscleDetailModal and ScientificMuscle type
- Wrap muscle rows in button with active:bg-primary-700/50 tap feedback
- Add MuscleDetailModal integration using single-muscle mode
- Group headers unchanged - still expand/collapse on tap
Tasks completed: 2/2
- Add single-muscle mode to MuscleDetailModal
- Add tappable rows to MobileMuscleList

SUMMARY: .planning/phases/08-tap-interactions/08-01-SUMMARY.md
Temporarily disable tap-to-modal on muscle list rows per user request.
Heatmap modals remain functional. TODO comments mark re-enablement points.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 8 verified:
- HEAT-04: Heatmap tap opens modal (working)
- LIST-05: List tap modal (disabled per user request)
- Bilateral tap animation
- Clean tap experience (no blue highlights)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 09: Custom Grouping
- Implementation decisions documented
- Phase boundary established
Phase 9: Custom Grouping
- Standard stack identified (@dnd-kit/core, @dnd-kit/sortable)
- Architecture patterns documented (sortable contexts, auto-save)
- Database schema design (MuscleGroupConfig in Profile)
- Pitfalls catalogued (muscle duplication, empty groups)
- Integration points mapped (MobileMuscleList, MobileHeatmap, Settings)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 09: Custom Grouping
- 3 plans in 3 waves
- Wave 1: Schema + data layer (types, defaults, hook)
- Wave 2: Settings UI (drag-drop editor, picker modal)
- Wave 3: Integration into MobileMuscleList + MobileHeatmap
- Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 09-01 Task 1: Clarify exactly 4 default groups (Push, Pull, Legs, Core)
  with arms distributed into Push/Pull, not 5 groups with empty Arms
- 09-01 Task 2: Add complete hook spec with null profileId handling,
  loading states, and isUsingDefault flag
- 09-02 Task 3: Use crypto.randomUUID() for group IDs instead of
  generateId for guaranteed dnd-kit stability
- 09-03: Add persistence truth for GROUP-05 verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CustomMuscleGroup and MuscleGroupConfig interfaces to schema.ts
- Add optional customMuscleGroups field to Profile interface
- Create muscle-groups.ts with DEFAULT_MUSCLE_GROUP_CONFIG (4 groups: Push, Pull, Legs, Core)
- Add validateMuscleGroupConfig function to enforce business rules
- Add moveMuscle helper for immutable muscle relocation
- MAX_GROUPS constant set to 8
- Add useEffectiveMuscleGroupConfig hook with isUsingDefault flag
- Handle null profileId, loading states, and custom vs default detection
- Add useMuscleGroupMutations hook with saveConfig and resetToDefaults
- Validate config before saving using validateMuscleGroupConfig
- Export hooks from db/hooks/index.ts
Tasks completed: 2/2
- Add schema types and default configuration
- Create useMuscleGroups hook

SUMMARY: .planning/phases/09-custom-grouping/09-01-SUMMARY.md
- Install @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities
- Create ConfirmationDialog with customizable title/message/buttons
- Create SortableMuscleItem with drag handle and remove button
- MusclePickerModal groups available muscles by UI_MUSCLE_GROUPS
- SortableGroupRow handles expand/collapse, inline rename, nested drag
- Delete confirmation when group has muscles
- Muscle reordering via nested DndContext
- MuscleGroupEditor with group drag-and-drop via @dnd-kit
- Add/rename/delete groups with validation (max 8)
- Ungrouped section (fixed at top) for priority muscles
- Hidden section (fixed at bottom) for excluded muscles
- Reset to defaults with confirmation dialog
- Auto-save on every change via useMuscleGroupMutations
- Group IDs use crypto.randomUUID() for dnd-kit stability
Tasks completed: 3/3
- Install dnd-kit and create base components
- Create MusclePickerModal and SortableGroupRow
- Create MuscleGroupEditor and integrate into Settings

SUMMARY: .planning/phases/09-custom-grouping/09-02-SUMMARY.md
- Replace static UI_MUSCLE_GROUPS with useEffectiveMuscleGroupConfig
- Render ungrouped muscles as flat list at top (no accordion)
- Custom groups render in user-defined order with accordion
- Hidden muscles excluded entirely from display
- Group expansion tracks by group.id for stability
- Add useEffectiveMuscleGroupConfig to get hidden muscles set
- Mark regions with all primary muscles hidden as isHidden
- Add frequency 8 (gray color) for hidden regions in body highlighter
- Hidden muscles visible on body (maintains full body shape) but grayed out
- Fix muscle removal: moves to ungrouped instead of deleting (validation error)
- Enable cross-group muscle moves via picker (shows all muscles not in current group)
- Update WeeklyGoalEditor to use custom group config instead of static UI_MUSCLE_GROUPS
- Pass profileId to WeeklyGoalEditor, show ungrouped at top, hide hidden muscles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Refactor MuscleGroupEditor with unified DndContext for both groups and muscles
- Add prefixed muscle IDs (muscle:name) to distinguish from group IDs
- Enable drag-drop of muscles between groups and ungrouped section
- Update MusclePickerModal to display muscles grouped by custom config
- Add new groups at top of list for immediate visibility
- Auto-collapse expanded groups during group drag for cleaner UX
- Remove nested DndContext from SortableGroupRow (now uses parent's)
- Add DragOverlay for visual feedback during muscle drag

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In @.planning/phases/07-detail-pop-up/07-02-PLAN.md:
- Around line 92-100: The plan uses a single muscles prop when rendering
MuscleDetailModal but the component implementation (see MobileHeatmap.tsx)
expects primaryMuscles and relatedMuscles; update the plan to pass the two props
or change the component to accept the single muscles array. Concretely, when
rendering MuscleDetailModal from the plan, replace muscles={selectedRegion ?
REGION_TO_MUSCLES[selectedRegion] : []} with primaryMuscles={...} and
relatedMuscles={...} derived from REGION_TO_MUSCLES[selectedRegion] (or adjust
the MuscleDetailModal prop interface to accept muscles and map/split it inside
the component), and ensure the prop names in the plan match the component
symbols MuscleDetailModal, primaryMuscles, relatedMuscles, REGION_TO_MUSCLES,
and selectedRegion.

In @.planning/phases/07-detail-pop-up/07-CONTEXT.md:
- Around line 26-32: The document contains a contradictory spec between the
bullet "No X button — dismissed by tapping outside" and the later "X button in
corner (alternative dismiss method)"; update the "No X button" bullet under the
modal variant list to match the "Dismiss behavior" section by removing or
changing that line to indicate an X button is present (per 07-01-SUMMARY.md's
requirement of an X button in the top-right with pt-12 padding), so the "Dismiss
behavior" bullets and the modal variant text consistently state the X button
exists as an alternative dismiss method.

In @.planning/phases/09-custom-grouping/09-CONTEXT.md:
- Around line 16-58: The MD001 lint error is from heading level jumps: change
the h3 headings (the lines starting "### Settings UI Layout", "### Muscle
Assignment UX", "### Special Sections", "### Validation & Edge Cases", "###
Default Groups", and "### Claude's Discretion") to h2 (##) so headings are
consistent and the markdownlint rule MD001 is satisfied; update only those
header markers in the block (leave the content and lists unchanged).

In @.planning/ROADMAP.md:
- Line 107: Phase 5 success criteria still lists seven muscle regions (Back,
Chest, Shoulders, Arms, Legs, Core, Forearms) but the domain now merges Forearms
into Arms; update the ROADMAP.md entry "Muscles appear grouped under region
headers (LIST-01)" and the Phase 5 success criteria to list six groups (Back,
Chest, Shoulders, Arms — include Forearms in Arms, Legs, Core) and remove or
clarify any separate "Forearms" mention so the wording reflects the merged
grouping consistently.
- Line 74: Replace the phrase "high/low volume muscles" with the hyphenated
compound adjective "high/low‑volume muscles" (use a non‑breaking hyphen if
possible) in the ROADMAP line so the compound modifier is clear; search for the
exact string "high/low volume muscles" and update it to "high/low‑volume
muscles".

In `@src/ui/components/mobile/MobileHeatmap.tsx`:
- Around line 407-410: The setTimeout callbacks in the tap animation (calls
around onTap(mappedRegion); setTimeout(() => onTap(null), 150)) can run after
unmount; replace the inline timers with a state-driven effect: stop calling
setTimeout inside the click handlers (e.g., handleMuscleClick / any place
calling onTap + setTimeout) and instead set tappedRegion via
onTap(mappedRegion), then add a useEffect that watches tappedRegion and sets a
timer to call setTappedRegion(null) and clears that timer on cleanup;
alternatively track mounted state with a ref and clear any outstanding timers in
a cleanup to prevent calling onTap after unmount. Ensure you update references
to tappedRegion, setTappedRegion, onTap and remove the inline setTimeout calls.
- Around line 466-475: The generated CSS block can emit an empty selector when
REGION_TO_LIBRARY_MUSCLES[selectedRegion][type === 'anterior' ? 'front' :
'back'] is an empty array; update the template generation around selectedRegion
(using selectedRegion, type and scopeId) to only produce the CSS block when the
mapped array has at least one muscle (e.g., compute the array into a variable,
check .length > 0 or filter out falsy entries before join), so you avoid
emitting an empty selector and only inject the stroke rules when there are
actual polygon IDs to target.

In `@src/ui/components/settings/MusclePickerModal.tsx`:
- Around line 33-112: The section keys use the user-editable group.name which
can collide and break React reconciliation; update the grouped list render (the
groupedMuscles.map callback where you currently use key={group.name}) to use a
stable unique identifier instead—preferably an immutable group id property
(e.g., key={group.id}) if groupConfig.groups exposes it, or fallback to a
deterministic composite like key={`group-${index}-${group.name}`} when
building/rendering groupedMuscles; ensure the same stable id is used for that
group's element across renders.
🧹 Nitpick comments (16)
.planning/STATE.md (1)

46-46: Prefer a real heading instead of emphasis.

Markdown lint flags emphasis-as-heading here; use a heading level for consistency.

📝 Suggested tweak
-_Updated after each plan completion_
+#### Updated after each plan completion
src/ui/components/settings/SortableMuscleItem.tsx (1)

46-61: Add accessibility labels to icon-only buttons.

The drag handle and remove buttons contain only icons without accessible labels. Screen reader users won't understand their purpose.

♿ Proposed accessibility improvements
       <button
         type="button"
         className="cursor-grab touch-none text-primary-400 hover:text-white active:cursor-grabbing"
         {...attributes}
         {...listeners}
+        aria-label="Drag to reorder"
       >
         <GripVertical className="h-4 w-4" />
       </button>
       <span className="flex-1 text-sm text-white">{muscle}</span>
       <button
         type="button"
         onClick={() => onRemove(muscle)}
         className="text-primary-400 transition-colors hover:text-red-400"
+        aria-label={`Remove ${muscle}`}
       >
         <X className="h-4 w-4" />
       </button>
.planning/phases/09-custom-grouping/09-RESEARCH.md (1)

196-214: Consider adding language specifier to the file structure code block.

The fenced code block for the file structure lacks a language identifier. While this is a documentation file with minimal impact, adding a specifier improves syntax highlighting in some renderers.

📝 Suggested fix
-```
+```text
 src/
 ├── core/
 │   └── muscle-groups.ts           # DEFAULT config, validation, types
src/ui/components/mobile/MuscleDetailModal.tsx (1)

16-21: Use @ui path alias for local UI imports.

Replace the relative import to comply with the project alias convention. As per coding guidelines, prefer @ui/ path aliases in UI modules.

♻️ Suggested change
-import type { BodyRegion } from './MobileHeatmap';
+import type { BodyRegion } from '@ui/components/mobile/MobileHeatmap';
src/ui/components/settings/MusclePickerModal.tsx (1)

70-83: Add dialog semantics + close button label.

Helps screen readers identify the modal and the icon‑only close action.

♿ Suggested accessibility tweaks
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-      <div className="max-h-[80vh] w-full max-w-md overflow-hidden rounded-lg bg-primary-800">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="muscle-picker-title"
+        className="max-h-[80vh] w-full max-w-md overflow-hidden rounded-lg bg-primary-800"
+      >
@@
-          <h3 className="text-lg font-semibold text-white">Add Muscle</h3>
+          <h3 id="muscle-picker-title" className="text-lg font-semibold text-white">
+            Add Muscle
+          </h3>
           <button
             type="button"
             onClick={onClose}
+            aria-label="Close modal"
             className="text-primary-400 transition-colors hover:text-white"
           >
src/ui/components/settings/ConfirmationDialog.tsx (1)

31-53: Add dialog semantics for accessibility.

This makes the modal discoverable to assistive technologies.

♿ Suggested accessibility tweaks
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-      <div className="max-w-sm rounded-lg bg-primary-800 p-6">
-        <h3 className="mb-2 text-lg font-semibold text-white">{title}</h3>
-        <p className="mb-6 text-primary-200">{message}</p>
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="confirmation-dialog-title"
+        aria-describedby="confirmation-dialog-message"
+        className="max-w-sm rounded-lg bg-primary-800 p-6"
+      >
+        <h3 id="confirmation-dialog-title" className="mb-2 text-lg font-semibold text-white">
+          {title}
+        </h3>
+        <p id="confirmation-dialog-message" className="mb-6 text-primary-200">
+          {message}
+        </p>
.planning/phases/07-detail-pop-up/07-RESEARCH.md (1)

118-121: Clarify passive: false guidance vs. actual implementation.

The research document emphasizes using { passive: false } for touch event listeners (Lines 118-121, 210, 361-363), but the implementation summary (07-01-SUMMARY.md Line 79) states: "without passive: false to preserve vertical scrolling in the muscle list."

Consider adding a note here clarifying when to use passive: false (when blocking scroll is desired) vs. when to omit it (when preserving scrolling within the element is needed). This will help future implementers choose the right approach.

.planning/phases/08-tap-interactions/08-01-PLAN.md (1)

107-115: Consider type-safe approach instead of type assertion.

The plan suggests using muscle as ScientificMuscle (Line 110), which bypasses TypeScript's type checking. If the muscle variable's type doesn't already align with ScientificMuscle, consider updating the iteration to properly type the muscle or add a runtime validation.

This is minor since it's a planning document, but worth noting for the actual implementation.

.planning/phases/07-detail-pop-up/07-02-PLAN.md (1)

257-257: Convert bare URL to proper Markdown link.

Static analysis flagged a bare URL. Wrapping it in angle brackets or using Markdown link syntax improves accessibility and consistency.

Suggested fix
-   - Navigate to http://localhost:3000
+   - Navigate to <http://localhost:3000>
src/ui/components/mobile/MobileHeatmap.tsx (2)

382-384: Remove unnecessary variable assignment.

The highlightedColors variable is assigned on Line 383 but the module-level constant HIGHLIGHTED_COLORS is already accessible. Either use the constant directly or remove the assignment.

Suggested fix
-  // Use module-level constant for highlight colors
-  const highlightedColors = HIGHLIGHTED_COLORS;
-
   // Map library muscle slugs back to regions
   const muscleToRegion = useMemo(() => {

Then update line 430:

-          highlightedColors={highlightedColors}
+          highlightedColors={HIGHLIGHTED_COLORS}

401-403: Unsafe type assertion for region extraction.

The type assertion as BodyRegion (Line 402) doesn't validate that the value is actually a valid BodyRegion. If exercises[0] contains an unexpected value, this could lead to subtle bugs.

Suggested type guard
// Add helper function at module level
const BODY_REGIONS = new Set<string>([
  'chest', 'shoulders', 'traps', 'lats', 'lowerBack',
  'biceps', 'triceps', 'forearms', 'abs', 'obliques',
  'hipFlexors', 'quads', 'hamstrings', 'glutes', 'calves'
]);

function isBodyRegion(value: string | undefined): value is BodyRegion {
  return value !== undefined && BODY_REGIONS.has(value);
}

// Then in handleMuscleClick:
const regionName = stats.data?.exercises?.[0];
if (!isBodyRegion(regionName)) {
  // Fallback logic...
  return;
}
// regionName is now typed as BodyRegion
src/ui/components/WeeklyGoalEditor.tsx (1)

36-42: Verify useEffect dependency stability.

The config.groups dependency may cause the effect to re-run if the config object reference changes. However, based on the useEffectiveMuscleGroupConfig hook implementation which uses useMemo, the config reference should be stable.

Consider adding a condition to prevent resetting expandedGroups if it already has values:

💡 Optional improvement
  useEffect(() => {
    const firstGroup = config.groups[0];
-   if (!configLoading && firstGroup) {
+   if (!configLoading && firstGroup && expandedGroups.size === 0) {
      setExpandedGroups(new Set([firstGroup.id]));
    }
  }, [config.groups, configLoading]);
src/ui/components/settings/SortableGroupRow.tsx (2)

49-52: Potential stale state: editName may desync if group.name changes externally.

If group.name is updated from outside (e.g., an optimistic update fails and reverts), the local editName state won't reflect the change since it's only initialized once.

Consider adding a sync effect or using group.name directly when not editing:

♻️ Suggested fix
 const [isEditing, setIsEditing] = useState(false);
 const [editName, setEditName] = useState(group.name);
+
+// Sync editName when group.name changes externally (and not editing)
+useEffect(() => {
+  if (!isEditing) {
+    setEditName(group.name);
+  }
+}, [group.name, isEditing]);

140-155: Potential double-save: onBlur and save button onClick may both fire.

When clicking the save (Check) button, onBlur fires on the input before onClick fires on the button, potentially calling handleSaveName twice. While the current implementation has a guard (trimmedName !== group.name), subsequent calls still run the comparison logic unnecessarily.

Consider using onBlur with relatedTarget check or a ref-based approach:

♻️ Suggested fix
+const saveButtonRef = useRef<HTMLButtonElement>(null);
+
+const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
+  // Don't save on blur if clicking save/cancel buttons
+  if (e.relatedTarget === saveButtonRef.current) {
+    return;
+  }
+  handleSaveName();
+};
+
 <input
   ...
-  onBlur={handleSaveName}
+  onBlur={handleBlur}
   ...
 />
 <button
+  ref={saveButtonRef}
   ...
 >
src/ui/components/settings/MuscleGroupEditor.tsx (2)

122-131: Consider user feedback for save errors.

The save wrapper catches errors but only logs to console. Users won't see feedback if a save fails.

♻️ Suggested improvement
+const [saveError, setSaveError] = useState<string | null>(null);
+
 const save = useCallback(
   async (newConfig: MuscleGroupConfig): Promise<void> => {
     try {
+      setSaveError(null);
       await saveConfig(newConfig);
     } catch (err) {
       console.error('Failed to save muscle group config:', err);
+      setSaveError('Failed to save changes. Please try again.');
     }
   },
   [saveConfig]
 );

Then display saveError in the UI when non-null.


229-251: Subtle mutation pattern: arrays are reassigned after moveMuscle.

Lines 234 and 245 reassign arrays on the newConfig object returned by moveMuscle. While this works because moveMuscle creates new arrays, the direct assignment pattern (newConfig.ungrouped = ..., group.muscles = ...) can be confusing and error-prone if moveMuscle's implementation changes.

Consider using spread to make immutability explicit:

♻️ Suggested fix for clarity
 if (targetContainer === UNGROUPED_CONTAINER) {
   const newConfig = moveMuscle(config, muscle, { type: 'ungrouped' });
   if (targetIndex !== null && targetIndex !== newConfig.ungrouped.length - 1) {
     const currentIndex = newConfig.ungrouped.indexOf(muscle);
-    newConfig.ungrouped = arrayMove(newConfig.ungrouped, currentIndex, targetIndex);
+    void save({
+      ...newConfig,
+      ungrouped: arrayMove(newConfig.ungrouped, currentIndex, targetIndex),
+    });
+    return;
   }
   void save(newConfig);
 }

Comment thread .planning/phases/07-detail-pop-up/07-02-PLAN.md
Comment thread .planning/phases/07-detail-pop-up/07-CONTEXT.md
Comment thread .planning/phases/09-custom-grouping/09-CONTEXT.md
Comment thread .planning/ROADMAP.md
Comment thread .planning/ROADMAP.md
Comment thread src/ui/components/mobile/MobileHeatmap.tsx
Comment thread src/ui/components/mobile/MobileHeatmap.tsx Outdated
Comment thread src/ui/components/settings/MusclePickerModal.tsx
- Use useEffect to auto-clear tappedRegion instead of setTimeout
  (prevents state updates on unmounted components)
- Guard CSS selector generation when region has no muscles for view
  (fixes invalid empty selector)
- Use stable keys (group.id) in MusclePickerModal instead of group.name
  (prevents React reconciliation issues with user-editable names)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@pineapplestrikesback
pineapplestrikesback merged commit 10ed489 into main Jan 23, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants