From 27f42034808561676233aa1b6f4f5a89f35a453d Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Wed, 11 Feb 2026 15:30:20 -0800 Subject: [PATCH 01/16] Update MouseCoordinates to show zero depth --- packages/react-ui/src/Map/MouseCoordinates.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-ui/src/Map/MouseCoordinates.tsx b/packages/react-ui/src/Map/MouseCoordinates.tsx index 56851da7..9520c7e4 100644 --- a/packages/react-ui/src/Map/MouseCoordinates.tsx +++ b/packages/react-ui/src/Map/MouseCoordinates.tsx @@ -91,7 +91,8 @@ const MouseCoordinates: React.FC = ({ return (
- {depth?.depth && ` ${depth.depth.toPrecision(4)}m at `} + {typeof depth?.depth === 'number' && + ` ${depth.depth.toPrecision(4)}m at `} {formattedCoordinates}
) From 2868291d692f0d094d5949b6bfdf2464b1fd2c61 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Wed, 11 Feb 2026 15:36:44 -0800 Subject: [PATCH 02/16] Explicitly add Map types to avoid circular dependencies --- packages/react-ui/src/Map/Map.stories.tsx | 14 +++-- packages/react-ui/src/Map/Map.tsx | 63 +++-------------------- packages/react-ui/src/Map/Map.types.ts | 52 +++++++++++++++++++ packages/react-ui/src/index.ts | 5 ++ 4 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 packages/react-ui/src/Map/Map.types.ts diff --git a/packages/react-ui/src/Map/Map.stories.tsx b/packages/react-ui/src/Map/Map.stories.tsx index ed0be266..306b9af0 100644 --- a/packages/react-ui/src/Map/Map.stories.tsx +++ b/packages/react-ui/src/Map/Map.stories.tsx @@ -1,11 +1,16 @@ import { Story, Meta } from '@storybook/react' -import Map, { MapProps } from './Map' +import Map, { MapProps, MapDepthDisplay } from './Map' import { useCallback, useState } from 'react' export default { title: 'Maps/Map', } as Meta +const mockDepthRequest = async (_lat: number, _lng: number) => ({ + depth: (Math.random() * 10000.0) / 100.0, + status: 'success', +}) + const Template: Story = (args) => { const [center, setCenter] = useState(args.center) const onRequestCoordinate = useCallback(() => { @@ -19,7 +24,9 @@ const Template: Story = (args) => { className="h-96 w-full" onRequestCoordinate={onRequestCoordinate} center={center} - /> + > + + ) } @@ -27,9 +34,6 @@ const Template: Story = (args) => { const args: MapProps = { center: [37.7749, -122.4194], zoom: 10, - onRequestDepth: async () => { - return (Math.random() * 10000.0) / 100.0 - }, } export const Primary = Template.bind({}) diff --git a/packages/react-ui/src/Map/Map.tsx b/packages/react-ui/src/Map/Map.tsx index 4bd36d2c..ffe89c0f 100644 --- a/packages/react-ui/src/Map/Map.tsx +++ b/packages/react-ui/src/Map/Map.tsx @@ -16,7 +16,6 @@ import '@mbari/react-ui/dist/mbari-ui.css' import '@mbari/react-ui/src/css/base.css' import Tippy from '@tippyjs/react' import 'tippy.js/dist/tippy.css' -import MouseCoordinates, { MouseCoordinatesProps } from './MouseCoordinates' import { useMapBaseLayer, BaseLayerOption } from './useMapBaseLayer' import 'leaflet-mouse-position' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -33,6 +32,7 @@ import { Measurement } from './Measurement' import MovingDot from './MovingDot' import { AreaComponent, PathComponent, MeasurementProps } from './Measurement' import { CenterView } from './MapViews' +import type { MapProps } from './Map.types' import { createLogger, loadGoogleMapsOnce } from '@mbari/utils' import VehicleColorsModal from '@mbari/lrauv-dash2/components/VehicleColorsModal' @@ -83,56 +83,7 @@ interface StoredMarker { savedToLayer?: boolean } -export interface MapProps extends React.HTMLAttributes { - className?: string - style?: React.CSSProperties - center?: [number, number] - centerZoom?: number - zoom?: number - minZoom?: number - maxZoom?: number - maxNativeZoom?: number - fitBounds?: [[number, number], [number, number]] - viewMode?: 'center' | 'bounds' | null - scrollWheelZoom?: boolean - isAddingMarkers?: boolean - onToggleMarkerMode?: () => void - onRequestMarkers?: (position?: { top: number; left: number }) => void - onRequestDepth?: MouseCoordinatesProps['onRequestDepth'] - onRequestCoordinate?: () => void - onRequestFitBounds?: () => void - onRequestPlatforms?: () => void - onRequestStations?: (position?: { top: number; left: number }) => void - onRequestVehicleColors?: (vehicleName?: string) => void - whenCreated?: (map: L.Map) => void - onMapReady?: (map: L.Map) => void - trackedVehicles?: Array<{ id: string; name: string }> - dmsCoord?: string - mapCoord?: string - children?: React.ReactNode - renderMapClickHandler?: (props: { - isAddingMarkers: boolean - isEditingMarker: boolean - onAddMarker: (lat: number, lng: number) => number - }) => React.ReactNode - renderCustomMarkerSet?: (props: { - isAddingMarkers: boolean - setIsAddingMarkers: React.Dispatch> - }) => React.ReactNode - renderDraggableMarkers?: (props: { - markers: Array<{ - id: number - lat: number - lng: number - index: number - label: string - }> - handleMarkerDragEnd: ( - id: number, - position: { lat: number; lng: number } - ) => void - }) => React.ReactNode -} +export type { MapProps } from './Map.types' export type MeasureMode = 'open' | 'measuring' | 'closed' | 'cancelled' @@ -146,7 +97,7 @@ const Map = React.forwardRef( zoom = 17, minZoom = 4, maxZoom = 17, - maxNativeZoom = 13, + maxNativeZoom = 19, fitBounds, viewMode, trackedVehicles = [], @@ -154,7 +105,6 @@ const Map = React.forwardRef( isAddingMarkers = false, onToggleMarkerMode, onRequestMarkers, - onRequestDepth, onRequestCoordinate, onRequestFitBounds, onRequestPlatforms, @@ -837,6 +787,7 @@ const Map = React.forwardRef( ( ( )} - - - {children} {/* TRACKDB/STATIONS CONTROLS - Now in separate Control component */} @@ -1266,3 +1215,5 @@ const Map = React.forwardRef( Map.displayName = 'Map.Map' export default Map +export { default as MapDepthDisplay } from './MapDepthDisplay' +export type { MapDepthDisplayProps, DepthRequestFn } from './MapDepthDisplay' diff --git a/packages/react-ui/src/Map/Map.types.ts b/packages/react-ui/src/Map/Map.types.ts new file mode 100644 index 00000000..d9776282 --- /dev/null +++ b/packages/react-ui/src/Map/Map.types.ts @@ -0,0 +1,52 @@ +import type React from 'react' +import type L from 'leaflet' + +export interface MapProps extends React.HTMLAttributes { + className?: string + style?: React.CSSProperties + center?: [number, number] + centerZoom?: number + zoom?: number + minZoom?: number + maxZoom?: number + maxNativeZoom?: number + fitBounds?: [[number, number], [number, number]] + viewMode?: 'center' | 'bounds' | null + scrollWheelZoom?: boolean + isAddingMarkers?: boolean + onToggleMarkerMode?: () => void + onRequestMarkers?: (position?: { top: number; left: number }) => void + onRequestCoordinate?: () => void + onRequestFitBounds?: () => void + onRequestPlatforms?: () => void + onRequestStations?: (position?: { top: number; left: number }) => void + onRequestVehicleColors?: (vehicleName?: string) => void + whenCreated?: (map: L.Map) => void + onMapReady?: (map: L.Map) => void + trackedVehicles?: Array<{ id: string; name: string }> + dmsCoord?: string + mapCoord?: string + children?: React.ReactNode + renderMapClickHandler?: (props: { + isAddingMarkers: boolean + isEditingMarker: boolean + onAddMarker: (lat: number, lng: number) => number + }) => React.ReactNode + renderCustomMarkerSet?: (props: { + isAddingMarkers: boolean + setIsAddingMarkers: React.Dispatch> + }) => React.ReactNode + renderDraggableMarkers?: (props: { + markers: Array<{ + id: number + lat: number + lng: number + index: number + label: string + }> + handleMarkerDragEnd: ( + id: number, + position: { lat: number; lng: number } + ) => void + }) => React.ReactNode +} diff --git a/packages/react-ui/src/index.ts b/packages/react-ui/src/index.ts index 5bc429bc..221109c9 100644 --- a/packages/react-ui/src/index.ts +++ b/packages/react-ui/src/index.ts @@ -14,3 +14,8 @@ export * from './Icons' export * from './PopUps' export * from './Tables' export * from './Map/useMapBaseLayer' +export type { MapProps } from './Map/Map.types' +export type { + MapDepthDisplayProps, + DepthRequestFn, +} from './Map/MapDepthDisplay' From f5bcdf6aaf5b676af491e6fb65348873db7a90cf Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Wed, 11 Feb 2026 15:38:07 -0800 Subject: [PATCH 03/16] Create MapDepthDisplay component --- packages/react-ui/src/Map/MapDepthDisplay.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/react-ui/src/Map/MapDepthDisplay.tsx diff --git a/packages/react-ui/src/Map/MapDepthDisplay.tsx b/packages/react-ui/src/Map/MapDepthDisplay.tsx new file mode 100644 index 00000000..112758ad --- /dev/null +++ b/packages/react-ui/src/Map/MapDepthDisplay.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useState, useLayoutEffect } from 'react' +import { createPortal } from 'react-dom' +import { useMap } from 'react-leaflet' +import { + useDepthRequest, + DepthRequestOptions, +} from '@mbari/utils/useDepthRequest' +import MouseCoordinates from './MouseCoordinates' + +const TOPRIGHT_PANE_SELECTOR = '.leaflet-top.leaflet-right' + +export type DepthRequestFn = ( + lat: number, + lng: number +) => Promise<{ depth: number | null; status: string }> + +export interface MapDepthDisplayProps { + depthRequest: DepthRequestFn + options?: DepthRequestOptions +} + +/** + * Self-contained map layer that owns depth request state and displays coordinates + depth. + * Renders inside Map as a child; only this component re-renders when depth data updates. + * Uses a React portal into the map's topright pane to avoid DOM conflicts with react-leaflet-custom-control. + */ +const MapDepthDisplay: React.FC = ({ + depthRequest, + options = {}, +}) => { + const map = useMap() + const [wrapper, setWrapper] = useState(null) + + useLayoutEffect(() => { + const pane = map + .getContainer() + .querySelector(TOPRIGHT_PANE_SELECTOR) as HTMLElement | null + if (!pane) return + const div = document.createElement('div') + pane.prepend(div) + setWrapper(div) + return () => { + div.remove() + setWrapper(null) + } + }, [map]) + + const { handleDepthRequestWithFeedback } = useDepthRequest( + depthRequest, + options + ) + + const onRequestDepth = useCallback( + (lat: number, lng: number) => + handleDepthRequestWithFeedback(lat, lng).then((r) => r.depth ?? 0), + [handleDepthRequestWithFeedback] + ) + + if (!wrapper) return null + + return createPortal( +
+ +
, + wrapper + ) +} + +export default MapDepthDisplay From a40c2524b9bfc1c5e15f1b3806a9280b9fd9f409 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Wed, 11 Feb 2026 15:48:42 -0800 Subject: [PATCH 04/16] Use MapDepthDisplay as Map child and pass depthRequest --- apps/lrauv-dash2/components/DeploymentMap.tsx | 37 +++++------ apps/lrauv-dash2/pages/index.tsx | 62 +++++++------------ 2 files changed, 40 insertions(+), 59 deletions(-) diff --git a/apps/lrauv-dash2/components/DeploymentMap.tsx b/apps/lrauv-dash2/components/DeploymentMap.tsx index 25ea7796..a24c4f02 100644 --- a/apps/lrauv-dash2/components/DeploymentMap.tsx +++ b/apps/lrauv-dash2/components/DeploymentMap.tsx @@ -11,14 +11,20 @@ import toast from 'react-hot-toast' import { createLogger } from '@mbari/utils' import VehicleColorsModal from './VehicleColorsModal' import useTrackedVehicles from '../lib/useTrackedVehicles' -import { useDepthRequest } from '@mbari/utils/useDepthRequest' - // This is a tricky workaround to prevent leaflet from crashing next.js // SSR. If we don't do this, the leaflet map will be loaded server side // and throw a window error. const Map = dynamic(() => import('@mbari/react-ui/dist/Map/Map'), { ssr: false, }) + +const MapDepthDisplay = dynamic( + () => + import('@mbari/react-ui/dist/Map/Map').then((m) => ({ + default: m.MapDepthDisplay, + })), + { ssr: false } +) const DraggableMarker = dynamic(() => import('./DraggableMarker'), { ssr: false, }) @@ -80,18 +86,7 @@ const DeploymentMap: React.FC = ({ ), [updatedWaypoints, handleWaypointsUpdate] ) - const { handleDepthRequest, elevationAvailable } = useGoogleElevator() - // Depth request hook - const { handleDepthRequestWithFeedback } = useDepthRequest( - handleDepthRequest, - { - warningToastId: 'depth-unavailable', - errorToastId: 'depth-result', - loadingToastId: 'depth-loading', - warningToastClass: 'blue-toast', - toastDuration: 5000, - } - ) + const { handleDepthRequest } = useGoogleElevator() // Filter out waypoints with NaN lat/lon const plottedWaypoints = updatedWaypoints.filter( @@ -558,10 +553,6 @@ const DeploymentMap: React.FC = ({ logger.debug('Map is ready!') mapRef.current = map }} - onRequestDepth={async (lat, lng) => { - const result = await handleDepthRequestWithFeedback(lat, lng) - return result.depth ?? 0 - }} center={center} centerZoom={centerZoom} fitBounds={bounds} @@ -624,6 +615,16 @@ const DeploymentMap: React.FC = ({ ) } > + {selectedStations.map((station) => { const lng = station.geojson.geometry.coordinates[0] const lat = station.geojson.geometry.coordinates[1] diff --git a/apps/lrauv-dash2/pages/index.tsx b/apps/lrauv-dash2/pages/index.tsx index b79cd7c0..c8bdddaf 100644 --- a/apps/lrauv-dash2/pages/index.tsx +++ b/apps/lrauv-dash2/pages/index.tsx @@ -20,20 +20,24 @@ import { StationsListModal } from '../components/StationsListModal' import { MapLayersListModal } from '../components/MapLayersListModal' import { useSelectedStations } from '../components/SelectedStationContext' import { useMarkers } from '../components/MarkerContext' -import { useDepthRequest } from '@mbari/utils/useDepthRequest' import toast from 'react-hot-toast' -import type { MapProps } from '@mbari/react-ui/dist/Map/Map' import { createLogger } from '@mbari/utils' import { PlatformsListModal } from '../components/PlatformsListModal' // This is a tricky workaround to prevent leaflet from crashing next.js // SSR. If we don't do this, the leaflet map will be loaded server side // and throw a window error. -const Map = dynamic( - () => import('@mbari/react-ui/dist/Map/Map'), - { - ssr: false, - } +// Map types are not imported from @mbari/react-ui to avoid module resolution issues. +const Map = dynamic(() => import('@mbari/react-ui/dist/Map/Map'), { + ssr: false, +}) + +const MapDepthDisplay = dynamic( + () => + import('@mbari/react-ui/dist/Map/Map').then((m) => ({ + default: m.MapDepthDisplay, + })), + { ssr: false } ) const VehiclePath = dynamic(() => import('../components/VehiclePath'), { @@ -73,14 +77,6 @@ const styles = { 'flex w-full flex-shrink-0 flex-col bg-white border-t-2 border-secondary-300/60', } -// Interface CustomMarkerProps -type CustomMapProps = MapProps & - React.RefAttributes & { - isAddingMarkers?: boolean - onToggleMarkerMode?: () => void - trackedVehicles?: { name: string; id?: string }[] // Match the actual type being used - } - // Interface MarkerData interface MarkerData { id: number @@ -111,16 +107,6 @@ const OverViewMap: React.FC<{ const [selectedMarkerId, setSelectedMarkerId] = useState(null) const [defaultMarkerColor, setDefaultMarkerColor] = useState('red') const { selectedStations } = useSelectedStations() - const { handleDepthRequestWithFeedback } = useDepthRequest( - handleDepthRequest, - { - warningToastId: 'depth-unavailable', - errorToastId: 'depth-result', - loadingToastId: 'depth-loading', - warningToastClass: 'blue-toast', - toastDuration: 5000, - } - ) const [layersModalPosition, setLayersModalPosition] = useState({ top: 0, left: 0, @@ -614,22 +600,6 @@ const OverViewMap: React.FC<{ ...vehicle, id: vehicle.id || vehicle.name, // Ensure id is always present }))} - onRequestDepth={async (lat, lng) => { - try { - // Try to remove any leading zeros - const formattedLat = parseFloat(String(lat).replace(/^0+/, '')) - // Then use in depth request - const result = await handleDepthRequestWithFeedback( - formattedLat, - lng - ) - return result.depth !== null ? result.depth : 0 - } catch (error) { - logger.warn('❌ Error in depth request:', error) - toast.error('Depth data unavailable', { id: 'depth-error' }) - return 0 - } - }} center={center} centerZoom={centerZoom} fitBounds={bounds} @@ -692,6 +662,16 @@ const OverViewMap: React.FC<{ ) } > + {uniqueTrackedVehicles?.map((name, index) => ( Date: Wed, 11 Feb 2026 15:50:03 -0800 Subject: [PATCH 05/16] Fix leaflet tiling issues --- apps/lrauv-dash2/pages/vehicle/[...deployment].tsx | 4 +++- apps/lrauv-dash2/styles/globals.css | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx b/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx index bf228d0a..cd395ff7 100644 --- a/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx +++ b/apps/lrauv-dash2/pages/vehicle/[...deployment].tsx @@ -461,7 +461,9 @@ const Vehicle: NextPage = () => { className="min-h-0" > - {primarySection} +
+ {primarySection} +
{secondarySection} diff --git a/apps/lrauv-dash2/styles/globals.css b/apps/lrauv-dash2/styles/globals.css index cde44e14..d2a5932f 100644 --- a/apps/lrauv-dash2/styles/globals.css +++ b/apps/lrauv-dash2/styles/globals.css @@ -15,9 +15,11 @@ a { box-sizing: border-box; } -/* This is a workaround to make sure that every parent element of the map has a min height on load to prevent Leaflet from showing gray tiles */ - +/* Min-height 0 on flex parents so the map can shrink and Leaflet doesn't show gray tiles. + Needed for: legacy split-view classes, Allotment panes, and the Leaflet container. */ .split-view-container, -.split-view-view { +.split-view-view, +.leaflet-container, +[class*='splitViewView'] { min-height: 0 !important; } From 0a9736cd505360cfc07f523a50ca6e302583c34c Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:14:31 -0800 Subject: [PATCH 06/16] Fix react-ui tsconfig so dist/index.d.ts is emitted --- packages/react-ui/tsconfig.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/tsconfig.json b/packages/react-ui/tsconfig.json index db488c57..01eeb444 100644 --- a/packages/react-ui/tsconfig.json +++ b/packages/react-ui/tsconfig.json @@ -1,9 +1,14 @@ { "extends": "tsconfig/react-library.json", - "include": ["."], - "exclude": ["dist", "build", "node_modules"], "compilerOptions": { - "outDir": "./dist" + "baseUrl": ".", + "paths": {}, + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "declarationMap": true }, + "include": ["src"], + "exclude": ["dist", "build", "node_modules"], "types": ["resize-observer-browser"] } From feac61e8460e461490f13891fcd9166b63963fda Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:18:16 -0800 Subject: [PATCH 07/16] Make Map pure: no Google/plugin loading, vehicle colors via callback, fix imports --- packages/react-ui/src/Map/Map.tsx | 126 ++---------------- packages/react-ui/src/Map/Map.types.ts | 2 +- packages/react-ui/src/Map/MapDepthDisplay.tsx | 9 +- .../Modals/MissionModalSteps/MissionStep.tsx | 2 +- packages/utils/src/index.ts | 1 + 5 files changed, 15 insertions(+), 125 deletions(-) diff --git a/packages/react-ui/src/Map/Map.tsx b/packages/react-ui/src/Map/Map.tsx index ffe89c0f..4a97bf46 100644 --- a/packages/react-ui/src/Map/Map.tsx +++ b/packages/react-ui/src/Map/Map.tsx @@ -8,12 +8,13 @@ import { useMapEvents, } from 'react-leaflet' import ReactLeafletGoogleLayer from 'react-leaflet-google-layer' +const GoogleLayerAny = + ReactLeafletGoogleLayer as unknown as React.ComponentType import Control from 'react-leaflet-custom-control' import L from 'leaflet' import 'leaflet/dist/leaflet.css' import 'leaflet-measure/dist/leaflet-measure.css' import '@mbari/react-ui/dist/mbari-ui.css' -import '@mbari/react-ui/src/css/base.css' import Tippy from '@tippyjs/react' import 'tippy.js/dist/tippy.css' import { useMapBaseLayer, BaseLayerOption } from './useMapBaseLayer' @@ -33,8 +34,7 @@ import MovingDot from './MovingDot' import { AreaComponent, PathComponent, MeasurementProps } from './Measurement' import { CenterView } from './MapViews' import type { MapProps } from './Map.types' -import { createLogger, loadGoogleMapsOnce } from '@mbari/utils' -import VehicleColorsModal from '@mbari/lrauv-dash2/components/VehicleColorsModal' +import { createLogger } from '@mbari/utils' const logger = createLogger('Map') @@ -127,9 +127,6 @@ const Map = React.forwardRef( const layersButtonRef = useRef(null) const [mapReady, setMapReady] = useState(false) - const [googleMapsStatus, setGoogleMapsStatus] = useState< - 'pending' | 'loading' | 'loaded' | 'error' - >('pending') const [isMeasuring, setIsMeasuring] = useState(false) const [isAddingMarkersLocal, setIsAddingMarkersLocal] = useState(isAddingMarkers) @@ -140,9 +137,7 @@ const Map = React.forwardRef( }, [setBaseLayer] ) - const [showVehicleColorsModal, setShowVehicleColorsModal] = useState(false) const vehicleColorsButtonRef = useRef(null) - const [modalPosition, setModalPosition] = useState({ top: 0, left: 0 }) const validatedCenter: [number, number] = Array.isArray(center) && @@ -151,87 +146,6 @@ const Map = React.forwardRef( ? center : DEFAULT_CENTER - // Google Maps initialization - useEffect(() => { - // Only initialize Google Maps after map is ready - const handleMapReady = async (event: CustomEvent) => { - // Extract the map instance from the event - const map = event.detail as L.Map - if (!map) { - logger.error('Map instance not found in mapready event') - setGoogleMapsStatus('error') - return - } - - // Use safeLogger for non-critical logs - safeLogger.debug('Map ready event received, initializing Google Maps') - setGoogleMapsStatus('loading') - - try { - safeLogger.debug('Loading leaflet.gridlayer.googlemutant...') - - // Load Google Maps API first (this ensures custom elements are registered once) - await loadGoogleMapsOnce() - - // Load the Leaflet plugin - let GoogleMutant - try { - GoogleMutant = await import('leaflet.gridlayer.googlemutant') - } catch (err) { - safeLogger.debug('Trying alternative import path...') - try { - GoogleMutant = await import('leaflet.gridlayer.googlemutant') - } catch (err2) { - logger.error('Failed to import from node_modules path:', err2) - throw err - } - } - - if (!window.google) { - logger.error('Google Maps API still not available after loading') - setGoogleMapsStatus('error') - return - } - - // Check if Google Maps is already loaded - safeLogger.debug('Creating Google Maps layer...') - const googleLayer = L.gridLayer.googleMutant({ - type: 'hybrid', - maxZoom: maxZoom, - maxNativeZoom: maxNativeZoom, - }) - - safeLogger.debug('Adding Google Maps layer to map...') - googleLayer.addTo(map) - - // Store the layer for future reference - // @ts-ignore - Adding custom property - map._googleLayer = googleLayer - - setGoogleMapsStatus('loaded') - safeLogger.debug('✅ Google Maps layer added successfully!') - } catch (error) { - setGoogleMapsStatus('error') - logger.error('Failed to initialize Google Maps layer:', error) - } - } - - // Listen for mapReady event - if (typeof window !== 'undefined') { - window.addEventListener( - 'mapready', - handleMapReady as unknown as EventListener - ) - - return () => { - window.removeEventListener( - 'mapready', - handleMapReady as unknown as EventListener - ) - } - } - }, [maxZoom, maxNativeZoom]) - // Create measurements const [measurements, setMeasurements] = useState< { @@ -577,14 +491,12 @@ const Map = React.forwardRef( // Handle mouse over event for the Vehicle Colors button const handleVehicleColorsClick = () => { + let anchor: { top: number; left: number } | undefined if (vehicleColorsButtonRef.current) { const rect = vehicleColorsButtonRef.current.getBoundingClientRect() - setModalPosition({ - top: rect.bottom + 40, - left: rect.left, - }) + anchor = { top: rect.bottom + 40, left: rect.left } } - setShowVehicleColorsModal(!showVehicleColorsModal) + onRequestVehicleColors?.(anchor) } // Remove Measurement @@ -749,12 +661,12 @@ const Map = React.forwardRef( })} - {mapReady && ( + {mapReady && typeof window !== 'undefined' && window.google?.maps && ( - ( ) : null}
- {showVehicleColorsModal && ( - setShowVehicleColorsModal(false)} - anchorPosition={modalPosition} - trackedVehicles={(trackedVehicles || []).map( - // If trackedVehicles is array of objects with name property: - (vehicle) => - typeof vehicle === 'string' ? vehicle : vehicle.name - )} - /> - )} - {/* {showVehicleColorsModal && ( - setShowVehicleColorsModal(false)} - anchorPosition={modalPosition} - trackedVehicles={(trackedVehicles || []).map( - (vehicle) => vehicle.name - )} - /> - )} */} ) } diff --git a/packages/react-ui/src/Map/Map.types.ts b/packages/react-ui/src/Map/Map.types.ts index d9776282..8061deb1 100644 --- a/packages/react-ui/src/Map/Map.types.ts +++ b/packages/react-ui/src/Map/Map.types.ts @@ -20,7 +20,7 @@ export interface MapProps extends React.HTMLAttributes { onRequestFitBounds?: () => void onRequestPlatforms?: () => void onRequestStations?: (position?: { top: number; left: number }) => void - onRequestVehicleColors?: (vehicleName?: string) => void + onRequestVehicleColors?: (anchor?: { top: number; left: number }) => void whenCreated?: (map: L.Map) => void onMapReady?: (map: L.Map) => void trackedVehicles?: Array<{ id: string; name: string }> diff --git a/packages/react-ui/src/Map/MapDepthDisplay.tsx b/packages/react-ui/src/Map/MapDepthDisplay.tsx index 112758ad..60ccb4be 100644 --- a/packages/react-ui/src/Map/MapDepthDisplay.tsx +++ b/packages/react-ui/src/Map/MapDepthDisplay.tsx @@ -1,10 +1,7 @@ import React, { useCallback, useState, useLayoutEffect } from 'react' import { createPortal } from 'react-dom' import { useMap } from 'react-leaflet' -import { - useDepthRequest, - DepthRequestOptions, -} from '@mbari/utils/useDepthRequest' +import { useDepthRequest, type DepthRequestOptions } from '@mbari/utils' import MouseCoordinates from './MouseCoordinates' const TOPRIGHT_PANE_SELECTOR = '.leaflet-top.leaflet-right' @@ -52,7 +49,9 @@ const MapDepthDisplay: React.FC = ({ const onRequestDepth = useCallback( (lat: number, lng: number) => - handleDepthRequestWithFeedback(lat, lng).then((r) => r.depth ?? 0), + handleDepthRequestWithFeedback(lat, lng).then( + (r: { depth: number | null }) => r.depth ?? 0 + ), [handleDepthRequestWithFeedback] ) diff --git a/packages/react-ui/src/Modals/MissionModalSteps/MissionStep.tsx b/packages/react-ui/src/Modals/MissionModalSteps/MissionStep.tsx index b782b901..cfe106b2 100644 --- a/packages/react-ui/src/Modals/MissionModalSteps/MissionStep.tsx +++ b/packages/react-ui/src/Modals/MissionModalSteps/MissionStep.tsx @@ -3,7 +3,7 @@ import { SelectOption } from '../../Fields/Select' import { Mission, MissionTable } from '../../Tables/MissionTable' import { Input, SelectField } from '../../Fields' import { sortByProperty } from '@mbari/utils' -import { SortDirection } from 'react-ui/src/Data/TableHeader' +import { SortDirection } from '../../Data/TableHeader' export interface MissionStepProps { vehicleName: string diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 04c20caa..0202cb93 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -46,3 +46,4 @@ export * from './nextCommsTimeFormatting' export * from './calculateNextComm' export * from './decodeHtmlEntities' export * from './formatEventEntries' +export * from './useDepthRequest' From bb611b5ca61b41540e10224458cd491b20843a86 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:18:59 -0800 Subject: [PATCH 08/16] Single Google and Leaflet plugin initializer in app --- .../components/GoogleMapsProvider.tsx | 58 +++---------- apps/lrauv-dash2/lib/leafletPlugins.ts | 82 +++++++++++++++++++ 2 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 apps/lrauv-dash2/lib/leafletPlugins.ts diff --git a/apps/lrauv-dash2/components/GoogleMapsProvider.tsx b/apps/lrauv-dash2/components/GoogleMapsProvider.tsx index d04cee7b..6e572bc8 100644 --- a/apps/lrauv-dash2/components/GoogleMapsProvider.tsx +++ b/apps/lrauv-dash2/components/GoogleMapsProvider.tsx @@ -2,12 +2,10 @@ import React, { useState, useEffect } from 'react' import { useGoogleMapsApiKey } from './useGoogleMapsApiKey' import toast from 'react-hot-toast' import { createLogger } from '@mbari/utils' +import { initLeafletGoogle } from '../lib/leafletPlugins' const logger = createLogger('GoogleMapsProvider') -// Global variable to track if script has been loaded -let googleMapsScriptAdded = false - interface GoogleMapsProviderProps { children: React.ReactNode } @@ -15,10 +13,9 @@ interface GoogleMapsProviderProps { export const GoogleMapsProvider: React.FC = ({ children, }) => { - const { apiKey, isLoading, error, keySource } = useGoogleMapsApiKey() + const { apiKey, isLoading, keySource } = useGoogleMapsApiKey() const [isLoaded, setIsLoaded] = useState(false) - // Check if Maps API is already available on mount useEffect(() => { if (typeof window !== 'undefined' && window.google?.maps) { setIsLoaded(true) @@ -26,17 +23,14 @@ export const GoogleMapsProvider: React.FC = ({ } }, []) - // Handle script loading based on API key useEffect(() => { if (isLoading) return - // Log the source of the API key if (keySource === 'server') { logger.debug('Using Google Maps from Tethys API') } else if (keySource === 'local') { logger.debug('Using Google Maps from local .env') } else if (keySource === 'none' && !apiKey) { - // Notify but don't block interface toast.error( 'Google Maps API unavailable - Google Hybrid map and elevation data unavailable‼️ ', { @@ -48,62 +42,32 @@ export const GoogleMapsProvider: React.FC = ({ return } - // If Maps already loaded, nothing more to do if (window.google?.maps) { logger.debug('Google Maps already loaded via window.google') setIsLoaded(true) return } - // If we already started loading the script, don't add it again - if (googleMapsScriptAdded) { - logger.debug('Google Maps script tag already added') - return - } - - // Load the script if we have an API key - if (apiKey) { - logger.debug('Loading Google Maps API script') - - // Check if script already exists - const existingScript = document.getElementById('google-maps-script') - if (existingScript) { - logger.debug('Found existing Google Maps script tag') - return - } + if (!apiKey) return - const script = document.createElement('script') - script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places` - script.async = true - script.defer = true - script.id = 'google-maps-script' - - script.onload = () => { - logger.debug('✅ Google Maps API script loaded successfully') + logger.debug('Loading Google Maps API via initializer') + initLeafletGoogle(apiKey) + .then(() => { + logger.debug('✅ Google Maps API and Leaflet plugins ready') setIsLoaded(true) - } - - script.onerror = (e) => { - logger.error('❌ Google Maps script failed to load:', e) + }) + .catch((e) => { + logger.error('❌ Google Maps initializer failed:', e) toast.error('Google Hybrid map unavailable', { duration: 3000, id: 'maps-loading-error', }) - } - - document.head.appendChild(script) - - // Mark as added to prevent duplicate loading - googleMapsScriptAdded = true - } + }) }, [isLoading, keySource, apiKey]) - // Just a minimal loading indicator that doesn't take much space if (isLoading) { return
Loading maps...
} - // Always render children - maps will be available when loaded - // This avoids the flash of content and UI blocking return <>{children} } diff --git a/apps/lrauv-dash2/lib/leafletPlugins.ts b/apps/lrauv-dash2/lib/leafletPlugins.ts new file mode 100644 index 00000000..61d78c89 --- /dev/null +++ b/apps/lrauv-dash2/lib/leafletPlugins.ts @@ -0,0 +1,82 @@ +/** + * Single initializer for Google Maps JS + Leaflet googlemutant plugin. + * Run only in the browser. Call from GoogleMapsProvider; do not load Google elsewhere. + */ + +let initPromise: Promise | null = null + +/** + * Ensures Google Maps JS is loaded (with places,elevation) then loads + * leaflet.gridlayer.googlemutant (registers L.gridLayer.googleMutant). + * Idempotent: repeated calls reuse the same promise. + */ +export function initLeafletGoogle(apiKey: string): Promise { + if (typeof window === 'undefined') { + return Promise.reject( + new Error('initLeafletGoogle must run in the browser') + ) + } + + if (window.google?.maps) { + return initPromise != null + ? initPromise + : (initPromise = loadGoogleMutantOnly()) + } + + if (initPromise) { + return initPromise + } + + initPromise = (async () => { + const existing = document.getElementById('google-maps-script') + if (existing) { + await waitForGoogle() + await loadGoogleMutantOnly() + return + } + + const script = document.createElement('script') + script.id = 'google-maps-script' + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,elevation` + script.async = true + script.defer = true + + await new Promise((resolve, reject) => { + script.onload = () => resolve() + script.onerror = (e) => reject(e) + document.head.appendChild(script) + }) + + await waitForGoogle() + await loadGoogleMutantOnly() + })() + + return initPromise +} + +function waitForGoogle(): Promise { + if (window.google?.maps) return Promise.resolve() + return new Promise((resolve) => { + const check = () => { + if (window.google?.maps) { + resolve() + return + } + requestAnimationFrame(check) + } + check() + }) +} + +async function loadLeafletAndExposeGlobal(): Promise { + if (typeof window === 'undefined') return + if ((window as any).L) return + + const leaflet = await import('leaflet') + ;(window as any).L = leaflet.default ?? leaflet +} + +async function loadGoogleMutantOnly(): Promise { + await loadLeafletAndExposeGlobal() + await import('leaflet.gridlayer.googlemutant/dist/Leaflet.GoogleMutant.js') +} From e14cf9c1c14ebe98ed9744f0d77f927a7ac30899 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:20:35 -0800 Subject: [PATCH 09/16] Use Google availability hook only; drop custom babel rule in Next --- apps/lrauv-dash2/lib/useGoogleMaps.ts | 36 +++++++++++++-------------- apps/lrauv-dash2/next.config.js | 26 ------------------- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/apps/lrauv-dash2/lib/useGoogleMaps.ts b/apps/lrauv-dash2/lib/useGoogleMaps.ts index 93163dc4..af0fc009 100644 --- a/apps/lrauv-dash2/lib/useGoogleMaps.ts +++ b/apps/lrauv-dash2/lib/useGoogleMaps.ts @@ -1,30 +1,28 @@ -import { useTethysApiContext } from '@mbari/api-client' import { useState, useEffect } from 'react' -import { Loader } from '@googlemaps/js-api-loader' +/** + * Simple "is Google Maps available" hook. Does not load or inject Google; + * the provider/initializer is the only loader. + */ export const useGoogleMaps = () => { - const { siteConfig } = useTethysApiContext() - const googleMapsApiKey = siteConfig?.appConfig.googleApiKey + const [mapsLoaded, setMapsLoaded] = useState( + typeof window !== 'undefined' && !!window.google?.maps + ) - const [mapsLoaded, setMapsLoaded] = useState(false) useEffect(() => { - if (typeof google !== 'undefined' && !mapsLoaded) { + if (typeof window === 'undefined') return + if (window.google?.maps) { setMapsLoaded(true) + return } - if (!googleMapsApiKey || mapsLoaded || typeof google !== 'undefined') return - new Loader({ - apiKey: googleMapsApiKey, - version: 'weekly', - libraries: ['elevation'], - }) - .importLibrary('elevation') - .then(() => { + const check = () => { + if (window.google?.maps) { setMapsLoaded(true) - }) - .catch((e) => { - console.warn('Error loading Google Maps API', e) - }) - }, [googleMapsApiKey, mapsLoaded, setMapsLoaded]) + } + } + const id = setInterval(check, 200) + return () => clearInterval(id) + }, []) return { mapsLoaded } } diff --git a/apps/lrauv-dash2/next.config.js b/apps/lrauv-dash2/next.config.js index f88860ca..0e39deb4 100644 --- a/apps/lrauv-dash2/next.config.js +++ b/apps/lrauv-dash2/next.config.js @@ -9,32 +9,6 @@ const nextConfig = { // Next.js 13+ uses transpilePackages transpilePackages: ['@mbari/react-ui', '@mbari/utils', '@mbari/api-client'], - - // Make sure webpack can handle workspace TypeScript files - webpack: (config, { isServer }) => { - // Add TypeScript loader for workspace dependencies - config.module.rules.push({ - test: /\.(tsx|ts)$/, - include: [ - /node_modules\/@mbari\/react-ui/, - /node_modules\/@mbari\/utils/, - /node_modules\/@mbari\/api-client/, - /packages\/react-ui/, - /packages\/utils/, - /packages\/api-client/, - ], - use: [ - { - loader: 'babel-loader', - options: { - presets: ['next/babel'], - }, - }, - ], - }) - - return config - }, } module.exports = nextConfig From e8e6a02909fcf36db8b00828504680a404757b57 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:21:35 -0800 Subject: [PATCH 10/16] Centralize Window.google in app types; add GoogleMutant module shim --- apps/lrauv-dash2/lib/elevationService.ts | 3 +-- apps/lrauv-dash2/types/global.d.ts | 7 +++++++ apps/lrauv-dash2/types/leaflet-googlemutant-shim.d.ts | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 apps/lrauv-dash2/types/global.d.ts create mode 100644 apps/lrauv-dash2/types/leaflet-googlemutant-shim.d.ts diff --git a/apps/lrauv-dash2/lib/elevationService.ts b/apps/lrauv-dash2/lib/elevationService.ts index 15785aaa..e70b50c0 100644 --- a/apps/lrauv-dash2/lib/elevationService.ts +++ b/apps/lrauv-dash2/lib/elevationService.ts @@ -1,10 +1,9 @@ import { createLogger } from '@mbari/utils' -// Extend the Window interface +// Extend the Window interface (google is declared in types/global.d.ts) declare global { interface Window { [GOOGLE_MAPS_LOADED_FLAG]?: boolean - google?: any } } diff --git a/apps/lrauv-dash2/types/global.d.ts b/apps/lrauv-dash2/types/global.d.ts new file mode 100644 index 00000000..21bdc972 --- /dev/null +++ b/apps/lrauv-dash2/types/global.d.ts @@ -0,0 +1,7 @@ +export {} + +declare global { + interface Window { + google?: any + } +} diff --git a/apps/lrauv-dash2/types/leaflet-googlemutant-shim.d.ts b/apps/lrauv-dash2/types/leaflet-googlemutant-shim.d.ts new file mode 100644 index 00000000..38dc7f7b --- /dev/null +++ b/apps/lrauv-dash2/types/leaflet-googlemutant-shim.d.ts @@ -0,0 +1 @@ +declare module 'leaflet.gridlayer.googlemutant/dist/Leaflet.GoogleMutant.js' From 1e69c88feb490fc36d65617e0ec02a2c59aa93da Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:22:15 -0800 Subject: [PATCH 11/16] App owns vehicle colors modal; import Modal from react-ui; DeploymentMap uses anchor callback --- apps/lrauv-dash2/components/DeploymentMap.tsx | 25 ++++++++----------- .../components/VehicleColorsModal.tsx | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/apps/lrauv-dash2/components/DeploymentMap.tsx b/apps/lrauv-dash2/components/DeploymentMap.tsx index a24c4f02..f3d5d617 100644 --- a/apps/lrauv-dash2/components/DeploymentMap.tsx +++ b/apps/lrauv-dash2/components/DeploymentMap.tsx @@ -472,20 +472,17 @@ const DeploymentMap: React.FC = ({ setShowLayersModal(false) }, []) - const handleVehicleColorRequest = useCallback(() => { - // Add debugging to verify values - logger.debug('Opening color modal with:', { - vehicleName, - trackedVehicles, - modalTrackedVehicles: vehicleName ? [vehicleName] : [], - }) - - setColorModalPosition({ - top: 100, - left: 100, - }) - setColorModalOpen(true) - }, [vehicleName, trackedVehicles]) + const handleVehicleColorRequest = useCallback( + (anchor?: { top: number; left: number }) => { + if (anchor) { + setColorModalPosition(anchor) + } else { + setColorModalPosition({ top: 100, left: 100 }) + } + setColorModalOpen(true) + }, + [] + ) const handleCloseVehicleColors = useCallback((vehicleName?: string) => { setShowVehicleColors(false) diff --git a/apps/lrauv-dash2/components/VehicleColorsModal.tsx b/apps/lrauv-dash2/components/VehicleColorsModal.tsx index 4ddbbb57..37de75a3 100644 --- a/apps/lrauv-dash2/components/VehicleColorsModal.tsx +++ b/apps/lrauv-dash2/components/VehicleColorsModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef, useCallback } from 'react' import { useVehicleColors } from './VehicleColorsContext' -import { Modal } from '@mbari/react-ui/src/Modal/Modal' +import { Modal } from '@mbari/react-ui' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes, faEyeDropper } from '@fortawesome/free-solid-svg-icons' import { SketchPicker } from 'react-color' From 93e3208c7b2e6761349455b224bb2210e2b48392 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:26:41 -0800 Subject: [PATCH 12/16] Index page vehicle colors handler accepts anchor and sets modal position --- apps/lrauv-dash2/pages/index.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/lrauv-dash2/pages/index.tsx b/apps/lrauv-dash2/pages/index.tsx index c8bdddaf..83fe1a5a 100644 --- a/apps/lrauv-dash2/pages/index.tsx +++ b/apps/lrauv-dash2/pages/index.tsx @@ -111,6 +111,10 @@ const OverViewMap: React.FC<{ top: 0, left: 0, }) + const [colorModalPosition, setColorModalPosition] = useState({ + top: 100, + left: 100, + }) // Marker state const { @@ -517,10 +521,14 @@ const OverViewMap: React.FC<{ setShowPlatformsModal(false) }, []) - // handleVehicleColorRequest- Show vehicle colors - const handleVehicleColorRequest = useCallback((vehicleName?: string) => { - setShowVehicleColors(true) - }, []) + // handleVehicleColorRequest - Show vehicle colors at anchor (from Map button) + const handleVehicleColorRequest = useCallback( + (anchor?: { top: number; left: number }) => { + setColorModalPosition(anchor ?? { top: 100, left: 100 }) + setShowVehicleColors(true) + }, + [] + ) // handleCloseVehicleColors - vehicle colors modal is closed const handleCloseVehicleColors = useCallback((vehicleName?: string) => { From c825d3d296f82832c57d27b7d3cc7d2d710a4030 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:33:14 -0800 Subject: [PATCH 13/16] Render vehicle colors modal outside Map on overview and deployment --- apps/lrauv-dash2/components/DeploymentMap.tsx | 16 ++++++++-------- apps/lrauv-dash2/pages/index.tsx | 10 ++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/lrauv-dash2/components/DeploymentMap.tsx b/apps/lrauv-dash2/components/DeploymentMap.tsx index f3d5d617..80b0a3c5 100644 --- a/apps/lrauv-dash2/components/DeploymentMap.tsx +++ b/apps/lrauv-dash2/components/DeploymentMap.tsx @@ -673,15 +673,15 @@ const DeploymentMap: React.FC = ({ disableAutoFit={isTimelineScrubbing} /> )} - setColorModalOpen(false)} - anchorPosition={colorModalPosition} - trackedVehicles={vehicleName ? [vehicleName] : []} - activeVehicle={vehicleName || undefined} - forceShowAll={true} - /> + setColorModalOpen(false)} + anchorPosition={colorModalPosition} + trackedVehicles={vehicleName ? [vehicleName] : []} + activeVehicle={vehicleName || undefined} + forceShowAll={true} + /> ) } diff --git a/apps/lrauv-dash2/pages/index.tsx b/apps/lrauv-dash2/pages/index.tsx index 83fe1a5a..e2de3dde 100644 --- a/apps/lrauv-dash2/pages/index.tsx +++ b/apps/lrauv-dash2/pages/index.tsx @@ -23,6 +23,7 @@ import { useMarkers } from '../components/MarkerContext' import toast from 'react-hot-toast' import { createLogger } from '@mbari/utils' import { PlatformsListModal } from '../components/PlatformsListModal' +import VehicleColorsModal from '../components/VehicleColorsModal' // This is a tricky workaround to prevent leaflet from crashing next.js // SSR. If we don't do this, the leaflet map will be loaded server side @@ -707,6 +708,15 @@ const OverViewMap: React.FC<{ ) })} + {showVehicleColors ? ( + v.name) ?? []} + forceShowAll={true} + /> + ) : null} ) } From 34a90841c93e37e48639eb3e69ee201cdb8166c1 Mon Sep 17 00:00:00 2001 From: Zack Lyon Date: Tue, 24 Feb 2026 13:49:03 -0800 Subject: [PATCH 14/16] Add map development guide document --- docs/map-development-guide.md | 249 ++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/map-development-guide.md diff --git a/docs/map-development-guide.md b/docs/map-development-guide.md new file mode 100644 index 00000000..08d95057 --- /dev/null +++ b/docs/map-development-guide.md @@ -0,0 +1,249 @@ +Map development guide + +Goal + +Make map features easy to add without breaking SSR, Leaflet, Google Maps, or creating circular dependencies between the app and @mbari/react-ui. + +⸻ + +The golden rules + +Rule 1: The library map stays “pure” + +packages/react-ui must never import from any app (like apps/lrauv-dash2). + +✅ OK in @mbari/react-ui +• React + react-leaflet components +• Map controls, base layers, measurement UI +• Generic map helpers and types +• Callback props (to ask the app to do something) + +❌ Not OK in @mbari/react-ui +• Importing app modals/components +• Loading Google Maps scripts +• Importing Leaflet side-effect plugins (googlemutant, markercluster, etc.) +• Reading env variables or calling app APIs directly + +⸻ + +Rule 2: The app owns side effects and plugins + +Anything that: +• touches window, document, navigator +• loads a