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