@trackunit/react-map 0.2.217 → 0.2.219

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
@@ -32,6 +32,10 @@ var defaultTranslations = {
32
32
  "controls.stepper.decrease": "Decrease",
33
33
  "controls.stepper.increase": "Increase",
34
34
  "controls.zoom": "Zoom",
35
+ "draw.clear": "Clear drawing",
36
+ "draw.polygon": "Draw polygon",
37
+ "draw.select": "Edit polygon",
38
+ "draw.selectDisabled": "Draw a polygon first",
35
39
  "map.loading": "Loading map",
36
40
  "map.loadingData": "Loading map data",
37
41
  "map.loadingErrorMessage": "Unable to load the map. Please try again later.",
@@ -1375,6 +1379,8 @@ const useMap = (adapterConfig) => {
1375
1379
  setShowRoads: adapter.setShowRoads.bind(adapter),
1376
1380
  // Optional capability: stays undefined for adapters without traffic support
1377
1381
  setShowTraffic: adapter.setShowTraffic?.bind(adapter),
1382
+ // Optional capability: stays undefined for adapters without drawing support (ADR 0030)
1383
+ draw: adapter.draw ?? undefined,
1378
1384
  }), [adapter]);
1379
1385
  // Stable event subscription callback
1380
1386
  const on = react.useMemo(() => adapter.on.bind(adapter), [adapter]);
@@ -3383,6 +3389,108 @@ const useControls = (base, areas) => {
3383
3389
  }, [base, areas]);
3384
3390
  };
3385
3391
 
