Skip to content

Commit c19adc3

Browse files
feat(06-02): add bilateral persistent muscle highlighting
- Issue 4: Bilateral highlighting - tapping one side highlights both left and right - Issue 5: Persistent highlights - selection stays visible until another muscle is tapped Implementation: - Added selectedRegion state to track active muscle selection - When region selected, both front and back muscles highlighted with frequency 6 (amber color) - Click handler maps library muscle slugs back to regions for bilateral highlighting - Highlights persist across view flips and until user selects different region or taps same region to deselect
1 parent a18b834 commit c19adc3

1 file changed

Lines changed: 68 additions & 8 deletions

File tree

src/ui/components/mobile/MobileHeatmap.tsx

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* and session-persisted view state (TOGGLE-01, TOGGLE-03).
99
*/
1010

11-
import { useMemo, useId } from 'react';
11+
import { useMemo, useId, useState, useEffect, useCallback } from 'react';
1212
import Model from 'react-body-highlighter';
1313
import type { IExerciseData, Muscle } from 'react-body-highlighter';
1414
import { useScientificMuscleVolume } from '@db/hooks';
@@ -109,6 +109,7 @@ export function MobileHeatmap({ profileId, daysBack = 7 }: MobileHeatmapProps):
109109
'scientificmuscle_heatmap_view',
110110
'front'
111111
);
112+
const [selectedRegion, setSelectedRegion] = useState<BodyRegion | null>(null);
112113

113114
// Map stats to muscle-level data
114115
const muscleStats = useMemo((): MuscleStats[] => {
@@ -208,7 +209,12 @@ export function MobileHeatmap({ profileId, daysBack = 7 }: MobileHeatmapProps):
208209
WebkitBackfaceVisibility: 'hidden',
209210
}}
210211
>
211-
<MobileBodyHighlighter type="anterior" regionStats={regionStats} />
212+
<MobileBodyHighlighter
213+
type="anterior"
214+
regionStats={regionStats}
215+
selectedRegion={selectedRegion}
216+
onRegionClick={setSelectedRegion}
217+
/>
212218
</div>
213219

214220
{/* Back Face (Posterior) - pre-rotated 180deg */}
@@ -220,7 +226,12 @@ export function MobileHeatmap({ profileId, daysBack = 7 }: MobileHeatmapProps):
220226
transform: 'rotateY(180deg)',
221227
}}
222228
>
223-
<MobileBodyHighlighter type="posterior" regionStats={regionStats} />
229+
<MobileBodyHighlighter
230+
type="posterior"
231+
regionStats={regionStats}
232+
selectedRegion={selectedRegion}
233+
onRegionClick={setSelectedRegion}
234+
/>
224235
</div>
225236
</div>
226237
</div>
@@ -259,9 +270,13 @@ export function MobileHeatmap({ profileId, daysBack = 7 }: MobileHeatmapProps):
259270
function MobileBodyHighlighter({
260271
type,
261272
regionStats,
273+
selectedRegion,
274+
onRegionClick,
262275
}: {
263276
type: 'anterior' | 'posterior';
264277
regionStats: Map<BodyRegion, { percentage: number }>;
278+
selectedRegion: BodyRegion | null;
279+
onRegionClick: (region: BodyRegion | null) => void;
265280
}): React.ReactElement {
266281
// Generate unique ID for scoped styles
267282
const scopeId = useId().replace(/:/g, '');
@@ -275,7 +290,8 @@ function MobileBodyHighlighter({
275290
const muscles = REGION_TO_LIBRARY_MUSCLES[region][viewKey];
276291

277292
if (muscles.length > 0) {
278-
const frequency = getFrequencyLevel(stats.percentage);
293+
// Use max frequency (6) for selected region to create persistent bilateral highlight
294+
const frequency = selectedRegion === region ? 6 : getFrequencyLevel(stats.percentage);
279295

280296
muscles.forEach((muscle) => {
281297
data.push({
@@ -288,7 +304,7 @@ function MobileBodyHighlighter({
288304
});
289305

290306
return data;
291-
}, [type, regionStats]);
307+
}, [type, regionStats, selectedRegion]);
292308

293309
// Color array for highlighting - uses centralized color scale
294310
const highlightedColors = useMemo(() => {
@@ -298,9 +314,52 @@ function MobileBodyHighlighter({
298314
getVolumeColor(62.5), // frequency 3: 50-75%
299315
getVolumeColor(87.5), // frequency 4: 75-100%
300316
getVolumeColor(100), // frequency 5: 100%+
317+
'rgb(245, 158, 11)', // frequency 6: selected state (amber highlight)
301318
];
302319
}, []);
303320

321+
// Map library muscle slugs back to regions
322+
const muscleToRegion = useMemo(() => {
323+
const map = new Map<string, BodyRegion>();
324+
Object.entries(REGION_TO_LIBRARY_MUSCLES).forEach(([region, views]) => {
325+
[...views.front, ...views.back].forEach((muscle) => {
326+
map.set(muscle, region as BodyRegion);
327+
});
328+
});
329+
return map;
330+
}, []);
331+
332+
// Handle muscle clicks - find region and toggle selection
333+
const handleMuscleClick = useCallback(
334+
(event: MouseEvent) => {
335+
const target = event.target as SVGElement;
336+
if (target.tagName !== 'polygon') return;
337+
338+
// Get muscle ID from polygon's data attribute or class
339+
const muscleId = target.getAttribute('id') || target.classList[0];
340+
if (!muscleId) return;
341+
342+
// Map muscle to region
343+
const region = muscleToRegion.get(muscleId);
344+
if (!region) return;
345+
346+
// Toggle selection
347+
onRegionClick(selectedRegion === region ? null : region);
348+
},
349+
[muscleToRegion, onRegionClick, selectedRegion]
350+
);
351+
352+
// Attach click listener to SVG polygons
353+
useEffect((): (() => void) | undefined => {
354+
const container = document.querySelector(`[data-mobile-heatmap="${scopeId}"]`);
355+
if (!container) return;
356+
357+
container.addEventListener('click', handleMuscleClick as EventListener);
358+
return (): void => {
359+
container.removeEventListener('click', handleMuscleClick as EventListener);
360+
};
361+
}, [scopeId, handleMuscleClick]);
362+
304363
return (
305364
<>
306365
<div className="flex justify-center py-2" data-mobile-heatmap={scopeId}>
@@ -325,12 +384,13 @@ function MobileBodyHighlighter({
325384
transition: all 0.15s ease;
326385
stroke: rgb(24, 24, 27);
327386
stroke-width: 0.5px;
387+
cursor: pointer;
328388
}
329389
330390
[data-mobile-heatmap="${scopeId}"] .rbh polygon:active {
331-
filter: brightness(1.3);
332-
stroke: rgb(245, 158, 11);
333-
stroke-width: 1px;
391+
filter: brightness(1.2);
392+
stroke: rgba(245, 158, 11, 0.5);
393+
stroke-width: 0.75px;
334394
}
335395
336396
@media (hover: hover) {

0 commit comments

Comments
 (0)