@trackunit/react-map 0.1.0 → 0.1.1

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 CHANGED
@@ -1670,6 +1670,20 @@ const collapseControls = (config, mode, metadata) => {
1670
1670
  return { navigation, settings, tools, statistics: config.statistics };
1671
1671
  };
1672
1672
 
1673
+ const RIGHT_CONTROL_POPOVER_PLACEMENT = "right-start";
1674
+ const RIGHT_CONTROL_TOOLTIP_PLACEMENT = "right";
1675
+
1676
+ /**
1677
+ * Wraps standalone map controls with a right-aligned tooltip when `overlaySide` is `"right"`.
1678
+ * Callers should omit `title` on {@link IconButton} in that mode so tooltips do not stack.
1679
+ */
1680
+ const StandaloneControlTooltip = ({ children, disabled = false, label, overlaySide = undefined, }) => {
1681
+ if (overlaySide !== "right") {
1682
+ return children;
1683
+ }
1684
+ return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, disabled: disabled, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: children }));
1685
+ };
1686
+
1673
1687
  /**
1674
1688
  * Shared standalone renderer for boolean controls (toggle and checkbox).
1675
1689
  * Both render identically as an IconButton with tooltip in standalone mode.
@@ -1678,7 +1692,7 @@ const collapseControls = (config, mode, metadata) => {
1678
1692
  *
1679
1693
  * @internal
1680
1694
  */
1681
- const BooleanButton = ({ config, resolved }) => {
1695
+ const BooleanButton = ({ config, overlaySide = undefined, resolved }) => {
1682
1696
  const { className, "data-testid": dataTestId, label, onChange: onClickChange, value } = config;
1683
1697
  const { icon, size } = resolved;
1684
1698
  const [t] = useTranslation();
@@ -1686,7 +1700,8 @@ const BooleanButton = ({ config, resolved }) => {
1686
1700
  return null;
1687
1701
  }
1688
1702
  const tooltipLabel = t("controls.boolean.tooltip", { label });
1689
- return (jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": label, "aria-pressed": value, className: className, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, title: tooltipLabel, variant: value ? "primary" : "secondary" }));
1703
+ const useRightOverlay = overlaySide === "right";
1704
+ return (jsxRuntime.jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, children: jsxRuntime.jsx(reactComponents.IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: tooltipLabel }), "aria-pressed": value, className: className, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }));
1690
1705
  };
1691
1706
 
1692
1707
  /**
@@ -1695,13 +1710,14 @@ const BooleanButton = ({ config, resolved }) => {
1695
1710
  *
1696
1711
  * @internal
1697
1712
  */
1698
- const Button = ({ config, resolved }) => {
1713
+ const Button = ({ config, overlaySide = undefined, resolved }) => {
1699
1714
  const { className, "data-testid": dataTestId, label, onClick } = config;
1700
1715
  const { icon, size } = resolved;
1701
1716
  if (icon === undefined) {
1702
1717
  return null;
1703
1718
  }
1704
- return (jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": label, className: className, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: icon, size: "small" }), onClick: onClick, size: size, title: label, variant: "secondary" }));
1719
+ const useRightOverlay = overlaySide === "right";
1720
+ return (jsxRuntime.jsx(StandaloneControlTooltip, { label: label, overlaySide: overlaySide, children: jsxRuntime.jsx(reactComponents.IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: label }), className: className, "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: icon, size: "small" }), onClick: onClick, size: size, variant: "secondary" }) }));
1705
1721
  };
1706
1722
 
1707
1723
  /**
@@ -1717,7 +1733,7 @@ const Button = ({ config, resolved }) => {
1717
1733
  *
1718
1734
  * @internal
1719
1735
  */
1720
- const Custom = ({ config, resolved, placement }) => {
1736
+ const Custom = ({ config, overlaySide = undefined, resolved, placement }) => {
1721
1737
  if (!resolved.visible) {
1722
1738
  return null;
1723
1739
  }
@@ -1730,7 +1746,7 @@ const Custom = ({ config, resolved, placement }) => {
1730
1746
  // A Fragment cannot accept those props — use an inert wrapper with `contents` layout.
1731
1747
  const wrapped = jsxRuntime.jsx("span", { className: "contents", children: content });
1732
1748
  const useStandaloneTooltip = placement === "standalone" && (config.autoTooltip ?? "standalone") === "standalone";
1733
- return useStandaloneTooltip ? (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: config.label, children: wrapped })) : (wrapped);
1749
+ return useStandaloneTooltip ? (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: config.label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: wrapped })) : (wrapped);
1734
1750
  };
1735
1751
 
1736
1752
  /** Shared padding for interactive menu items (button, toggle, stepper, select, radioGroup). */
@@ -2231,14 +2247,26 @@ const MenuContent = ({ availableSpace, items, presentation }) => {
2231
2247
  const groups = groupItemsBySection(items);
2232
2248
  return (jsxRuntime.jsx("div", { className: presentation === "popover" ? "rounded-lg border border-neutral-300 bg-white py-1 shadow" : "py-1", 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))] }, groupIndex))) }));
2233
2249
  };
2250
+ /** Popover opens right; tooltip is suppressed while the menu is open. */
2251
+ const MenuRightPopover = ({ availableSpace, config, customPortalId, resolved, }) => {
2252
+ const { className, "data-testid": dataTestId, items, label } = config;
2253
+ const isFullScreen = reactComponents.useIsFullscreen();
2254
+ 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, items: items, presentation: "popover" }) })] })) }) }));
2255
+ };
2234
2256
  /** Popover presentation -- renders a MoreMenu with position-aware placement. */