3392
+ const noopUnsubscribe$1 = () => undefined;
3393
+ const subscribeNoop = () => noopUnsubscribe$1;
3394
+ const getIdle = () => "idle";
3395
+ const getEmpty = () => reactMapAdapterShared.EMPTY_DRAW_VALUE;
3396
+ /**
3397
+ * Feature-facing hook for the optional drawing capability (ADR 0030).
3398
+ *
3399
+ * Calling this hook is the consumer's opt-in: a map only offers drawing where a
3400
+ * component renders `controls` (typically appended to the `tools` area through
3401
+ * `useControls`). Feature code sees GeoJSON in and GeoJSON out; the engine behind
3402
+ * the adapter is never exposed.
3403
+ *
3404
+ * @example
3405
+ * ```tsx
3406
+ * const [Map, api] = useMap(adapter);
3407
+ * const draw = useDraw(api);
3408
+ * const controls = useControls(api, { tools: { append: draw.controls } });
3409
+ *
3410
+ * return (
3411
+ * <Map>
3412
+ * <Controls api={api} controls={controls} />
3413
+ * </Map>
3414
+ * );
3415
+ * ```
3416
+ */
3417
+ const useDraw = (api, options) => {
3418
+ const [t] = useTranslation();
3419
+ const port = api.actions.draw;
3420
+ const enabled = options?.enabled ?? true;
3421
+ const isSupported = port !== undefined;
3422
+ // Call through the port object (never pass its methods around unbound): adapters
3423
+ // implement DrawPort as a class whose methods read instance state.
3424
+ const subscribe = react.useMemo(() => (port === undefined ? subscribeNoop : (listener) => port.subscribe(listener)), [port]);
3425
+ const getMode = react.useMemo(() => (port === undefined ? getIdle : () => port.getMode()), [port]);
3426
+ const getValue = react.useMemo(() => (port === undefined ? getEmpty : () => port.getValue()), [port]);
3427
+ const mode = react.useSyncExternalStore(subscribe, getMode, getIdle);
3428
+ const value = react.useSyncExternalStore(subscribe, getValue, getEmpty);
3429
+ // A disabled hook must not leave the map in a drawing mode it can no longer exit.
3430
+ react.useEffect(() => {
3431
+ if (!enabled)
3432
+ port?.setMode("idle");
3433
+ }, [enabled, port]);
3434
+ const setMode = react.useCallback((nextMode) => {
3435
+ port?.setMode(nextMode);
3436
+ }, [port]);
3437
+ const setValue = react.useCallback((nextValue) => {
3438
+ port?.setValue(nextValue);
3439
+ }, [port]);
3440
+ const clear = react.useCallback(() => {
3441
+ port?.setValue(reactMapAdapterShared.EMPTY_DRAW_VALUE);
3442
+ port?.setMode("idle");
3443
+ }, [port]);
3444
+ const hasPolygons = value.features.length > 0;
3445
+ const controls = react.useMemo(() => {
3446
+ if (!isSupported || !enabled)
3447
+ return [];
3448
+ const polygonToggle = {
3449
+ type: "toggle",
3450
+ id: "draw-polygon",
3451
+ icon: { on: "Pencil", off: "Pencil" },
3452
+ label: t("draw.polygon"),
3453
+ section: t("controls.category.tools"),
3454
+ value: mode === "polygon",
3455
+ onChange: on => setMode(on ? "polygon" : "idle"),
3456
+ "data-testid": "map-draw-polygon-toggle",
3457
+ };
3458
+ const selectToggle = {
3459
+ type: "toggle",
3460
+ id: "draw-select",
3461
+ icon: { on: "CursorArrowRays", off: "CursorArrowRays" },
3462
+ label: t("draw.select"),
3463
+ section: t("controls.category.tools"),
3464
+ value: mode === "select",
3465
+ onChange: on => setMode(on ? "select" : "idle"),
3466
+ // Never disable the active toggle: the user must always be able to switch it off.
3467
+ disabled: !hasPolygons && mode !== "select",
3468
+ disabledReason: hasPolygons || mode === "select" ? undefined : t("draw.selectDisabled"),
3469
+ "data-testid": "map-draw-select-toggle",
3470
+ };
3471
+ const clearButton = {
3472
+ type: "button",
3473
+ id: "draw-clear",
3474
+ icon: "Trash",
3475
+ label: t("draw.clear"),
3476
+ section: t("controls.category.tools"),
3477
+ onClick: clear,
3478
+ "data-testid": "map-draw-clear-button",
3479
+ };
3480
+ return [polygonToggle, selectToggle, clearButton];
3481
+ }, [clear, enabled, hasPolygons, isSupported, mode, setMode, t]);
3482
+ return react.useMemo(() => ({
3483
+ isSupported,
3484
+ isDrawing: mode !== "idle",
3485
+ mode,
3486
+ value,
3487
+ setMode,
3488
+ setValue,
3489
+ clear,
3490
+ controls,
3491
+ }), [clear, controls, isSupported, mode, setMode, setValue, value]);
3492
+ };
3493
+
3386
3494
  /**
3387
3495
  * Reads the persisted map appearance once (on mount) so it can seed the adapter
3388
3496
  * factory, letting the FIRST map be constructed with the user's stored theme,
@@ -13355,6 +13463,10 @@ Object.defineProperty(exports, "DEFAULT_MAP_APPEARANCE", {
13355
13463
  enumerable: true,
13356
13464
  get: function () { return reactMapAdapterShared.DEFAULT_MAP_APPEARANCE; }
13357
13465
  });
13466
+ Object.defineProperty(exports, "EMPTY_DRAW_VALUE", {
13467
+ enumerable: true,
13468
+ get: function () { return reactMapAdapterShared.EMPTY_DRAW_VALUE; }
13469
+ });
13358
13470
  Object.defineProperty(exports, "HIT_SURFACE_SELECTOR", {
13359
13471
  enumerable: true,
13360
13472
  get: function () { return reactMapAdapterShared.HIT_SURFACE_SELECTOR; }
@@ -13430,6 +13542,7 @@ exports.useControlStack = useControlStack;
13430
13542
  exports.useControls = useControls;
13431
13543
  exports.useDefaultControls = useDefaultControls;
13432
13544
  exports.useDirectionIndicator = useDirectionIndicator;
13545
+ exports.useDraw = useDraw;
13433
13546
  exports.useEntitiesNearCursor = useEntitiesNearCursor;
13434
13547
  exports.useExpandedIds = useExpandedIds;
13435
13548
  exports.useFitFeatureBounds = useFitFeatureBounds;
package/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
2
  import { useNamespaceTranslation, registerTranslations } from '@trackunit/i18n-library-translation';
3
- import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, mapAppearanceSchema, DEFAULT_MAP_APPEARANCE, WORLD_BBOX, INITIAL_INTERACTION_STATE, mapThemeSchema, mergeAntimeridianFeatures, extractSourceData, computeMarkerDomPortalZIndex, densifyGeodesicFeatures, angularDistance, intermediatePoint, GEODESIC_MAX_SEGMENT_KM, geometryTypeToShapeType, resolveStrokeColors } from '@trackunit/react-map-adapter-shared';
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';
3
+ import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, mapAppearanceSchema, DEFAULT_MAP_APPEARANCE, EMPTY_DRAW_VALUE, WORLD_BBOX, INITIAL_INTERACTION_STATE, mapThemeSchema, mergeAntimeridianFeatures, extractSourceData, computeMarkerDomPortalZIndex, densifyGeodesicFeatures, angularDistance, intermediatePoint, GEODESIC_MAX_SEGMENT_KM, geometryTypeToShapeType, resolveStrokeColors } from '@trackunit/react-map-adapter-shared';
4
+ export { ANCHOR_SELECTOR, DEFAULT_MAP_APPEARANCE, EMPTY_DRAW_VALUE, 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 { cva } from '@trackunit/css-class-variance-utilities';
7
7
  import { Tooltip, useMergeRefs, Icon, Spinner, Text, useMeasure, useContainerBreakpoints, useDebounce, useWatch, useKeyboardShortcut, IconButton, ToggleGroup as ToggleGroup$1, useIsFullscreen, Popover, PopoverTrigger, PopoverContent, MoreMenu, useHover as useHover$1, cvaInteractableItem, useLocalStorageReducer } from '@trackunit/react-components';
@@ -31,6 +31,10 @@ var defaultTranslations = {
31
31
  "controls.stepper.decrease": "Decrease",
32
32
  "controls.stepper.increase": "Increase",
33
33
  "controls.zoom": "Zoom",
34
+ "draw.clear": "Clear drawing",
35
+ "draw.polygon": "Draw polygon",
36
+ "draw.select": "Edit polygon",
37
+ "draw.selectDisabled": "Draw a polygon first",
34
38
  "map.loading": "Loading map",
35
39
  "map.loadingData": "Loading map data",
36
40
  "map.loadingErrorMessage": "Unable to load the map. Please try again later.",
@@ -1374,6 +1378,8 @@ const useMap = (adapterConfig) => {
1374
1378
  setShowRoads: adapter.setShowRoads.bind(adapter),
1375
1379
  // Optional capability: stays undefined for adapters without traffic support
1376
1380
  setShowTraffic: adapter.setShowTraffic?.bind(adapter),
1381
+ // Optional capability: stays undefined for adapters without drawing support (ADR 0030)
1382
+ draw: adapter.draw ?? undefined,
1377
1383
  }), [adapter]);
1378
1384
  // Stable event subscription callback
1379
1385
  const on = useMemo(() => adapter.on.bind(adapter), [adapter]);
@@ -3382,6 +3388,108 @@ const useControls = (base, areas) => {
3382
3388
  }, [base, areas]);
3383
3389
  };
3384
3390
 
3391
+ const noopUnsubscribe$1 = () => undefined;
3392
+ const subscribeNoop = () => noopUnsubscribe$1;
3393
+ const getIdle = () => "idle";
3394
+ const getEmpty = () => EMPTY_DRAW_VALUE;
3395
+ /**
3396
+ * Feature-facing hook for the optional drawing capability (ADR 0030).
3397
+ *
3398
+ * Calling this hook is the consumer's opt-in: a map only offers drawing where a
3399
+ * component renders `controls` (typically appended to the `tools` area through
3400
+ * `useControls`). Feature code sees GeoJSON in and GeoJSON out; the engine behind
3401
+ * the adapter is never exposed.
3402
+ *
3403
+ * @example
3404
+ * ```tsx
3405
+ * const [Map, api] = useMap(adapter);
3406
+ * const draw = useDraw(api);
3407
+ * const controls = useControls(api, { tools: { append: draw.controls } });
3408
+ *
3409
+ * return (
3410
+ * <Map>
3411
+ * <Controls api={api} controls={controls} />
3412
+ * </Map>
3413
+ * );
3414
+ * ```
3415
+ */
3416
+ const useDraw = (api, options) => {
3417
+ const [t] = useTranslation();
3418
+ const port = api.actions.draw;
3419
+ const enabled = options?.enabled ?? true;
3420
+ const isSupported = port !== undefined;
3421
+ // Call through the port object (never pass its methods around unbound): adapters
3422
+ // implement DrawPort as a class whose methods read instance state.
3423
+ const subscribe = useMemo(() => (port === undefined ? subscribeNoop : (listener) => port.subscribe(listener)), [port]);
3424
+ const getMode = useMemo(() => (port === undefined ? getIdle : () => port.getMode()), [port]);
3425
+ const getValue = useMemo(() => (port === undefined ? getEmpty : () => port.getValue()), [port]);
3426
+ const mode = useSyncExternalStore(subscribe, getMode, getIdle);
3427
+ const value = useSyncExternalStore(subscribe, getValue, getEmpty);
3428
+ // A disabled hook must not leave the map in a drawing mode it can no longer exit.
3429
+ useEffect(() => {
3430
+ if (!enabled)
3431
+ port?.setMode("idle");
3432
+ }, [enabled, port]);
3433
+ const setMode = useCallback((nextMode) => {
3434
+ port?.setMode(nextMode);
3435
+ }, [port]);
3436
+ const setValue = useCallback((nextValue) => {
3437
+ port?.setValue(nextValue);
3438
+ }, [port]);
3439
+ const clear = useCallback(() => {
3440
+ port?.setValue(EMPTY_DRAW_VALUE);
3441
+ port?.setMode("idle");
3442
+ }, [port]);
3443
+ const hasPolygons = value.features.length > 0;
3444
+ const controls = useMemo(() => {
3445
+ if (!isSupported || !enabled)
3446
+ return [];
3447
+ const polygonToggle = {
3448
+ type: "toggle",
3449
+ id: "draw-polygon",
3450
+ icon: { on: "Pencil", off: "Pencil" },
3451
+ label: t("draw.polygon"),
3452
+ section: t("controls.category.tools"),
3453
+ value: mode === "polygon",
3454
+ onChange: on => setMode(on ? "polygon" : "idle"),
3455
+ "data-testid": "map-draw-polygon-toggle",
3456
+ };
3457
+ const selectToggle = {
3458
+ type: "toggle",
3459
+ id: "draw-select",
3460
+ icon: { on: "CursorArrowRays", off: "CursorArrowRays" },
3461
+ label: t("draw.select"),
3462
+ section: t("controls.category.tools"),
3463
+ value: mode === "select",
3464
+ onChange: on => setMode(on ? "select" : "idle"),
3465
+ // Never disable the active toggle: the user must always be able to switch it off.
3466
+ disabled: !hasPolygons && mode !== "select",
3467
+ disabledReason: hasPolygons || mode === "select" ? undefined : t("draw.selectDisabled"),
3468
+ "data-testid": "map-draw-select-toggle",
3469
+ };
3470
+ const clearButton = {
3471
+ type: "button",
3472
+ id: "draw-clear",
3473
+ icon: "Trash",
3474
+ label: t("draw.clear"),
3475
+ section: t("controls.category.tools"),
3476
+ onClick: clear,
3477
+ "data-testid": "map-draw-clear-button",
3478
+ };
3479
+ return [polygonToggle, selectToggle, clearButton];
3480
+ }, [clear, enabled, hasPolygons, isSupported, mode, setMode, t]);
3481
+ return useMemo(() => ({
3482
+ isSupported,
3483
+ isDrawing: mode !== "idle",
3484
+ mode,
3485
+ value,
3486
+ setMode,
3487
+ setValue,
3488
+ clear,
3489
+ controls,
3490
+ }), [clear, controls, isSupported, mode, setMode, setValue, value]);
3491
+ };
3492
+
3385
3493
  /**
3386
3494
  * Reads the persisted map appearance once (on mount) so it can seed the adapter
3387
3495
  * factory, letting the FIRST map be constructed with the user's stored theme,
@@ -13346,4 +13454,4 @@ const mockMapApi = (overrides) => {
13346
13454
  */
