@accelint/map-toolkit 1.3.0 → 1.5.0
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/CHANGELOG.md +31 -0
- package/catalog-info.yaml +3 -3
- package/dist/camera/store.js +1 -1
- package/dist/camera/store.js.map +1 -1
- package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
- package/dist/deckgl/base-map/index.d.ts +2 -2
- package/dist/deckgl/base-map/index.js +24 -7
- package/dist/deckgl/base-map/index.js.map +1 -1
- package/dist/deckgl/base-map/provider.d.ts +2 -2
- package/dist/deckgl/base-map/provider.js +2 -4
- package/dist/deckgl/base-map/provider.js.map +1 -1
- package/dist/deckgl/index.js +3 -3
- package/dist/deckgl/shapes/display-shape-layer/index.js +1 -1
- package/dist/deckgl/shapes/display-shape-layer/store.js +7 -1
- package/dist/deckgl/shapes/display-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/index.d.ts +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/index.js +3 -3
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js +7 -7
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/store.js +37 -3
- package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/use-draw-shape.js +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/constants.js +2 -1
- package/dist/deckgl/shapes/edit-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/index.d.ts +2 -2
- package/dist/deckgl/shapes/edit-shape-layer/index.js +30 -12
- package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js +6 -6
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js +10 -4
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.js +129 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/point-translate-mode.js.map +1 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js +19 -4
- package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/types.d.ts +4 -3
- package/dist/deckgl/shapes/edit-shape-layer/use-edit-shape.js +1 -1
- package/dist/deckgl/shapes/index.js +4 -4
- package/dist/deckgl/shapes/shared/constants.js +5 -5
- package/dist/deckgl/shapes/shared/constants.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/layer-config.js +1 -1
- package/dist/hotkey-manager/dist/react/use-hotkey.js +39 -0
- package/dist/hotkey-manager/dist/react/use-hotkey.js.map +1 -0
- package/dist/shared/cleanup.d.ts +58 -0
- package/dist/shared/cleanup.js +93 -0
- package/dist/shared/cleanup.js.map +1 -0
- package/dist/shared/create-map-store.d.ts +12 -0
- package/dist/shared/create-map-store.js +8 -3
- package/dist/shared/create-map-store.js.map +1 -1
- package/dist/viewport/viewport-size.d.ts +2 -2
- package/package.json +10 -8
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at https://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import { clearCameraState } from "../camera/store.js";
|
|
15
|
+
import { clearCursorCoordinateState } from "../cursor-coordinates/store.js";
|
|
16
|
+
import { clearSelectionState } from "../deckgl/shapes/display-shape-layer/store.js";
|
|
17
|
+
import { clearCursorState } from "../map-cursor/store.js";
|
|
18
|
+
import { clearDrawingState } from "../deckgl/shapes/draw-shape-layer/store.js";
|
|
19
|
+
import { clearEditingState } from "../deckgl/shapes/edit-shape-layer/store.js";
|
|
20
|
+
import { clearMapModeState } from "../map-mode/store.js";
|
|
21
|
+
import { clearViewportState } from "../viewport/store.js";
|
|
22
|
+
|
|
23
|
+
//#region src/shared/cleanup.ts
|
|
24
|
+
/**
|
|
25
|
+
* Tracks how many times each map instance has been cleaned up.
|
|
26
|
+
*
|
|
27
|
+
* This counter is module-level (not React state), so React 19's Activity component
|
|
28
|
+
* does NOT preserve it across deactivation/reactivation cycles. Each time
|
|
29
|
+
* `clearAllMapStores` runs (during Activity deactivation cleanup), the generation
|
|
30
|
+
* increments. On reactivation, `BaseMap` reads the new generation during render and
|
|
31
|
+
* passes it as `key` to `<MapLibre>`, forcing a clean remount of react-map-gl's Map
|
|
32
|
+
* component — which resets `mapInstance` to `null` and prevents the crash caused by
|
|
33
|
+
* `setProps` being called on a destroyed MapLibre instance (one whose `map.style` was
|
|
34
|
+
* set to `undefined` by `map.remove()`).
|
|
35
|
+
*/
|
|
36
|
+
const mapGenerations = /* @__PURE__ */ new Map();
|
|
37
|
+
/**
|
|
38
|
+
* Returns the current generation counter for a map instance.
|
|
39
|
+
*
|
|
40
|
+
* Use as a `key` prop on `<MapLibre>` to force a clean remount whenever the map's
|
|
41
|
+
* stores are cleared (e.g., on Activity deactivation).
|
|
42
|
+
*
|
|
43
|
+
* @param mapId - The map instance ID
|
|
44
|
+
* @returns The current generation (0 on first render, increments with each cleanup)
|
|
45
|
+
*/
|
|
46
|
+
function getMapGeneration(mapId) {
|
|
47
|
+
return mapGenerations.get(mapId) ?? 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clears ALL map store state for a given map instance.
|
|
51
|
+
*
|
|
52
|
+
* This function calls cleanup for every store in the map-toolkit. It's called
|
|
53
|
+
* automatically by MapProvider when a map instance unmounts.
|
|
54
|
+
*
|
|
55
|
+
* **⚠️ IMPORTANT: When creating a new store with createMapStore():**
|
|
56
|
+
* 1. Export a `clear*State(mapId)` function from your store
|
|
57
|
+
* 2. Import and add it to this `clearAllMapStores()` function
|
|
58
|
+
* 3. The cleanup function should call your store's internal cleanup mechanism
|
|
59
|
+
*
|
|
60
|
+
* @param mapId - The map instance ID to clean up
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // In your store file (e.g., my-feature/store.ts)
|
|
65
|
+
* export function clearMyFeatureState(mapId: UniqueId): void {
|
|
66
|
+
* myFeatureStore.cleanup(mapId);
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* // Then add to this file:
|
|
70
|
+
* import { clearMyFeatureState } from '../my-feature/store';
|
|
71
|
+
*
|
|
72
|
+
* export function clearAllMapStores(mapId: UniqueId): void {
|
|
73
|
+
* // ... existing cleanups
|
|
74
|
+
* clearMyFeatureState(mapId);
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function clearAllMapStores(mapId) {
|
|
79
|
+
const nextGen = (mapGenerations.get(mapId) ?? 0) + 1;
|
|
80
|
+
mapGenerations.set(mapId, nextGen);
|
|
81
|
+
clearMapModeState(mapId);
|
|
82
|
+
clearCursorState(mapId);
|
|
83
|
+
clearCameraState(mapId);
|
|
84
|
+
clearViewportState(mapId);
|
|
85
|
+
clearCursorCoordinateState(mapId);
|
|
86
|
+
clearDrawingState(mapId);
|
|
87
|
+
clearEditingState(mapId);
|
|
88
|
+
clearSelectionState(mapId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
export { clearAllMapStores, getMapGeneration };
|
|
93
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.js","names":[],"sources":["../../src/shared/cleanup.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { clearCameraState } from '../camera/store';\nimport { clearCursorCoordinateState } from '../cursor-coordinates/store';\nimport { clearSelectionState } from '../deckgl/shapes/display-shape-layer/store';\nimport { clearDrawingState } from '../deckgl/shapes/draw-shape-layer/store';\nimport { clearEditingState } from '../deckgl/shapes/edit-shape-layer/store';\nimport { clearCursorState } from '../map-cursor/store';\nimport { clearMapModeState } from '../map-mode/store';\nimport { clearViewportState } from '../viewport/store';\nimport type { UniqueId } from '@accelint/core';\n\n/**\n * Tracks how many times each map instance has been cleaned up.\n *\n * This counter is module-level (not React state), so React 19's Activity component\n * does NOT preserve it across deactivation/reactivation cycles. Each time\n * `clearAllMapStores` runs (during Activity deactivation cleanup), the generation\n * increments. On reactivation, `BaseMap` reads the new generation during render and\n * passes it as `key` to `<MapLibre>`, forcing a clean remount of react-map-gl's Map\n * component — which resets `mapInstance` to `null` and prevents the crash caused by\n * `setProps` being called on a destroyed MapLibre instance (one whose `map.style` was\n * set to `undefined` by `map.remove()`).\n */\nconst mapGenerations = new Map<UniqueId, number>();\n\n/**\n * Returns the current generation counter for a map instance.\n *\n * Use as a `key` prop on `<MapLibre>` to force a clean remount whenever the map's\n * stores are cleared (e.g., on Activity deactivation).\n *\n * @param mapId - The map instance ID\n * @returns The current generation (0 on first render, increments with each cleanup)\n */\nexport function getMapGeneration(mapId: UniqueId): number {\n return mapGenerations.get(mapId) ?? 0;\n}\n\n/**\n * Clears ALL map store state for a given map instance.\n *\n * This function calls cleanup for every store in the map-toolkit. It's called\n * automatically by MapProvider when a map instance unmounts.\n *\n * **⚠️ IMPORTANT: When creating a new store with createMapStore():**\n * 1. Export a `clear*State(mapId)` function from your store\n * 2. Import and add it to this `clearAllMapStores()` function\n * 3. The cleanup function should call your store's internal cleanup mechanism\n *\n * @param mapId - The map instance ID to clean up\n *\n * @example\n * ```typescript\n * // In your store file (e.g., my-feature/store.ts)\n * export function clearMyFeatureState(mapId: UniqueId): void {\n * myFeatureStore.cleanup(mapId);\n * }\n *\n * // Then add to this file:\n * import { clearMyFeatureState } from '../my-feature/store';\n *\n * export function clearAllMapStores(mapId: UniqueId): void {\n * // ... existing cleanups\n * clearMyFeatureState(mapId);\n * }\n * ```\n */\nexport function clearAllMapStores(mapId: UniqueId): void {\n // Increment generation so BaseMap's next render passes a new key to <MapLibre>,\n // forcing a clean remount and resetting react-map-gl's internal mapInstance state.\n const nextGen = (mapGenerations.get(mapId) ?? 0) + 1;\n mapGenerations.set(mapId, nextGen);\n\n // Core stores\n clearMapModeState(mapId);\n clearCursorState(mapId);\n clearCameraState(mapId);\n clearViewportState(mapId);\n clearCursorCoordinateState(mapId);\n\n // Shape layer stores\n clearDrawingState(mapId);\n clearEditingState(mapId);\n clearSelectionState(mapId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,iCAAiB,IAAI,KAAuB;;;;;;;;;;AAWlD,SAAgB,iBAAiB,OAAyB;AACxD,QAAO,eAAe,IAAI,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCtC,SAAgB,kBAAkB,OAAuB;CAGvD,MAAM,WAAW,eAAe,IAAI,MAAM,IAAI,KAAK;AACnD,gBAAe,IAAI,OAAO,QAAQ;AAGlC,mBAAkB,MAAM;AACxB,kBAAiB,MAAM;AACvB,kBAAiB,MAAM;AACvB,oBAAmB,MAAM;AACzB,4BAA2B,MAAM;AAGjC,mBAAkB,MAAM;AACxB,mBAAkB,MAAM;AACxB,qBAAoB,MAAM"}
|
|
@@ -167,6 +167,18 @@ type MapStore<TState, TActions> = {
|
|
|
167
167
|
* Clear instance state (for tests or manual cleanup).
|
|
168
168
|
*/
|
|
169
169
|
clear: (mapId: UniqueId) => void;
|
|
170
|
+
/**
|
|
171
|
+
* Set initial state to be used when instance is created or updated.
|
|
172
|
+
* Handles both initialization scenarios:
|
|
173
|
+
* - If instance doesn't exist yet: stores pending state for getInstance
|
|
174
|
+
* - If instance already exists: updates existing instance directly
|
|
175
|
+
*
|
|
176
|
+
* This dual-path approach ensures correct initialization regardless of
|
|
177
|
+
* React lifecycle timing (e.g., React Strict Mode double-mount).
|
|
178
|
+
*
|
|
179
|
+
* Safe to call during render. Idempotent for repeated calls with same state.
|
|
180
|
+
*/
|
|
181
|
+
setInitialState: (mapId: UniqueId, state: TState) => void;
|
|
170
182
|
/**
|
|
171
183
|
* Low-level access for custom hooks or useSyncExternalStore.
|
|
172
184
|
*/
|
|
@@ -103,16 +103,18 @@ function mapClear() {
|
|
|
103
103
|
function createMapStore(config) {
|
|
104
104
|
const { defaultState, actions: createActions, bus, onCleanup } = config;
|
|
105
105
|
const instances = /* @__PURE__ */ new Map();
|
|
106
|
+
const pendingInitialState = /* @__PURE__ */ new Map();
|
|
106
107
|
const subscriptionCache = /* @__PURE__ */ new Map();
|
|
107
108
|
const snapshotCache = /* @__PURE__ */ new Map();
|
|
108
109
|
function getInstance(mapId) {
|
|
109
110
|
let instance = instances.get(mapId);
|
|
110
111
|
if (!instance) {
|
|
111
112
|
instance = {
|
|
112
|
-
state: { ...defaultState },
|
|
113
|
+
state: pendingInitialState.get(mapId) ?? { ...defaultState },
|
|
113
114
|
subscribers: /* @__PURE__ */ new Set()
|
|
114
115
|
};
|
|
115
116
|
instances.set(mapId, instance);
|
|
117
|
+
pendingInitialState.delete(mapId);
|
|
116
118
|
}
|
|
117
119
|
return instance;
|
|
118
120
|
}
|
|
@@ -154,8 +156,6 @@ function createMapStore(config) {
|
|
|
154
156
|
if (onCleanup) onCleanup(mapId, instance.state);
|
|
155
157
|
if (instance.busCleanup) instance.busCleanup();
|
|
156
158
|
instances.delete(mapId);
|
|
157
|
-
subscriptionCache.delete(mapId);
|
|
158
|
-
snapshotCache.delete(mapId);
|
|
159
159
|
}
|
|
160
160
|
function subscribe(mapId) {
|
|
161
161
|
let cached = subscriptionCache.get(mapId);
|
|
@@ -236,6 +236,11 @@ function createMapStore(config) {
|
|
|
236
236
|
const instance = instances.get(mapId);
|
|
237
237
|
if (instance) cleanupInstance(mapId, instance);
|
|
238
238
|
},
|
|
239
|
+
setInitialState: (mapId, state) => {
|
|
240
|
+
const instance = instances.get(mapId);
|
|
241
|
+
if (instance) instance.state = state;
|
|
242
|
+
pendingInitialState.set(mapId, state);
|
|
243
|
+
},
|
|
239
244
|
subscribe,
|
|
240
245
|
snapshot,
|
|
241
246
|
serverSnapshot
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-map-store.js","names":[],"sources":["../../src/shared/create-map-store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { useRef, useSyncExternalStore } from 'react';\nimport type { UniqueId } from '@accelint/core';\n\n// =============================================================================\n// Immutable Map Helpers\n// =============================================================================\n\n/**\n * Create a new Map with an entry added or updated (immutable).\n *\n * @param map - The Map to copy\n * @param key - The key to set\n * @param value - The value to set\n * @returns A new Map with the entry added/updated\n *\n * @example\n * ```ts\n * const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V> {\n const newMap = new Map(map);\n newMap.set(key, value);\n return newMap;\n}\n\n/**\n * Create a new Map with an entry removed (immutable).\n *\n * @param map - The Map to copy\n * @param key - The key to remove\n * @returns A new Map with the entry removed\n *\n * @example\n * ```ts\n * const newMap = mapDelete(state.cursorOwners, 'draw-layer');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V> {\n const newMap = new Map(map);\n newMap.delete(key);\n return newMap;\n}\n\n/**\n * Create a new empty Map (immutable replacement for Map.clear()).\n *\n * @returns A new empty Map\n *\n * @example\n * ```ts\n * set({ pendingRequests: mapClear<string, PendingRequest>() });\n * ```\n */\nexport function mapClear<K, V>(): Map<K, V> {\n return new Map<K, V>();\n}\n\n/**\n * Helper methods passed to action creators and bus setup functions.\n *\n * This type is exported for consumers building custom store extensions or\n * helper functions that need to interact with store state.\n *\n * @example\n * ```ts\n * import type { StoreHelpers } from '@accelint/map-toolkit/shared';\n *\n * function createCustomAction<T>(helpers: StoreHelpers<T>) {\n * return () => {\n * const current = helpers.get();\n * helpers.set({ ...current, modified: true });\n * };\n * }\n * ```\n */\nexport type StoreHelpers<TState> = {\n /** Get current state */\n get: () => TState;\n /** Update state (partial merge) and notify subscribers */\n set: (updates: Partial<TState>) => void;\n /** Replace entire state and notify subscribers */\n replace: (state: TState) => void;\n /** Notify subscribers without changing state */\n notify: () => void;\n};\n\n/**\n * Configuration for creating a map store\n */\nexport type MapStoreConfig<TState, TActions> = {\n /** Default state for new instances and SSR */\n defaultState: TState;\n\n /**\n * Action creators - receives mapId and helpers, returns action methods.\n * Actions are cached per mapId for referential stability.\n */\n actions: (mapId: UniqueId, helpers: StoreHelpers<TState>) => TActions;\n\n /**\n * Optional bus listener setup. Called when first subscriber mounts.\n * Return cleanup function to unsubscribe.\n */\n bus?: (mapId: UniqueId, helpers: StoreHelpers<TState>) => () => void;\n\n /**\n * Optional cleanup when instance is destroyed (last subscriber unmounts).\n */\n onCleanup?: (mapId: UniqueId, state: TState) => void;\n};\n\n/**\n * Instance data for a single map\n */\ntype Instance<TState, TActions> = {\n state: TState;\n actions?: TActions;\n subscribers: Set<() => void>;\n busCleanup?: () => void;\n};\n\n/**\n * The store object returned by createMapStore\n */\nexport type MapStore<TState, TActions> = {\n /**\n * React hook - the primary way to use the store.\n * Returns state and actions with proper memoization.\n */\n use: (mapId: UniqueId) => { state: TState } & TActions;\n\n /**\n * React hook with selector for derived state.\n * Only re-renders when the underlying state changes.\n *\n * The selector result is memoized - it only recomputes when the **state reference**\n * changes, not on every render or when the selector function changes. This means:\n *\n * - Selectors that create new objects/arrays are safe without additional memoization\n * - Changing the selector function does NOT trigger recomputation (by design)\n * - This prevents infinite re-render loops when using inline arrow functions\n *\n * **Important**: The selector function is intentionally NOT tracked as a dependency.\n * If you need the selector to change dynamically, extract the changing value as a\n * separate dependency and use it within a stable selector, or use the `use()` hook\n * with your own `useMemo` for derived state.\n *\n * @example\n * ```ts\n * // Returns primitive - recomputes when state.count changes\n * const count = store.useSelector(mapId, (s) => s.count);\n *\n * // Returns existing reference - recomputes when state.items ref changes\n * const items = store.useSelector(mapId, (s) => s.items);\n *\n * // Safe: derived object is memoized internally, no infinite loops\n * const derived = store.useSelector(mapId, (s) => ({ doubled: s.count * 2 }));\n *\n * // If you need dynamic selector behavior, use the base hook instead:\n * const { state } = store.use(mapId);\n * const filtered = useMemo(() => filterFn(state.items), [state.items, filterFn]);\n * ```\n */\n useSelector: <TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ) => TSelected;\n\n /**\n * Get actions without subscribing to state changes.\n * Useful for event handlers or effects.\n */\n actions: (mapId: UniqueId) => TActions;\n\n /**\n * Get current state (non-reactive, for imperative code).\n */\n get: (mapId: UniqueId) => TState;\n\n /**\n * Update state directly (usually prefer actions).\n */\n set: (mapId: UniqueId, updates: Partial<TState>) => void;\n\n /**\n * Check if instance exists (has been initialized).\n */\n exists: (mapId: UniqueId) => boolean;\n\n /**\n * Clear instance state (for tests or manual cleanup).\n */\n clear: (mapId: UniqueId) => void;\n\n /**\n * Low-level access for custom hooks or useSyncExternalStore.\n */\n subscribe: (mapId: UniqueId) => (callback: () => void) => () => void;\n snapshot: (mapId: UniqueId) => () => TState;\n serverSnapshot: () => TState;\n};\n\n/**\n * Creates a store for managing state across multiple map instances.\n *\n * @param config - Store configuration including default state, actions, and optional bus setup\n * @returns A MapStore instance with hooks and methods for accessing/updating state\n *\n * @example\n * ```ts\n * const cursorStore = createMapStore({\n * defaultState: { cursor: 'default', owner: null },\n *\n * actions: (mapId, { get, set }) => ({\n * setCursor: (cursor: string, owner: string) => {\n * set({ cursor, owner });\n * },\n * clearCursor: () => {\n * set({ cursor: 'default', owner: null });\n * },\n * }),\n *\n * bus: (mapId, { set }) => {\n * return cursorBus.on(CursorEvents.change, (e) => {\n * if (e.payload.id === mapId) {\n * set({ cursor: e.payload.cursor });\n * }\n * });\n * },\n * });\n *\n * // In component:\n * function CursorDisplay({ mapId }) {\n * const { state, setCursor } = cursorStore.use(mapId);\n * return <div style={{ cursor: state.cursor }} />;\n * }\n * ```\n */\nexport function createMapStore<TState, TActions>(\n config: MapStoreConfig<TState, TActions>,\n): MapStore<TState, TActions> {\n const { defaultState, actions: createActions, bus, onCleanup } = config;\n\n const instances = new Map<UniqueId, Instance<TState, TActions>>();\n\n // Cached functions for referential stability\n const subscriptionCache = new Map<\n UniqueId,\n (callback: () => void) => () => void\n >();\n const snapshotCache = new Map<UniqueId, () => TState>();\n\n function getInstance(mapId: UniqueId): Instance<TState, TActions> {\n let instance = instances.get(mapId);\n if (!instance) {\n instance = {\n state: { ...defaultState },\n subscribers: new Set(),\n };\n instances.set(mapId, instance);\n }\n return instance;\n }\n\n function notify(mapId: UniqueId): void {\n const instance = instances.get(mapId);\n if (instance) {\n for (const callback of instance.subscribers) {\n callback();\n }\n }\n }\n\n function getHelpers(mapId: UniqueId): StoreHelpers<TState> {\n return {\n get: () => getInstance(mapId).state,\n set: (updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n replace: (state) => {\n const instance = getInstance(mapId);\n instance.state = state;\n notify(mapId);\n },\n notify: () => notify(mapId),\n };\n }\n\n function getActions(mapId: UniqueId): TActions {\n const instance = getInstance(mapId);\n if (!instance.actions) {\n instance.actions = createActions(mapId, getHelpers(mapId));\n }\n return instance.actions;\n }\n\n /**\n * Clean up instance when last subscriber unmounts.\n *\n * @param mapId - Unique identifier for the map instance\n * @param instance - The instance to clean up\n */\n function cleanupInstance(\n mapId: UniqueId,\n instance: Instance<TState, TActions>,\n ): void {\n if (onCleanup) {\n onCleanup(mapId, instance.state);\n }\n if (instance.busCleanup) {\n instance.busCleanup();\n }\n instances.delete(mapId);\n subscriptionCache.delete(mapId);\n snapshotCache.delete(mapId);\n }\n\n function subscribe(mapId: UniqueId): (callback: () => void) => () => void {\n let cached = subscriptionCache.get(mapId);\n if (!cached) {\n cached = (callback: () => void) => {\n const instance = getInstance(mapId);\n\n // Setup bus on first subscriber\n if (instance.subscribers.size === 0 && bus) {\n instance.busCleanup = bus(mapId, getHelpers(mapId));\n }\n\n instance.subscribers.add(callback);\n\n return () => {\n instance.subscribers.delete(callback);\n\n // Cleanup when last subscriber unmounts\n if (instance.subscribers.size === 0) {\n cleanupInstance(mapId, instance);\n }\n };\n };\n subscriptionCache.set(mapId, cached);\n }\n return cached;\n }\n\n function snapshot(mapId: UniqueId): () => TState {\n let cached = snapshotCache.get(mapId);\n if (!cached) {\n cached = () => {\n // State is already a new object reference when updated via set()\n // which creates { ...instance.state, ...updates }\n return getInstance(mapId).state;\n };\n snapshotCache.set(mapId, cached);\n }\n return cached;\n }\n\n function serverSnapshot(): TState {\n return defaultState;\n }\n\n /**\n * Main hook - returns state and actions.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Object containing state and all actions\n */\n function use(mapId: UniqueId): { state: TState } & TActions {\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n const actions = getActions(mapId);\n\n // Return merged object with state wrapper for clarity\n return { state, ...actions };\n }\n\n /**\n * Selector hook - only re-renders when selected value changes.\n *\n * Note: The selector function is intentionally NOT tracked as a dependency.\n * This prevents infinite re-render loops when using inline arrow functions.\n * If you need dynamic selector behavior, use the `use()` hook with `useMemo`.\n *\n * @param mapId - Unique identifier for the map instance\n * @param selector - Function to select derived state\n * @returns The selected value\n */\n function useSelector<TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ): TSelected {\n // Cache the previous state and selected value to avoid unnecessary re-computation.\n // We intentionally do NOT track selector changes - only state changes trigger\n // recomputation. This prevents infinite loops with inline selectors.\n const cache = useRef<{ state: TState; selected: TSelected } | null>(null);\n\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n // Only recompute if state reference changed (selector changes are ignored)\n if (cache.current === null || cache.current.state !== state) {\n cache.current = {\n state,\n selected: selector(state),\n };\n }\n\n return cache.current.selected;\n }\n\n return {\n use,\n useSelector,\n actions: getActions,\n get: (mapId) => getInstance(mapId).state,\n set: (mapId, updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n exists: (mapId) => instances.has(mapId),\n clear: (mapId) => {\n const instance = instances.get(mapId);\n if (instance) {\n cleanupInstance(mapId, instance);\n }\n },\n subscribe,\n snapshot,\n serverSnapshot,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-map-store.js","names":[],"sources":["../../src/shared/create-map-store.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { useRef, useSyncExternalStore } from 'react';\nimport type { UniqueId } from '@accelint/core';\n\n// =============================================================================\n// Immutable Map Helpers\n// =============================================================================\n\n/**\n * Create a new Map with an entry added or updated (immutable).\n *\n * @param map - The Map to copy\n * @param key - The key to set\n * @param value - The value to set\n * @returns A new Map with the entry added/updated\n *\n * @example\n * ```ts\n * const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V> {\n const newMap = new Map(map);\n newMap.set(key, value);\n return newMap;\n}\n\n/**\n * Create a new Map with an entry removed (immutable).\n *\n * @param map - The Map to copy\n * @param key - The key to remove\n * @returns A new Map with the entry removed\n *\n * @example\n * ```ts\n * const newMap = mapDelete(state.cursorOwners, 'draw-layer');\n * set({ cursorOwners: newMap });\n * ```\n */\nexport function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V> {\n const newMap = new Map(map);\n newMap.delete(key);\n return newMap;\n}\n\n/**\n * Create a new empty Map (immutable replacement for Map.clear()).\n *\n * @returns A new empty Map\n *\n * @example\n * ```ts\n * set({ pendingRequests: mapClear<string, PendingRequest>() });\n * ```\n */\nexport function mapClear<K, V>(): Map<K, V> {\n return new Map<K, V>();\n}\n\n/**\n * Helper methods passed to action creators and bus setup functions.\n *\n * This type is exported for consumers building custom store extensions or\n * helper functions that need to interact with store state.\n *\n * @example\n * ```ts\n * import type { StoreHelpers } from '@accelint/map-toolkit/shared';\n *\n * function createCustomAction<T>(helpers: StoreHelpers<T>) {\n * return () => {\n * const current = helpers.get();\n * helpers.set({ ...current, modified: true });\n * };\n * }\n * ```\n */\nexport type StoreHelpers<TState> = {\n /** Get current state */\n get: () => TState;\n /** Update state (partial merge) and notify subscribers */\n set: (updates: Partial<TState>) => void;\n /** Replace entire state and notify subscribers */\n replace: (state: TState) => void;\n /** Notify subscribers without changing state */\n notify: () => void;\n};\n\n/**\n * Configuration for creating a map store\n */\nexport type MapStoreConfig<TState, TActions> = {\n /** Default state for new instances and SSR */\n defaultState: TState;\n\n /**\n * Action creators - receives mapId and helpers, returns action methods.\n * Actions are cached per mapId for referential stability.\n */\n actions: (mapId: UniqueId, helpers: StoreHelpers<TState>) => TActions;\n\n /**\n * Optional bus listener setup. Called when first subscriber mounts.\n * Return cleanup function to unsubscribe.\n */\n bus?: (mapId: UniqueId, helpers: StoreHelpers<TState>) => () => void;\n\n /**\n * Optional cleanup when instance is destroyed (last subscriber unmounts).\n */\n onCleanup?: (mapId: UniqueId, state: TState) => void;\n};\n\n/**\n * Instance data for a single map\n */\ntype Instance<TState, TActions> = {\n state: TState;\n actions?: TActions;\n subscribers: Set<() => void>;\n busCleanup?: () => void;\n};\n\n/**\n * The store object returned by createMapStore\n */\nexport type MapStore<TState, TActions> = {\n /**\n * React hook - the primary way to use the store.\n * Returns state and actions with proper memoization.\n */\n use: (mapId: UniqueId) => { state: TState } & TActions;\n\n /**\n * React hook with selector for derived state.\n * Only re-renders when the underlying state changes.\n *\n * The selector result is memoized - it only recomputes when the **state reference**\n * changes, not on every render or when the selector function changes. This means:\n *\n * - Selectors that create new objects/arrays are safe without additional memoization\n * - Changing the selector function does NOT trigger recomputation (by design)\n * - This prevents infinite re-render loops when using inline arrow functions\n *\n * **Important**: The selector function is intentionally NOT tracked as a dependency.\n * If you need the selector to change dynamically, extract the changing value as a\n * separate dependency and use it within a stable selector, or use the `use()` hook\n * with your own `useMemo` for derived state.\n *\n * @example\n * ```ts\n * // Returns primitive - recomputes when state.count changes\n * const count = store.useSelector(mapId, (s) => s.count);\n *\n * // Returns existing reference - recomputes when state.items ref changes\n * const items = store.useSelector(mapId, (s) => s.items);\n *\n * // Safe: derived object is memoized internally, no infinite loops\n * const derived = store.useSelector(mapId, (s) => ({ doubled: s.count * 2 }));\n *\n * // If you need dynamic selector behavior, use the base hook instead:\n * const { state } = store.use(mapId);\n * const filtered = useMemo(() => filterFn(state.items), [state.items, filterFn]);\n * ```\n */\n useSelector: <TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ) => TSelected;\n\n /**\n * Get actions without subscribing to state changes.\n * Useful for event handlers or effects.\n */\n actions: (mapId: UniqueId) => TActions;\n\n /**\n * Get current state (non-reactive, for imperative code).\n */\n get: (mapId: UniqueId) => TState;\n\n /**\n * Update state directly (usually prefer actions).\n */\n set: (mapId: UniqueId, updates: Partial<TState>) => void;\n\n /**\n * Check if instance exists (has been initialized).\n */\n exists: (mapId: UniqueId) => boolean;\n\n /**\n * Clear instance state (for tests or manual cleanup).\n */\n clear: (mapId: UniqueId) => void;\n\n /**\n * Set initial state to be used when instance is created or updated.\n * Handles both initialization scenarios:\n * - If instance doesn't exist yet: stores pending state for getInstance\n * - If instance already exists: updates existing instance directly\n *\n * This dual-path approach ensures correct initialization regardless of\n * React lifecycle timing (e.g., React Strict Mode double-mount).\n *\n * Safe to call during render. Idempotent for repeated calls with same state.\n */\n setInitialState: (mapId: UniqueId, state: TState) => void;\n\n /**\n * Low-level access for custom hooks or useSyncExternalStore.\n */\n subscribe: (mapId: UniqueId) => (callback: () => void) => () => void;\n snapshot: (mapId: UniqueId) => () => TState;\n serverSnapshot: () => TState;\n};\n\n/**\n * Creates a store for managing state across multiple map instances.\n *\n * @param config - Store configuration including default state, actions, and optional bus setup\n * @returns A MapStore instance with hooks and methods for accessing/updating state\n *\n * @example\n * ```ts\n * const cursorStore = createMapStore({\n * defaultState: { cursor: 'default', owner: null },\n *\n * actions: (mapId, { get, set }) => ({\n * setCursor: (cursor: string, owner: string) => {\n * set({ cursor, owner });\n * },\n * clearCursor: () => {\n * set({ cursor: 'default', owner: null });\n * },\n * }),\n *\n * bus: (mapId, { set }) => {\n * return cursorBus.on(CursorEvents.change, (e) => {\n * if (e.payload.id === mapId) {\n * set({ cursor: e.payload.cursor });\n * }\n * });\n * },\n * });\n *\n * // In component:\n * function CursorDisplay({ mapId }) {\n * const { state, setCursor } = cursorStore.use(mapId);\n * return <div style={{ cursor: state.cursor }} />;\n * }\n * ```\n */\nexport function createMapStore<TState, TActions>(\n config: MapStoreConfig<TState, TActions>,\n): MapStore<TState, TActions> {\n const { defaultState, actions: createActions, bus, onCleanup } = config;\n\n const instances = new Map<UniqueId, Instance<TState, TActions>>();\n\n // Stores initial state to be used when instance is first created\n // This allows setting initial state BEFORE any getInstance call\n const pendingInitialState = new Map<UniqueId, TState>();\n\n // Cached functions for referential stability\n const subscriptionCache = new Map<\n UniqueId,\n (callback: () => void) => () => void\n >();\n const snapshotCache = new Map<UniqueId, () => TState>();\n\n function getInstance(mapId: UniqueId): Instance<TState, TActions> {\n let instance = instances.get(mapId);\n if (!instance) {\n // Check for pending initial state - use it instead of default\n const initialState = pendingInitialState.get(mapId);\n instance = {\n state: initialState ?? { ...defaultState },\n subscribers: new Set(),\n };\n instances.set(mapId, instance);\n pendingInitialState.delete(mapId); // Clear after use\n }\n return instance;\n }\n\n function notify(mapId: UniqueId): void {\n const instance = instances.get(mapId);\n if (instance) {\n for (const callback of instance.subscribers) {\n callback();\n }\n }\n }\n\n function getHelpers(mapId: UniqueId): StoreHelpers<TState> {\n return {\n get: () => getInstance(mapId).state,\n set: (updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n replace: (state) => {\n const instance = getInstance(mapId);\n instance.state = state;\n notify(mapId);\n },\n notify: () => notify(mapId),\n };\n }\n\n function getActions(mapId: UniqueId): TActions {\n const instance = getInstance(mapId);\n if (!instance.actions) {\n instance.actions = createActions(mapId, getHelpers(mapId));\n }\n return instance.actions;\n }\n\n /**\n * Clean up instance when last subscriber unmounts.\n *\n * @param mapId - Unique identifier for the map instance\n * @param instance - The instance to clean up\n */\n function cleanupInstance(\n mapId: UniqueId,\n instance: Instance<TState, TActions>,\n ): void {\n if (onCleanup) {\n onCleanup(mapId, instance.state);\n }\n if (instance.busCleanup) {\n instance.busCleanup();\n }\n instances.delete(mapId);\n // NOTE: Do NOT delete subscriptionCache or snapshotCache here!\n // These are function reference caches. Clearing them causes React's\n // useSyncExternalStore to see a new subscribe function reference on the next\n // render, which triggers re-subscription, which triggers cleanupInstance again,\n // creating an infinite cycle. The cached functions call getInstance() dynamically,\n // so they work correctly even after the instance is recreated.\n //\n // NOTE: Do NOT delete pendingInitialState here!\n // In React Strict Mode, cleanup runs but then subscribe re-runs BEFORE render.\n // The pending state must survive cleanup so it's available when getInstance\n // creates a new instance during the Strict Mode remount.\n }\n\n function subscribe(mapId: UniqueId): (callback: () => void) => () => void {\n let cached = subscriptionCache.get(mapId);\n if (!cached) {\n cached = (callback: () => void) => {\n const instance = getInstance(mapId);\n\n // Setup bus on first subscriber\n if (instance.subscribers.size === 0 && bus) {\n instance.busCleanup = bus(mapId, getHelpers(mapId));\n }\n\n instance.subscribers.add(callback);\n\n return () => {\n instance.subscribers.delete(callback);\n\n // Cleanup when last subscriber unmounts\n if (instance.subscribers.size === 0) {\n cleanupInstance(mapId, instance);\n }\n };\n };\n subscriptionCache.set(mapId, cached);\n }\n return cached;\n }\n\n function snapshot(mapId: UniqueId): () => TState {\n let cached = snapshotCache.get(mapId);\n if (!cached) {\n cached = () => {\n // State is already a new object reference when updated via set()\n // which creates { ...instance.state, ...updates }\n return getInstance(mapId).state;\n };\n snapshotCache.set(mapId, cached);\n }\n return cached;\n }\n\n function serverSnapshot(): TState {\n return defaultState;\n }\n\n /**\n * Main hook - returns state and actions.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Object containing state and all actions\n */\n function use(mapId: UniqueId): { state: TState } & TActions {\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n const actions = getActions(mapId);\n\n // Return merged object with state wrapper for clarity\n return { state, ...actions };\n }\n\n /**\n * Selector hook - only re-renders when selected value changes.\n *\n * Note: The selector function is intentionally NOT tracked as a dependency.\n * This prevents infinite re-render loops when using inline arrow functions.\n * If you need dynamic selector behavior, use the `use()` hook with `useMemo`.\n *\n * @param mapId - Unique identifier for the map instance\n * @param selector - Function to select derived state\n * @returns The selected value\n */\n function useSelector<TSelected>(\n mapId: UniqueId,\n selector: (state: TState) => TSelected,\n ): TSelected {\n // Cache the previous state and selected value to avoid unnecessary re-computation.\n // We intentionally do NOT track selector changes - only state changes trigger\n // recomputation. This prevents infinite loops with inline selectors.\n const cache = useRef<{ state: TState; selected: TSelected } | null>(null);\n\n const state = useSyncExternalStore(\n subscribe(mapId),\n snapshot(mapId),\n serverSnapshot,\n );\n\n // Only recompute if state reference changed (selector changes are ignored)\n if (cache.current === null || cache.current.state !== state) {\n cache.current = {\n state,\n selected: selector(state),\n };\n }\n\n return cache.current.selected;\n }\n\n return {\n use,\n useSelector,\n actions: getActions,\n get: (mapId) => getInstance(mapId).state,\n set: (mapId, updates) => {\n const instance = getInstance(mapId);\n instance.state = { ...instance.state, ...updates };\n notify(mapId);\n },\n exists: (mapId) => instances.has(mapId),\n clear: (mapId) => {\n const instance = instances.get(mapId);\n if (instance) {\n cleanupInstance(mapId, instance);\n }\n },\n setInitialState: (mapId, state) => {\n // If instance already exists, update it directly.\n // This handles React Strict Mode where subscribe() might create the instance\n // BEFORE setInitialState is called during the re-render.\n const instance = instances.get(mapId);\n if (instance) {\n instance.state = state;\n // Don't call notify() - this is initialization, not a state change that\n // should trigger re-renders. The component will get the state on next render.\n }\n\n // Always also set pending state for the case where getInstance is called later\n pendingInitialState.set(mapId, state);\n },\n subscribe,\n snapshot,\n serverSnapshot,\n };\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { SupportedDistanceUnit } from "./types.js";
|
|
14
14
|
import { ComponentPropsWithRef } from "react";
|
|
15
15
|
import { UniqueId } from "@accelint/core";
|
|
16
|
-
import * as
|
|
16
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
17
17
|
|
|
18
18
|
//#region src/viewport/viewport-size.d.ts
|
|
19
19
|
type ViewportSizeProps = ComponentPropsWithRef<'span'> & {
|
|
@@ -48,7 +48,7 @@ declare function ViewportSize({
|
|
|
48
48
|
instanceId,
|
|
49
49
|
unit,
|
|
50
50
|
...rest
|
|
51
|
-
}: ViewportSizeProps):
|
|
51
|
+
}: ViewportSizeProps): react_jsx_runtime1.JSX.Element;
|
|
52
52
|
//#endregion
|
|
53
53
|
export { ViewportSize, ViewportSizeProps };
|
|
54
54
|
//# sourceMappingURL=viewport-size.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@accelint/map-toolkit",
|
|
3
3
|
"description": "A collection of components and utilities to simplify visualizing and working with geospatial data.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"author": "https://hypergiant.com",
|
|
6
6
|
"$schema": "https://json.schemastore.org/package",
|
|
7
7
|
"devDependencies": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@accelint/biome-config": "1.1.0",
|
|
46
46
|
"@accelint/bus": "3.0.2",
|
|
47
47
|
"@accelint/core": "0.5.2",
|
|
48
|
-
"@accelint/design-foundation": "
|
|
49
|
-
"@accelint/design-toolkit": "9.
|
|
50
|
-
"@accelint/geo": "0.
|
|
51
|
-
"@accelint/logger": "0.
|
|
52
|
-
"@accelint/postcss-tailwind-css-modules": "1.0.1",
|
|
48
|
+
"@accelint/design-foundation": "3.0.0",
|
|
49
|
+
"@accelint/design-toolkit": "9.7.0",
|
|
50
|
+
"@accelint/geo": "0.6.0",
|
|
51
|
+
"@accelint/logger": "1.0.0",
|
|
53
52
|
"@accelint/smeegl": "0.3.5",
|
|
53
|
+
"@accelint/postcss-tailwind-css-modules": "1.0.1",
|
|
54
54
|
"@accelint/typescript-config": "0.1.4",
|
|
55
55
|
"@accelint/vitest-config": "0.1.6"
|
|
56
56
|
},
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"./camera/store": "./dist/camera/store.js",
|
|
65
65
|
"./camera/types": "./dist/camera/types.js",
|
|
66
66
|
"./cursor-coordinates": "./dist/cursor-coordinates/index.js",
|
|
67
|
+
"./cursor-coordinates/constants": "./dist/cursor-coordinates/constants.js",
|
|
67
68
|
"./cursor-coordinates/store": "./dist/cursor-coordinates/store.js",
|
|
68
69
|
"./cursor-coordinates/types": "./dist/cursor-coordinates/types.js",
|
|
69
70
|
"./cursor-coordinates/use-cursor-coordinates": "./dist/cursor-coordinates/use-cursor-coordinates.js",
|
|
@@ -112,6 +113,7 @@
|
|
|
112
113
|
"./map-mode/use-map-mode": "./dist/map-mode/use-map-mode.js",
|
|
113
114
|
"./maplibre": "./dist/maplibre/index.js",
|
|
114
115
|
"./maplibre/hooks/use-maplibre": "./dist/maplibre/hooks/use-maplibre.js",
|
|
116
|
+
"./shared/cleanup": "./dist/shared/cleanup.js",
|
|
115
117
|
"./shared/constants": "./dist/shared/constants.js",
|
|
116
118
|
"./shared/create-map-store": "./dist/shared/create-map-store.js",
|
|
117
119
|
"./shared/units": "./dist/shared/units.js",
|
|
@@ -159,8 +161,8 @@
|
|
|
159
161
|
"react": "^19",
|
|
160
162
|
"@accelint/bus": "3.0.2",
|
|
161
163
|
"@accelint/core": "0.5.2",
|
|
162
|
-
"@accelint/geo": "0.
|
|
163
|
-
"@accelint/logger": "0.
|
|
164
|
+
"@accelint/geo": "0.6.0",
|
|
165
|
+
"@accelint/logger": "1.0.0"
|
|
164
166
|
},
|
|
165
167
|
"private": false,
|
|
166
168
|
"publishConfig": {
|