@trackunit/react-map 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +522 -351
- package/index.esm.js +520 -351
- package/package.json +1 -1
- package/src/appearance/useMapAppearanceControls.d.ts +2 -2
- package/src/controls/Controls.d.ts +3 -6
- package/src/controls/DefaultControls.d.ts +37 -0
- package/src/controls/composeControlAreas.d.ts +24 -0
- package/src/controls/types.d.ts +17 -25
- package/src/controls/useControlStack.d.ts +11 -9
- package/src/controls/useControls.d.ts +20 -0
- package/src/controls/useDefaultControls.d.ts +62 -0
- package/src/core/types.d.ts +0 -14
- package/src/core/useMap.d.ts +1 -3
- package/src/index.d.ts +16 -12
- package/src/layers/image-overlay/useImageOverlay.d.ts +6 -3
- package/src/layers/routes/useRoute.d.ts +6 -3
- package/src/layers/types.d.ts +7 -3
- package/src/layers/useLayers.d.ts +6 -3
- package/src/layers/useMarkers/useMarkers.d.ts +6 -3
- package/src/layers/useShapes/useShapes.d.ts +6 -3
- package/src/controls/useControlsConfig.d.ts +0 -27
package/index.esm.js
CHANGED
|
@@ -1238,7 +1238,6 @@ function useMapAdapterState(adapterConfig) {
|
|
|
1238
1238
|
* All action functions and the Map component reference are stable across re-renders.
|
|
1239
1239
|
*
|
|
1240
1240
|
* @param adapterConfig - Adapter configuration from an adapter factory (e.g., googleMapsAdapter)
|
|
1241
|
-
* @param options - Optional configuration for controls and other features
|
|
1242
1241
|
* @returns Tuple of [Map component, API object]
|
|
1243
1242
|
* @example Basic usage
|
|
1244
1243
|
* ```tsx
|
|
@@ -1277,7 +1276,7 @@ function useMapAdapterState(adapterConfig) {
|
|
|
1277
1276
|
* };
|
|
1278
1277
|
* ```
|
|
1279
1278
|
*/
|
|
1280
|
-
const useMap = (adapterConfig
|
|
1279
|
+
const useMap = (adapterConfig) => {
|
|
1281
1280
|
const { adapter, renderConfig } = useMapAdapterState(adapterConfig);
|
|
1282
1281
|
// Subscribe to status changes only (isReady, appearance, etc. — low frequency).
|
|
1283
1282
|
// Camera state (center/zoom/bounds) is accessed via useCameraState(api) instead,
|
|
@@ -1354,7 +1353,6 @@ const useMap = (adapterConfig, options) => {
|
|
|
1354
1353
|
actions,
|
|
1355
1354
|
on,
|
|
1356
1355
|
containerRef: containerRefHolder,
|
|
1357
|
-
options,
|
|
1358
1356
|
adapterConfig: renderConfig,
|
|
1359
1357
|
annotations: annotationStore,
|
|
1360
1358
|
loadingIndicator: loadingIndicatorStore,
|
|
@@ -1366,7 +1364,6 @@ const useMap = (adapterConfig, options) => {
|
|
|
1366
1364
|
actions,
|
|
1367
1365
|
on,
|
|
1368
1366
|
containerRefHolder,
|
|
1369
|
-
options,
|
|
1370
1367
|
renderConfig,
|
|
1371
1368
|
annotationStore,
|
|
1372
1369
|
loadingIndicatorStore,
|
|
@@ -1606,7 +1603,12 @@ const collapseAll = (categories, allControls) => {
|
|
|
1606
1603
|
}
|
|
1607
1604
|
// Nothing to show
|
|
1608
1605
|
if (allItems.length === 0) {
|
|
1609
|
-
return {
|
|
1606
|
+
return {
|
|
1607
|
+
navigation: { controls: [] },
|
|
1608
|
+
settings: { controls: [] },
|
|
1609
|
+
tools: { controls: [] },
|
|
1610
|
+
statistics: { controls: [] },
|
|
1611
|
+
};
|
|
1610
1612
|
}
|
|
1611
1613
|
const singleMenu = {
|
|
1612
1614
|
type: "menu",
|
|
@@ -1619,7 +1621,7 @@ const collapseAll = (categories, allControls) => {
|
|
|
1619
1621
|
return {
|
|
1620
1622
|
navigation: { controls: [singleMenu] },
|
|
1621
1623
|
settings: { controls: [] },
|
|
1622
|
-
tools:
|
|
1624
|
+
tools: { controls: [] },
|
|
1623
1625
|
statistics: { controls: [] },
|
|
1624
1626
|
};
|
|
1625
1627
|
};
|
|
@@ -1643,10 +1645,12 @@ const collapseControls = (config, mode, metadata) => {
|
|
|
1643
1645
|
if (mode === "full")
|
|
1644
1646
|
return config;
|
|
1645
1647
|
// Categories that participate in collapse (statistics excluded — summary widgets stay visible).
|
|
1648
|
+
// Tools fold into the single all-controls menu in "collapsed" mode; in "full"/"categorized"
|
|
1649
|
+
// they render standalone in the top-right corner.
|
|
1646
1650
|
const categories = [
|
|
1647
1651
|
{ key: "navigation", label: metadata.navigation.label, controls: config.navigation.controls },
|
|
1648
1652
|
{ key: "settings", label: metadata.settings.label, controls: config.settings.controls },
|
|
1649
|
-
|
|
1653
|
+
{ key: "tools", label: metadata.tools.label, controls: config.tools.controls },
|
|
1650
1654
|
];
|
|
1651
1655
|
if (mode === "collapsed") {
|
|
1652
1656
|
const collapsed = collapseAll(categories, metadata.allControls);
|
|
@@ -1659,7 +1663,10 @@ const collapseControls = (config, mode, metadata) => {
|
|
|
1659
1663
|
const settings = {
|
|
1660
1664
|
controls: collapseCategory(config.settings.controls, "settings", metadata.settings),
|
|
1661
1665
|
};
|
|
1662
|
-
|
|
1666
|
+
const tools = {
|
|
1667
|
+
controls: collapseCategory(config.tools.controls, "tools", metadata.tools),
|
|
1668
|
+
};
|
|
1669
|
+
return { navigation, settings, tools, statistics: config.statistics };
|
|
1663
1670
|
};
|
|
1664
1671
|
|
|
1665
1672
|
/**
|
|
@@ -2412,6 +2419,109 @@ const GLOBE_CENTERS = [
|
|
|
2412
2419
|
/** Pick the globe icon whose projection center is closest to the given longitude. */
|
|
2413
2420
|
const longitudeToGlobeIcon = (longitude) => GLOBE_CENTERS.reduce((closest, current) => Math.abs(current.center - longitude) < Math.abs(closest.center - longitude) ? current : closest).icon;
|
|
2414
2421
|
|
|
2422
|
+
/**
|
|
2423
|
+
* Internal component that renders controls.
|
|
2424
|
+
* Separated to allow Suspense boundary in parent.
|
|
2425
|
+
*/
|
|
2426
|
+
const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
|
|
2427
|
+
const [t] = useTranslation();
|
|
2428
|
+
// Measure container to derive available space and responsive mode
|
|
2429
|
+
const containerRef = useRef(null);
|
|
2430
|
+
const { ref: measureRef, geometry } = useMeasure();
|
|
2431
|
+
const mergedRef = useMergeRefs([containerRef, measureRef]);
|
|
2432
|
+
const { displayAnnotations, isExiting, shouldRender: shouldRenderAnnotations, } = useAnnotationPresence(api.annotations);
|
|
2433
|
+
const skipContainerBreakpoints = baseControlsConfig.navigation.controls.length === 0 &&
|
|
2434
|
+
baseControlsConfig.settings.controls.length === 0 &&
|
|
2435
|
+
baseControlsConfig.tools.controls.length === 0 &&
|
|
2436
|
+
baseControlsConfig.statistics.controls.length === 0 &&
|
|
2437
|
+
!shouldRenderAnnotations;
|
|
2438
|
+
const breakpoints = useContainerBreakpoints(containerRef, { skip: skipContainerBreakpoints });
|
|
2439
|
+
const containerHeight = geometry?.height ?? Infinity;
|
|
2440
|
+
const safeAreaLayout = useContext(SafeAreaLayoutContext);
|
|
2441
|
+
const measuredAvailableSpace = breakpoints.isSm && containerHeight >= HEIGHT_COMFORTABLE_THRESHOLD ? "comfortable" : "constrained";
|
|
2442
|
+
const measuredResponsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
2443
|
+
const availableSpace = safeAreaLayout?.availableSpace ?? measuredAvailableSpace;
|
|
2444
|
+
const responsiveMode = safeAreaLayout?.responsiveMode ?? measuredResponsiveMode;
|
|
2445
|
+
// Menus use modals only in collapsed mode (narrowest containers); popovers otherwise
|
|
2446
|
+
const menuPresentation = responsiveMode === "collapsed" ? "modal" : "popover";
|
|
2447
|
+
// Derive portal ID from the map container's stable ID (set by createMapComponent)
|
|
2448
|
+
const portalId = api.containerRef.current?.id;
|
|
2449
|
+
// Geolocation position for region-appropriate globe icon (updated if the user clicks "My Location")
|
|
2450
|
+
const { position } = useGeolocation();
|
|
2451
|
+
const navigationIcon = position !== null ? longitudeToGlobeIcon(position[0]) : "GlobeEuropeAfrica";
|
|
2452
|
+
// Category metadata: icon + translated label for menu triggers, tooltips, and section headers
|
|
2453
|
+
const categoryMetadata = useMemo(() => ({
|
|
2454
|
+
navigation: { icon: navigationIcon, label: t("controls.category.navigation") },
|
|
2455
|
+
settings: { icon: "Cog6Tooth", label: t("controls.category.settings") },
|
|
2456
|
+
tools: { icon: "Wrench", label: t("controls.category.tools") },
|
|
2457
|
+
statistics: { icon: "ChartBarSquare", label: t("controls.category.statistics") },
|
|
2458
|
+
allControls: { icon: "EllipsisHorizontal", label: t("controls.allControls") },
|
|
2459
|
+
}), [t, navigationIcon]);
|
|
2460
|
+
// Apply responsive collapsing -- restructures config based on available space
|
|
2461
|
+
const controlsConfig = useMemo(() => collapseControls(baseControlsConfig, responsiveMode, categoryMetadata), [baseControlsConfig, responsiveMode, categoryMetadata]);
|
|
2462
|
+
useLayoutEffect(() => api.annotations.claimRenderer(), [api.annotations]);
|
|
2463
|
+
const { navigation, settings, tools, statistics } = controlsConfig;
|
|
2464
|
+
const hasNavigationControls = navigation.controls.length > 0;
|
|
2465
|
+
const hasSettingsControls = settings.controls.length > 0;
|
|
2466
|
+
const hasToolsControls = tools.controls.length > 0;
|
|
2467
|
+
const hasStatisticsControls = statistics.controls.length > 0;
|
|
2468
|
+
const hasStatisticsAreaContent = shouldRenderAnnotations || hasStatisticsControls;
|
|
2469
|
+
if (!hasNavigationControls && !hasSettingsControls && !hasToolsControls && !hasStatisticsAreaContent) {
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
return (jsxs("div", { className: twMerge(
|
|
2473
|
+
// Overlay positioning within Map's ZStack grid
|
|
2474
|
+
"col-start-1 row-start-1",
|
|
2475
|
+
// Pointer events only on children, not the overlay itself
|
|
2476
|
+
"pointer-events-none",
|
|
2477
|
+
// Layout grid for semantic areas
|
|
2478
|
+
"grid h-full w-full",
|
|
2479
|
+
// Responsive grid: navigation bottom-right, settings top-left, statistics bottom-left
|
|
2480
|
+
"grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
|
|
2481
|
+
// Isolate creates a new stacking context to avoid z-index issues
|
|
2482
|
+
"isolate", className), "data-testid": "map-controls", ref: mergedRef, children: [hasToolsControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-1 flex flex-col gap-1", "data-testid": "map-controls-tools", children: tools.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasNavigationControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-3 flex flex-col gap-1", children: navigation.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasSettingsControls ? (jsx("div", { className: "pointer-events-auto col-start-1 row-start-1 flex w-fit flex-col gap-1", "data-testid": "map-controls-settings", children: settings.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasStatisticsAreaContent ? (jsxs("div", { className: twMerge("pointer-events-none col-start-1 row-start-3 flex max-h-full max-w-[20rem] flex-col items-start justify-end gap-1 overflow-visible", !hasStatisticsControls && "self-end"), "data-testid": "map-controls-statistics", children: [shouldRenderAnnotations ? (jsx("div", { className: "pointer-events-auto", "data-testid": "map-controls-statistics-annotations", children: jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) })) : null, hasStatisticsControls ? (jsx("div", { className: "pointer-events-auto flex max-h-full w-fit max-w-full flex-col gap-1 overflow-auto", "data-testid": "map-controls-statistics-controls", children: statistics.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null] })) : null] }));
|
|
2483
|
+
};
|
|
2484
|
+
/**
|
|
2485
|
+
* Map controls renderer — lays out a `ControlsConfig` across the four semantic
|
|
2486
|
+
* areas of the map (top-right tools, bottom-right navigation, top-left settings,
|
|
2487
|
+
* bottom-left statistics + annotations).
|
|
2488
|
+
*
|
|
2489
|
+
* Build the `controls` prop with `useDefaultControls`, `useControls`, or
|
|
2490
|
+
* `composeControlAreas`. For the common "default built-ins only" case prefer
|
|
2491
|
+
* the `<DefaultControls>` wrapper which combines these two steps.
|
|
2492
|
+
*
|
|
2493
|
+
* Inherits layout decisions from the surrounding SafeAreaOverlay when rendered
|
|
2494
|
+
* inside `<Map>`, and falls back to its own container measurement when
|
|
2495
|
+
* rendered standalone.
|
|
2496
|
+
*
|
|
2497
|
+
* Uses Suspense internally to handle translation loading without blocking the map.
|
|
2498
|
+
*
|
|
2499
|
+
* @example
|
|
2500
|
+
* ```tsx
|
|
2501
|
+
* import { useMap, useDefaultControls, Controls } from "@trackunit/react-map";
|
|
2502
|
+
* import { googleMapsAdapter } from "@trackunit/react-map-adapter-google";
|
|
2503
|
+
*
|
|
2504
|
+
* const MyMap = () => {
|
|
2505
|
+
* const [Map, api] = useMap(googleMapsAdapter({ apiKey }));
|
|
2506
|
+
* const controls = useDefaultControls(api);
|
|
2507
|
+
*
|
|
2508
|
+
* return (
|
|
2509
|
+
* <Map>
|
|
2510
|
+
* <Controls api={api} controls={controls} />
|
|
2511
|
+
* </Map>
|
|
2512
|
+
* );
|
|
2513
|
+
* };
|
|
2514
|
+
* ```
|
|
2515
|
+
*/
|
|
2516
|
+
const ControlsInner = ({ api, controls, className }) => {
|
|
2517
|
+
// Wrap in Suspense to handle useTranslation loading without blocking parent components
|
|
2518
|
+
return (jsx(Suspense, { fallback: null, children: jsx(ControlsContent, { api: api, className: className, controls: controls }) }));
|
|
2519
|
+
};
|
|
2520
|
+
// `api` identity is stable between camera events since useMap only re-runs its useMemo
|
|
2521
|
+
// when MapStatus (isReady, appearance, etc.) changes — not on pan/zoom. Default React.memo
|
|
2522
|
+
// shallow equality is sufficient; no custom comparator needed.
|
|
2523
|
+
const Controls = memo(ControlsInner);
|
|
2524
|
+
|
|
2415
2525
|
const cvaMapPreview = cvaMerge([
|
|
2416
2526
|
"relative",
|
|
2417
2527
|
"h-[72px]",
|
|
@@ -2607,11 +2717,11 @@ const buildAppearanceOptions = (themes, mapTypes, t) => {
|
|
|
2607
2717
|
* const controlsConfig: ControlsConfig = {
|
|
2608
2718
|
* navigation: { controls: navigationControls },
|
|
2609
2719
|
* settings: { controls: appearanceControls },
|
|
2610
|
-
* tools:
|
|
2720
|
+
* tools: { controls: [] },
|
|
2611
2721
|
* statistics: { controls: [] },
|
|
2612
2722
|
* };
|
|
2613
2723
|
*
|
|
2614
|
-
* <Controls api={api}
|
|
2724
|
+
* <Controls api={api} controls={controlsConfig} />
|
|
2615
2725
|
* ```
|
|
2616
2726
|
*/
|
|
2617
2727
|
const useMapAppearanceControls = ({ api, options: appearanceOptions, persistenceKey = "map-appearance-settings", }) => {
|
|
@@ -2729,17 +2839,36 @@ const defineControlStack = (controls) => {
|
|
|
2729
2839
|
return stack;
|
|
2730
2840
|
};
|
|
2731
2841
|
|
|
2842
|
+
// ============================================================================
|
|
2843
|
+
// Hook
|
|
2844
|
+
// ============================================================================
|
|
2732
2845
|
/**
|
|
2733
|
-
* Builds the controls configuration
|
|
2846
|
+
* Builds the default built-in controls configuration (zoom, fullscreen, my
|
|
2847
|
+
* location, map style) from the map API.
|
|
2848
|
+
*
|
|
2849
|
+
* Replaces `useControlsConfig` with a cleaner, flat options structure. Each
|
|
2850
|
+
* control can be disabled individually via `options`. Omitting `options`
|
|
2851
|
+
* enables all controls.
|
|
2734
2852
|
*
|
|
2735
|
-
*
|
|
2736
|
-
*
|
|
2737
|
-
*
|
|
2853
|
+
* Returns a `BuiltInControlsConfig` — a `ControlsConfig` enriched with named
|
|
2854
|
+
* handles for each built-in control so consumers can reference them without
|
|
2855
|
+
* searching by id.
|
|
2856
|
+
*
|
|
2857
|
+
* @example
|
|
2858
|
+
* ```tsx
|
|
2859
|
+
* const builtIn = useDefaultControls(api);
|
|
2860
|
+
* return <Controls controls={builtIn} api={api} />;
|
|
2861
|
+
* ```
|
|
2862
|
+
* @example Disable specific controls
|
|
2863
|
+
* ```tsx
|
|
2864
|
+
* const builtIn = useDefaultControls(api, {
|
|
2865
|
+
* navigation: { myLocation: { enabled: false } },
|
|
2866
|
+
* });
|
|
2867
|
+
* ```
|
|
2738
2868
|
*/
|
|
2739
|
-
const
|
|
2869
|
+
const useDefaultControls = (api, options) => {
|
|
2740
2870
|
const [t] = useTranslation();
|
|
2741
2871
|
const { getPosition } = useGeolocation();
|
|
2742
|
-
// Fullscreen state and callbacks derived from the container ref
|
|
2743
2872
|
const isFullscreen = useIsFullscreen();
|
|
2744
2873
|
const requestFullscreen = useCallback(() => {
|
|
2745
2874
|
void api.containerRef.current?.requestFullscreen();
|
|
@@ -2747,20 +2876,18 @@ const useControlsConfig = ({ options, api }) => {
|
|
|
2747
2876
|
const exitFullscreen = useCallback(() => {
|
|
2748
2877
|
void document.exitFullscreen();
|
|
2749
2878
|
}, []);
|
|
2750
|
-
const zoomControl = useMemo(() => options?.
|
|
2879
|
+
const zoomControl = useMemo(() => options?.navigation?.zoom?.enabled === false
|
|
2751
2880
|
? undefined
|
|
2752
2881
|
: {
|
|
2753
|
-
// Zoom stepper control - uses relative zoom(delta)
|
|
2754
2882
|
type: "stepper",
|
|
2755
2883
|
id: "zoom",
|
|
2756
2884
|
label: t("controls.zoom"),
|
|
2757
2885
|
increment: () => void api.actions.zoomBy(1),
|
|
2758
2886
|
decrement: () => void api.actions.zoomBy(-1),
|
|
2759
|
-
}, [api.actions, options?.
|
|
2760
|
-
const fullscreenControl = useMemo(() => options?.
|
|
2887
|
+
}, [api.actions, options?.navigation?.zoom?.enabled, t]);
|
|
2888
|
+
const fullscreenControl = useMemo(() => options?.navigation?.fullscreen?.enabled === false
|
|
2761
2889
|
? undefined
|
|
2762
2890
|
: {
|
|
2763
|
-
// Fullscreen toggle control
|
|
2764
2891
|
type: "toggle",
|
|
2765
2892
|
id: "fullscreen",
|
|
2766
2893
|
icon: { on: "ArrowsPointingIn", off: "ArrowsPointingOut" },
|
|
@@ -2775,11 +2902,10 @@ const useControlsConfig = ({ options, api }) => {
|
|
|
2775
2902
|
}
|
|
2776
2903
|
},
|
|
2777
2904
|
"data-testid": "map-fullscreen-button",
|
|
2778
|
-
}, [exitFullscreen, isFullscreen, options?.
|
|
2779
|
-
const myLocationControl = useMemo(() => options?.
|
|
2905
|
+
}, [exitFullscreen, isFullscreen, options?.navigation?.fullscreen?.enabled, requestFullscreen, t]);
|
|
2906
|
+
const myLocationControl = useMemo(() => options?.navigation?.myLocation?.enabled === false
|
|
2780
2907
|
? undefined
|
|
2781
2908
|
: {
|
|
2782
|
-
// My location button - zooms the map to a ~2x2 km box around the user's position
|
|
2783
2909
|
type: "button",
|
|
2784
2910
|
id: "my-location",
|
|
2785
2911
|
icon: "Gps",
|
|
@@ -2789,7 +2915,6 @@ const useControlsConfig = ({ options, api }) => {
|
|
|
2789
2915
|
if (pos === null)
|
|
2790
2916
|
return;
|
|
2791
2917
|
const [longitude, latitude] = pos;
|
|
2792
|
-
// ~1 km in degrees: lat is constant, lon varies by cos(lat)
|
|
2793
2918
|
const latOffset = 1 / 111;
|
|
2794
2919
|
const lonOffset = 1 / (111 * Math.cos((latitude * Math.PI) / 180));
|
|
2795
2920
|
void api.actions.fitBounds([
|
|
@@ -2801,11 +2926,10 @@ const useControlsConfig = ({ options, api }) => {
|
|
|
2801
2926
|
});
|
|
2802
2927
|
},
|
|
2803
2928
|
"data-testid": "map-my-location-button",
|
|
2804
|
-
}, [api.actions, getPosition, options?.
|
|
2929
|
+
}, [api.actions, getPosition, options?.navigation?.myLocation?.enabled, t]);
|
|
2805
2930
|
const navigationControls = useMemo(() => defineControlStack([zoomControl, fullscreenControl, myLocationControl]), [fullscreenControl, myLocationControl, zoomControl]);
|
|
2806
|
-
// Map appearance controls (settings area) - live mini-map previews for switching theme/style
|
|
2807
2931
|
const mapAppearanceControls = useMapAppearanceControls({ api });
|
|
2808
|
-
const mapStyleControl = useMemo(() => options?.
|
|
2932
|
+
const mapStyleControl = useMemo(() => options?.settings?.mapStyle?.enabled === false ? undefined : mapAppearanceControls[0], [mapAppearanceControls, options?.settings?.mapStyle?.enabled]);
|
|
2809
2933
|
const settingsControls = useMemo(() => defineControlStack([mapStyleControl]), [mapStyleControl]);
|
|
2810
2934
|
return useMemo(() => ({
|
|
2811
2935
|
navigation: {
|
|
@@ -2818,107 +2942,112 @@ const useControlsConfig = ({ options, api }) => {
|
|
|
2818
2942
|
controls: settingsControls,
|
|
2819
2943
|
mapStyle: mapStyleControl,
|
|
2820
2944
|
},
|
|
2821
|
-
tools:
|
|
2945
|
+
tools: { controls: [] },
|
|
2822
2946
|
statistics: { controls: [] },
|
|
2823
2947
|
}), [fullscreenControl, mapStyleControl, myLocationControl, navigationControls, settingsControls, zoomControl]);
|
|
2824
2948
|
};
|
|
2825
2949
|
|
|
2826
2950
|
/**
|
|
2827
|
-
*
|
|
2828
|
-
*
|
|
2829
|
-
*/
|
|
2830
|
-
const ControlsContent = ({ api, className, controlsConfigProp }) => {
|
|
2831
|
-
const [t] = useTranslation();
|
|
2832
|
-
// Measure container to derive available space and responsive mode
|
|
2833
|
-
const containerRef = useRef(null);
|
|
2834
|
-
const { ref: measureRef, geometry } = useMeasure();
|
|
2835
|
-
const mergedRef = useMergeRefs([containerRef, measureRef]);
|
|
2836
|
-
const builtControlsConfig = useControlsConfig({ options: api.options, api });
|
|
2837
|
-
const baseControlsConfig = controlsConfigProp ?? builtControlsConfig;
|
|
2838
|
-
const { displayAnnotations, isExiting, shouldRender: shouldRenderAnnotations, } = useAnnotationPresence(api.annotations);
|
|
2839
|
-
const skipContainerBreakpoints = baseControlsConfig.navigation.controls.length === 0 &&
|
|
2840
|
-
baseControlsConfig.settings.controls.length === 0 &&
|
|
2841
|
-
baseControlsConfig.statistics.controls.length === 0 &&
|
|
2842
|
-
!shouldRenderAnnotations;
|
|
2843
|
-
const breakpoints = useContainerBreakpoints(containerRef, { skip: skipContainerBreakpoints });
|
|
2844
|
-
const containerHeight = geometry?.height ?? Infinity;
|
|
2845
|
-
const safeAreaLayout = useContext(SafeAreaLayoutContext);
|
|
2846
|
-
const measuredAvailableSpace = breakpoints.isSm && containerHeight >= HEIGHT_COMFORTABLE_THRESHOLD ? "comfortable" : "constrained";
|
|
2847
|
-
const measuredResponsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
2848
|
-
const availableSpace = safeAreaLayout?.availableSpace ?? measuredAvailableSpace;
|
|
2849
|
-
const responsiveMode = safeAreaLayout?.responsiveMode ?? measuredResponsiveMode;
|
|
2850
|
-
// Menus use modals only in collapsed mode (narrowest containers); popovers otherwise
|
|
2851
|
-
const menuPresentation = responsiveMode === "collapsed" ? "modal" : "popover";
|
|
2852
|
-
// Derive portal ID from the map container's stable ID (set by createMapComponent)
|
|
2853
|
-
const portalId = api.containerRef.current?.id;
|
|
2854
|
-
// Geolocation position for region-appropriate globe icon (updated if the user clicks "My Location")
|
|
2855
|
-
const { position } = useGeolocation();
|
|
2856
|
-
const navigationIcon = position !== null ? longitudeToGlobeIcon(position[0]) : "GlobeEuropeAfrica";
|
|
2857
|
-
// Category metadata: icon + translated label for menu triggers, tooltips, and section headers
|
|
2858
|
-
const categoryMetadata = useMemo(() => ({
|
|
2859
|
-
navigation: { icon: navigationIcon, label: t("controls.category.navigation") },
|
|
2860
|
-
settings: { icon: "Cog6Tooth", label: t("controls.category.settings") },
|
|
2861
|
-
tools: { icon: "Wrench", label: t("controls.category.tools") },
|
|
2862
|
-
statistics: { icon: "ChartBarSquare", label: t("controls.category.statistics") },
|
|
2863
|
-
allControls: { icon: "EllipsisHorizontal", label: t("controls.allControls") },
|
|
2864
|
-
}), [t, navigationIcon]);
|
|
2865
|
-
// Apply responsive collapsing -- restructures config based on available space
|
|
2866
|
-
const controlsConfig = useMemo(() => collapseControls(baseControlsConfig, responsiveMode, categoryMetadata), [baseControlsConfig, responsiveMode, categoryMetadata]);
|
|
2867
|
-
useLayoutEffect(() => api.annotations.claimRenderer(), [api.annotations]);
|
|
2868
|
-
const { navigation, settings, statistics } = controlsConfig;
|
|
2869
|
-
const hasNavigationControls = navigation.controls.length > 0;
|
|
2870
|
-
const hasSettingsControls = settings.controls.length > 0;
|
|
2871
|
-
const hasStatisticsControls = statistics.controls.length > 0;
|
|
2872
|
-
const hasStatisticsAreaContent = shouldRenderAnnotations || hasStatisticsControls;
|
|
2873
|
-
if (!hasNavigationControls && !hasSettingsControls && !hasStatisticsAreaContent) {
|
|
2874
|
-
return null;
|
|
2875
|
-
}
|
|
2876
|
-
return (jsxs("div", { className: twMerge(
|
|
2877
|
-
// Overlay positioning within Map's ZStack grid
|
|
2878
|
-
"col-start-1 row-start-1",
|
|
2879
|
-
// Pointer events only on children, not the overlay itself
|
|
2880
|
-
"pointer-events-none",
|
|
2881
|
-
// Layout grid for semantic areas
|
|
2882
|
-
"grid h-full w-full",
|
|
2883
|
-
// Responsive grid: navigation bottom-right, settings top-left, statistics bottom-left
|
|
2884
|
-
"grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
|
|
2885
|
-
// Isolate creates a new stacking context to avoid z-index issues
|
|
2886
|
-
"isolate", className), "data-testid": "map-controls", ref: mergedRef, children: [hasNavigationControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-3 flex flex-col gap-1", children: navigation.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasSettingsControls ? (jsx("div", { className: "pointer-events-auto col-start-1 row-start-1 flex w-fit flex-col gap-1", "data-testid": "map-controls-settings", children: settings.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasStatisticsAreaContent ? (jsxs("div", { className: twMerge("pointer-events-none col-start-1 row-start-3 flex max-h-full max-w-[20rem] flex-col items-start justify-end gap-1 overflow-visible", !hasStatisticsControls && "self-end"), "data-testid": "map-controls-statistics", children: [shouldRenderAnnotations ? (jsx("div", { className: "pointer-events-auto", "data-testid": "map-controls-statistics-annotations", children: jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) })) : null, hasStatisticsControls ? (jsx("div", { className: "pointer-events-auto flex max-h-full w-fit max-w-full flex-col gap-1 overflow-auto", "data-testid": "map-controls-statistics-controls", children: statistics.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null] })) : null] }));
|
|
2887
|
-
};
|
|
2888
|
-
/**
|
|
2889
|
-
* Map controls component that renders navigation and settings controls.
|
|
2890
|
-
*
|
|
2891
|
-
* Builds controls configuration internally using useControlsConfig hook.
|
|
2892
|
-
* Uses semantic areas (navigation, settings, tools, statistics) for organization.
|
|
2893
|
-
* All layout decisions are owned by this component for responsive positioning.
|
|
2894
|
-
*
|
|
2895
|
-
* Inherits layout decisions from the surrounding SafeAreaOverlay when rendered
|
|
2896
|
-
* inside <Map>, and falls back to its own measurement when rendered standalone.
|
|
2951
|
+
* Convenience wrapper that calls `useDefaultControls` and passes the result
|
|
2952
|
+
* directly to `<Controls>`.
|
|
2897
2953
|
*
|
|
2898
|
-
*
|
|
2954
|
+
* Use this for the common case where only the built-in controls (zoom,
|
|
2955
|
+
* fullscreen, my location, map style) are needed, with no custom additions.
|
|
2956
|
+
* For custom control stacks, call `useDefaultControls` + `useControls` (or
|
|
2957
|
+
* `composeControlAreas`) and pass the result to `<Controls>` directly.
|
|
2899
2958
|
*
|
|
2900
2959
|
* @example
|
|
2901
2960
|
* ```tsx
|
|
2902
|
-
* import { useMap,
|
|
2961
|
+
* import { useMap, DefaultControls } from "@trackunit/react-map";
|
|
2903
2962
|
* import { googleMapsAdapter } from "@trackunit/react-map-adapter-google";
|
|
2904
2963
|
*
|
|
2905
|
-
* const
|
|
2964
|
+
* const MyMap = () => {
|
|
2965
|
+
* const [Map, api] = useMap(googleMapsAdapter({ apiKey }));
|
|
2906
2966
|
*
|
|
2907
|
-
*
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
*
|
|
2911
|
-
*
|
|
2967
|
+
* return (
|
|
2968
|
+
* <Map>
|
|
2969
|
+
* <DefaultControls api={api} />
|
|
2970
|
+
* </Map>
|
|
2971
|
+
* );
|
|
2972
|
+
* };
|
|
2912
2973
|
* ```
|
|
2913
2974
|
*/
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
return
|
|
2975
|
+
const DefaultControls = ({ api, options, className }) => {
|
|
2976
|
+
const controls = useDefaultControls(api, options);
|
|
2977
|
+
return jsx(Controls, { api: api, className: className, controls: controls });
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2980
|
+
// ============================================================================
|
|
2981
|
+
// Internal helpers
|
|
2982
|
+
// ============================================================================
|
|
2983
|
+
/**
|
|
2984
|
+
* Applies prepend/append modifiers to a single area section.
|
|
2985
|
+
* Returns the original section reference unchanged when no modifiers are given.
|
|
2986
|
+
*/
|
|
2987
|
+
const applyAreaModifiers = (section, area) => {
|
|
2988
|
+
const { prepend, append } = area;
|
|
2989
|
+
const hasPrepend = prepend !== undefined && prepend.length > 0;
|
|
2990
|
+
const hasAppend = append !== undefined && append.length > 0;
|
|
2991
|
+
if (!hasPrepend && !hasAppend)
|
|
2992
|
+
return section;
|
|
2993
|
+
return {
|
|
2994
|
+
...section,
|
|
2995
|
+
controls: defineControlStack([...(prepend ?? []), ...section.controls, ...(append ?? [])]),
|
|
2996
|
+
};
|
|
2997
|
+
};
|
|
2998
|
+
const CATEGORY_KEYS = ["navigation", "settings", "tools", "statistics"];
|
|
2999
|
+
// ============================================================================
|
|
3000
|
+
// Hook
|
|
3001
|
+
// ============================================================================
|
|
3002
|
+
/**
|
|
3003
|
+
* Merges per-area `prepend`/`append` modifier lists into a base `ControlsConfig`.
|
|
3004
|
+
*
|
|
3005
|
+
* Memoizes internally — stable references are returned when neither `base` nor
|
|
3006
|
+
* `areas` has changed. Modifiers are applied via `defineControlStack`, which
|
|
3007
|
+
* filters `undefined` entries and rejects duplicate control ids.
|
|
3008
|
+
*
|
|
3009
|
+
* When `areas` is omitted the `base` reference is returned as-is.
|
|
3010
|
+
*
|
|
3011
|
+
* @example
|
|
3012
|
+
* ```ts
|
|
3013
|
+
* const builtIn = useDefaultControls(api);
|
|
3014
|
+
* const controls = useControls(builtIn, {
|
|
3015
|
+
* navigation: { append: [layerNavControl] },
|
|
3016
|
+
* settings: { prepend: [layerSettingsControl] },
|
|
3017
|
+
* });
|
|
3018
|
+
* ```
|
|
3019
|
+
*/
|
|
3020
|
+
const useControls = (base, areas) => {
|
|
3021
|
+
return useMemo(() => {
|
|
3022
|
+
if (areas === undefined)
|
|
3023
|
+
return base;
|
|
3024
|
+
let hasChanges = false;
|
|
3025
|
+
const updated = {
|
|
3026
|
+
navigation: base.navigation,
|
|
3027
|
+
settings: base.settings,
|
|
3028
|
+
tools: base.tools,
|
|
3029
|
+
statistics: base.statistics,
|
|
3030
|
+
};
|
|
3031
|
+
for (const key of CATEGORY_KEYS) {
|
|
3032
|
+
const area = areas[key];
|
|
3033
|
+
if (area === undefined)
|
|
3034
|
+
continue;
|
|
3035
|
+
const modified = applyAreaModifiers(base[key], area);
|
|
3036
|
+
if (modified !== base[key]) {
|
|
3037
|
+
updated[key] = modified;
|
|
3038
|
+
hasChanges = true;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (!hasChanges)
|
|
3042
|
+
return base;
|
|
3043
|
+
return {
|
|
3044
|
+
navigation: updated.navigation,
|
|
3045
|
+
settings: updated.settings,
|
|
3046
|
+
tools: updated.tools,
|
|
3047
|
+
statistics: updated.statistics,
|
|
3048
|
+
};
|
|
3049
|
+
}, [base, areas]);
|
|
2917
3050
|
};
|
|
2918
|
-
// `api` identity is stable between camera events since useMap only re-runs its useMemo
|
|
2919
|
-
// when MapStatus (isReady, appearance, etc.) changes — not on pan/zoom. Default React.memo
|
|
2920
|
-
// shallow equality is sufficient; no custom comparator needed.
|
|
2921
|
-
const Controls = memo(ControlsInner);
|
|
2922
3051
|
|
|
2923
3052
|
const MARKER_SPRING_CONFIG = {
|
|
2924
3053
|
stiffness: 380,
|
|
@@ -4591,7 +4720,7 @@ const useLayerReady = (loading) => {
|
|
|
4591
4720
|
// ============================================================================
|
|
4592
4721
|
// Constants
|
|
4593
4722
|
// ============================================================================
|
|
4594
|
-
const EMPTY_CONTROLS$
|
|
4723
|
+
const EMPTY_CONTROLS$3 = {};
|
|
4595
4724
|
// ============================================================================
|
|
4596
4725
|
// Hook
|
|
4597
4726
|
// ============================================================================
|
|
@@ -4631,7 +4760,7 @@ const useImageOverlay = (options) => {
|
|
|
4631
4760
|
ready: layerReady.ready,
|
|
4632
4761
|
getBounds,
|
|
4633
4762
|
counts,
|
|
4634
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
4763
|
+
controls: controls ?? EMPTY_CONTROLS$3,
|
|
4635
4764
|
fitParticipation,
|
|
4636
4765
|
layerType: "image-overlay",
|
|
4637
4766
|
url,
|
|
@@ -4912,7 +5041,7 @@ const createLazyGetter = (compute) => {
|
|
|
4912
5041
|
// ============================================================================
|
|
4913
5042
|
// Constants
|
|
4914
5043
|
// ============================================================================
|
|
4915
|
-
const EMPTY_CONTROLS$
|
|
5044
|
+
const EMPTY_CONTROLS$2 = {};
|
|
4916
5045
|
const EMPTY_FEATURES$1 = { type: "FeatureCollection", features: [] };
|
|
4917
5046
|
// ============================================================================
|
|
4918
5047
|
// Internal helpers
|
|
@@ -5000,7 +5129,7 @@ const useRoute = (options) => {
|
|
|
5000
5129
|
ready: layerReady.ready,
|
|
5001
5130
|
getBounds,
|
|
5002
5131
|
counts,
|
|
5003
|
-
controls: controls ?? EMPTY_CONTROLS$
|
|
5132
|
+
controls: controls ?? EMPTY_CONTROLS$2,
|
|
5004
5133
|
fitParticipation,
|
|
5005
5134
|
layerType: "route",
|
|
5006
5135
|
features,
|
|
@@ -5190,7 +5319,12 @@ const interactionReducer = (state, action) => {
|
|
|
5190
5319
|
// ============================================================================
|
|
5191
5320
|
// Constants
|
|
5192
5321
|
// ============================================================================
|
|
5193
|
-
const
|
|
5322
|
+
const EMPTY_AREA_CONTROLS = {
|
|
5323
|
+
navigation: [],
|
|
5324
|
+
settings: [],
|
|
5325
|
+
tools: [],
|
|
5326
|
+
statistics: [],
|
|
5327
|
+
};
|
|
5194
5328
|
const RESOLVED_PROMISE = Promise.resolve();
|
|
5195
5329
|
// ============================================================================
|
|
5196
5330
|
// Hook
|
|
@@ -5256,17 +5390,28 @@ const useLayers = (api, layers) => {
|
|
|
5256
5390
|
const promises = layers.map(layer => layer.ready);
|
|
5257
5391
|
return Promise.all(promises).then(() => undefined);
|
|
5258
5392
|
}, [layers]);
|
|
5259
|
-
// ---- Controls (aggregated from all layers) ----
|
|
5393
|
+
// ---- Controls (aggregated per area from all layers) ----
|
|
5260
5394
|
const controls = useMemo(() => {
|
|
5261
|
-
const
|
|
5395
|
+
const areaKeys = ["navigation", "settings", "tools", "statistics"];
|
|
5396
|
+
const areas = {
|
|
5397
|
+
navigation: [],
|
|
5398
|
+
settings: [],
|
|
5399
|
+
tools: [],
|
|
5400
|
+
statistics: [],
|
|
5401
|
+
};
|
|
5402
|
+
let hasAny = false;
|
|
5262
5403
|
for (const layer of layers) {
|
|
5263
|
-
|
|
5264
|
-
|
|
5404
|
+
for (const key of areaKeys) {
|
|
5405
|
+
const areaControls = layer.controls[key];
|
|
5406
|
+
if (areaControls !== undefined && areaControls.length > 0) {
|
|
5407
|
+
areas[key].push(...areaControls);
|
|
5408
|
+
hasAny = true;
|
|
5409
|
+
}
|
|
5265
5410
|
}
|
|
5266
5411
|
}
|
|
5267
|
-
if (
|
|
5268
|
-
return
|
|
5269
|
-
return
|
|
5412
|
+
if (!hasAny)
|
|
5413
|
+
return EMPTY_AREA_CONTROLS;
|
|
5414
|
+
return areas;
|
|
5270
5415
|
}, [layers]);
|
|
5271
5416
|
// ---- Build the return object ----
|
|
5272
5417
|
return useMemo(() => ({
|
|
@@ -5482,7 +5627,7 @@ const resolveServerClusters = (cluster, data, getId, resolveGroups, viewportCtx)
|
|
|
5482
5627
|
// Empty constants (stable references)
|
|
5483
5628
|
// ============================================================================
|
|
5484
5629
|
const EMPTY_FEATURES = { type: "FeatureCollection", features: [] };
|
|
5485
|
-
const EMPTY_CONTROLS$1 =
|
|
5630
|
+
const EMPTY_CONTROLS$1 = {};
|
|
5486
5631
|
/**
|
|
5487
5632
|
* Filter items to only those with valid positions and extract id + position.
|
|
5488
5633
|
* Invalid positions (e.g. latitude out of range) are skipped. Logs once when any are filtered.
|
|
@@ -6183,7 +6328,7 @@ const buildMultiPartDecorations = (feature, strokeColor, pointRadius, strokeWidt
|
|
|
6183
6328
|
// ============================================================================
|
|
6184
6329
|
// Constants
|
|
6185
6330
|
// ============================================================================
|
|
6186
|
-
const EMPTY_CONTROLS =
|
|
6331
|
+
const EMPTY_CONTROLS = {};
|
|
6187
6332
|
const EMPTY_STYLE = {};
|
|
6188
6333
|
// ============================================================================
|
|
6189
6334
|
// Internal helpers
|
|
@@ -9254,285 +9399,165 @@ const ShapeAnnotationLabel = ({ geometry, color, label, theme, isHovered = false
|
|
|
9254
9399
|
return jsx("div", { className: "pointer-events-none", children: pill });
|
|
9255
9400
|
};
|
|
9256
9401
|
|
|
9257
|
-
const markerStateFromParams = (params) => {
|
|
9258
|
-
if ("markerState" in params) {
|
|
9259
|
-
return params.markerState;
|
|
9260
|
-
}
|
|
9261
|
-
const input = {
|
|
9262
|
-
selected: params.selected,
|
|
9263
|
-
hovered: params.hovered,
|
|
9264
|
-
expandedRank: params.expandedRank,
|
|
9265
|
-
};
|
|
9266
|
-
return resolveMarkerState(input);
|
|
9267
|
-
};
|
|
9268
|
-
const stackPhaseForMarkerState = (markerState) => {
|
|
9269
|
-
if (markerState === "selected")
|
|
9270
|
-
return "selected";
|
|
9271
|
-
if (markerState === "hovered")
|
|
9272
|
-
return "hovered";
|
|
9273
|
-
if (markerState === "expanded")
|
|
9274
|
-
return "expanded";
|
|
9275
|
-
return "idle";
|
|
9276
|
-
};
|
|
9277
9402
|
/**
|
|
9278
|
-
*
|
|
9279
|
-
* `markerRender.resolveDomPortalStacking` in `<Layers>`.
|
|
9403
|
+
* Build-your-own hook: stable reference to the cluster count formatter.
|
|
9280
9404
|
*
|
|
9281
|
-
*
|
|
9282
|
-
*
|
|
9283
|
-
|
|
9284
|
-
const resolveMapMarkerDomPortalStacking = (params) => {
|
|
9285
|
-
const { hasStick, isMounting, labelVisible } = params;
|
|
9286
|
-
if (isMounting === true) {
|
|
9287
|
-
return { geometry: "circle", phase: "idle" };
|
|
9288
|
-
}
|
|
9289
|
-
const markerState = markerStateFromParams(params);
|
|
9290
|
-
const form = resolveMarkerForm(markerState, labelVisible);
|
|
9291
|
-
const geometry = hasStick === true ? "stick" : form === "pill" ? "pill" : "circle";
|
|
9292
|
-
return {
|
|
9293
|
-
geometry,
|
|
9294
|
-
phase: stackPhaseForMarkerState(markerState),
|
|
9295
|
-
};
|
|
9296
|
-
};
|
|
9297
|
-
|
|
9298
|
-
/**
|
|
9299
|
-
* Build-your-own hook: stable references to the full adaptive marker kit.
|
|
9300
|
-
*
|
|
9301
|
-
* Returns the complete set of primitives needed to hand-roll an adaptive marker
|
|
9302
|
-
* layer (DOM ↔ symbol mode switching, zoom-based sizing, portal stacking) while
|
|
9303
|
-
* staying consistent with `useMarkers`'s built-in rendering pipeline.
|
|
9405
|
+
* Returns `formatClusterCount` for consumers building custom cluster markers
|
|
9406
|
+
* that need consistent `"1.2k / 3.4m"` formatting without reimplementing
|
|
9407
|
+
* the truncation rules.
|
|
9304
9408
|
*
|
|
9305
9409
|
* @example
|
|
9306
9410
|
* ```tsx
|
|
9307
|
-
* const {
|
|
9308
|
-
*
|
|
9309
|
-
*
|
|
9310
|
-
* } = useAdaptiveMarkerHelpers();
|
|
9311
|
-
*
|
|
9312
|
-
* // Inside your adaptive render callback:
|
|
9313
|
-
* const medium = pickRenderMedium(ctx);
|
|
9314
|
-
* if (medium === "symbol") return pickSymbolDescriptor(color, theme, ctx);
|
|
9315
|
-
* const size = pickMarkerSize(ctx);
|
|
9316
|
-
* const state = resolveAdaptiveMarkerState({ selected, hovered });
|
|
9411
|
+
* const { formatClusterCount } = useClusterCountFormat();
|
|
9412
|
+
* // Inside your cluster render:
|
|
9413
|
+
* return <div>{formatClusterCount(clusterCount)}</div>;
|
|
9317
9414
|
* ```
|
|
9318
9415
|
*/
|
|
9319
|
-
const
|
|
9416
|
+
const useClusterCountFormat = () => {
|
|
9320
9417
|
return useMemo(() => ({
|
|
9321
|
-
|
|
9322
|
-
pickRenderMedium,
|
|
9323
|
-
pickSymbolDescriptor,
|
|
9324
|
-
pickSymbolDiameter,
|
|
9325
|
-
resolveAdaptiveMarkerState,
|
|
9326
|
-
downgradeMarkerDomSize,
|
|
9327
|
-
upgradeMarkerDomSize,
|
|
9328
|
-
resolveMapMarkerDomPortalStacking,
|
|
9329
|
-
DEFAULT_MARKER_SIZE_BREAKPOINTS,
|
|
9330
|
-
MARKER_DISC_BORDER_WIDTH_PX,
|
|
9418
|
+
formatClusterCount,
|
|
9331
9419
|
}), []);
|
|
9332
9420
|
};
|
|
9333
9421
|
|
|
9334
9422
|
/**
|
|
9335
|
-
*
|
|
9423
|
+
* Pure function that builds a `ControlsConfig` from per-area control stacks.
|
|
9336
9424
|
*
|
|
9337
|
-
*
|
|
9338
|
-
*
|
|
9339
|
-
*
|
|
9425
|
+
* Each area in `areas` is passed through `defineControlStack` (which filters
|
|
9426
|
+
* `undefined` entries and rejects duplicate ids). Omitted areas default to
|
|
9427
|
+
* `{ controls: [] }`.
|
|
9340
9428
|
*
|
|
9341
9429
|
* @example
|
|
9342
|
-
* ```
|
|
9343
|
-
* const
|
|
9344
|
-
*
|
|
9345
|
-
*
|
|
9346
|
-
* ```
|
|
9347
|
-
*/
|
|
9348
|
-
const useFitFeatureBounds = () => {
|
|
9349
|
-
return useMemo(() => ({
|
|
9350
|
-
fitFeatureBounds,
|
|
9351
|
-
}), []);
|
|
9352
|
-
};
|
|
9353
|
-
|
|
9354
|
-
const PADDING_PX = 12;
|
|
9355
|
-
/**
|
|
9356
|
-
* Default auto-pan resolver: pans the minimum amount to give the panel the
|
|
9357
|
-
* best possible screen space. Handles two cases:
|
|
9358
|
-
*
|
|
9359
|
-
* 1. **Clipping** — panel bleeds outside the map container on any side. Pans
|
|
9360
|
-
* to clear the bleed with 12 px of breathing room.
|
|
9361
|
-
*
|
|
9362
|
-
* 2. **Scroll-squish** — panel is within the container but shift-pressed against
|
|
9363
|
-
* an edge, so content overflows and the panel scrolls. Pans to give the panel
|
|
9364
|
-
* room to grow, up to the CSS `max-height` cap (`panelCssMaxHeight`).
|
|
9365
|
-
*
|
|
9366
|
-
* Returns `null` when the panel already fits without scrolling and without
|
|
9367
|
-
* clipping — meaning no pan is needed.
|
|
9368
|
-
*
|
|
9369
|
-
* Pass as `resolveAutoPan` to {@link usePanel} for the standard behavior:
|
|
9370
|
-
*
|
|
9371
|
-
* ```tsx
|
|
9372
|
-
* usePanel(api, {
|
|
9373
|
-
* id: "my-panel",
|
|
9374
|
-
* open: isOpen,
|
|
9375
|
-
* resolveAutoPan: defaultAutoPanResolver,
|
|
9376
|
-
* children: <MyPanelContent />,
|
|
9430
|
+
* ```ts
|
|
9431
|
+
* const controls = composeControlAreas({
|
|
9432
|
+
* navigation: [builtIn.navigation.zoom, builtIn.navigation.fullscreen],
|
|
9433
|
+
* settings: [builtIn.settings.mapStyle],
|
|
9377
9434
|
* });
|
|
9378
9435
|
* ```
|
|
9379
9436
|
*/
|
|
9380
|
-
const
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
// Cap at panelCssMaxHeight — panning further cannot reduce scrolling.
|
|
9387
|
-
const squishAmount = Math.max(0, Math.min(panelScrollHeight, panelCssMaxHeight) - panelRect.height);
|
|
9388
|
-
let squishY = 0;
|
|
9389
|
-
if (squishAmount > 0) {
|
|
9390
|
-
if (placement.startsWith("top")) {
|
|
9391
|
-
// Panel is above the anchor. Move anchor down (negative y) to open space above.
|
|
9392
|
-
squishY = -(squishAmount + PADDING_PX);
|
|
9393
|
-
}
|
|
9394
|
-
else if (placement.startsWith("bottom")) {
|
|
9395
|
-
// Panel is below the anchor. Move anchor up (positive y) to open space below.
|
|
9396
|
-
squishY = squishAmount + PADDING_PX;
|
|
9397
|
-
}
|
|
9398
|
-
else {
|
|
9399
|
-
// left/right placements: panel is vertically centered on the anchor.
|
|
9400
|
-
// Infer which edge shift pressed the panel against by comparing the
|
|
9401
|
-
// panel's natural center position to its actual rendered top.
|
|
9402
|
-
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
|
|
9403
|
-
const naturalPanelTop = anchorCenterY - panelRect.height / 2;
|
|
9404
|
-
const shiftDelta = panelRect.top - naturalPanelTop;
|
|
9405
|
-
if (shiftDelta > 1) {
|
|
9406
|
-
// Shift pressed panel down (anchor near top) — move anchor down.
|
|
9407
|
-
squishY = -(squishAmount + PADDING_PX);
|
|
9408
|
-
}
|
|
9409
|
-
else if (shiftDelta < -1) {
|
|
9410
|
-
// Shift pressed panel up (anchor near bottom) — move anchor up.
|
|
9411
|
-
squishY = squishAmount + PADDING_PX;
|
|
9412
|
-
}
|
|
9413
|
-
// shiftDelta ≈ 0: squish is from CSS max-height alone, not position — skip.
|
|
9414
|
-
}
|
|
9415
|
-
}
|
|
9416
|
-
// After shift, clippingY and squishY are mutually exclusive on the same axis
|
|
9417
|
-
// (shift keeps the panel within container bounds). clippingY takes priority as
|
|
9418
|
-
// a defensive fallback for the edge case where both are non-zero.
|
|
9419
|
-
const y = clippingY || squishY;
|
|
9420
|
-
if (x === 0 && y === 0)
|
|
9421
|
-
return null;
|
|
9422
|
-
return { x, y, restoreOnDismiss: true };
|
|
9423
|
-
};
|
|
9437
|
+
const composeControlAreas = (areas) => ({
|
|
9438
|
+
navigation: { controls: defineControlStack(areas.navigation ?? []) },
|
|
9439
|
+
settings: { controls: defineControlStack(areas.settings ?? []) },
|
|
9440
|
+
tools: { controls: defineControlStack(areas.tools ?? []) },
|
|
9441
|
+
statistics: { controls: defineControlStack(areas.statistics ?? []) },
|
|
9442
|
+
});
|
|
9424
9443
|
|
|
9425
9444
|
/**
|
|
9426
|
-
* Build-your-own hook: stable
|
|
9445
|
+
* Build-your-own hook: stable references to control composition helpers.
|
|
9427
9446
|
*
|
|
9428
|
-
* Returns `
|
|
9429
|
-
*
|
|
9430
|
-
*
|
|
9447
|
+
* Returns `defineControlStack` and `composeControlAreas` for consumers building
|
|
9448
|
+
* custom control layouts without duplicating the stack-ordering logic already
|
|
9449
|
+
* used by `Controls`.
|
|
9431
9450
|
*
|
|
9432
9451
|
* @example
|
|
9433
9452
|
* ```tsx
|
|
9434
|
-
* const {
|
|
9435
|
-
*
|
|
9436
|
-
*
|
|
9437
|
-
*
|
|
9438
|
-
* resolveAutoPan: defaultAutoPanResolver,
|
|
9439
|
-
* children: <MyContent />,
|
|
9453
|
+
* const { defineControlStack, composeControlAreas } = useControlStack();
|
|
9454
|
+
* const controls = composeControlAreas({
|
|
9455
|
+
* navigation: [builtIn.navigation.zoom, builtIn.navigation.fullscreen],
|
|
9456
|
+
* settings: [builtIn.settings.mapStyle],
|
|
9440
9457
|
* });
|
|
9441
9458
|
* ```
|
|
9442
9459
|
*/
|
|
9443
|
-
const
|
|
9460
|
+
const useControlStack = () => {
|
|
9444
9461
|
return useMemo(() => ({
|
|
9445
|
-
|
|
9462
|
+
defineControlStack,
|
|
9463
|
+
composeControlAreas,
|
|
9446
9464
|
}), []);
|
|
9447
9465
|
};
|
|
9448
9466
|
|
|
9449
9467
|
/**
|
|
9450
|
-
* Build-your-own hook: stable reference to the
|
|
9468
|
+
* Build-your-own hook: stable reference to the `fitFeatureBounds` map action.
|
|
9451
9469
|
*
|
|
9452
|
-
* Returns `
|
|
9453
|
-
*
|
|
9454
|
-
*
|
|
9470
|
+
* Returns the `fitFeatureBounds` helper for consumers who need to programmatically
|
|
9471
|
+
* zoom/fit the viewport to a GeoJSON geometry — for example in click handlers that
|
|
9472
|
+
* should navigate to a selected shape or annotation.
|
|
9455
9473
|
*
|
|
9456
9474
|
* @example
|
|
9457
9475
|
* ```tsx
|
|
9458
|
-
* const {
|
|
9459
|
-
* //
|
|
9460
|
-
*
|
|
9476
|
+
* const { fitFeatureBounds } = useFitFeatureBounds();
|
|
9477
|
+
* // On click:
|
|
9478
|
+
* fitFeatureBounds(api, feature.geometry);
|
|
9461
9479
|
* ```
|
|
9462
9480
|
*/
|
|
9463
|
-
const
|
|
9481
|
+
const useFitFeatureBounds = () => {
|
|
9464
9482
|
return useMemo(() => ({
|
|
9465
|
-
|
|
9483
|
+
fitFeatureBounds,
|
|
9466
9484
|
}), []);
|
|
9467
9485
|
};
|
|
9468
9486
|
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9487
|
+
const markerStateFromParams = (params) => {
|
|
9488
|
+
if ("markerState" in params) {
|
|
9489
|
+
return params.markerState;
|
|
9490
|
+
}
|
|
9491
|
+
const input = {
|
|
9492
|
+
selected: params.selected,
|
|
9493
|
+
hovered: params.hovered,
|
|
9494
|
+
expandedRank: params.expandedRank,
|
|
9495
|
+
};
|
|
9496
|
+
return resolveMarkerState(input);
|
|
9497
|
+
};
|
|
9498
|
+
const stackPhaseForMarkerState = (markerState) => {
|
|
9499
|
+
if (markerState === "selected")
|
|
9500
|
+
return "selected";
|
|
9501
|
+
if (markerState === "hovered")
|
|
9502
|
+
return "hovered";
|
|
9503
|
+
if (markerState === "expanded")
|
|
9504
|
+
return "expanded";
|
|
9505
|
+
return "idle";
|
|
9488
9506
|
};
|
|
9489
|
-
|
|
9490
9507
|
/**
|
|
9491
|
-
*
|
|
9492
|
-
*
|
|
9493
|
-
* Returns the `defaultMarkerColorConfig` factory and `resolveMarkerColors` resolver
|
|
9494
|
-
* for consumers building custom marker components that need to stay visually
|
|
9495
|
-
* consistent with `MapMarker`'s pill/indicator color system.
|
|
9508
|
+
* Maps `<MapMarker>` presentation + state to `DomPortalStackingResult` for use with
|
|
9509
|
+
* `markerRender.resolveDomPortalStacking` in `<Layers>`.
|
|
9496
9510
|
*
|
|
9497
|
-
*
|
|
9498
|
-
*
|
|
9499
|
-
* const { defaultMarkerColorConfig, resolveMarkerColors } = useMarkerColors();
|
|
9500
|
-
* const config = defaultMarkerColorConfig(theme);
|
|
9501
|
-
* const colors = resolveMarkerColors(fillColor, state, theme, config);
|
|
9502
|
-
* // Spread colors.cssVars onto the marker root element.
|
|
9503
|
-
* ```
|
|
9511
|
+
* Pass either `expandedRank` (for `resolveMarkerState`) or a pre-resolved `markerState`
|
|
9512
|
+
* when the expanded state is data-driven and does not use `expandedRank` (for example, story helpers).
|
|
9504
9513
|
*/
|
|
9505
|
-
const
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9514
|
+
const resolveMapMarkerDomPortalStacking = (params) => {
|
|
9515
|
+
const { hasStick, isMounting, labelVisible } = params;
|
|
9516
|
+
if (isMounting === true) {
|
|
9517
|
+
return { geometry: "circle", phase: "idle" };
|
|
9518
|
+
}
|
|
9519
|
+
const markerState = markerStateFromParams(params);
|
|
9520
|
+
const form = resolveMarkerForm(markerState, labelVisible);
|
|
9521
|
+
const geometry = hasStick === true ? "stick" : form === "pill" ? "pill" : "circle";
|
|
9522
|
+
return {
|
|
9523
|
+
geometry,
|
|
9524
|
+
phase: stackPhaseForMarkerState(markerState),
|
|
9525
|
+
};
|
|
9512
9526
|
};
|
|
9513
9527
|
|
|
9514
9528
|
/**
|
|
9515
|
-
* Build-your-own hook: stable references to
|
|
9529
|
+
* Build-your-own hook: stable references to the full adaptive marker kit.
|
|
9516
9530
|
*
|
|
9517
|
-
* Returns
|
|
9518
|
-
*
|
|
9519
|
-
* with
|
|
9531
|
+
* Returns the complete set of primitives needed to hand-roll an adaptive marker
|
|
9532
|
+
* layer (DOM ↔ symbol mode switching, zoom-based sizing, portal stacking) while
|
|
9533
|
+
* staying consistent with `useMarkers`'s built-in rendering pipeline.
|
|
9520
9534
|
*
|
|
9521
9535
|
* @example
|
|
9522
9536
|
* ```tsx
|
|
9523
|
-
* const {
|
|
9524
|
-
*
|
|
9525
|
-
*
|
|
9537
|
+
* const {
|
|
9538
|
+
* pickRenderMedium, pickMarkerSize, pickSymbolDescriptor,
|
|
9539
|
+
* resolveAdaptiveMarkerState, resolveMapMarkerDomPortalStacking,
|
|
9540
|
+
* } = useAdaptiveMarkerHelpers();
|
|
9541
|
+
*
|
|
9542
|
+
* // Inside your adaptive render callback:
|
|
9543
|
+
* const medium = pickRenderMedium(ctx);
|
|
9544
|
+
* if (medium === "symbol") return pickSymbolDescriptor(color, theme, ctx);
|
|
9545
|
+
* const size = pickMarkerSize(ctx);
|
|
9546
|
+
* const state = resolveAdaptiveMarkerState({ selected, hovered });
|
|
9526
9547
|
* ```
|
|
9527
9548
|
*/
|
|
9528
|
-
const
|
|
9549
|
+
const useAdaptiveMarkerHelpers = () => {
|
|
9529
9550
|
return useMemo(() => ({
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9551
|
+
pickMarkerSize,
|
|
9552
|
+
pickRenderMedium,
|
|
9553
|
+
pickSymbolDescriptor,
|
|
9554
|
+
pickSymbolDiameter,
|
|
9534
9555
|
resolveAdaptiveMarkerState,
|
|
9535
|
-
|
|
9556
|
+
downgradeMarkerDomSize,
|
|
9557
|
+
upgradeMarkerDomSize,
|
|
9558
|
+
resolveMapMarkerDomPortalStacking,
|
|
9559
|
+
DEFAULT_MARKER_SIZE_BREAKPOINTS,
|
|
9560
|
+
MARKER_DISC_BORDER_WIDTH_PX,
|
|
9536
9561
|
}), []);
|
|
9537
9562
|
};
|
|
9538
9563
|
|
|
@@ -9630,6 +9655,150 @@ const useShapeLabelHelpers = () => {
|
|
|
9630
9655
|
}), []);
|
|
9631
9656
|
};
|
|
9632
9657
|
|
|
9658
|
+
/**
|
|
9659
|
+
* Build-your-own hook: stable references to marker state resolution primitives.
|
|
9660
|
+
*
|
|
9661
|
+
* Returns a memoized object of the pure resolver functions for consumers that
|
|
9662
|
+
* want to hand-roll their own MapMarker replacement while staying consistent
|
|
9663
|
+
* with the library's state priority rules (selected → expanded → hovered → default).
|
|
9664
|
+
*
|
|
9665
|
+
* @example
|
|
9666
|
+
* ```tsx
|
|
9667
|
+
* const { resolveMarkerState, resolveMarkerForm } = useMarkerStateResolvers();
|
|
9668
|
+
* const state = resolveMarkerState({ selected, hovered, expandedRank });
|
|
9669
|
+
* const form = resolveMarkerForm(state, labelVisible);
|
|
9670
|
+
* ```
|
|
9671
|
+
*/
|
|
9672
|
+
const useMarkerStateResolvers = () => {
|
|
9673
|
+
return useMemo(() => ({
|
|
9674
|
+
resolveMarkerState,
|
|
9675
|
+
resolveMarkerForm,
|
|
9676
|
+
resolveEffectiveSize,
|
|
9677
|
+
resolveMarkerDomSize,
|
|
9678
|
+
resolveAdaptiveMarkerState,
|
|
9679
|
+
MARKER_SIZE_MAP,
|
|
9680
|
+
}), []);
|
|
9681
|
+
};
|
|
9682
|
+
|
|
9683
|
+
/**
|
|
9684
|
+
* Build-your-own hook: stable references to theme-aware marker color primitives.
|
|
9685
|
+
*
|
|
9686
|
+
* Returns the `defaultMarkerColorConfig` factory and `resolveMarkerColors` resolver
|
|
9687
|
+
* for consumers building custom marker components that need to stay visually
|
|
9688
|
+
* consistent with `MapMarker`'s pill/indicator color system.
|
|
9689
|
+
*
|
|
9690
|
+
* @example
|
|
9691
|
+
* ```tsx
|
|
9692
|
+
* const { defaultMarkerColorConfig, resolveMarkerColors } = useMarkerColors();
|
|
9693
|
+
* const config = defaultMarkerColorConfig(theme);
|
|
9694
|
+
* const colors = resolveMarkerColors(fillColor, state, theme, config);
|
|
9695
|
+
* // Spread colors.cssVars onto the marker root element.
|
|
9696
|
+
* ```
|
|
9697
|
+
*/
|
|
9698
|
+
const useMarkerColors = () => {
|
|
9699
|
+
return useMemo(() => ({
|
|
9700
|
+
defaultMarkerColorConfig,
|
|
9701
|
+
resolveMarkerColors,
|
|
9702
|
+
MARKER_DARK_PILL,
|
|
9703
|
+
MARKER_LIGHT_PILL,
|
|
9704
|
+
}), []);
|
|
9705
|
+
};
|
|
9706
|
+
|
|
9707
|
+
const PADDING_PX = 12;
|
|
9708
|
+
/**
|
|
9709
|
+
* Default auto-pan resolver: pans the minimum amount to give the panel the
|
|
9710
|
+
* best possible screen space. Handles two cases:
|
|
9711
|
+
*
|
|
9712
|
+
* 1. **Clipping** — panel bleeds outside the map container on any side. Pans
|
|
9713
|
+
* to clear the bleed with 12 px of breathing room.
|
|
9714
|
+
*
|
|
9715
|
+
* 2. **Scroll-squish** — panel is within the container but shift-pressed against
|
|
9716
|
+
* an edge, so content overflows and the panel scrolls. Pans to give the panel
|
|
9717
|
+
* room to grow, up to the CSS `max-height` cap (`panelCssMaxHeight`).
|
|
9718
|
+
*
|
|
9719
|
+
* Returns `null` when the panel already fits without scrolling and without
|
|
9720
|
+
* clipping — meaning no pan is needed.
|
|
9721
|
+
*
|
|
9722
|
+
* Pass as `resolveAutoPan` to {@link usePanel} for the standard behavior:
|
|
9723
|
+
*
|
|
9724
|
+
* ```tsx
|
|
9725
|
+
* usePanel(api, {
|
|
9726
|
+
* id: "my-panel",
|
|
9727
|
+
* open: isOpen,
|
|
9728
|
+
* resolveAutoPan: defaultAutoPanResolver,
|
|
9729
|
+
* children: <MyPanelContent />,
|
|
9730
|
+
* });
|
|
9731
|
+
* ```
|
|
9732
|
+
*/
|
|
9733
|
+
const defaultAutoPanResolver = (ctx) => {
|
|
9734
|
+
const { clipping, placement, panelRect, anchorRect, panelScrollHeight, panelCssMaxHeight } = ctx;
|
|
9735
|
+
// --- Clipping: clear overflow past container edges ---
|
|
9736
|
+
const x = clipping.right > 0 ? clipping.right + PADDING_PX : clipping.left > 0 ? -(clipping.left + PADDING_PX) : 0;
|
|
9737
|
+
const clippingY = clipping.bottom > 0 ? clipping.bottom + PADDING_PX : clipping.top > 0 ? -(clipping.top + PADDING_PX) : 0;
|
|
9738
|
+
// --- Scroll-squish: pan to give the panel room to stop scrolling.
|
|
9739
|
+
// Cap at panelCssMaxHeight — panning further cannot reduce scrolling.
|
|
9740
|
+
const squishAmount = Math.max(0, Math.min(panelScrollHeight, panelCssMaxHeight) - panelRect.height);
|
|
9741
|
+
let squishY = 0;
|
|
9742
|
+
if (squishAmount > 0) {
|
|
9743
|
+
if (placement.startsWith("top")) {
|
|
9744
|
+
// Panel is above the anchor. Move anchor down (negative y) to open space above.
|
|
9745
|
+
squishY = -(squishAmount + PADDING_PX);
|
|
9746
|
+
}
|
|
9747
|
+
else if (placement.startsWith("bottom")) {
|
|
9748
|
+
// Panel is below the anchor. Move anchor up (positive y) to open space below.
|
|
9749
|
+
squishY = squishAmount + PADDING_PX;
|
|
9750
|
+
}
|
|
9751
|
+
else {
|
|
9752
|
+
// left/right placements: panel is vertically centered on the anchor.
|
|
9753
|
+
// Infer which edge shift pressed the panel against by comparing the
|
|
9754
|
+
// panel's natural center position to its actual rendered top.
|
|
9755
|
+
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
|
|
9756
|
+
const naturalPanelTop = anchorCenterY - panelRect.height / 2;
|
|
9757
|
+
const shiftDelta = panelRect.top - naturalPanelTop;
|
|
9758
|
+
if (shiftDelta > 1) {
|
|
9759
|
+
// Shift pressed panel down (anchor near top) — move anchor down.
|
|
9760
|
+
squishY = -(squishAmount + PADDING_PX);
|
|
9761
|
+
}
|
|
9762
|
+
else if (shiftDelta < -1) {
|
|
9763
|
+
// Shift pressed panel up (anchor near bottom) — move anchor up.
|
|
9764
|
+
squishY = squishAmount + PADDING_PX;
|
|
9765
|
+
}
|
|
9766
|
+
// shiftDelta ≈ 0: squish is from CSS max-height alone, not position — skip.
|
|
9767
|
+
}
|
|
9768
|
+
}
|
|
9769
|
+
// After shift, clippingY and squishY are mutually exclusive on the same axis
|
|
9770
|
+
// (shift keeps the panel within container bounds). clippingY takes priority as
|
|
9771
|
+
// a defensive fallback for the edge case where both are non-zero.
|
|
9772
|
+
const y = clippingY || squishY;
|
|
9773
|
+
if (x === 0 && y === 0)
|
|
9774
|
+
return null;
|
|
9775
|
+
return { x, y, restoreOnDismiss: true };
|
|
9776
|
+
};
|
|
9777
|
+
|
|
9778
|
+
/**
|
|
9779
|
+
* Build-your-own hook: stable reference to the default auto-pan resolver.
|
|
9780
|
+
*
|
|
9781
|
+
* Returns `defaultAutoPanResolver` for consumers who want to pass it to
|
|
9782
|
+
* `usePanel({ resolveAutoPan })` or use it as a baseline to wrap/replace
|
|
9783
|
+
* with custom logic.
|
|
9784
|
+
*
|
|
9785
|
+
* @example
|
|
9786
|
+
* ```tsx
|
|
9787
|
+
* const { defaultAutoPanResolver } = useAutoPanResolver();
|
|
9788
|
+
* usePanel(api, {
|
|
9789
|
+
* id: "my-panel",
|
|
9790
|
+
* open: isOpen,
|
|
9791
|
+
* resolveAutoPan: defaultAutoPanResolver,
|
|
9792
|
+
* children: <MyContent />,
|
|
9793
|
+
* });
|
|
9794
|
+
* ```
|
|
9795
|
+
*/
|
|
9796
|
+
const useAutoPanResolver = () => {
|
|
9797
|
+
return useMemo(() => ({
|
|
9798
|
+
defaultAutoPanResolver,
|
|
9799
|
+
}), []);
|
|
9800
|
+
};
|
|
9801
|
+
|
|
9633
9802
|
const VIEWPORT_BOUNDS = [-50, -50, 50, 50];
|
|
9634
9803
|
const INITIAL_STATE = {
|
|
9635
9804
|
center: [0, 0],
|
|
@@ -9749,4 +9918,4 @@ const mockMapApi = (overrides) => {
|
|
|
9749
9918
|
*/
|
|
9750
9919
|
setupLibraryTranslations();
|
|
9751
9920
|
|
|
9752
|
-
export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack,
|
|
9921
|
+
export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|