13347
13455
  setupLibraryTranslations();
13348
13456
 
13349
- export { ClusterMarker, ClusterStick, Controls, DEFAULT_APPEARANCE_PERSISTENCE_KEY, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useHeatmap, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePersistedAppearanceSeed, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
13457
+ export { ClusterMarker, ClusterStick, Controls, DEFAULT_APPEARANCE_PERSISTENCE_KEY, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useDraw, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useHeatmap, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePersistedAppearanceSeed, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.217",
3
+ "version": "0.2.219",
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": "3.8.10",
11
- "@trackunit/css-class-variance-utilities": "2.3.10",
12
- "@trackunit/react-form-components": "2.10.17",
13
- "@trackunit/react-core-hooks": "1.25.11",
14
- "@trackunit/geo-json-utils": "1.18.10",
15
- "@trackunit/i18n-library-translation": "2.8.11",
10
+ "@trackunit/react-components": "3.9.0",
11
+ "@trackunit/css-class-variance-utilities": "2.4.0",
12
+ "@trackunit/react-form-components": "2.11.0",
13
+ "@trackunit/react-core-hooks": "1.26.0",
14
+ "@trackunit/geo-json-utils": "1.19.0",
15
+ "@trackunit/i18n-library-translation": "2.9.0",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.162",
18
- "@trackunit/react-map-color-utils": "0.0.136",
19
- "@trackunit/shared-utils": "1.19.10",
20
- "@trackunit/ui-design-tokens": "1.18.9",
17
+ "@trackunit/react-map-adapter-shared": "0.0.164",
18
+ "@trackunit/react-map-color-utils": "0.0.138",
19
+ "@trackunit/shared-utils": "1.20.0",
20
+ "@trackunit/ui-design-tokens": "1.19.0",
21
21
  "@floating-ui/react": "^0.26.25",
22
22
  "es-toolkit": "^1.52.0",
23
23
  "tailwind-merge": "^2.0.0",
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_MAP_APPEARANCE, INITIAL_CAMERA_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, mapAppearanceSchema, mapThemeSchema, mapTypeSchema, } from "@trackunit/react-map-adapter-shared";
2
2
  export type { CameraState, ContainerRefHolder, FitBoundsOptions, GeoJsonBbox, GeoJsonPosition, InitialViewport, MapAppearance, MapComponentProps, MapEvent, MapEventHandler, MapEventOfType, MapState, MapStatus, MapTheme, MapType, StoredEventHandler, } from "@trackunit/react-map-adapter-shared";
3
- import type { AdapterConfig, CameraState, ContainerRefHolder, FitBoundsOptions, GeoJsonBbox, GeoJsonPosition, MapComponentProps, MapEvent, MapEventHandler, MapStatus, MapTheme, MapType } from "@trackunit/react-map-adapter-shared";
3
+ import type { AdapterConfig, CameraState, ContainerRefHolder, DrawPort, FitBoundsOptions, GeoJsonBbox, GeoJsonPosition, MapComponentProps, MapEvent, MapEventHandler, MapStatus, MapTheme, MapType } from "@trackunit/react-map-adapter-shared";
4
4
  import type { ComponentType } from "react";
5
5
  import type { MapAnnotationStore } from "../annotations/mapAnnotations";
6
6
  import type { PanelStore } from "../panel/store/panels";
@@ -92,6 +92,15 @@ export type MapActions = Readonly<{
92
92
  * @returns Promise that resolves when the change is applied
93
93
  */
94
94
  setShowTraffic?: (showTraffic: boolean) => Promise<void>;
95
+ /**
96
+ * Interactive polygon drawing (ADR 0030).
97
+ *
98
+ * Undefined when the adapter cannot draw — `useDraw` reports `isSupported: false`
99
+ * and renders no controls in that case. The port is a controlled input: set
100
+ * `mode` and `value`, subscribe for user edits. Prefer `useDraw(api)` from React
101
+ * components; this is the lower-level handle.
102
+ */
103
+ draw?: DrawPort;
95
104
  }>;
96
105
  /**
97
106
  * The API object returned as the second element of the useMap tuple.
@@ -0,0 +1,62 @@
1
+ import { type DrawMode, type DrawValue } from "@trackunit/react-map-adapter-shared";
2
+ import type { ControlConfig } from "../controls/types";
3
+ import type { MapApi } from "../core/types";
4
+ /** Minimal slice of `MapApi` consumed by `useDraw`. */
5
+ type DrawApi = Readonly<{
6
+ actions: Pick<MapApi["actions"], "draw">;
7
+ }>;
8
+ export type UseDrawOptions = Readonly<{
9
+ /**
10
+ * Hide the draw controls and force the map back to `"idle"` without unmounting
11
+ * the hook, for example while a feature flag is off. Default `true`.
12
+ */
13
+ enabled?: boolean;
14
+ }>;
15
+ export type UseDrawReturn = Readonly<{
16
+ /** False when the adapter has no drawing capability; `controls` is then empty. */
17
+ isSupported: boolean;
18
+ /** True while `mode` is not `"idle"`. Same signal as `api.state.drawing`. */
19
+ isDrawing: boolean;
20
+ /** Current drawing mode. */
21
+ mode: DrawMode;
22
+ /**
23
+ * Committed polygons as GeoJSON. Updates when the user finishes, edits or deletes a
24
+ * polygon, and when `setValue` is called.
25
+ */
26
+ value: DrawValue;
27
+ /** Enter a drawing mode; `"idle"` ends drawing. */
28
+ setMode: (mode: DrawMode) => void;
29
+ /** Replace the committed polygons (controlled-input style). */
30
+ setValue: (value: DrawValue) => void;
31
+ /** Remove every polygon and return to `"idle"`. */
32
+ clear: () => void;
33
+ /**
34
+ * Ready-made controls for the `tools` area: a "Draw polygon" toggle, an "Edit
35
+ * polygon" toggle and a "Clear drawing" button. Empty when drawing is unsupported
36
+ * or disabled.
37
+ */
38
+ controls: ReadonlyArray<ControlConfig>;
39
+ }>;
40
+ /**
41
+ * Feature-facing hook for the optional drawing capability (ADR 0030).
42
+ *
43
+ * Calling this hook is the consumer's opt-in: a map only offers drawing where a
44
+ * component renders `controls` (typically appended to the `tools` area through
45
+ * `useControls`). Feature code sees GeoJSON in and GeoJSON out; the engine behind
46
+ * the adapter is never exposed.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const [Map, api] = useMap(adapter);
51
+ * const draw = useDraw(api);
52
+ * const controls = useControls(api, { tools: { append: draw.controls } });
53
+ *
54
+ * return (
55
+ * <Map>
56
+ * <Controls api={api} controls={controls} />
57
+ * </Map>
58
+ * );
59
+ * ```
60
+ */
61
+ export declare const useDraw: (api: DrawApi, options?: UseDrawOptions) => UseDrawReturn;
62
+ export {};
package/src/index.d.ts CHANGED
@@ -16,6 +16,9 @@ export type { ControlStackItem } from "./controls/defineControlStack";
16
16
  export type { UseControlsAreas } from "./controls/types";
17
17
  export { useControls } from "./controls/useControls";
18
18
  export { useDefaultControls, type UseDefaultControlsOptions } from "./controls/useDefaultControls";
19
+ export { EMPTY_DRAW_VALUE } from "@trackunit/react-map-adapter-shared";
20
+ export type { DrawMode, DrawPort, DrawValue } from "@trackunit/react-map-adapter-shared";
21
+ export { useDraw, type UseDrawOptions, type UseDrawReturn } from "./draw/useDraw";
19
22
  export { DEFAULT_APPEARANCE_PERSISTENCE_KEY } from "./appearance/appearanceTypes";
20
23
  export type { AppearanceAction, AppearanceOption, AppearanceOptions, MapAppearanceApi, PersistedAppearance, UseMapAppearanceControlsParams, } from "./appearance/appearanceTypes";
21
24
  export { useMapAppearanceControls } from "./appearance/useMapAppearanceControls";