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' ;
1212import Model from 'react-body-highlighter' ;
1313import type { IExerciseData , Muscle } from 'react-body-highlighter' ;
1414import { 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):
259270function 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