@@ -15,6 +15,8 @@ type Props = {
1515 fitVersion : number
1616 /** what the map actually shows (filters + selection applied) */
1717 visible : Activity [ ]
18+ /** history pages are still streaming in */
19+ loading : boolean
1820 hasSelection : boolean
1921 showBadge : boolean
2022 selSummary : string
@@ -81,7 +83,7 @@ function endpointEl(color: string): HTMLDivElement {
8183}
8284
8385const MapView = forwardRef < MapHandle , Props > ( function MapView (
84- { styleUrl, allActivities, fitVersion, visible, hasSelection, showBadge, selSummary, units, detailId, panelOpen, onOpenDetail, children } ,
86+ { styleUrl, allActivities, fitVersion, visible, loading , hasSelection, showBadge, selSummary, units, detailId, panelOpen, onOpenDetail, children } ,
8587 ref ,
8688) {
8789 const containerRef = useRef < HTMLDivElement > ( null )
@@ -107,6 +109,31 @@ const MapView = forwardRef<MapHandle, Props>(function MapView(
107109 left : panelOpenRef . current ? 410 : base ,
108110 } )
109111
112+ // Single fit path for every trigger, so map-load vs data-arrival ordering
113+ // doesn't matter: whichever side is ready last completes the pending fit.
114+ // The first fit is instant and frames whatever the map is showing — a
115+ // shared/featured selection included; later fits animate to the full
116+ // dataset and never override a selection the user is presenting.
117+ const didFitRef = useRef ( false )
118+ const pendingFitRef = useRef ( false )
119+ const fitNow = ( ) => {
120+ const map = mapRef . current
121+ if ( ! map || ! loadedRef . current ) {
122+ pendingFitRef . current = true
123+ return
124+ }
125+ if ( didFitRef . current && hasSelectionRef . current ) return
126+ const b = boundsOf ( hasSelectionRef . current ? visibleRef . current : allRef . current )
127+ if ( ! b ) {
128+ pendingFitRef . current = true
129+ return
130+ }
131+ pendingFitRef . current = false
132+ const first = ! didFitRef . current
133+ didFitRef . current = true
134+ map . fitBounds ( b , { padding : fitPadding ( 60 ) , duration : first ? 0 : 700 } )
135+ }
136+
110137 useEffect ( ( ) => {
111138 if ( ! containerRef . current || mapRef . current ) return
112139 const map = new maplibregl . Map ( {
@@ -150,8 +177,7 @@ const MapView = forwardRef<MapHandle, Props>(function MapView(
150177 ; ( window as unknown as Record < string , unknown > ) . __dashMap = map
151178 loadedRef . current = true
152179 render ( visibleRef . current , hasSelectionRef . current , detailIdRef . current )
153- const b = boundsOf ( visibleRef . current )
154- if ( b ) map . fitBounds ( b , { padding : 60 , duration : 0 } )
180+ fitNow ( )
155181 } )
156182 return ( ) => {
157183 ro . disconnect ( )
@@ -215,13 +241,9 @@ const MapView = forwardRef<MapHandle, Props>(function MapView(
215241 } , [ visible , hasSelection , detailId ] )
216242
217243 // Re-fit when App asks for it (new range's first page, or history finished
218- // streaming) — not on every appended page, and never over a selection the
219- // user is presenting.
244+ // streaming) — not on every appended page.
220245 useEffect ( ( ) => {
221- const map = mapRef . current
222- if ( ! map || ! loadedRef . current || hasSelectionRef . current ) return
223- const b = boundsOf ( allRef . current )
224- if ( b ) map . fitBounds ( b , { padding : fitPadding ( 60 ) , duration : 700 } )
246+ fitNow ( )
225247 // eslint-disable-next-line react-hooks/exhaustive-deps
226248 } , [ fitVersion ] )
227249
@@ -243,6 +265,16 @@ const MapView = forwardRef<MapHandle, Props>(function MapView(
243265 < div style = { { flex : 1 , position : 'relative' , minWidth : 0 , background : '#dfe3ea' } } >
244266 < div ref = { containerRef } style = { { position : 'absolute' , inset : 0 } } />
245267
268+ { loading && (
269+ < div style = { { ...glass , position : 'absolute' , top : 16 , left : '50%' , transform : 'translateX(-50%)' , zIndex : 3 , display : 'flex' , alignItems : 'center' , gap : 9 , borderRadius : 11 , padding : '8px 14px' } } >
270+ < svg className = "spin" width = "14" height = "14" viewBox = "0 0 16 16" >
271+ < circle cx = "8" cy = "8" r = "6" stroke = "var(--muted)" strokeWidth = "2.2" fill = "none" opacity = "0.35" />
272+ < path d = "M8 2 a6 6 0 0 1 6 6" stroke = "#e0223a" strokeWidth = "2.2" fill = "none" strokeLinecap = "round" />
273+ </ svg >
274+ < span style = { { fontSize : 12 , fontWeight : 600 , color : 'var(--text)' } } > Loading history…</ span >
275+ </ div >
276+ ) }
277+
246278 { showBadge && (
247279 < div style = { { position : 'absolute' , top : 16 , left : 16 , zIndex : 2 , display : 'flex' , alignItems : 'center' , gap : 9 , background : 'rgba(224,34,58,0.94)' , borderRadius : 11 , padding : '8px 14px' , boxShadow : '0 6px 22px rgba(0,0,0,0.28)' } } >
248280 < span style = { { width : 8 , height : 8 , borderRadius : '50%' , background : '#fff' , boxShadow : '0 0 0 3px rgba(255,255,255,0.35)' } } />
0 commit comments