feat: Phase 06 - Carousel Navigation#46
Conversation
- 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
- 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>
There was a problem hiding this comment.
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 completionsrc/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, typessrc/ui/components/mobile/MuscleDetailModal.tsx (1)
16-21: Use@uipath 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: Clarifypassive: falseguidance 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: "withoutpassive: falseto 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 themusclevariable's type doesn't already align withScientificMuscle, 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
highlightedColorsvariable is assigned on Line 383 but the module-level constantHIGHLIGHTED_COLORSis 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 validBodyRegion. Ifexercises[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 BodyRegionsrc/ui/components/WeeklyGoalEditor.tsx (1)
36-42: Verify useEffect dependency stability.The
config.groupsdependency may cause the effect to re-run if theconfigobject reference changes. However, based on theuseEffectiveMuscleGroupConfighook implementation which usesuseMemo, the config reference should be stable.Consider adding a condition to prevent resetting
expandedGroupsif 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:editNamemay desync ifgroup.namechanges externally.If
group.nameis updated from outside (e.g., an optimistic update fails and reverts), the localeditNamestate won't reflect the change since it's only initialized once.Consider adding a sync effect or using
group.namedirectly 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:onBlurand save buttononClickmay both fire.When clicking the save (Check) button,
onBlurfires on the input beforeonClickfires on the button, potentially callinghandleSaveNametwice. While the current implementation has a guard (trimmedName !== group.name), subsequent calls still run the comparison logic unnecessarily.Consider using
onBlurwithrelatedTargetcheck 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
savewrapper 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
saveErrorin the UI when non-null.
229-251: Subtle mutation pattern: arrays are reassigned aftermoveMuscle.Lines 234 and 245 reassign arrays on the
newConfigobject returned bymoveMuscle. While this works becausemoveMusclecreates new arrays, the direct assignment pattern (newConfig.ungrouped = ...,group.muscles = ...) can be confusing and error-prone ifmoveMuscle'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); }
- 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>
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)
embla-carousel-reactv8.6.0 for physics-based swipe handlingMobileCarousel.tsxcomponent with:loop: false,dragFree: false,startIndex: 0Wave 2: Dashboard Integration + UX Improvements (06-02)
Requirements Completed
Commits
Verification
All automated checks passed:
Human testing confirmed smooth swipe UX on mobile devices.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Visual System
Refactor
Settings
Tests & Docs
✏️ Tip: You can customize this high-level summary in your review settings.