@trackunit/react-map 0.2.1 → 0.2.3
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 +43 -56
- package/index.esm.js +43 -56
- package/package.json +10 -11
- package/src/controls/ControlRenderer.d.ts +2 -3
- package/src/controls/renderers/menu/Menu.d.ts +5 -7
- package/src/controls/renderers/select/MenuSelect.d.ts +15 -1
- package/src/controls/types.d.ts +0 -11
- package/src/core/SafeAreaLayoutContext.d.ts +7 -0
- package/migrations/entry.js.map +0 -1
- package/src/controls/renderers/menu/Modal.d.ts +0 -21
package/index.cjs.js
CHANGED
|
@@ -12,7 +12,6 @@ var reactDom = require('react-dom');
|
|
|
12
12
|
var esToolkit = require('es-toolkit');
|
|
13
13
|
var reactCoreHooks = require('@trackunit/react-core-hooks');
|
|
14
14
|
var reactFormComponents = require('@trackunit/react-form-components');
|
|
15
|
-
var reactModal = require('@trackunit/react-modal');
|
|
16
15
|
var zod = require('zod');
|
|
17
16
|
var reactMinimalPieChart = require('react-minimal-pie-chart');
|
|
18
17
|
var uiDesignTokens = require('@trackunit/ui-design-tokens');
|
|
@@ -38,6 +37,7 @@ var defaultTranslations = {
|
|
|
38
37
|
"map.loadingData": "Loading map data",
|
|
39
38
|
"map.loadingErrorMessage": "Unable to load the map. Please try again later.",
|
|
40
39
|
"multiGeometry.badge.tooltip": "Part of a multi-part shape",
|
|
40
|
+
"panels.close": "Close",
|
|
41
41
|
"settings.mapStyle.dark": "Dark",
|
|
42
42
|
"settings.mapStyle.light": "Light",
|
|
43
43
|
"settings.mapStyle.roadmap": "Road map",
|
|
@@ -1180,13 +1180,14 @@ const createMapComponent = ({ adapter, Renderer, adapterName, containerRefHolder
|
|
|
1180
1180
|
const containerHeight = geometry?.height ?? Infinity;
|
|
1181
1181
|
const availableSpace = breakpoints.isSm && containerHeight >= HEIGHT_COMFORTABLE_THRESHOLD ? "comfortable" : "constrained";
|
|
1182
1182
|
const responsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
1183
|
+
const [safeAreaContentEl, setSafeAreaContentEl] = react.useState(null);
|
|
1184
|
+
const [safeAreaPanelPortalEl, setSafeAreaPanelPortalEl] = react.useState(null);
|
|
1183
1185
|
const layout = react.useMemo(() => ({
|
|
1184
1186
|
availableSpace,
|
|
1185
1187
|
responsiveMode,
|
|
1186
|
-
|
|
1188
|
+
safeAreaContentEl,
|
|
1189
|
+
}), [availableSpace, responsiveMode, safeAreaContentEl]);
|
|
1187
1190
|
const mapMoveSource = react.useMemo(() => ({ on: adapter.on.bind(adapter) }), []);
|
|
1188
|
-
const [safeAreaContentEl, setSafeAreaContentEl] = react.useState(null);
|
|
1189
|
-
const [safeAreaPanelPortalEl, setSafeAreaPanelPortalEl] = react.useState(null);
|
|
1190
1191
|
const content = (jsxRuntime.jsxs("div", { "aria-describedby": ariaDescribedBy, "aria-label": ariaLabel ?? "Interactive map", className: tailwindMerge.twMerge(
|
|
1191
1192
|
// ZStack: full-size grid with a single cell so children overlay the map
|
|
1192
1193
|
"isolate grid h-full w-full grid-cols-1 grid-rows-1", className), "data-testid": dataTestId, id: containerId, ref: mergedRef, role: "application", style: style, tabIndex: 0, children: [jsxRuntime.jsxs("div", { className: "relative col-start-1 row-start-1 grid h-full min-h-[200px] w-full", children: [jsxRuntime.jsx(Renderer, { adapterInstance: adapter, className: "h-full w-full", style: { height: "100%", width: "100%" } }), jsxRuntime.jsx(AdapterReadinessOverlay, { adapter: adapter })] }), jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 col-start-1 row-start-1 overflow-visible", "data-testid": "map-marker-overlay-portal" }), jsxRuntime.jsx("div", { className: "pointer-events-none relative isolate col-start-1 row-start-1 h-full min-h-0 w-full overflow-clip", "data-testid": "map-panel-overlay-portal", ref: setSafeAreaPanelPortalEl }), panelStore !== undefined ? (jsxRuntime.jsx(PanelOrchestrator, { anchoredPortalEl: safeAreaPanelPortalEl, containerEl: safeAreaContentEl ?? containerEl, mapMoveSource: mapMoveSource, store: panelStore })) : null, jsxRuntime.jsx(SafeAreaLayoutContext.Provider, { value: layout, children: jsxRuntime.jsxs(SafeAreaOverlay, { safeAreaContentRef: setSafeAreaContentEl, safeAreaInsets: safeAreaInsets ?? null, children: [children, annotationStore !== undefined ? jsxRuntime.jsx(FallbackAnnotationOverlay, { store: annotationStore }) : null, loadingIndicatorStore !== undefined ? (jsxRuntime.jsx(LoadingOverlay, { adapter: adapter, store: loadingIndicatorStore })) : null] }) })] }));
|
|
@@ -2127,13 +2128,26 @@ const MenuSearch = ({ config, resolved: _resolved }) => {
|
|
|
2127
2128
|
* Purely presentational -- all rendering decisions come from resolved props.
|
|
2128
2129
|
* `data-testid` (when set) is on the menu-item wrapper only — not duplicated on the inner `SelectField`.
|
|
2129
2130
|
*
|
|
2131
|
+
* The dropdown portals outside the menu's clipped/scrolled chrome (`overflow-clip`/`overflow-y-auto`
|
|
2132
|
+
* on `PopoverContent`/`MenuContent`) so long option lists are never truncated -- same default
|
|
2133
|
+
* behavior as the standalone toolbar `Select`. In fullscreen mode it portals into the map's own
|
|
2134
|
+
* `customPortalId` element instead of `document.body`, since real `document.body` content is
|
|
2135
|
+
* invisible while the map container is in the Fullscreen API (mirrors `PopoverContent`'s
|
|
2136
|
+
* `portalId={isFullScreen ? customPortalId : undefined}` pattern).
|
|
2137
|
+
*
|
|
2138
|
+
* The dropdown portal always stacks at the `z-popover` layer (`SelectField`'s default), the same
|
|
2139
|
+
* layer as the menu's own `PopoverContent` -- so it reliably paints on top rather than underneath
|
|
2140
|
+
* the menu wherever their bounds overlap, since both portal near the top of the DOM tree outside
|
|
2141
|
+
* the menu's own clipped subtree.
|
|
2142
|
+
*
|
|
2130
2143
|
* @internal
|
|
2131
2144
|
*/
|
|
2132
|
-
const MenuSelect = ({ config, resolved: _resolved }) => {
|
|
2145
|
+
const MenuSelect = ({ config, customPortalId, isFullScreen, resolved: _resolved }) => {
|
|
2133
2146
|
const { className, "data-testid": dataTestId, id, label, onChange, options, value } = config;
|
|
2147
|
+
const menuPortalTarget = isFullScreen && customPortalId !== undefined ? (document.getElementById(customPortalId) ?? undefined) : undefined;
|
|
2134
2148
|
return (jsxRuntime.jsx("div", { className: cvaControlMenuItemBase({
|
|
2135
2149
|
className: ["block", className],
|
|
2136
|
-
}), "data-testid": dataTestId, children: jsxRuntime.jsx(reactFormComponents.SelectField, { className: undefined, "data-testid": undefined, id: id, label: label, menuPortalTarget:
|
|
2150
|
+
}), "data-testid": dataTestId, children: jsxRuntime.jsx(reactFormComponents.SelectField, { className: undefined, "data-testid": undefined, id: id, label: label, menuPortalTarget: menuPortalTarget, onChange: event => {
|
|
2137
2151
|
const newValue = event.target.value;
|
|
2138
2152
|
if (newValue !== undefined) {
|
|
2139
2153
|
onChange(newValue);
|
|
@@ -2217,22 +2231,6 @@ const MenuToggleGroup = ({ config, resolved }) => {
|
|
|
2217
2231
|
}, isIconOnly: false, size: resolved.size }) }));
|
|
2218
2232
|
};
|
|
2219
2233
|
|
|
2220
|
-
/**
|
|
2221
|
-
* Renders a menu control as a modal instead of a popover.
|
|
2222
|
-
* Used on narrow containers where popovers would be clipped or awkward.
|
|
2223
|
-
*
|
|
2224
|
-
* The trigger is the same IconButton used by the popover path.
|
|
2225
|
-
* The content is rendered inside a Modal with ModalHeader (label + close)
|
|
2226
|
-
* and ModalBody (scrollable menu items).
|
|
2227
|
-
*
|
|
2228
|
-
* @internal
|
|
2229
|
-
*/
|
|
2230
|
-
const Modal = ({ config, resolved, children }) => {
|
|
2231
|
-
const { className, "data-testid": dataTestId, label } = config;
|
|
2232
|
-
const modal = reactModal.useModal({ role: "dialog" });
|
|
2233
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": label, className: className, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), onClick: modal.open, size: resolved.size, title: label, variant: "secondary" }), jsxRuntime.jsxs(reactModal.Modal, { ...modal, "data-testid": dataTestId ? `${dataTestId}-modal` : undefined, size: "small", children: [jsxRuntime.jsx(reactModal.ModalHeader, { "data-testid": dataTestId ? `${dataTestId}-modal` : undefined, heading: label, onClickClose: modal.close }), jsxRuntime.jsx(reactModal.ModalBody, { children: children })] })] }));
|
|
2234
|
-
};
|
|
2235
|
-
|
|
2236
2234
|
/**
|
|
2237
2235
|
* Renders a section header within a menu.
|
|
2238
2236
|
* Non-interactive, purely visual grouping element.
|
|
@@ -2253,7 +2251,7 @@ const Separator = ({ className }) => (jsxRuntime.jsx("div", { className: tailwin
|
|
|
2253
2251
|
* Calls resolveControl with placement: "menu" for each item.
|
|
2254
2252
|
* The sizing middleware will always derive compact sizing for menu items.
|
|
2255
2253
|
*/
|
|
2256
|
-
const renderMenuItem = (item, index, availableSpace) => {
|
|
2254
|
+
const renderMenuItem = (item, index, availableSpace, customPortalId, isFullScreen) => {
|
|
2257
2255
|
const resolved = resolveControl(item, { placement: "menu", availableSpace });
|
|
2258
2256
|
switch (item.type) {
|
|
2259
2257
|
case "separator":
|
|
@@ -2265,7 +2263,7 @@ const renderMenuItem = (item, index, availableSpace) => {
|
|
|
2265
2263
|
case "button":
|
|
2266
2264
|
return jsxRuntime.jsx(MenuButton, { config: item, resolved: resolved }, item.id);
|
|
2267
2265
|
case "select":
|
|
2268
|
-
return jsxRuntime.jsx(MenuSelect, { config: item, resolved: resolved }, item.id);
|
|
2266
|
+
return (jsxRuntime.jsx(MenuSelect, { config: item, customPortalId: customPortalId, isFullScreen: isFullScreen, resolved: resolved }, item.id));
|
|
2269
2267
|
case "radioGroup":
|
|
2270
2268
|
return jsxRuntime.jsx(MenuRadioGroup, { config: item, resolved: resolved }, item.id);
|
|
2271
2269
|
case "toggleGroup":
|
|
@@ -2305,53 +2303,44 @@ const groupItemsBySection = (items) => {
|
|
|
2305
2303
|
return groups;
|
|
2306
2304
|
};
|
|
2307
2305
|
/**
|
|
2308
|
-
* Renders the grouped menu items with section headers
|
|
2309
|
-
*
|
|
2306
|
+
* Renders the grouped menu items with section headers as a plain block list inside the popover's
|
|
2307
|
+
* own card styling (border, background, shadow).
|
|
2310
2308
|
*
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2309
|
+
* Scrolls internally (rather than on the outer `PopoverContent`) so the native scrollbar stays
|
|
2310
|
+
* inside this div's own rounded border instead of riding the popover's unstyled outer edge --
|
|
2311
|
+
* mirrors the cvaMenu/cvaMenuList split used elsewhere for the same reason.
|
|
2314
2312
|
*/
|
|
2315
|
-
const MenuContent = ({ availableSpace,
|
|
2313
|
+
const MenuContent = ({ availableSpace, customPortalId, isFullScreen, items }) => {
|
|
2316
2314
|
const groups = groupItemsBySection(items);
|
|
2317
|
-
return (jsxRuntime.jsx("div", { className:
|
|
2315
|
+
return (jsxRuntime.jsx("div", { className: "max-h-[max(40dvh,var(--spacing-64))] overflow-y-auto rounded-lg border border-neutral-300 bg-white py-1 shadow", role: "menu", children: groups.map((group, groupIndex) => (jsxRuntime.jsxs(react.Fragment, { children: [group.section !== undefined ? jsxRuntime.jsx(SectionHeader, { title: group.section }) : null, group.items.map((item, itemIndex) => renderMenuItem(item, groupIndex * 100 + itemIndex, availableSpace, customPortalId, isFullScreen))] }, groupIndex))) }));
|
|
2318
2316
|
};
|
|
2319
2317
|
/** Popover opens right; tooltip is suppressed while the menu is open. */
|
|
2320
2318
|
const MenuRightPopover = ({ availableSpace, config, customPortalId, resolved, }) => {
|
|
2321
2319
|
const { className, "data-testid": dataTestId, items, label } = config;
|
|
2322
2320
|
const isFullScreen = reactComponents.useIsFullscreen();
|
|
2323
|
-
return (jsxRuntime.jsx("div", { className: className ?? "w-fit", children: jsxRuntime.jsx(reactComponents.Popover, { placement: RIGHT_CONTROL_POPOVER_PLACEMENT, children: ({ isOpen }) => (jsxRuntime.jsxs(react.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, disabled: isOpen, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsxRuntime.jsx(reactComponents.IconButton, { ariaLabel: label, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { portalId: isFullScreen ? customPortalId : undefined, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace,
|
|
2321
|
+
return (jsxRuntime.jsx("div", { className: className ?? "w-fit", children: jsxRuntime.jsx(reactComponents.Popover, { placement: RIGHT_CONTROL_POPOVER_PLACEMENT, children: ({ isOpen }) => (jsxRuntime.jsxs(react.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, disabled: isOpen, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsxRuntime.jsx(reactComponents.IconButton, { ariaLabel: label, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] })) }) }));
|
|
2324
2322
|
};
|
|
2325
|
-
/** Popover presentation -- renders a
|
|
2323
|
+
/** Popover presentation -- renders a position-aware popover anchored to the trigger button. */
|
|
2326
2324
|
const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId, resolved, }) => {
|
|
2327
2325
|
const { className, "data-testid": dataTestId, items, label } = config;
|
|
2328
2326
|
const isFullScreen = reactComponents.useIsFullscreen();
|
|
2329
2327
|
const triggerRef = react.useRef(null);
|
|
2330
2328
|
const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
|
|
2331
|
-
return (jsxRuntime.jsx("div", { className: "w-fit", ref: triggerRef, children: jsxRuntime.
|
|
2329
|
+
return (jsxRuntime.jsx("div", { className: className ?? "w-fit", ref: triggerRef, children: jsxRuntime.jsxs(reactComponents.Popover, { placement: popoverPlacement, children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.IconButton, { "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, title: label, variant: "secondary" }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] }) }));
|
|
2332
2330
|
};
|
|
2333
|
-
const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
|
|
2334
|
-
if (overlaySide === "right") {
|
|
2335
|
-
return (jsxRuntime.jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
|
|
2336
|
-
}
|
|
2337
|
-
return (jsxRuntime.jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
|
|
2338
|
-
};
|
|
2339
|
-
/** Modal presentation -- renders a Modal with no popover-specific hooks. */
|
|
2340
|
-
const MenuModal = ({ availableSpace, config, resolved }) => (jsxRuntime.jsx(Modal, { config: config, resolved: resolved, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace, items: config.items, presentation: "modal" }) }));
|
|
2341
2331
|
/**
|
|
2342
|
-
* Renders a menu control as
|
|
2343
|
-
*
|
|
2332
|
+
* Renders a menu control as a popover -- either anchored to the trigger (auto-placed) or
|
|
2333
|
+
* opening to a fixed right placement, depending on `overlaySide`.
|
|
2344
2334
|
*
|
|
2345
|
-
* Delegates to
|
|
2346
|
-
* only runs the hooks it needs.
|
|
2335
|
+
* Delegates to sub-components so each path only runs the hooks it needs.
|
|
2347
2336
|
*
|
|
2348
2337
|
* @internal
|
|
2349
2338
|
*/
|
|
2350
|
-
const Menu = ({ availableSpace, config, containerRef, customPortalId,
|
|
2351
|
-
if (
|
|
2352
|
-
return jsxRuntime.jsx(
|
|
2339
|
+
const Menu = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
|
|
2340
|
+
if (overlaySide === "right") {
|
|
2341
|
+
return (jsxRuntime.jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
|
|
2353
2342
|
}
|
|
2354
|
-
return (jsxRuntime.jsx(
|
|
2343
|
+
return (jsxRuntime.jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
|
|
2355
2344
|
};
|
|
2356
2345
|
|
|
2357
2346
|
const fieldConfigForStandalonePopover = (config) => {
|
|
@@ -2482,7 +2471,7 @@ const ToggleGroup = ({ config, overlaySide: _overlaySide = undefined, resolved }
|
|
|
2482
2471
|
*
|
|
2483
2472
|
* @internal
|
|
2484
2473
|
*/
|
|
2485
|
-
const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId,
|
|
2474
|
+
const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, overlaySide = undefined, }) => {
|
|
2486
2475
|
const resolved = resolveControl(control, { placement: "standalone", availableSpace });
|
|
2487
2476
|
switch (control.type) {
|
|
2488
2477
|
case "button":
|
|
@@ -2501,7 +2490,7 @@ const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId
|
|
|
2501
2490
|
case "toggleGroup":
|
|
2502
2491
|
return jsxRuntime.jsx(ToggleGroup, { config: control, overlaySide: overlaySide, resolved: resolved });
|
|
2503
2492
|
case "menu":
|
|
2504
|
-
return (jsxRuntime.jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId,
|
|
2493
|
+
return (jsxRuntime.jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
|
|
2505
2494
|
case "custom":
|
|
2506
2495
|
return jsxRuntime.jsx(Custom, { config: control, overlaySide: overlaySide, placement: "standalone", resolved: resolved });
|
|
2507
2496
|
default: {
|
|
@@ -2543,8 +2532,6 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
|
|
|
2543
2532
|
const measuredResponsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
2544
2533
|
const availableSpace = safeAreaLayout?.availableSpace ?? measuredAvailableSpace;
|
|
2545
2534
|
const responsiveMode = safeAreaLayout?.responsiveMode ?? measuredResponsiveMode;
|
|
2546
|
-
// Menus use modals only in collapsed mode (narrowest containers); popovers otherwise
|
|
2547
|
-
const menuPresentation = responsiveMode === "collapsed" ? "modal" : "popover";
|
|
2548
2535
|
// Derive portal ID from the map container's stable ID (set by createMapComponent)
|
|
2549
2536
|
const portalId = api.containerRef.current?.id;
|
|
2550
2537
|
// Geolocation position for region-appropriate globe icon (updated if the user clicks "My Location")
|
|
@@ -2580,7 +2567,7 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
|
|
|
2580
2567
|
// Responsive grid: navigation bottom-right, settings top-left, tools top-right, statistics bottom-left
|
|
2581
2568
|
"grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
|
|
2582
2569
|
// Isolate creates a new stacking context to avoid z-index issues
|
|
2583
|
-
"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 self-end justify-self-end", children: navigation.controls.map(control => (jsxRuntime.jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId
|
|
2570
|
+
"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 self-end justify-self-end", children: navigation.controls.map(control => (jsxRuntime.jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId }, 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, overlaySide: "right" }, control.id))) })) : null, hasToolsControls ? (jsxRuntime.jsx("div", { className: "pointer-events-auto col-start-2 row-start-1 flex flex-col gap-1 justify-self-end", "data-testid": "map-controls-tools", children: tools.controls.map(control => (jsxRuntime.jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId }, 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 }, control.id))) })) : null] })) : null] }));
|
|
2584
2571
|
};
|
|
2585
2572
|
/**
|
|
2586
2573
|
* Map controls renderer — lays out a `ControlsConfig` across the four semantic
|
|
@@ -2686,7 +2673,7 @@ const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMa
|
|
|
2686
2673
|
}
|
|
2687
2674
|
return configs;
|
|
2688
2675
|
}, [adapterConfig, options]);
|
|
2689
|
-
return (jsxRuntime.jsx("div", { className: "flex items-start gap-2
|
|
2676
|
+
return (jsxRuntime.jsx("div", { className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2690
2677
|
const derivedConfig = derivedConfigs.get(option.theme);
|
|
2691
2678
|
if (derivedConfig === undefined) {
|
|
2692
2679
|
return null;
|
package/index.esm.js
CHANGED
|
@@ -11,7 +11,6 @@ import { createPortal } from 'react-dom';
|
|
|
11
11
|
import { isEqual } from 'es-toolkit';
|
|
12
12
|
import { useGeolocation } from '@trackunit/react-core-hooks';
|
|
13
13
|
import { Checkbox, ToggleSwitch, RadioGroup as RadioGroup$1, RadioItem, FormGroup, Search as Search$1, SelectField, BaseSelect } from '@trackunit/react-form-components';
|
|
14
|
-
import { useModal, Modal as Modal$1, ModalHeader, ModalBody } from '@trackunit/react-modal';
|
|
15
14
|
import { z } from 'zod';
|
|
16
15
|
import { PieChart } from 'react-minimal-pie-chart';
|
|
17
16
|
import { tailwindPalette } from '@trackunit/ui-design-tokens';
|
|
@@ -37,6 +36,7 @@ var defaultTranslations = {
|
|
|
37
36
|
"map.loadingData": "Loading map data",
|
|
38
37
|
"map.loadingErrorMessage": "Unable to load the map. Please try again later.",
|
|
39
38
|
"multiGeometry.badge.tooltip": "Part of a multi-part shape",
|
|
39
|
+
"panels.close": "Close",
|
|
40
40
|
"settings.mapStyle.dark": "Dark",
|
|
41
41
|
"settings.mapStyle.light": "Light",
|
|
42
42
|
"settings.mapStyle.roadmap": "Road map",
|
|
@@ -1179,13 +1179,14 @@ const createMapComponent = ({ adapter, Renderer, adapterName, containerRefHolder
|
|
|
1179
1179
|
const containerHeight = geometry?.height ?? Infinity;
|
|
1180
1180
|
const availableSpace = breakpoints.isSm && containerHeight >= HEIGHT_COMFORTABLE_THRESHOLD ? "comfortable" : "constrained";
|
|
1181
1181
|
const responsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
1182
|
+
const [safeAreaContentEl, setSafeAreaContentEl] = useState(null);
|
|
1183
|
+
const [safeAreaPanelPortalEl, setSafeAreaPanelPortalEl] = useState(null);
|
|
1182
1184
|
const layout = useMemo(() => ({
|
|
1183
1185
|
availableSpace,
|
|
1184
1186
|
responsiveMode,
|
|
1185
|
-
|
|
1187
|
+
safeAreaContentEl,
|
|
1188
|
+
}), [availableSpace, responsiveMode, safeAreaContentEl]);
|
|
1186
1189
|
const mapMoveSource = useMemo(() => ({ on: adapter.on.bind(adapter) }), []);
|
|
1187
|
-
const [safeAreaContentEl, setSafeAreaContentEl] = useState(null);
|
|
1188
|
-
const [safeAreaPanelPortalEl, setSafeAreaPanelPortalEl] = useState(null);
|
|
1189
1190
|
const content = (jsxs("div", { "aria-describedby": ariaDescribedBy, "aria-label": ariaLabel ?? "Interactive map", className: twMerge(
|
|
1190
1191
|
// ZStack: full-size grid with a single cell so children overlay the map
|
|
1191
1192
|
"isolate grid h-full w-full grid-cols-1 grid-rows-1", className), "data-testid": dataTestId, id: containerId, ref: mergedRef, role: "application", style: style, tabIndex: 0, children: [jsxs("div", { className: "relative col-start-1 row-start-1 grid h-full min-h-[200px] w-full", children: [jsx(Renderer, { adapterInstance: adapter, className: "h-full w-full", style: { height: "100%", width: "100%" } }), jsx(AdapterReadinessOverlay, { adapter: adapter })] }), jsx("div", { className: "pointer-events-none absolute inset-0 col-start-1 row-start-1 overflow-visible", "data-testid": "map-marker-overlay-portal" }), jsx("div", { className: "pointer-events-none relative isolate col-start-1 row-start-1 h-full min-h-0 w-full overflow-clip", "data-testid": "map-panel-overlay-portal", ref: setSafeAreaPanelPortalEl }), panelStore !== undefined ? (jsx(PanelOrchestrator, { anchoredPortalEl: safeAreaPanelPortalEl, containerEl: safeAreaContentEl ?? containerEl, mapMoveSource: mapMoveSource, store: panelStore })) : null, jsx(SafeAreaLayoutContext.Provider, { value: layout, children: jsxs(SafeAreaOverlay, { safeAreaContentRef: setSafeAreaContentEl, safeAreaInsets: safeAreaInsets ?? null, children: [children, annotationStore !== undefined ? jsx(FallbackAnnotationOverlay, { store: annotationStore }) : null, loadingIndicatorStore !== undefined ? (jsx(LoadingOverlay, { adapter: adapter, store: loadingIndicatorStore })) : null] }) })] }));
|
|
@@ -2126,13 +2127,26 @@ const MenuSearch = ({ config, resolved: _resolved }) => {
|
|
|
2126
2127
|
* Purely presentational -- all rendering decisions come from resolved props.
|
|
2127
2128
|
* `data-testid` (when set) is on the menu-item wrapper only — not duplicated on the inner `SelectField`.
|
|
2128
2129
|
*
|
|
2130
|
+
* The dropdown portals outside the menu's clipped/scrolled chrome (`overflow-clip`/`overflow-y-auto`
|
|
2131
|
+
* on `PopoverContent`/`MenuContent`) so long option lists are never truncated -- same default
|
|
2132
|
+
* behavior as the standalone toolbar `Select`. In fullscreen mode it portals into the map's own
|
|
2133
|
+
* `customPortalId` element instead of `document.body`, since real `document.body` content is
|
|
2134
|
+
* invisible while the map container is in the Fullscreen API (mirrors `PopoverContent`'s
|
|
2135
|
+
* `portalId={isFullScreen ? customPortalId : undefined}` pattern).
|
|
2136
|
+
*
|
|
2137
|
+
* The dropdown portal always stacks at the `z-popover` layer (`SelectField`'s default), the same
|
|
2138
|
+
* layer as the menu's own `PopoverContent` -- so it reliably paints on top rather than underneath
|
|
2139
|
+
* the menu wherever their bounds overlap, since both portal near the top of the DOM tree outside
|
|
2140
|
+
* the menu's own clipped subtree.
|
|
2141
|
+
*
|
|
2129
2142
|
* @internal
|
|
2130
2143
|
*/
|
|
2131
|
-
const MenuSelect = ({ config, resolved: _resolved }) => {
|
|
2144
|
+
const MenuSelect = ({ config, customPortalId, isFullScreen, resolved: _resolved }) => {
|
|
2132
2145
|
const { className, "data-testid": dataTestId, id, label, onChange, options, value } = config;
|
|
2146
|
+
const menuPortalTarget = isFullScreen && customPortalId !== undefined ? (document.getElementById(customPortalId) ?? undefined) : undefined;
|
|
2133
2147
|
return (jsx("div", { className: cvaControlMenuItemBase({
|
|
2134
2148
|
className: ["block", className],
|
|
2135
|
-
}), "data-testid": dataTestId, children: jsx(SelectField, { className: undefined, "data-testid": undefined, id: id, label: label, menuPortalTarget:
|
|
2149
|
+
}), "data-testid": dataTestId, children: jsx(SelectField, { className: undefined, "data-testid": undefined, id: id, label: label, menuPortalTarget: menuPortalTarget, onChange: event => {
|
|
2136
2150
|
const newValue = event.target.value;
|
|
2137
2151
|
if (newValue !== undefined) {
|
|
2138
2152
|
onChange(newValue);
|
|
@@ -2216,22 +2230,6 @@ const MenuToggleGroup = ({ config, resolved }) => {
|
|
|
2216
2230
|
}, isIconOnly: false, size: resolved.size }) }));
|
|
2217
2231
|
};
|
|
2218
2232
|
|
|
2219
|
-
/**
|
|
2220
|
-
* Renders a menu control as a modal instead of a popover.
|
|
2221
|
-
* Used on narrow containers where popovers would be clipped or awkward.
|
|
2222
|
-
*
|
|
2223
|
-
* The trigger is the same IconButton used by the popover path.
|
|
2224
|
-
* The content is rendered inside a Modal with ModalHeader (label + close)
|
|
2225
|
-
* and ModalBody (scrollable menu items).
|
|
2226
|
-
*
|
|
2227
|
-
* @internal
|
|
2228
|
-
*/
|
|
2229
|
-
const Modal = ({ config, resolved, children }) => {
|
|
2230
|
-
const { className, "data-testid": dataTestId, label } = config;
|
|
2231
|
-
const modal = useModal({ role: "dialog" });
|
|
2232
|
-
return (jsxs(Fragment, { children: [jsx(IconButton, { "aria-label": label, className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), onClick: modal.open, size: resolved.size, title: label, variant: "secondary" }), jsxs(Modal$1, { ...modal, "data-testid": dataTestId ? `${dataTestId}-modal` : undefined, size: "small", children: [jsx(ModalHeader, { "data-testid": dataTestId ? `${dataTestId}-modal` : undefined, heading: label, onClickClose: modal.close }), jsx(ModalBody, { children: children })] })] }));
|
|
2233
|
-
};
|
|
2234
|
-
|
|
2235
2233
|
/**
|
|
2236
2234
|
* Renders a section header within a menu.
|
|
2237
2235
|
* Non-interactive, purely visual grouping element.
|
|
@@ -2252,7 +2250,7 @@ const Separator = ({ className }) => (jsx("div", { className: twMerge("my-1 h-px
|
|
|
2252
2250
|
* Calls resolveControl with placement: "menu" for each item.
|
|
2253
2251
|
* The sizing middleware will always derive compact sizing for menu items.
|
|
2254
2252
|
*/
|
|
2255
|
-
const renderMenuItem = (item, index, availableSpace) => {
|
|
2253
|
+
const renderMenuItem = (item, index, availableSpace, customPortalId, isFullScreen) => {
|
|
2256
2254
|
const resolved = resolveControl(item, { placement: "menu", availableSpace });
|
|
2257
2255
|
switch (item.type) {
|
|
2258
2256
|
case "separator":
|
|
@@ -2264,7 +2262,7 @@ const renderMenuItem = (item, index, availableSpace) => {
|
|
|
2264
2262
|
case "button":
|
|
2265
2263
|
return jsx(MenuButton, { config: item, resolved: resolved }, item.id);
|
|
2266
2264
|
case "select":
|
|
2267
|
-
return jsx(MenuSelect, { config: item, resolved: resolved }, item.id);
|
|
2265
|
+
return (jsx(MenuSelect, { config: item, customPortalId: customPortalId, isFullScreen: isFullScreen, resolved: resolved }, item.id));
|
|
2268
2266
|
case "radioGroup":
|
|
2269
2267
|
return jsx(MenuRadioGroup, { config: item, resolved: resolved }, item.id);
|
|
2270
2268
|
case "toggleGroup":
|
|
@@ -2304,53 +2302,44 @@ const groupItemsBySection = (items) => {
|
|
|
2304
2302
|
return groups;
|
|
2305
2303
|
};
|
|
2306
2304
|
/**
|
|
2307
|
-
* Renders the grouped menu items with section headers
|
|
2308
|
-
*
|
|
2305
|
+
* Renders the grouped menu items with section headers as a plain block list inside the popover's
|
|
2306
|
+
* own card styling (border, background, shadow).
|
|
2309
2307
|
*
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2308
|
+
* Scrolls internally (rather than on the outer `PopoverContent`) so the native scrollbar stays
|
|
2309
|
+
* inside this div's own rounded border instead of riding the popover's unstyled outer edge --
|
|
2310
|
+
* mirrors the cvaMenu/cvaMenuList split used elsewhere for the same reason.
|
|
2313
2311
|
*/
|
|
2314
|
-
const MenuContent = ({ availableSpace,
|
|
2312
|
+
const MenuContent = ({ availableSpace, customPortalId, isFullScreen, items }) => {
|
|
2315
2313
|
const groups = groupItemsBySection(items);
|
|
2316
|
-
return (jsx("div", { className:
|
|
2314
|
+
return (jsx("div", { className: "max-h-[max(40dvh,var(--spacing-64))] overflow-y-auto rounded-lg border border-neutral-300 bg-white py-1 shadow", role: "menu", children: groups.map((group, groupIndex) => (jsxs(Fragment$1, { children: [group.section !== undefined ? jsx(SectionHeader, { title: group.section }) : null, group.items.map((item, itemIndex) => renderMenuItem(item, groupIndex * 100 + itemIndex, availableSpace, customPortalId, isFullScreen))] }, groupIndex))) }));
|
|
2317
2315
|
};
|
|
2318
2316
|
/** Popover opens right; tooltip is suppressed while the menu is open. */
|
|
2319
2317
|
const MenuRightPopover = ({ availableSpace, config, customPortalId, resolved, }) => {
|
|
2320
2318
|
const { className, "data-testid": dataTestId, items, label } = config;
|
|
2321
2319
|
const isFullScreen = useIsFullscreen();
|
|
2322
|
-
return (jsx("div", { className: className ?? "w-fit", children: jsx(Popover, { placement: RIGHT_CONTROL_POPOVER_PLACEMENT, children: ({ isOpen }) => (jsxs(Fragment$1, { children: [jsx(PopoverTrigger, { children: jsx(Tooltip, { delayed: true, disabled: isOpen, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsx(IconButton, { ariaLabel: label, "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsx(PopoverContent, { portalId: isFullScreen ? customPortalId : undefined, children: jsx(MenuContent, { availableSpace: availableSpace,
|
|
2320
|
+
return (jsx("div", { className: className ?? "w-fit", children: jsx(Popover, { placement: RIGHT_CONTROL_POPOVER_PLACEMENT, children: ({ isOpen }) => (jsxs(Fragment$1, { children: [jsx(PopoverTrigger, { children: jsx(Tooltip, { delayed: true, disabled: isOpen, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: jsx(IconButton, { ariaLabel: label, "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsx(PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] })) }) }));
|
|
2323
2321
|
};
|
|
2324
|
-
/** Popover presentation -- renders a
|
|
2322
|
+
/** Popover presentation -- renders a position-aware popover anchored to the trigger button. */
|
|
2325
2323
|
const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId, resolved, }) => {
|
|
2326
2324
|
const { className, "data-testid": dataTestId, items, label } = config;
|
|
2327
2325
|
const isFullScreen = useIsFullscreen();
|
|
2328
2326
|
const triggerRef = useRef(null);
|
|
2329
2327
|
const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
|
|
2330
|
-
return (jsx("div", { className: "w-fit", ref: triggerRef, children:
|
|
2328
|
+
return (jsx("div", { className: className ?? "w-fit", ref: triggerRef, children: jsxs(Popover, { placement: popoverPlacement, children: [jsx(PopoverTrigger, { children: jsx(IconButton, { "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, title: label, variant: "secondary" }) }), jsx(PopoverContent, { className: "overflow-clip", portalId: isFullScreen ? customPortalId : undefined, children: jsx(MenuContent, { availableSpace: availableSpace, customPortalId: customPortalId, isFullScreen: isFullScreen, items: items }) })] }) }));
|
|
2331
2329
|
};
|
|
2332
|
-
const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
|
|
2333
|
-
if (overlaySide === "right") {
|
|
2334
|
-
return (jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
|
|
2335
|
-
}
|
|
2336
|
-
return (jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
|
|
2337
|
-
};
|
|
2338
|
-
/** Modal presentation -- renders a Modal with no popover-specific hooks. */
|
|
2339
|
-
const MenuModal = ({ availableSpace, config, resolved }) => (jsx(Modal, { config: config, resolved: resolved, children: jsx(MenuContent, { availableSpace: availableSpace, items: config.items, presentation: "modal" }) }));
|
|
2340
2330
|
/**
|
|
2341
|
-
* Renders a menu control as
|
|
2342
|
-
*
|
|
2331
|
+
* Renders a menu control as a popover -- either anchored to the trigger (auto-placed) or
|
|
2332
|
+
* opening to a fixed right placement, depending on `overlaySide`.
|
|
2343
2333
|
*
|
|
2344
|
-
* Delegates to
|
|
2345
|
-
* only runs the hooks it needs.
|
|
2334
|
+
* Delegates to sub-components so each path only runs the hooks it needs.
|
|
2346
2335
|
*
|
|
2347
2336
|
* @internal
|
|
2348
2337
|
*/
|
|
2349
|
-
const Menu = ({ availableSpace, config, containerRef, customPortalId,
|
|
2350
|
-
if (
|
|
2351
|
-
return jsx(
|
|
2338
|
+
const Menu = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
|
|
2339
|
+
if (overlaySide === "right") {
|
|
2340
|
+
return (jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
|
|
2352
2341
|
}
|
|
2353
|
-
return (jsx(
|
|
2342
|
+
return (jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
|
|
2354
2343
|
};
|
|
2355
2344
|
|
|
2356
2345
|
const fieldConfigForStandalonePopover = (config) => {
|
|
@@ -2481,7 +2470,7 @@ const ToggleGroup = ({ config, overlaySide: _overlaySide = undefined, resolved }
|
|
|
2481
2470
|
*
|
|
2482
2471
|
* @internal
|
|
2483
2472
|
*/
|
|
2484
|
-
const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId,
|
|
2473
|
+
const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, overlaySide = undefined, }) => {
|
|
2485
2474
|
const resolved = resolveControl(control, { placement: "standalone", availableSpace });
|
|
2486
2475
|
switch (control.type) {
|
|
2487
2476
|
case "button":
|
|
@@ -2500,7 +2489,7 @@ const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId
|
|
|
2500
2489
|
case "toggleGroup":
|
|
2501
2490
|
return jsx(ToggleGroup, { config: control, overlaySide: overlaySide, resolved: resolved });
|
|
2502
2491
|
case "menu":
|
|
2503
|
-
return (jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId,
|
|
2492
|
+
return (jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
|
|
2504
2493
|
case "custom":
|
|
2505
2494
|
return jsx(Custom, { config: control, overlaySide: overlaySide, placement: "standalone", resolved: resolved });
|
|
2506
2495
|
default: {
|
|
@@ -2542,8 +2531,6 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
|
|
|
2542
2531
|
const measuredResponsiveMode = deriveResponsiveMode(breakpoints.isSm, breakpoints.isMd, containerHeight);
|
|
2543
2532
|
const availableSpace = safeAreaLayout?.availableSpace ?? measuredAvailableSpace;
|
|
2544
2533
|
const responsiveMode = safeAreaLayout?.responsiveMode ?? measuredResponsiveMode;
|
|
2545
|
-
// Menus use modals only in collapsed mode (narrowest containers); popovers otherwise
|
|
2546
|
-
const menuPresentation = responsiveMode === "collapsed" ? "modal" : "popover";
|
|
2547
2534
|
// Derive portal ID from the map container's stable ID (set by createMapComponent)
|
|
2548
2535
|
const portalId = api.containerRef.current?.id;
|
|
2549
2536
|
// Geolocation position for region-appropriate globe icon (updated if the user clicks "My Location")
|
|
@@ -2579,7 +2566,7 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
|
|
|
2579
2566
|
// Responsive grid: navigation bottom-right, settings top-left, tools top-right, statistics bottom-left
|
|
2580
2567
|
"grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
|
|
2581
2568
|
// Isolate creates a new stacking context to avoid z-index issues
|
|
2582
|
-
"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 self-end justify-self-end", children: navigation.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId
|
|
2569
|
+
"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 self-end justify-self-end", children: navigation.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId }, 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, overlaySide: "right" }, control.id))) })) : null, hasToolsControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-1 flex flex-col gap-1 justify-self-end", "data-testid": "map-controls-tools", children: tools.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId }, 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 }, control.id))) })) : null] })) : null] }));
|
|
2583
2570
|
};
|
|
2584
2571
|
/**
|
|
2585
2572
|
* Map controls renderer — lays out a `ControlsConfig` across the four semantic
|
|
@@ -2685,7 +2672,7 @@ const AppearancePreviewGrid = ({ adapterConfig, options, currentTheme, currentMa
|
|
|
2685
2672
|
}
|
|
2686
2673
|
return configs;
|
|
2687
2674
|
}, [adapterConfig, options]);
|
|
2688
|
-
return (jsx("div", { className: "flex items-start gap-2
|
|
2675
|
+
return (jsx("div", { className: "flex flex-wrap items-start gap-2 px-3 py-2", role: "radiogroup", children: options.map(option => {
|
|
2689
2676
|
const derivedConfig = derivedConfigs.get(option.theme);
|
|
2690
2677
|
if (derivedConfig === undefined) {
|
|
2691
2678
|
return null;
|
package/package.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-components": "2.2.
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "1.13.
|
|
12
|
-
"@trackunit/react-form-components": "2.2.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.18.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.14.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.1.
|
|
16
|
-
"@trackunit/react-modal": "2.2.1",
|
|
10
|
+
"@trackunit/react-components": "2.2.1",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "1.13.60",
|
|
12
|
+
"@trackunit/react-form-components": "2.2.2",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.18.1",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.14.63",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.1.1",
|
|
17
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
18
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
19
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
20
|
-
"@trackunit/ui-design-tokens": "1.13.
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.44",
|
|
18
|
+
"@trackunit/react-map-color-utils": "0.0.29",
|
|
19
|
+
"@trackunit/ui-design-tokens": "1.13.59",
|
|
21
20
|
"@floating-ui/react": "^0.26.25",
|
|
22
21
|
"es-toolkit": "^1.39.10",
|
|
23
22
|
"tailwind-merge": "^2.0.0",
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { ComponentRef, RefObject } from "react";
|
|
2
2
|
import type { ControlOverlaySide } from "./controlOverlaySide";
|
|
3
3
|
import { type AvailableSpace } from "./renderingRules";
|
|
4
|
-
import type { ControlConfig
|
|
4
|
+
import type { ControlConfig } from "./types";
|
|
5
5
|
type ControlRendererProps = Readonly<{
|
|
6
6
|
availableSpace: AvailableSpace;
|
|
7
7
|
/** Ref to the controls overlay container, used to compute popover placement */
|
|
8
8
|
containerRef: RefObject<ComponentRef<"div"> | null>;
|
|
9
9
|
control: ControlConfig;
|
|
10
10
|
customPortalId?: string;
|
|
11
|
-
menuPresentation: MenuPresentation;
|
|
12
11
|
overlaySide?: ControlOverlaySide;
|
|
13
12
|
}>;
|
|
14
13
|
/**
|
|
@@ -18,5 +17,5 @@ type ControlRendererProps = Readonly<{
|
|
|
18
17
|
*
|
|
19
18
|
* @internal
|
|
20
19
|
*/
|
|
21
|
-
export declare const ControlRenderer: ({ availableSpace, containerRef, control, customPortalId,
|
|
20
|
+
export declare const ControlRenderer: ({ availableSpace, containerRef, control, customPortalId, overlaySide, }: ControlRendererProps) => import("react/jsx-runtime").JSX.Element;
|
|
22
21
|
export {};
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import { type ComponentRef, type RefObject } from "react";
|
|
2
2
|
import { type ControlOverlaySide } from "../../controlOverlaySide";
|
|
3
3
|
import { type AvailableSpace, type ResolvedControlProps } from "../../renderingRules";
|
|
4
|
-
import type { MenuControlConfig
|
|
4
|
+
import type { MenuControlConfig } from "../../types";
|
|
5
5
|
type MenuProps = Readonly<{
|
|
6
6
|
availableSpace: AvailableSpace;
|
|
7
7
|
config: MenuControlConfig;
|
|
8
8
|
/** Ref to the controls overlay container, used to compute popover placement */
|
|
9
9
|
containerRef: RefObject<ComponentRef<"div"> | null>;
|
|
10
10
|
customPortalId?: string;
|
|
11
|
-
menuPresentation: MenuPresentation;
|
|
12
11
|
overlaySide?: ControlOverlaySide;
|
|
13
12
|
resolved: ResolvedControlProps;
|
|
14
13
|
}>;
|
|
15
14
|
/**
|
|
16
|
-
* Renders a menu control as
|
|
17
|
-
*
|
|
15
|
+
* Renders a menu control as a popover -- either anchored to the trigger (auto-placed) or
|
|
16
|
+
* opening to a fixed right placement, depending on `overlaySide`.
|
|
18
17
|
*
|
|
19
|
-
* Delegates to
|
|
20
|
-
* only runs the hooks it needs.
|
|
18
|
+
* Delegates to sub-components so each path only runs the hooks it needs.
|
|
21
19
|
*
|
|
22
20
|
* @internal
|
|
23
21
|
*/
|
|
24
|
-
export declare const Menu: ({ availableSpace, config, containerRef, customPortalId,
|
|
22
|
+
export declare const Menu: ({ availableSpace, config, containerRef, customPortalId, overlaySide, resolved, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
|
|
25
23
|
export {};
|
|
@@ -2,6 +2,8 @@ import { type ResolvedControlProps } from "../../renderingRules";
|
|
|
2
2
|
import type { SelectControlConfig } from "../../types";
|
|
3
3
|
type MenuSelectProps = Readonly<{
|
|
4
4
|
config: SelectControlConfig;
|
|
5
|
+
customPortalId?: string;
|
|
6
|
+
isFullScreen: boolean;
|
|
5
7
|
resolved: ResolvedControlProps;
|
|
6
8
|
}>;
|
|
7
9
|
/**
|
|
@@ -9,7 +11,19 @@ type MenuSelectProps = Readonly<{
|
|
|
9
11
|
* Purely presentational -- all rendering decisions come from resolved props.
|
|
10
12
|
* `data-testid` (when set) is on the menu-item wrapper only — not duplicated on the inner `SelectField`.
|
|
11
13
|
*
|
|
14
|
+
* The dropdown portals outside the menu's clipped/scrolled chrome (`overflow-clip`/`overflow-y-auto`
|
|
15
|
+
* on `PopoverContent`/`MenuContent`) so long option lists are never truncated -- same default
|
|
16
|
+
* behavior as the standalone toolbar `Select`. In fullscreen mode it portals into the map's own
|
|
17
|
+
* `customPortalId` element instead of `document.body`, since real `document.body` content is
|
|
18
|
+
* invisible while the map container is in the Fullscreen API (mirrors `PopoverContent`'s
|
|
19
|
+
* `portalId={isFullScreen ? customPortalId : undefined}` pattern).
|
|
20
|
+
*
|
|
21
|
+
* The dropdown portal always stacks at the `z-popover` layer (`SelectField`'s default), the same
|
|
22
|
+
* layer as the menu's own `PopoverContent` -- so it reliably paints on top rather than underneath
|
|
23
|
+
* the menu wherever their bounds overlap, since both portal near the top of the DOM tree outside
|
|
24
|
+
* the menu's own clipped subtree.
|
|
25
|
+
*
|
|
12
26
|
* @internal
|
|
13
27
|
*/
|
|
14
|
-
export declare const MenuSelect: ({ config, resolved: _resolved }: MenuSelectProps) => import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare const MenuSelect: ({ config, customPortalId, isFullScreen, resolved: _resolved }: MenuSelectProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
29
|
export {};
|
package/src/controls/types.d.ts
CHANGED
|
@@ -350,17 +350,6 @@ export interface BuiltInControlsConfig extends ControlsConfig {
|
|
|
350
350
|
readonly navigation: BuiltInNavigationConfig;
|
|
351
351
|
readonly settings: BuiltInSettingsConfig;
|
|
352
352
|
}
|
|
353
|
-
/**
|
|
354
|
-
* How a menu presents its content.
|
|
355
|
-
*
|
|
356
|
-
* - `"popover"` — floating `MoreMenu` popover anchored to the trigger button.
|
|
357
|
-
* - `"modal"` — bottom `Sheet` dialog (`variant="modal"`) with a backdrop.
|
|
358
|
-
*
|
|
359
|
-
* `Controls` derives this from `availableSpace`: `"constrained"` containers get
|
|
360
|
-
* `"modal"` (Sheet bottom-sheet, better on small screens); `"comfortable"` containers
|
|
361
|
-
* get `"popover"` (floating popover, better when there is room).
|
|
362
|
-
*/
|
|
363
|
-
export type MenuPresentation = "popover" | "modal";
|
|
364
353
|
/**
|
|
365
354
|
* How aggressively controls are collapsed based on available space.
|
|
366
355
|
*
|
|
@@ -2,4 +2,11 @@ import type { AvailableSpace, ResponsiveMode } from "../controls/types";
|
|
|
2
2
|
export declare const SafeAreaLayoutContext: import("react").Context<Readonly<{
|
|
3
3
|
availableSpace: AvailableSpace;
|
|
4
4
|
responsiveMode: ResponsiveMode;
|
|
5
|
+
/**
|
|
6
|
+
* The inner content node of `SafeAreaOverlay` — the `relative` grid div whose border box
|
|
7
|
+
* equals the padded usable area. Sheet portals that should respect adapter safe-area insets
|
|
8
|
+
* (e.g. the Controls menu Sheet) should use this as their `container` prop so they do not
|
|
9
|
+
* paint over the attribution bar.
|
|
10
|
+
*/
|
|
11
|
+
safeAreaContentEl: HTMLDivElement | null;
|
|
5
12
|
}> | null>;
|
package/migrations/entry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { ResolvedControlProps } from "../../renderingRules";
|
|
3
|
-
import type { MenuControlConfig } from "../../types";
|
|
4
|
-
type ModalProps = Readonly<{
|
|
5
|
-
config: MenuControlConfig;
|
|
6
|
-
resolved: ResolvedControlProps;
|
|
7
|
-
/** The menu content (grouped sections and items) to render inside the modal body */
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
}>;
|
|
10
|
-
/**
|
|
11
|
-
* Renders a menu control as a modal instead of a popover.
|
|
12
|
-
* Used on narrow containers where popovers would be clipped or awkward.
|
|
13
|
-
*
|
|
14
|
-
* The trigger is the same IconButton used by the popover path.
|
|
15
|
-
* The content is rendered inside a Modal with ModalHeader (label + close)
|
|
16
|
-
* and ModalBody (scrollable menu items).
|
|
17
|
-
*
|
|
18
|
-
* @internal
|
|
19
|
-
*/
|
|
20
|
-
export declare const Modal: ({ config, resolved, children }: ModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
-
export {};
|