2235
- const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, resolved }) => {
2257
+ const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId, resolved, }) => {
2236
2258
  const { className, "data-testid": dataTestId, items, label } = config;
2237
2259
  const isFullScreen = reactComponents.useIsFullscreen();
2238
2260
  const triggerRef = react.useRef(null);
2239
2261
  const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
2240
2262
  return (jsxRuntime.jsx("div", { className: "w-fit", ref: triggerRef, children: jsxRuntime.jsx(reactComponents.MoreMenu, { className: className, customButton: 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" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace, items: items, presentation: "popover" }) }) }));
2241
2263
  };
2264
+ const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2265
+ if (overlaySide === "right") {
2266
+ return (jsxRuntime.jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
2267
+ }
2268
+ return (jsxRuntime.jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2269
+ };
2242
2270
  /** Modal presentation -- renders a Modal with no popover-specific hooks. */
2243
2271
  const MenuModal = ({ availableSpace, config, resolved }) => (jsxRuntime.jsx(Modal, { config: config, resolved: resolved, children: jsxRuntime.jsx(MenuContent, { availableSpace: availableSpace, items: config.items, presentation: "modal" }) }));
2244
2272
  /**
@@ -2250,11 +2278,11 @@ const MenuModal = ({ availableSpace, config, resolved }) => (jsxRuntime.jsx(Moda
2250
2278
  *
2251
2279
  * @internal
2252
2280
  */
2253
- const Menu = ({ availableSpace, config, containerRef, customPortalId, menuPresentation, resolved, }) => {
2281
+ const Menu = ({ availableSpace, config, containerRef, customPortalId, menuPresentation, overlaySide = undefined, resolved, }) => {
2254
2282
  if (menuPresentation === "modal") {
2255
2283
  return jsxRuntime.jsx(MenuModal, { availableSpace: availableSpace, config: config, resolved: resolved });
2256
2284
  }
2257
- return (jsxRuntime.jsx(MenuPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2285
+ return (jsxRuntime.jsx(MenuPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2258
2286
  };
2259
2287
 
2260
2288
  const fieldConfigForStandalonePopover = (config) => {
@@ -2272,13 +2300,16 @@ const fieldConfigForStandalonePopover = (config) => {
2272
2300
  const RadioGroupPanel = ({ fieldConfig }) => {
2273
2301
  return (jsxRuntime.jsx("div", { className: "rounded-lg border border-neutral-300 bg-white px-3 py-2 shadow", children: jsxRuntime.jsx(RadioGroupField, { config: fieldConfig }) }));
2274
2302
  };
2275
- const RadioGroupPopover = ({ config, containerRef, customPortalId, resolved }) => {
2303
+ const RadioGroupPopover = ({ config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2276
2304
  const { className, "data-testid": dataTestId, label } = config;
2277
2305
  const isFullScreen = reactComponents.useIsFullscreen();
2278
2306
  const triggerRef = react.useRef(null);
2279
- // Menu path uses MenuRadioGroup (no popover). Only standalone uses MoreMenu.
2280
- const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
2281
- return (jsxRuntime.jsx("div", { className: "w-fit", ref: triggerRef, children: jsxRuntime.jsx(reactComponents.MoreMenu, { className: className, customButton: jsxRuntime.jsx(reactComponents.IconButton, { "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, title: label, variant: "secondary" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsxRuntime.jsx(RadioGroupPanel, { fieldConfig: fieldConfigForStandalonePopover(config) }) }) }));
2307
+ const popoverPlacement = usePopoverPlacement(triggerRef, containerRef, { skip: overlaySide === "right" });
2308
+ const fieldConfig = fieldConfigForStandalonePopover(config);
2309
+ if (overlaySide === "right") {
2310
+ return (jsxRuntime.jsx("div", { className: "w-fit", ref: triggerRef, 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 ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { portalId: isFullScreen ? customPortalId : undefined, children: jsxRuntime.jsx(RadioGroupPanel, { fieldConfig: fieldConfig }) })] })) }) }));
2311
+ }
2312
+ return (jsxRuntime.jsx("div", { className: "w-fit", ref: triggerRef, children: jsxRuntime.jsx(reactComponents.MoreMenu, { className: className, customButton: jsxRuntime.jsx(reactComponents.IconButton, { "data-testid": dataTestId, icon: jsxRuntime.jsx(reactComponents.Icon, { name: resolved.icon ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, title: label, variant: "secondary" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsxRuntime.jsx(RadioGroupPanel, { fieldConfig: fieldConfig }) }) }));
2282
2313
  };
2283
2314
  /**
2284
2315
  * Standalone radio group: icon button opens a popover (`MoreMenu`).
@@ -2288,8 +2319,8 @@ const RadioGroupPopover = ({ config, containerRef, customPortalId, resolved }) =
2288
2319
  *
2289
2320
  * @internal
2290
2321
  */
2291
- const RadioGroup = ({ config, containerRef, customPortalId, resolved }) => {
2292
- return (jsxRuntime.jsx(RadioGroupPopover, { config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2322
+ const RadioGroup = ({ config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2323
+ return (jsxRuntime.jsx(RadioGroupPopover, { config: config, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2293
2324
  };
2294
2325
 
2295
2326
  /**
@@ -2298,10 +2329,10 @@ const RadioGroup = ({ config, containerRef, customPortalId, resolved }) => {
2298
2329
  *
2299
2330
  * @internal
2300
2331
  */
2301
- const Search = ({ config, resolved }) => {
2332
+ const Search = ({ config, overlaySide = undefined, resolved }) => {
2302
2333
  const { className, "data-testid": dataTestId, id, label, onChange, onClear, placeholder, value } = config;
2303
2334
  const { size } = resolved;
2304
- return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: label, children: jsxRuntime.jsx(reactFormComponents.Search, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, onChange: event => {
2335
+ return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsxRuntime.jsx(reactFormComponents.Search, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, onChange: event => {
2305
2336
  onChange(event.target.value);
2306
2337
  }, onClear: onClear, placeholder: placeholder ?? label, value: value }) }));
2307
2338
  };
@@ -2312,12 +2343,12 @@ const Search = ({ config, resolved }) => {
2312
2343
  *
2313
2344
  * @internal
2314
2345
  */
2315
- const Select = ({ config, resolved }) => {
2346
+ const Select = ({ config, overlaySide = undefined, resolved }) => {
2316
2347
  const { className, "data-testid": dataTestId, id, label, onChange, options, value } = config;
2317
2348
  const { size } = resolved;
2318
2349
  const selectOptions = [...options];
2319
2350
  const selectedOption = selectOptions.find(option => option.value === value) ?? null;
2320
- return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: label, children: jsxRuntime.jsx(reactFormComponents.BaseSelect, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, isSearchable: false, onChange: option => {
2351
+ return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsxRuntime.jsx(reactFormComponents.BaseSelect, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, isSearchable: false, onChange: option => {
2321
2352
  if (option !== null) {
2322
2353
  onChange(option.value);
2323
2354
  }
@@ -2334,7 +2365,7 @@ const Select = ({ config, resolved }) => {
2334
2365
  *
2335
2366
  * @internal
2336
2367
  */
2337
- const Stepper = ({ config, resolved }) => {
2368
+ const Stepper = ({ config, overlaySide = undefined, resolved }) => {
2338
2369
  const { className, "data-testid": dataTestId, decrement: onClickDecrement, increment: onClickIncrement, label, } = config;
2339
2370
  const { size } = resolved;
2340
2371
  const [t] = useTranslation();
@@ -2347,7 +2378,7 @@ const Stepper = ({ config, resolved }) => {
2347
2378
  : decrementHover.hovering
2348
2379
  ? `${label}, ${decreaseLabel}`
2349
2380
  : label;
2350
- return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: tooltipLabel, children: jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("grid", className), "data-testid": dataTestId, children: [jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": increaseLabel, className: "rounded-b-none", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "Plus", size: "small" }), onClick: onClickIncrement, onMouseEnter: incrementHover.onMouseEnter, onMouseLeave: incrementHover.onMouseLeave, size: size, title: increaseLabel, variant: "secondary" }), jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": decreaseLabel, className: "translate-y-[-1px] rounded-t-none", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "Minus", size: "small" }), onClick: onClickDecrement, onMouseEnter: decrementHover.onMouseEnter, onMouseLeave: decrementHover.onMouseLeave, size: size, title: decreaseLabel, variant: "secondary" })] }) }));
2381
+ return (jsxRuntime.jsx(reactComponents.Tooltip, { delayed: true, label: tooltipLabel, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("grid", className), "data-testid": dataTestId, children: [jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": increaseLabel, className: "rounded-b-none", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "Plus", size: "small" }), onClick: onClickIncrement, onMouseEnter: incrementHover.onMouseEnter, onMouseLeave: incrementHover.onMouseLeave, size: size, title: increaseLabel, variant: "secondary" }), jsxRuntime.jsx(reactComponents.IconButton, { "aria-label": decreaseLabel, className: "translate-y-[-1px] rounded-t-none", icon: jsxRuntime.jsx(reactComponents.Icon, { name: "Minus", size: "small" }), onClick: onClickDecrement, onMouseEnter: decrementHover.onMouseEnter, onMouseLeave: decrementHover.onMouseLeave, size: size, title: decreaseLabel, variant: "secondary" })] }) }));
2351
2382
  };
2352
2383
 
2353
2384
  function assertStandaloneToggleGroupIcons(config) {
@@ -2364,7 +2395,7 @@ function assertStandaloneToggleGroupIcons(config) {
2364
2395
  *
2365
2396
  * @internal
2366
2397
  */
2367
- const ToggleGroup = ({ config, resolved }) => {
2398
+ const ToggleGroup = ({ config, overlaySide: _overlaySide = undefined, resolved }) => {
2368
2399
  const { className, "data-testid": dataTestId, disabled, label, onChange, options, value } = config;
2369
2400
  assertStandaloneToggleGroupIcons(config);
2370
2401
  return (jsxRuntime.jsx("div", { "aria-label": label, className: tailwindMerge.twMerge("w-fit", className), "data-testid": dataTestId, role: "group", children: jsxRuntime.jsx("div", { className: "relative after:pointer-events-none after:absolute after:inset-0 after:rounded-md after:ring-1 after:ring-inset after:ring-neutral-300 after:content-['']", children: jsxRuntime.jsx(ToggleGroupField, { config: {
@@ -2382,28 +2413,28 @@ const ToggleGroup = ({ config, resolved }) => {
2382
2413
  *
2383
2414
  * @internal
2384
2415
  */
2385
- const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, menuPresentation, }) => {
2416
+ const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, menuPresentation, overlaySide = undefined, }) => {
2386
2417
  const resolved = resolveControl(control, { placement: "standalone", availableSpace });
2387
2418
  switch (control.type) {
2388
2419
  case "button":
2389
- return jsxRuntime.jsx(Button, { config: control, resolved: resolved });
2420
+ return jsxRuntime.jsx(Button, { config: control, overlaySide: overlaySide, resolved: resolved });
2390
2421
  case "stepper":
2391
- return jsxRuntime.jsx(Stepper, { config: control, resolved: resolved });
2422
+ return jsxRuntime.jsx(Stepper, { config: control, overlaySide: overlaySide, resolved: resolved });
2392
2423
  case "toggle":
2393
2424
  case "checkbox":
2394
- return jsxRuntime.jsx(BooleanButton, { config: control, resolved: resolved });
2425
+ return jsxRuntime.jsx(BooleanButton, { config: control, overlaySide: overlaySide, resolved: resolved });
2395
2426
  case "select":
2396
- return jsxRuntime.jsx(Select, { config: control, resolved: resolved });
2427
+ return jsxRuntime.jsx(Select, { config: control, overlaySide: overlaySide, resolved: resolved });
2397
2428
  case "search":
2398
- return jsxRuntime.jsx(Search, { config: control, resolved: resolved });
2429
+ return jsxRuntime.jsx(Search, { config: control, overlaySide: overlaySide, resolved: resolved });
2399
2430
  case "radioGroup":
2400
- return (jsxRuntime.jsx(RadioGroup, { config: control, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2431
+ return (jsxRuntime.jsx(RadioGroup, { config: control, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2401
2432
  case "toggleGroup":
2402
- return jsxRuntime.jsx(ToggleGroup, { config: control, resolved: resolved });
2433
+ return jsxRuntime.jsx(ToggleGroup, { config: control, overlaySide: overlaySide, resolved: resolved });
2403
2434
  case "menu":
2404
- return (jsxRuntime.jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, menuPresentation: menuPresentation, resolved: resolved }));
2435
+ return (jsxRuntime.jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, menuPresentation: menuPresentation, overlaySide: overlaySide, resolved: resolved }));
2405
2436
  case "custom":
2406
- return jsxRuntime.jsx(Custom, { config: control, placement: "standalone", resolved: resolved });
2437
+ return jsxRuntime.jsx(Custom, { config: control, overlaySide: overlaySide, placement: "standalone", resolved: resolved });
2407
2438
  default: {
2408
2439
  const exhaustiveCheck = control;
2409
2440
  throw new Error(`Unknown control type: ${exhaustiveCheck}`);
@@ -2477,10 +2508,10 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
2477
2508
  "pointer-events-none",
2478
2509
  // Layout grid for semantic areas
2479
2510
  "grid h-full w-full",
2480
- // Responsive grid: navigation bottom-right, settings top-left, statistics bottom-left
2511
+ // Responsive grid: navigation bottom-right, settings top-left, tools top-right, statistics bottom-left
2481
2512
  "grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
2482
2513
  // 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] }));
2514
+ "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, 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, 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, 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
2515
  };
2485
2516
  /**
2486
2517
  * Map controls renderer — lays out a `ControlsConfig` across the four semantic
package/index.esm.js CHANGED
@@ -4,7 +4,7 @@ import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD
4
4
  export { ANCHOR_SELECTOR, DEFAULT_MAP_APPEARANCE, HIT_SURFACE_SELECTOR, INITIAL_CAMERA_STATE, INITIAL_INTERACTION_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, computeMarkerDomPortalZIndex, defineAdapter, geometryTypeToShapeType, mapAppearanceSchema, mapThemeSchema, mapTypeSchema } from '@trackunit/react-map-adapter-shared';
5
5
  import { useSyncExternalStore, useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect, useReducer, createContext, useContext, memo, Suspense, useId, Fragment as Fragment$1, forwardRef } from 'react';
6
6
  import { cvaMerge } from '@trackunit/css-class-variance-utilities';
7
- import { useMergeRefs, IconButton, Icon, Spinner, Text, useMeasure, useContainerBreakpoints, useDebounce, useWatch, useKeyboardShortcut, Tooltip, ToggleGroup as ToggleGroup$1, useIsFullscreen, MoreMenu, useHover as useHover$1, cvaInteractableItem, useLocalStorageReducer } from '@trackunit/react-components';
7
+ import { useMergeRefs, IconButton, Icon, Spinner, Text, useMeasure, useContainerBreakpoints, useDebounce, useWatch, useKeyboardShortcut, Tooltip, ToggleGroup as ToggleGroup$1, useIsFullscreen, Popover, PopoverTrigger, PopoverContent, MoreMenu, useHover as useHover$1, cvaInteractableItem, useLocalStorageReducer } from '@trackunit/react-components';
8
8
  import { twMerge } from 'tailwind-merge';
9
9
  import { useFloating, autoUpdate, offset, flip, useHover, safePolygon, useInteractions, shift, hide, useDismiss, useRole } from '@floating-ui/react';
10
10
  import { createPortal } from 'react-dom';
@@ -1669,6 +1669,20 @@ const collapseControls = (config, mode, metadata) => {
1669
1669
  return { navigation, settings, tools, statistics: config.statistics };
1670
1670
  };
1671
1671
 
1672
+ const RIGHT_CONTROL_POPOVER_PLACEMENT = "right-start";
1673
+ const RIGHT_CONTROL_TOOLTIP_PLACEMENT = "right";
1674
+
1675
+ /**
1676
+ * Wraps standalone map controls with a right-aligned tooltip when `overlaySide` is `"right"`.
1677
+ * Callers should omit `title` on {@link IconButton} in that mode so tooltips do not stack.
1678
+ */
1679
+ const StandaloneControlTooltip = ({ children, disabled = false, label, overlaySide = undefined, }) => {
1680
+ if (overlaySide !== "right") {
1681
+ return children;
1682
+ }
1683
+ return (jsx(Tooltip, { delayed: true, disabled: disabled, interactable: false, label: label, placement: RIGHT_CONTROL_TOOLTIP_PLACEMENT, children: children }));
1684
+ };
1685
+
1672
1686
  /**
1673
1687
  * Shared standalone renderer for boolean controls (toggle and checkbox).
1674
1688
  * Both render identically as an IconButton with tooltip in standalone mode.
@@ -1677,7 +1691,7 @@ const collapseControls = (config, mode, metadata) => {
1677
1691
  *
1678
1692
  * @internal
1679
1693
  */
1680
- const BooleanButton = ({ config, resolved }) => {
1694
+ const BooleanButton = ({ config, overlaySide = undefined, resolved }) => {
1681
1695
  const { className, "data-testid": dataTestId, label, onChange: onClickChange, value } = config;
1682
1696
  const { icon, size } = resolved;
1683
1697
  const [t] = useTranslation();
@@ -1685,7 +1699,8 @@ const BooleanButton = ({ config, resolved }) => {
1685
1699
  return null;
1686
1700
  }
1687
1701
  const tooltipLabel = t("controls.boolean.tooltip", { label });
1688
- return (jsx(IconButton, { "aria-label": label, "aria-pressed": value, className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, title: tooltipLabel, variant: value ? "primary" : "secondary" }));
1702
+ const useRightOverlay = overlaySide === "right";
1703
+ return (jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, children: jsx(IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: tooltipLabel }), "aria-pressed": value, className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }));
1689
1704
  };
1690
1705
 
1691
1706
  /**
@@ -1694,13 +1709,14 @@ const BooleanButton = ({ config, resolved }) => {
1694
1709
  *
1695
1710
  * @internal
1696
1711
  */
1697
- const Button = ({ config, resolved }) => {
1712
+ const Button = ({ config, overlaySide = undefined, resolved }) => {
1698
1713
  const { className, "data-testid": dataTestId, label, onClick } = config;
1699
1714
  const { icon, size } = resolved;
1700
1715
  if (icon === undefined) {
1701
1716
  return null;
1702
1717
  }
1703
- return (jsx(IconButton, { "aria-label": label, className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: icon, size: "small" }), onClick: onClick, size: size, title: label, variant: "secondary" }));
1718
+ const useRightOverlay = overlaySide === "right";
1719
+ return (jsx(StandaloneControlTooltip, { label: label, overlaySide: overlaySide, children: jsx(IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: label }), className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: icon, size: "small" }), onClick: onClick, size: size, variant: "secondary" }) }));
1704
1720
  };
1705
1721
 
1706
1722
  /**
@@ -1716,7 +1732,7 @@ const Button = ({ config, resolved }) => {
1716
1732
  *
1717
1733
  * @internal
1718
1734
  */
1719
- const Custom = ({ config, resolved, placement }) => {
1735
+ const Custom = ({ config, overlaySide = undefined, resolved, placement }) => {
1720
1736
  if (!resolved.visible) {
1721
1737
  return null;
1722
1738
  }
@@ -1729,7 +1745,7 @@ const Custom = ({ config, resolved, placement }) => {
1729
1745
  // A Fragment cannot accept those props — use an inert wrapper with `contents` layout.
1730
1746
  const wrapped = jsx("span", { className: "contents", children: content });
1731
1747
  const useStandaloneTooltip = placement === "standalone" && (config.autoTooltip ?? "standalone") === "standalone";
1732
- return useStandaloneTooltip ? (jsx(Tooltip, { delayed: true, label: config.label, children: wrapped })) : (wrapped);
1748
+ return useStandaloneTooltip ? (jsx(Tooltip, { delayed: true, label: config.label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: wrapped })) : (wrapped);
1733
1749
  };
1734
1750
 
1735
1751
  /** Shared padding for interactive menu items (button, toggle, stepper, select, radioGroup). */
@@ -2230,14 +2246,26 @@ const MenuContent = ({ availableSpace, items, presentation }) => {
2230
2246
  const groups = groupItemsBySection(items);
2231
2247
  return (jsx("div", { className: presentation === "popover" ? "rounded-lg border border-neutral-300 bg-white py-1 shadow" : "py-1", 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))] }, groupIndex))) }));
2232
2248
  };
2249
+ /** Popover opens right; tooltip is suppressed while the menu is open. */
2250
+ const MenuRightPopover = ({ availableSpace, config, customPortalId, resolved, }) => {
2251
+ const { className, "data-testid": dataTestId, items, label } = config;
2252
+ const isFullScreen = useIsFullscreen();
2253
+ 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, items: items, presentation: "popover" }) })] })) }) }));
2254
+ };
2233
2255
  /** Popover presentation -- renders a MoreMenu with position-aware placement. */
2234
- const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, resolved }) => {
2256
+ const MenuAutoPopover = ({ availableSpace, config, containerRef, customPortalId, resolved, }) => {
2235
2257
  const { className, "data-testid": dataTestId, items, label } = config;
2236
2258
  const isFullScreen = useIsFullscreen();
2237
2259
  const triggerRef = useRef(null);
2238
2260
  const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
2239
2261
  return (jsx("div", { className: "w-fit", ref: triggerRef, children: jsx(MoreMenu, { className: className, customButton: jsx(IconButton, { "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "QuestionMarkCircle", size: "small" }), size: resolved.size, title: label, variant: "secondary" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsx(MenuContent, { availableSpace: availableSpace, items: items, presentation: "popover" }) }) }));
2240
2262
  };
2263
+ const MenuPopover = ({ availableSpace, config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2264
+ if (overlaySide === "right") {
2265
+ return (jsx(MenuRightPopover, { availableSpace: availableSpace, config: config, customPortalId: customPortalId, resolved: resolved }));
2266
+ }
2267
+ return (jsx(MenuAutoPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2268
+ };
2241
2269
  /** Modal presentation -- renders a Modal with no popover-specific hooks. */
2242
2270
  const MenuModal = ({ availableSpace, config, resolved }) => (jsx(Modal, { config: config, resolved: resolved, children: jsx(MenuContent, { availableSpace: availableSpace, items: config.items, presentation: "modal" }) }));
2243
2271
  /**
@@ -2249,11 +2277,11 @@ const MenuModal = ({ availableSpace, config, resolved }) => (jsx(Modal, { config
2249
2277
  *
2250
2278
  * @internal
2251
2279
  */
2252
- const Menu = ({ availableSpace, config, containerRef, customPortalId, menuPresentation, resolved, }) => {
2280
+ const Menu = ({ availableSpace, config, containerRef, customPortalId, menuPresentation, overlaySide = undefined, resolved, }) => {
2253
2281
  if (menuPresentation === "modal") {
2254
2282
  return jsx(MenuModal, { availableSpace: availableSpace, config: config, resolved: resolved });
2255
2283
  }
2256
- return (jsx(MenuPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2284
+ return (jsx(MenuPopover, { availableSpace: availableSpace, config: config, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2257
2285
  };
2258
2286
 
2259
2287
  const fieldConfigForStandalonePopover = (config) => {
@@ -2271,13 +2299,16 @@ const fieldConfigForStandalonePopover = (config) => {
2271
2299
  const RadioGroupPanel = ({ fieldConfig }) => {
2272
2300
  return (jsx("div", { className: "rounded-lg border border-neutral-300 bg-white px-3 py-2 shadow", children: jsx(RadioGroupField, { config: fieldConfig }) }));
2273
2301
  };
2274
- const RadioGroupPopover = ({ config, containerRef, customPortalId, resolved }) => {
2302
+ const RadioGroupPopover = ({ config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2275
2303
  const { className, "data-testid": dataTestId, label } = config;
2276
2304
  const isFullScreen = useIsFullscreen();
2277
2305
  const triggerRef = useRef(null);
2278
- // Menu path uses MenuRadioGroup (no popover). Only standalone uses MoreMenu.
2279
- const popoverPlacement = usePopoverPlacement(triggerRef, containerRef);
2280
- return (jsx("div", { className: "w-fit", ref: triggerRef, children: jsx(MoreMenu, { className: className, customButton: jsx(IconButton, { "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, title: label, variant: "secondary" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsx(RadioGroupPanel, { fieldConfig: fieldConfigForStandalonePopover(config) }) }) }));
2306
+ const popoverPlacement = usePopoverPlacement(triggerRef, containerRef, { skip: overlaySide === "right" });
2307
+ const fieldConfig = fieldConfigForStandalonePopover(config);
2308
+ if (overlaySide === "right") {
2309
+ return (jsx("div", { className: "w-fit", ref: triggerRef, 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 ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, variant: "secondary" }) }) }), jsx(PopoverContent, { portalId: isFullScreen ? customPortalId : undefined, children: jsx(RadioGroupPanel, { fieldConfig: fieldConfig }) })] })) }) }));
2310
+ }
2311
+ return (jsx("div", { className: "w-fit", ref: triggerRef, children: jsx(MoreMenu, { className: className, customButton: jsx(IconButton, { "data-testid": dataTestId, icon: jsx(Icon, { name: resolved.icon ?? "EllipsisHorizontal", size: "small" }), size: resolved.size, title: label, variant: "secondary" }), customPortalId: isFullScreen ? customPortalId : undefined, "data-testid": dataTestId, popoverProps: { placement: popoverPlacement }, children: jsx(RadioGroupPanel, { fieldConfig: fieldConfig }) }) }));
2281
2312
  };
2282
2313
  /**
2283
2314
  * Standalone radio group: icon button opens a popover (`MoreMenu`).
@@ -2287,8 +2318,8 @@ const RadioGroupPopover = ({ config, containerRef, customPortalId, resolved }) =
2287
2318
  *
2288
2319
  * @internal
2289
2320
  */
2290
- const RadioGroup = ({ config, containerRef, customPortalId, resolved }) => {
2291
- return (jsx(RadioGroupPopover, { config: config, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2321
+ const RadioGroup = ({ config, containerRef, customPortalId, overlaySide = undefined, resolved, }) => {
2322
+ return (jsx(RadioGroupPopover, { config: config, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2292
2323
  };
2293
2324
 
2294
2325
  /**
@@ -2297,10 +2328,10 @@ const RadioGroup = ({ config, containerRef, customPortalId, resolved }) => {
2297
2328
  *
2298
2329
  * @internal
2299
2330
  */
2300
- const Search = ({ config, resolved }) => {
2331
+ const Search = ({ config, overlaySide = undefined, resolved }) => {
2301
2332
  const { className, "data-testid": dataTestId, id, label, onChange, onClear, placeholder, value } = config;
2302
2333
  const { size } = resolved;
2303
- return (jsx(Tooltip, { delayed: true, label: label, children: jsx(Search$1, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, onChange: event => {
2334
+ return (jsx(Tooltip, { delayed: true, label: label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsx(Search$1, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, onChange: event => {
2304
2335
  onChange(event.target.value);
2305
2336
  }, onClear: onClear, placeholder: placeholder ?? label, value: value }) }));
2306
2337
  };
@@ -2311,12 +2342,12 @@ const Search = ({ config, resolved }) => {
2311
2342
  *
2312
2343
  * @internal
2313
2344
  */
2314
- const Select = ({ config, resolved }) => {
2345
+ const Select = ({ config, overlaySide = undefined, resolved }) => {
2315
2346
  const { className, "data-testid": dataTestId, id, label, onChange, options, value } = config;
2316
2347
  const { size } = resolved;
2317
2348
  const selectOptions = [...options];
2318
2349
  const selectedOption = selectOptions.find(option => option.value === value) ?? null;
2319
- return (jsx(Tooltip, { delayed: true, label: label, children: jsx(BaseSelect, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, isSearchable: false, onChange: option => {
2350
+ return (jsx(Tooltip, { delayed: true, label: label, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsx(BaseSelect, { "aria-label": label, className: className, "data-testid": dataTestId, fieldSize: size, id: id, isSearchable: false, onChange: option => {
2320
2351
  if (option !== null) {
2321
2352
  onChange(option.value);
2322
2353
  }
@@ -2333,7 +2364,7 @@ const Select = ({ config, resolved }) => {
2333
2364
  *
2334
2365
  * @internal
2335
2366
  */
2336
- const Stepper = ({ config, resolved }) => {
2367
+ const Stepper = ({ config, overlaySide = undefined, resolved }) => {
2337
2368
  const { className, "data-testid": dataTestId, decrement: onClickDecrement, increment: onClickIncrement, label, } = config;
2338
2369
  const { size } = resolved;
2339
2370
  const [t] = useTranslation();
@@ -2346,7 +2377,7 @@ const Stepper = ({ config, resolved }) => {
2346
2377
  : decrementHover.hovering
2347
2378
  ? `${label}, ${decreaseLabel}`
2348
2379
  : label;
2349
- return (jsx(Tooltip, { delayed: true, label: tooltipLabel, children: jsxs("div", { className: twMerge("grid", className), "data-testid": dataTestId, children: [jsx(IconButton, { "aria-label": increaseLabel, className: "rounded-b-none", icon: jsx(Icon, { name: "Plus", size: "small" }), onClick: onClickIncrement, onMouseEnter: incrementHover.onMouseEnter, onMouseLeave: incrementHover.onMouseLeave, size: size, title: increaseLabel, variant: "secondary" }), jsx(IconButton, { "aria-label": decreaseLabel, className: "translate-y-[-1px] rounded-t-none", icon: jsx(Icon, { name: "Minus", size: "small" }), onClick: onClickDecrement, onMouseEnter: decrementHover.onMouseEnter, onMouseLeave: decrementHover.onMouseLeave, size: size, title: decreaseLabel, variant: "secondary" })] }) }));
2380
+ return (jsx(Tooltip, { delayed: true, label: tooltipLabel, placement: overlaySide === "right" ? RIGHT_CONTROL_TOOLTIP_PLACEMENT : undefined, children: jsxs("div", { className: twMerge("grid", className), "data-testid": dataTestId, children: [jsx(IconButton, { "aria-label": increaseLabel, className: "rounded-b-none", icon: jsx(Icon, { name: "Plus", size: "small" }), onClick: onClickIncrement, onMouseEnter: incrementHover.onMouseEnter, onMouseLeave: incrementHover.onMouseLeave, size: size, title: increaseLabel, variant: "secondary" }), jsx(IconButton, { "aria-label": decreaseLabel, className: "translate-y-[-1px] rounded-t-none", icon: jsx(Icon, { name: "Minus", size: "small" }), onClick: onClickDecrement, onMouseEnter: decrementHover.onMouseEnter, onMouseLeave: decrementHover.onMouseLeave, size: size, title: decreaseLabel, variant: "secondary" })] }) }));
2350
2381
  };
2351
2382
 
2352
2383
  function assertStandaloneToggleGroupIcons(config) {
@@ -2363,7 +2394,7 @@ function assertStandaloneToggleGroupIcons(config) {
2363
2394
  *
2364
2395
  * @internal
2365
2396
  */
2366
- const ToggleGroup = ({ config, resolved }) => {
2397
+ const ToggleGroup = ({ config, overlaySide: _overlaySide = undefined, resolved }) => {
2367
2398
  const { className, "data-testid": dataTestId, disabled, label, onChange, options, value } = config;
2368
2399
  assertStandaloneToggleGroupIcons(config);
2369
2400
  return (jsx("div", { "aria-label": label, className: twMerge("w-fit", className), "data-testid": dataTestId, role: "group", children: jsx("div", { className: "relative after:pointer-events-none after:absolute after:inset-0 after:rounded-md after:ring-1 after:ring-inset after:ring-neutral-300 after:content-['']", children: jsx(ToggleGroupField, { config: {
@@ -2381,28 +2412,28 @@ const ToggleGroup = ({ config, resolved }) => {
2381
2412
  *
2382
2413
  * @internal
2383
2414
  */
2384
- const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, menuPresentation, }) => {
2415
+ const ControlRenderer = ({ availableSpace, containerRef, control, customPortalId, menuPresentation, overlaySide = undefined, }) => {
2385
2416
  const resolved = resolveControl(control, { placement: "standalone", availableSpace });
2386
2417
  switch (control.type) {
2387
2418
  case "button":
2388
- return jsx(Button, { config: control, resolved: resolved });
2419
+ return jsx(Button, { config: control, overlaySide: overlaySide, resolved: resolved });
2389
2420
  case "stepper":
2390
- return jsx(Stepper, { config: control, resolved: resolved });
2421
+ return jsx(Stepper, { config: control, overlaySide: overlaySide, resolved: resolved });
2391
2422
  case "toggle":
2392
2423
  case "checkbox":
2393
- return jsx(BooleanButton, { config: control, resolved: resolved });
2424
+ return jsx(BooleanButton, { config: control, overlaySide: overlaySide, resolved: resolved });
2394
2425
  case "select":
2395
- return jsx(Select, { config: control, resolved: resolved });
2426
+ return jsx(Select, { config: control, overlaySide: overlaySide, resolved: resolved });
2396
2427
  case "search":
2397
- return jsx(Search, { config: control, resolved: resolved });
2428
+ return jsx(Search, { config: control, overlaySide: overlaySide, resolved: resolved });
2398
2429
  case "radioGroup":
2399
- return (jsx(RadioGroup, { config: control, containerRef: containerRef, customPortalId: customPortalId, resolved: resolved }));
2430
+ return (jsx(RadioGroup, { config: control, containerRef: containerRef, customPortalId: customPortalId, overlaySide: overlaySide, resolved: resolved }));
2400
2431
  case "toggleGroup":
2401
- return jsx(ToggleGroup, { config: control, resolved: resolved });
2432
+ return jsx(ToggleGroup, { config: control, overlaySide: overlaySide, resolved: resolved });
2402
2433
  case "menu":
2403
- return (jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, menuPresentation: menuPresentation, resolved: resolved }));
2434
+ return (jsx(Menu, { availableSpace: availableSpace, config: control, containerRef: containerRef, customPortalId: customPortalId, menuPresentation: menuPresentation, overlaySide: overlaySide, resolved: resolved }));
2404
2435
  case "custom":
2405
- return jsx(Custom, { config: control, placement: "standalone", resolved: resolved });
2436
+ return jsx(Custom, { config: control, overlaySide: overlaySide, placement: "standalone", resolved: resolved });
2406
2437
  default: {
2407
2438
  const exhaustiveCheck = control;
2408
2439
  throw new Error(`Unknown control type: ${exhaustiveCheck}`);
@@ -2476,10 +2507,10 @@ const ControlsContent = ({ api, controls: baseControlsConfig, className }) => {
2476
2507
  "pointer-events-none",
2477
2508
  // Layout grid for semantic areas
2478
2509
  "grid h-full w-full",
2479
- // Responsive grid: navigation bottom-right, settings top-left, statistics bottom-left
2510
+ // Responsive grid: navigation bottom-right, settings top-left, tools top-right, statistics bottom-left
2480
2511
  "grid-cols-[1fr_auto] grid-rows-[auto_1fr_auto]",
2481
2512
  // Isolate creates a new stacking context to avoid z-index issues
2482
- "isolate", className), "data-testid": "map-controls", ref: mergedRef, children: [hasToolsControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-1 flex flex-col gap-1", "data-testid": "map-controls-tools", children: tools.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasNavigationControls ? (jsx("div", { className: "pointer-events-auto col-start-2 row-start-3 flex flex-col gap-1", children: navigation.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasSettingsControls ? (jsx("div", { className: "pointer-events-auto col-start-1 row-start-1 flex w-fit flex-col gap-1", "data-testid": "map-controls-settings", children: settings.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null, hasStatisticsAreaContent ? (jsxs("div", { className: twMerge("pointer-events-none col-start-1 row-start-3 flex max-h-full max-w-[20rem] flex-col items-start justify-end gap-1 overflow-visible", !hasStatisticsControls && "self-end"), "data-testid": "map-controls-statistics", children: [shouldRenderAnnotations ? (jsx("div", { className: "pointer-events-auto", "data-testid": "map-controls-statistics-annotations", children: jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) })) : null, hasStatisticsControls ? (jsx("div", { className: "pointer-events-auto flex max-h-full w-fit max-w-full flex-col gap-1 overflow-auto", "data-testid": "map-controls-statistics-controls", children: statistics.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null] })) : null] }));
2513
+ "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, menuPresentation: menuPresentation }, control.id))) })) : null, hasSettingsControls ? (jsx("div", { className: "pointer-events-auto col-start-1 row-start-1 flex w-fit flex-col gap-1", "data-testid": "map-controls-settings", children: settings.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation, 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, menuPresentation: menuPresentation }, control.id))) })) : null, hasStatisticsAreaContent ? (jsxs("div", { className: twMerge("pointer-events-none col-start-1 row-start-3 flex max-h-full max-w-[20rem] flex-col items-start justify-end gap-1 overflow-visible", !hasStatisticsControls && "self-end"), "data-testid": "map-controls-statistics", children: [shouldRenderAnnotations ? (jsx("div", { className: "pointer-events-auto", "data-testid": "map-controls-statistics-annotations", children: jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) })) : null, hasStatisticsControls ? (jsx("div", { className: "pointer-events-auto flex max-h-full w-fit max-w-full flex-col gap-1 overflow-auto", "data-testid": "map-controls-statistics-controls", children: statistics.controls.map(control => (jsx(ControlRenderer, { availableSpace: availableSpace, containerRef: containerRef, control: control, customPortalId: portalId, menuPresentation: menuPresentation }, control.id))) })) : null] })) : null] }));
2483
2514
  };
2484
2515
  /**
2485
2516
  * Map controls renderer — lays out a `ControlsConfig` across the four semantic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -1,4 +1,5 @@
1
1
  import type { ComponentRef, RefObject } from "react";
2
+ import type { ControlOverlaySide } from "./controlOverlaySide";
2
3
  import { type AvailableSpace } from "./renderingRules";
3
4
  import type { ControlConfig, MenuPresentation } from "./types";
4
5
  type ControlRendererProps = Readonly<{
@@ -8,6 +9,7 @@ type ControlRendererProps = Readonly<{
8
9
  control: ControlConfig;
9
10
  customPortalId?: string;
10
11
  menuPresentation: MenuPresentation;
12
+ overlaySide?: ControlOverlaySide;
11
13
  }>;
12
14
  /**
13
15
  * Renders a control based on its type using exhaustive switch.
@@ -16,5 +18,5 @@ type ControlRendererProps = Readonly<{
16
18
  *
17
19
  * @internal
18
20
  */
19
- export declare const ControlRenderer: ({ availableSpace, containerRef, control, customPortalId, menuPresentation, }: ControlRendererProps) => import("react/jsx-runtime").JSX.Element;
21
+ export declare const ControlRenderer: ({ availableSpace, containerRef, control, customPortalId, menuPresentation, overlaySide, }: ControlRendererProps) => import("react/jsx-runtime").JSX.Element;
20
22
  export {};
@@ -0,0 +1,4 @@
1
+ /** Controls in edge zones where overlays should open toward the map interior. */
2
+ export type ControlOverlaySide = "right";
3
+ export declare const RIGHT_CONTROL_POPOVER_PLACEMENT: "right-start";
4
+ export declare const RIGHT_CONTROL_TOOLTIP_PLACEMENT: "right";
@@ -1,7 +1,9 @@
1
+ import type { ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import type { ResolvedControlProps } from "../../renderingRules";
2
3
  import type { BooleanControlConfig } from "../../types";
3
4
  type BooleanButtonProps = Readonly<{
4
5
  config: BooleanControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -12,5 +14,5 @@ type BooleanButtonProps = Readonly<{
12
14
  *
13
15
  * @internal
14
16
  */
15
- export declare const BooleanButton: ({ config, resolved }: BooleanButtonProps) => import("react/jsx-runtime").JSX.Element | null;
17
+ export declare const BooleanButton: ({ config, overlaySide, resolved }: BooleanButtonProps) => import("react/jsx-runtime").JSX.Element | null;
16
18
  export {};
@@ -1,7 +1,9 @@
1
+ import type { ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import { type ResolvedControlProps } from "../../renderingRules";
2
3
  import type { ButtonControlConfig } from "../../types";
3
4
  type ButtonProps = Readonly<{
4
5
  config: ButtonControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -10,5 +12,5 @@ type ButtonProps = Readonly<{
10
12
  *
11
13
  * @internal
12
14
  */
13
- export declare const Button: ({ config, resolved }: ButtonProps) => import("react/jsx-runtime").JSX.Element | null;
15
+ export declare const Button: ({ config, overlaySide, resolved }: ButtonProps) => import("react/jsx-runtime").JSX.Element | null;
14
16
  export {};
@@ -1,7 +1,9 @@
1
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import { type ResolvedControlProps } from "../../renderingRules";
2
3
  import type { CustomControlConfig, Placement } from "../../types";
3
4
  type CustomProps = Readonly<{
4
5
  config: CustomControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  placement: Placement;
7
9
  }>;
@@ -18,5 +20,5 @@ type CustomProps = Readonly<{
18
20
  *
19
21
  * @internal
20
22
  */
21
- export declare const Custom: ({ config, resolved, placement }: CustomProps) => import("react/jsx-runtime").JSX.Element | null;
23
+ export declare const Custom: ({ config, overlaySide, resolved, placement }: CustomProps) => import("react/jsx-runtime").JSX.Element | null;
22
24
  export {};
@@ -1,4 +1,5 @@
1
1
  import { type ComponentRef, type RefObject } from "react";
2
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
2
3
  import { type AvailableSpace, type ResolvedControlProps } from "../../renderingRules";
3
4
  import type { MenuControlConfig, MenuPresentation } from "../../types";
4
5
  type MenuProps = Readonly<{
@@ -8,6 +9,7 @@ type MenuProps = Readonly<{
8
9
  containerRef: RefObject<ComponentRef<"div"> | null>;
9
10
  customPortalId?: string;
10
11
  menuPresentation: MenuPresentation;
12
+ overlaySide?: ControlOverlaySide;
11
13
  resolved: ResolvedControlProps;
12
14
  }>;
13
15
  /**
@@ -19,5 +21,5 @@ type MenuProps = Readonly<{
19
21
  *
20
22
  * @internal
21
23
  */
22
- export declare const Menu: ({ availableSpace, config, containerRef, customPortalId, menuPresentation, resolved, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
24
+ export declare const Menu: ({ availableSpace, config, containerRef, customPortalId, menuPresentation, overlaySide, resolved, }: MenuProps) => import("react/jsx-runtime").JSX.Element;
23
25
  export {};
@@ -1,10 +1,12 @@
1
1
  import { type ComponentRef, type RefObject } from "react";
2
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
2
3
  import type { ResolvedControlProps } from "../../renderingRules";
3
4
  import type { RadioGroupControlConfig } from "../../types";
4
5
  type RadioGroupProps = Readonly<{
5
6
  config: RadioGroupControlConfig;
6
7
  containerRef: RefObject<ComponentRef<"div"> | null>;
7
8
  customPortalId?: string;
9
+ overlaySide?: ControlOverlaySide;
8
10
  resolved: ResolvedControlProps;
9
11
  }>;
10
12
  /**
@@ -15,5 +17,5 @@ type RadioGroupProps = Readonly<{
15
17
  *
16
18
  * @internal
17
19
  */
18
- export declare const RadioGroup: ({ config, containerRef, customPortalId, resolved }: RadioGroupProps) => import("react/jsx-runtime").JSX.Element;
20
+ export declare const RadioGroup: ({ config, containerRef, customPortalId, overlaySide, resolved, }: RadioGroupProps) => import("react/jsx-runtime").JSX.Element;
19
21
  export {};
@@ -1,7 +1,9 @@
1
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import type { ResolvedControlProps } from "../../renderingRules";
2
3
  import type { SearchControlConfig } from "../../types";
3
4
  type SearchProps = Readonly<{
4
5
  config: SearchControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -10,5 +12,5 @@ type SearchProps = Readonly<{
10
12
  *
11
13
  * @internal
12
14
  */
13
- export declare const Search: ({ config, resolved }: SearchProps) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const Search: ({ config, overlaySide, resolved }: SearchProps) => import("react/jsx-runtime").JSX.Element;
14
16
  export {};
@@ -1,7 +1,9 @@
1
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import type { ResolvedControlProps } from "../../renderingRules";
2
3
  import type { SelectControlConfig } from "../../types";
3
4
  type SelectProps = Readonly<{
4
5
  config: SelectControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -10,5 +12,5 @@ type SelectProps = Readonly<{
10
12
  *
11
13
  * @internal
12
14
  */
13
- export declare const Select: ({ config, resolved }: SelectProps) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const Select: ({ config, overlaySide, resolved }: SelectProps) => import("react/jsx-runtime").JSX.Element;
14
16
  export {};
@@ -0,0 +1,14 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
3
+ type StandaloneControlTooltipProps = Readonly<{
4
+ children: ReactElement;
5
+ disabled?: boolean;
6
+ label: string;
7
+ overlaySide?: ControlOverlaySide;
8
+ }>;
9
+ /**
10
+ * Wraps standalone map controls with a right-aligned tooltip when `overlaySide` is `"right"`.
11
+ * Callers should omit `title` on {@link IconButton} in that mode so tooltips do not stack.
12
+ */
13
+ export declare const StandaloneControlTooltip: ({ children, disabled, label, overlaySide, }: StandaloneControlTooltipProps) => ReactNode;
14
+ export {};
@@ -1,7 +1,9 @@
1
+ import { type ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import { type ResolvedControlProps } from "../../renderingRules";
2
3
  import type { StepperControlConfig } from "../../types";
3
4
  type StepperProps = Readonly<{
4
5
  config: StepperControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -14,5 +16,5 @@ type StepperProps = Readonly<{
14
16
  *
15
17
  * @internal
16
18
  */
17
- export declare const Stepper: ({ config, resolved }: StepperProps) => import("react/jsx-runtime").JSX.Element;
19
+ export declare const Stepper: ({ config, overlaySide, resolved }: StepperProps) => import("react/jsx-runtime").JSX.Element;
18
20
  export {};
@@ -1,7 +1,9 @@
1
+ import type { ControlOverlaySide } from "../../controlOverlaySide";
1
2
  import type { ResolvedControlProps } from "../../renderingRules";
2
3
  import type { ToggleGroupControlConfig } from "../../types";
3
4
  type ToggleGroupProps = Readonly<{
4
5
  config: ToggleGroupControlConfig;
6
+ overlaySide?: ControlOverlaySide;
5
7
  resolved: ResolvedControlProps;
6
8
  }>;
7
9
  /**
@@ -11,5 +13,5 @@ type ToggleGroupProps = Readonly<{
11
13
  *
12
14
  * @internal
13
15
  */
14
- export declare const ToggleGroup: ({ config, resolved }: ToggleGroupProps) => import("react/jsx-runtime").JSX.Element;
16
+ export declare const ToggleGroup: ({ config, overlaySide: _overlaySide, resolved }: ToggleGroupProps) => import("react/jsx-runtime").JSX.Element;
15
17
  export {};
@@ -315,7 +315,7 @@ export type SettingsConfig = Readonly<{
315
315
  export type StatisticsConfig = Readonly<{
316
316
  controls: ReadonlyArray<ControlConfig>;
317
317
  }>;
318
- /** Tools area - array of controls */
318
+ /** Tools area - array of controls (top-right on the map) */
319
319
  export type ToolsConfig = Readonly<{
320
320
  controls: ReadonlyArray<ControlConfig>;
321
321
  }>;
@@ -325,7 +325,7 @@ export type ControlsConfig = Readonly<{
325
325
  navigation: NavigationConfig;
326
326
  /** Settings: layer toggles, layer settings (future), map style (future) */
327
327
  settings: SettingsConfig;
328
- /** Tools: drawing, measurement, selection (future) */
328
+ /** Tools: top-right controls (e.g. find address, drawing, measurement) */
329
329
  tools: ToolsConfig;
330
330
  /** Statistics: data summaries, counts */
331
331
  statistics: StatisticsConfig;