@accelint/map-toolkit 1.2.0 → 1.3.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 +11 -0
- package/catalog-info.yaml +3 -3
- package/dist/camera/events.d.ts +45 -0
- package/dist/camera/events.js +45 -0
- package/dist/camera/events.js.map +1 -1
- package/dist/camera/store.d.ts +47 -0
- package/dist/camera/store.js +81 -0
- package/dist/camera/store.js.map +1 -1
- package/dist/camera/types.d.ts +81 -0
- package/dist/cursor-coordinates/constants.d.ts +8 -0
- package/dist/cursor-coordinates/constants.js +22 -0
- package/dist/cursor-coordinates/constants.js.map +1 -0
- package/dist/cursor-coordinates/store.d.ts +1 -0
- package/dist/cursor-coordinates/store.js +1 -0
- package/dist/cursor-coordinates/store.js.map +1 -1
- package/dist/cursor-coordinates/use-cursor-coordinates.d.ts +5 -0
- package/dist/cursor-coordinates/use-cursor-coordinates.js +23 -8
- package/dist/cursor-coordinates/use-cursor-coordinates.js.map +1 -1
- package/dist/deckgl/base-map/constants.d.ts +12 -0
- package/dist/deckgl/base-map/constants.js +12 -0
- package/dist/deckgl/base-map/constants.js.map +1 -1
- package/dist/deckgl/base-map/controls.d.ts +11 -1
- package/dist/deckgl/base-map/controls.js +5 -0
- package/dist/deckgl/base-map/controls.js.map +1 -1
- package/dist/deckgl/base-map/events.d.ts +30 -0
- package/dist/deckgl/base-map/events.js +30 -0
- package/dist/deckgl/base-map/events.js.map +1 -1
- package/dist/deckgl/base-map/index.d.ts +2 -2
- package/dist/deckgl/base-map/index.js +33 -3
- package/dist/deckgl/base-map/index.js.map +1 -1
- package/dist/deckgl/base-map/provider.d.ts +2 -2
- package/dist/deckgl/index.js +1 -1
- package/dist/deckgl/saved-viewports/index.d.ts +75 -0
- package/dist/deckgl/saved-viewports/index.js +58 -0
- package/dist/deckgl/saved-viewports/index.js.map +1 -1
- package/dist/deckgl/saved-viewports/storage.d.ts +51 -0
- package/dist/deckgl/saved-viewports/storage.js +64 -0
- package/dist/deckgl/saved-viewports/storage.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/constants.js +18 -6
- package/dist/deckgl/shapes/display-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/fiber.d.ts +7 -0
- package/dist/deckgl/shapes/display-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js +61 -4
- package/dist/deckgl/shapes/display-shape-layer/utils/display-style.js.map +1 -1
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.d.ts +22 -8
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js +75 -4
- package/dist/deckgl/shapes/display-shape-layer/utils/labels.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/constants.js +30 -0
- package/dist/deckgl/shapes/draw-shape-layer/constants.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js +36 -0
- package/dist/deckgl/shapes/draw-shape-layer/fiber.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/index.d.ts +2 -2
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-circle-mode-with-tooltip.js +32 -1
- 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 +37 -8
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-ellipse-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js +43 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-line-string-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js +44 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-polygon-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js +46 -3
- package/dist/deckgl/shapes/draw-shape-layer/modes/draw-rectangle-mode-with-tooltip.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js +37 -1
- package/dist/deckgl/shapes/draw-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/store.js +50 -2
- package/dist/deckgl/shapes/draw-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js +138 -17
- package/dist/deckgl/shapes/draw-shape-layer/utils/feature-conversion.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/events.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/events.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 +14 -0
- package/dist/deckgl/shapes/edit-shape-layer/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js +56 -8
- package/dist/deckgl/shapes/edit-shape-layer/modes/base-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js +26 -4
- package/dist/deckgl/shapes/edit-shape-layer/modes/bounding-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/circle-transform-mode.js +28 -3
- 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 +24 -0
- package/dist/deckgl/shapes/edit-shape-layer/modes/index.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js +33 -4
- package/dist/deckgl/shapes/edit-shape-layer/modes/rotate-mode-with-snap.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js +21 -2
- package/dist/deckgl/shapes/edit-shape-layer/modes/scale-mode-with-free-transform.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js +35 -11
- package/dist/deckgl/shapes/edit-shape-layer/modes/vertex-transform-mode.js.map +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js +1 -1
- package/dist/deckgl/shapes/edit-shape-layer/store.js.map +1 -1
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js +12 -0
- package/dist/deckgl/shapes/shared/hooks/use-shift-zoom-disable.js.map +1 -1
- package/dist/deckgl/shapes/shared/types.d.ts +3 -3
- package/dist/deckgl/shapes/shared/types.js +2 -2
- package/dist/deckgl/shapes/shared/types.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js +3 -3
- package/dist/deckgl/shapes/shared/utils/geometry-measurements.js.map +1 -1
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js +1 -1
- package/dist/deckgl/shapes/shared/utils/pick-filtering.js.map +1 -1
- package/dist/deckgl/symbol-layer/fiber.d.ts +18 -0
- package/dist/deckgl/symbol-layer/fiber.js.map +1 -1
- package/dist/deckgl/symbol-layer/index.d.ts +79 -1
- package/dist/deckgl/symbol-layer/index.js +72 -1
- package/dist/deckgl/symbol-layer/index.js.map +1 -1
- package/dist/deckgl/text-layer/character-sets.d.ts +30 -0
- package/dist/deckgl/text-layer/character-sets.js +26 -0
- package/dist/deckgl/text-layer/character-sets.js.map +1 -1
- package/dist/deckgl/text-layer/default-settings.d.ts +29 -0
- package/dist/deckgl/text-layer/default-settings.js +28 -0
- package/dist/deckgl/text-layer/default-settings.js.map +1 -1
- package/dist/deckgl/text-layer/index.d.ts +65 -0
- package/dist/deckgl/text-layer/index.js +56 -0
- package/dist/deckgl/text-layer/index.js.map +1 -1
- package/dist/map-cursor/events.d.ts +19 -0
- package/dist/map-cursor/events.js +19 -0
- package/dist/map-cursor/events.js.map +1 -1
- package/dist/map-cursor/store.d.ts +34 -2
- package/dist/map-cursor/store.js +44 -3
- package/dist/map-cursor/store.js.map +1 -1
- package/dist/map-mode/store.d.ts +43 -4
- package/dist/map-mode/store.js +56 -6
- package/dist/map-mode/store.js.map +1 -1
- package/dist/shared/create-map-store.d.ts +14 -0
- package/dist/shared/create-map-store.js +26 -2
- package/dist/shared/create-map-store.js.map +1 -1
- package/dist/shared/units.d.ts +24 -0
- package/dist/shared/units.js +24 -0
- package/dist/shared/units.js.map +1 -1
- package/dist/viewport/store.d.ts +1 -0
- package/dist/viewport/store.js +4 -0
- package/dist/viewport/store.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/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\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance\n * @internal - For internal map-toolkit use only\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n * @internal - For internal map-toolkit use only\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific instanceId.\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store.js","names":["requestsToReject: PendingRequest[]","matchingRequestOwner: string | null","matchingRequest: PendingRequest | null"],"sources":["../../src/map-mode/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\n/**\n * Map Mode Store\n *\n * Manages mode state with ownership-based authorization.\n *\n * @example\n * ```tsx\n * import { modeStore } from '@accelint/map-toolkit/map-mode';\n *\n * function MapControls({ mapId }) {\n * const { state, requestModeChange } = modeStore.use(mapId);\n *\n * return (\n * <div>\n * <p>Current mode: {state.mode}</p>\n * <button onClick={() => requestModeChange('draw', 'draw-layer')}>\n * Draw Mode\n * </button>\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { getLogger } from '@accelint/logger';\nimport {\n createMapStore,\n mapClear,\n mapDelete,\n mapSet,\n} from '../shared/create-map-store';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { StoreHelpers } from '../shared/create-map-store';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst logger = getLogger({\n enabled:\n process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',\n level: 'warn',\n prefix: '[MapMode]',\n pretty: true,\n});\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * State shape for map mode\n */\ntype MapModeState = {\n mode: string;\n modeOwners: Map<string, string>;\n pendingRequests: Map<string, PendingRequest>;\n};\n\n/**\n * Actions for map mode\n */\ntype MapModeActions = {\n /** Request a mode change */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Determine if a mode change request should be auto-accepted without authorization.\n *\n * Auto-accept conditions:\n * - Owner returning to default mode\n * - Owner switching between their own modes\n * - No ownership conflicts exist\n * - Entering an owned mode from default mode\n *\n * @param state - Current mode state\n * @param desiredMode - The mode being requested\n * @param requestOwner - The component requesting the mode change\n * @returns True if the request should be auto-accepted\n */\nfunction shouldAutoAcceptRequest(\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n): boolean {\n const currentModeOwner = state.modeOwners.get(state.mode);\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Owner returning to default mode\n if (desiredMode === DEFAULT_MODE && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (state.mode === DEFAULT_MODE && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Approve a request and reject all others (immutable update)\n */\nfunction approveRequestAndRejectOthers(\n instanceId: UniqueId,\n state: MapModeState,\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of state.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Build immutable updates: clear pending requests, update owners\n const newModeOwners =\n approvedRequest.desiredMode !== DEFAULT_MODE\n ? mapSet(\n state.modeOwners,\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n )\n : state.modeOwners;\n\n // Immutable update: clear pending requests, update owners, change mode\n set({\n mode: approvedRequest.desiredMode,\n pendingRequests: mapClear<string, PendingRequest>(),\n modeOwners: newModeOwners,\n });\n\n // Emit mode changed event\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode: state.mode,\n currentMode: approvedRequest.desiredMode,\n id: instanceId,\n });\n\n // Emit approval decision if requested\n if (emitApproval) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: instanceId,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: instanceId,\n });\n }\n}\n\n/**\n * Handle pending requests when returning to default mode (immutable update)\n */\nfunction handlePendingRequestsOnDefaultMode(\n instanceId: UniqueId,\n state: MapModeState,\n previousMode: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const firstEntry = Array.from(state.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = state.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === DEFAULT_MODE) {\n const allRequests = Array.from(state.pendingRequests.values());\n\n // Immutable update: clear pending requests\n set({ pendingRequests: mapClear<string, PendingRequest>() });\n\n for (const request of allRequests) {\n mapModeBus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: instanceId,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n approveRequestAndRejectOthers(\n instanceId,\n state,\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n set,\n );\n }\n}\n\n/**\n * Handle authorization decision (immutable update)\n */\nfunction handleAuthorizationDecision(\n instanceId: UniqueId,\n state: MapModeState,\n payload: {\n approved: boolean;\n authId: string;\n owner: string;\n },\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n const currentModeOwner = state.modeOwners.get(state.mode);\n if (decisionOwner !== currentModeOwner) {\n logger.warn(\n `Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${state.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of state.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n approveRequestAndRejectOthers(\n instanceId,\n state,\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n set,\n );\n } else {\n // Immutable update: remove the rejected request\n set({\n pendingRequests: mapDelete(state.pendingRequests, matchingRequestOwner),\n });\n }\n}\n\n/**\n * Handle mode change request logic (immutable update)\n */\nfunction handleModeChangeRequest(\n instanceId: UniqueId,\n state: MapModeState,\n desiredMode: string,\n requestOwner: string,\n set: StoreHelpers<MapModeState>['set'],\n): void {\n const desiredModeOwner = state.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (shouldAutoAcceptRequest(state, desiredMode, requestOwner)) {\n // Build immutable updates\n const newModeOwners =\n desiredMode !== DEFAULT_MODE && !desiredModeOwner\n ? mapSet(state.modeOwners, desiredMode, requestOwner)\n : state.modeOwners;\n\n // Clear requester's pending request since mode changed successfully\n const newPendingRequests = mapDelete(state.pendingRequests, requestOwner);\n\n const previousMode = state.mode;\n\n // Immutable update\n set({\n mode: desiredMode,\n modeOwners: newModeOwners,\n pendingRequests: newPendingRequests,\n });\n\n mapModeBus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: desiredMode,\n id: instanceId,\n });\n\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n // Immutable update: add pending request\n set({\n pendingRequests: mapSet(state.pendingRequests, requestOwner, {\n authId,\n desiredMode,\n currentMode: state.mode,\n requestOwner,\n }),\n });\n\n mapModeBus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: state.mode,\n id: instanceId,\n });\n}\n\n/**\n * Map mode store\n */\nexport const modeStore = createMapStore<MapModeState, MapModeActions>({\n defaultState: {\n mode: DEFAULT_MODE,\n modeOwners: new Map(),\n pendingRequests: new Map(),\n },\n\n actions: (instanceId) => ({\n requestModeChange: (desiredMode: string, requestOwner: string) => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n mapModeBus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: instanceId,\n });\n },\n }),\n\n bus: (instanceId, { get, set }) => {\n // Listen for mode change requests\n const unsubRequest = mapModeBus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n const state = get();\n // Filter: only handle if targeted at this map\n if (id !== instanceId || desiredMode === state.mode) {\n return;\n }\n\n handleModeChangeRequest(\n instanceId,\n state,\n desiredMode,\n requestOwner,\n set,\n );\n });\n\n // Listen for authorization decisions\n const unsubDecision = mapModeBus.on(\n MapModeEvents.changeDecision,\n (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n handleAuthorizationDecision(\n instanceId,\n get(),\n { approved, authId, owner },\n set,\n );\n },\n );\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = mapModeBus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== instanceId) {\n return;\n }\n\n const state = get();\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === DEFAULT_MODE && state.pendingRequests.size > 0) {\n handlePendingRequestsOnDefaultMode(\n instanceId,\n state,\n previousMode,\n set,\n );\n }\n });\n\n return () => {\n unsubRequest();\n unsubDecision();\n unsubChanged();\n };\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Get the current mode for a map instance.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The current active mode string\n *\n * @example\n * ```typescript\n * import { getMode } from '@accelint/map-toolkit/map-mode';\n *\n * const currentMode = getMode(mapId);\n * console.log('Current mode:', currentMode); // 'default', 'drawing', etc.\n * ```\n */\nexport function getMode(mapId: UniqueId): string {\n return modeStore.get(mapId).mode;\n}\n\n/**\n * Hook for current mode value.\n *\n * **Internal use only** - not exported from the public API.\n * Use `useMapMode` instead for better ergonomics and MapContext integration.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns The current active mode string\n */\nexport function useMode(mapId: UniqueId): string {\n return modeStore.useSelector(mapId, (state) => state.mode);\n}\n\n/**\n * Get the owner of the current mode for a given map instance.\n *\n * @internal - For internal map-toolkit use only\n * @param instanceId - Unique identifier for the map instance\n * @returns The owner ID of the current mode, or undefined if unowned\n */\nexport function getCurrentModeOwner(instanceId: UniqueId): string | undefined {\n const state = modeStore.get(instanceId);\n return state.modeOwners.get(state.mode);\n}\n\n/**\n * Check if a given owner is registered as the owner of any mode.\n * This includes both active mode owners and pending mode requests.\n *\n * @internal - For internal map-toolkit use only\n * @param instanceId - Unique identifier for the map instance\n * @param owner - The owner ID to check\n * @returns True if the owner is registered for any mode\n */\nexport function isRegisteredModeOwner(\n instanceId: UniqueId,\n owner: string,\n): boolean {\n const state = modeStore.get(instanceId);\n\n // Check active mode owners\n for (const modeOwner of state.modeOwners.values()) {\n if (modeOwner === owner) {\n return true;\n }\n }\n\n // Check pending mode requests (owner is the key in pendingRequests map)\n if (state.pendingRequests.has(owner)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Manually clear map mode state for a specific map instance.\n *\n * Removes all mode ownership data, pending requests, and resets to default state.\n * This is typically not needed as cleanup happens automatically.\n * Use only in advanced scenarios where manual cleanup is required.\n *\n * @param instanceId - Unique identifier for the map instance to clear\n * @returns void\n *\n * @example\n * ```typescript\n * import { clearMapModeState } from '@accelint/map-toolkit/map-mode';\n *\n * // Manual cleanup when destroying a map\n * clearMapModeState(mapId);\n * ```\n */\nexport function clearMapModeState(instanceId: UniqueId): void {\n modeStore.clear(instanceId);\n}\n"],"mappings":"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"}
|
|
@@ -16,6 +16,11 @@ import { UniqueId } from "@accelint/core";
|
|
|
16
16
|
/**
|
|
17
17
|
* Create a new Map with an entry added or updated (immutable).
|
|
18
18
|
*
|
|
19
|
+
* @param map - The Map to copy
|
|
20
|
+
* @param key - The key to set
|
|
21
|
+
* @param value - The value to set
|
|
22
|
+
* @returns A new Map with the entry added/updated
|
|
23
|
+
*
|
|
19
24
|
* @example
|
|
20
25
|
* ```ts
|
|
21
26
|
* const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');
|
|
@@ -26,6 +31,10 @@ declare function mapSet<K, V>(map: Map<K, V>, key: K, value: V): Map<K, V>;
|
|
|
26
31
|
/**
|
|
27
32
|
* Create a new Map with an entry removed (immutable).
|
|
28
33
|
*
|
|
34
|
+
* @param map - The Map to copy
|
|
35
|
+
* @param key - The key to remove
|
|
36
|
+
* @returns A new Map with the entry removed
|
|
37
|
+
*
|
|
29
38
|
* @example
|
|
30
39
|
* ```ts
|
|
31
40
|
* const newMap = mapDelete(state.cursorOwners, 'draw-layer');
|
|
@@ -36,6 +45,8 @@ declare function mapDelete<K, V>(map: Map<K, V>, key: K): Map<K, V>;
|
|
|
36
45
|
/**
|
|
37
46
|
* Create a new empty Map (immutable replacement for Map.clear()).
|
|
38
47
|
*
|
|
48
|
+
* @returns A new empty Map
|
|
49
|
+
*
|
|
39
50
|
* @example
|
|
40
51
|
* ```ts
|
|
41
52
|
* set({ pendingRequests: mapClear<string, PendingRequest>() });
|
|
@@ -166,6 +177,9 @@ type MapStore<TState, TActions> = {
|
|
|
166
177
|
/**
|
|
167
178
|
* Creates a store for managing state across multiple map instances.
|
|
168
179
|
*
|
|
180
|
+
* @param config - Store configuration including default state, actions, and optional bus setup
|
|
181
|
+
* @returns A MapStore instance with hooks and methods for accessing/updating state
|
|
182
|
+
*
|
|
169
183
|
* @example
|
|
170
184
|
* ```ts
|
|
171
185
|
* const cursorStore = createMapStore({
|
|
@@ -17,6 +17,11 @@ import { useRef, useSyncExternalStore } from "react";
|
|
|
17
17
|
/**
|
|
18
18
|
* Create a new Map with an entry added or updated (immutable).
|
|
19
19
|
*
|
|
20
|
+
* @param map - The Map to copy
|
|
21
|
+
* @param key - The key to set
|
|
22
|
+
* @param value - The value to set
|
|
23
|
+
* @returns A new Map with the entry added/updated
|
|
24
|
+
*
|
|
20
25
|
* @example
|
|
21
26
|
* ```ts
|
|
22
27
|
* const newMap = mapSet(state.cursorOwners, 'draw-layer', 'crosshair');
|
|
@@ -31,6 +36,10 @@ function mapSet(map, key, value) {
|
|
|
31
36
|
/**
|
|
32
37
|
* Create a new Map with an entry removed (immutable).
|
|
33
38
|
*
|
|
39
|
+
* @param map - The Map to copy
|
|
40
|
+
* @param key - The key to remove
|
|
41
|
+
* @returns A new Map with the entry removed
|
|
42
|
+
*
|
|
34
43
|
* @example
|
|
35
44
|
* ```ts
|
|
36
45
|
* const newMap = mapDelete(state.cursorOwners, 'draw-layer');
|
|
@@ -45,6 +54,8 @@ function mapDelete(map, key) {
|
|
|
45
54
|
/**
|
|
46
55
|
* Create a new empty Map (immutable replacement for Map.clear()).
|
|
47
56
|
*
|
|
57
|
+
* @returns A new empty Map
|
|
58
|
+
*
|
|
48
59
|
* @example
|
|
49
60
|
* ```ts
|
|
50
61
|
* set({ pendingRequests: mapClear<string, PendingRequest>() });
|
|
@@ -56,6 +67,9 @@ function mapClear() {
|
|
|
56
67
|
/**
|
|
57
68
|
* Creates a store for managing state across multiple map instances.
|
|
58
69
|
*
|
|
70
|
+
* @param config - Store configuration including default state, actions, and optional bus setup
|
|
71
|
+
* @returns A MapStore instance with hooks and methods for accessing/updating state
|
|
72
|
+
*
|
|
59
73
|
* @example
|
|
60
74
|
* ```ts
|
|
61
75
|
* const cursorStore = createMapStore({
|
|
@@ -131,7 +145,10 @@ function createMapStore(config) {
|
|
|
131
145
|
return instance.actions;
|
|
132
146
|
}
|
|
133
147
|
/**
|
|
134
|
-
* Clean up instance when last subscriber unmounts
|
|
148
|
+
* Clean up instance when last subscriber unmounts.
|
|
149
|
+
*
|
|
150
|
+
* @param mapId - Unique identifier for the map instance
|
|
151
|
+
* @param instance - The instance to clean up
|
|
135
152
|
*/
|
|
136
153
|
function cleanupInstance(mapId, instance) {
|
|
137
154
|
if (onCleanup) onCleanup(mapId, instance.state);
|
|
@@ -170,7 +187,10 @@ function createMapStore(config) {
|
|
|
170
187
|
return defaultState;
|
|
171
188
|
}
|
|
172
189
|
/**
|
|
173
|
-
* Main hook - returns state and actions
|
|
190
|
+
* Main hook - returns state and actions.
|
|
191
|
+
*
|
|
192
|
+
* @param mapId - Unique identifier for the map instance
|
|
193
|
+
* @returns Object containing state and all actions
|
|
174
194
|
*/
|
|
175
195
|
function use(mapId) {
|
|
176
196
|
return {
|
|
@@ -184,6 +204,10 @@ function createMapStore(config) {
|
|
|
184
204
|
* Note: The selector function is intentionally NOT tracked as a dependency.
|
|
185
205
|
* This prevents infinite re-render loops when using inline arrow functions.
|
|
186
206
|
* If you need dynamic selector behavior, use the `use()` hook with `useMemo`.
|
|
207
|
+
*
|
|
208
|
+
* @param mapId - Unique identifier for the map instance
|
|
209
|
+
* @param selector - Function to select derived state
|
|
210
|
+
* @returns The selected value
|
|
187
211
|
*/
|
|
188
212
|
function useSelector(mapId, selector) {
|
|
189
213
|
const cache = useRef(null);
|
|
@@ -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 * @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 * @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 * @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 * @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 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 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 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 * 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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"}
|
package/dist/shared/units.d.ts
CHANGED
|
@@ -24,14 +24,38 @@ type DistanceUnitAbbreviation = (typeof DISTANCE_UNIT_ABBREVIATIONS)[DistanceUni
|
|
|
24
24
|
declare const DEFAULT_DISTANCE_UNITS: DistanceUnit;
|
|
25
25
|
/**
|
|
26
26
|
* Get the full Turf.js unit name from an abbreviation.
|
|
27
|
+
*
|
|
27
28
|
* @param abbrev - The abbreviation (e.g., 'km', 'nm')
|
|
28
29
|
* @returns The full unit name (e.g., 'kilometers', 'nauticalmiles') or undefined
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { getDistanceUnitFromAbbreviation } from '@accelint/map-toolkit/shared';
|
|
34
|
+
*
|
|
35
|
+
* const unit = getDistanceUnitFromAbbreviation('km');
|
|
36
|
+
* console.log(unit); // 'kilometers'
|
|
37
|
+
*
|
|
38
|
+
* const unknown = getDistanceUnitFromAbbreviation('invalid');
|
|
39
|
+
* console.log(unknown); // undefined
|
|
40
|
+
* ```
|
|
29
41
|
*/
|
|
30
42
|
declare function getDistanceUnitFromAbbreviation(abbrev: string): DistanceUnit | undefined;
|
|
31
43
|
/**
|
|
32
44
|
* Get the abbreviation for a Turf.js unit name.
|
|
45
|
+
*
|
|
33
46
|
* @param unit - The full unit name (e.g., 'kilometers')
|
|
34
47
|
* @returns The abbreviation (e.g., 'km') or the input if not found
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* import { getDistanceUnitAbbreviation } from '@accelint/map-toolkit/shared';
|
|
52
|
+
*
|
|
53
|
+
* const abbrev = getDistanceUnitAbbreviation('kilometers');
|
|
54
|
+
* console.log(abbrev); // 'km'
|
|
55
|
+
*
|
|
56
|
+
* const fallback = getDistanceUnitAbbreviation('unknown');
|
|
57
|
+
* console.log(fallback); // 'unknown'
|
|
58
|
+
* ```
|
|
35
59
|
*/
|
|
36
60
|
declare function getDistanceUnitAbbreviation(unit: string): string;
|
|
37
61
|
//#endregion
|
package/dist/shared/units.js
CHANGED
|
@@ -29,16 +29,40 @@ const DISTANCE_UNIT_ABBREVIATIONS = {
|
|
|
29
29
|
const DEFAULT_DISTANCE_UNITS = "kilometers";
|
|
30
30
|
/**
|
|
31
31
|
* Get the full Turf.js unit name from an abbreviation.
|
|
32
|
+
*
|
|
32
33
|
* @param abbrev - The abbreviation (e.g., 'km', 'nm')
|
|
33
34
|
* @returns The full unit name (e.g., 'kilometers', 'nauticalmiles') or undefined
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { getDistanceUnitFromAbbreviation } from '@accelint/map-toolkit/shared';
|
|
39
|
+
*
|
|
40
|
+
* const unit = getDistanceUnitFromAbbreviation('km');
|
|
41
|
+
* console.log(unit); // 'kilometers'
|
|
42
|
+
*
|
|
43
|
+
* const unknown = getDistanceUnitFromAbbreviation('invalid');
|
|
44
|
+
* console.log(unknown); // undefined
|
|
45
|
+
* ```
|
|
34
46
|
*/
|
|
35
47
|
function getDistanceUnitFromAbbreviation(abbrev) {
|
|
36
48
|
return Object.entries(DISTANCE_UNIT_ABBREVIATIONS).find(([, a]) => a === abbrev)?.[0];
|
|
37
49
|
}
|
|
38
50
|
/**
|
|
39
51
|
* Get the abbreviation for a Turf.js unit name.
|
|
52
|
+
*
|
|
40
53
|
* @param unit - The full unit name (e.g., 'kilometers')
|
|
41
54
|
* @returns The abbreviation (e.g., 'km') or the input if not found
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { getDistanceUnitAbbreviation } from '@accelint/map-toolkit/shared';
|
|
59
|
+
*
|
|
60
|
+
* const abbrev = getDistanceUnitAbbreviation('kilometers');
|
|
61
|
+
* console.log(abbrev); // 'km'
|
|
62
|
+
*
|
|
63
|
+
* const fallback = getDistanceUnitAbbreviation('unknown');
|
|
64
|
+
* console.log(fallback); // 'unknown'
|
|
65
|
+
* ```
|
|
42
66
|
*/
|
|
43
67
|
function getDistanceUnitAbbreviation(unit) {
|
|
44
68
|
return DISTANCE_UNIT_ABBREVIATIONS[unit] ?? unit;
|
package/dist/shared/units.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"units.js","names":["DEFAULT_DISTANCE_UNITS: DistanceUnit"],"sources":["../../src/shared/units.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\n/**\n * Map of Turf.js unit names to display abbreviations.\n * These are the practical units users would select for map measurements.\n */\nexport const DISTANCE_UNIT_ABBREVIATIONS = {\n kilometers: 'km',\n meters: 'm',\n nauticalmiles: 'nm',\n miles: 'mi',\n feet: 'ft',\n} as const;\n\n/**\n * Turf.js unit name (e.g., 'kilometers', 'nauticalmiles').\n */\nexport type DistanceUnit = keyof typeof DISTANCE_UNIT_ABBREVIATIONS;\n\n/**\n * Display abbreviation (e.g., 'km', 'nm').\n */\nexport type DistanceUnitAbbreviation =\n (typeof DISTANCE_UNIT_ABBREVIATIONS)[DistanceUnit];\n\n/**\n * Default distance units for geographic measurements.\n */\nexport const DEFAULT_DISTANCE_UNITS: DistanceUnit = 'kilometers';\n\n/**\n * Get the full Turf.js unit name from an abbreviation.\n * @param abbrev - The abbreviation (e.g., 'km', 'nm')\n * @returns The full unit name (e.g., 'kilometers', 'nauticalmiles') or undefined\n */\nexport function getDistanceUnitFromAbbreviation(\n abbrev: string,\n): DistanceUnit | undefined {\n const entry = Object.entries(DISTANCE_UNIT_ABBREVIATIONS).find(\n ([, a]) => a === abbrev,\n );\n return entry?.[0] as DistanceUnit | undefined;\n}\n\n/**\n * Get the abbreviation for a Turf.js unit name.\n * @param unit - The full unit name (e.g., 'kilometers')\n * @returns The abbreviation (e.g., 'km') or the input if not found\n */\nexport function getDistanceUnitAbbreviation(unit: string): string {\n return DISTANCE_UNIT_ABBREVIATIONS[unit as DistanceUnit] ?? unit;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,8BAA8B;CACzC,YAAY;CACZ,QAAQ;CACR,eAAe;CACf,OAAO;CACP,MAAM;CACP;;;;AAgBD,MAAaA,yBAAuC
|
|
1
|
+
{"version":3,"file":"units.js","names":["DEFAULT_DISTANCE_UNITS: DistanceUnit"],"sources":["../../src/shared/units.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\n/**\n * Map of Turf.js unit names to display abbreviations.\n * These are the practical units users would select for map measurements.\n */\nexport const DISTANCE_UNIT_ABBREVIATIONS = {\n kilometers: 'km',\n meters: 'm',\n nauticalmiles: 'nm',\n miles: 'mi',\n feet: 'ft',\n} as const;\n\n/**\n * Turf.js unit name (e.g., 'kilometers', 'nauticalmiles').\n */\nexport type DistanceUnit = keyof typeof DISTANCE_UNIT_ABBREVIATIONS;\n\n/**\n * Display abbreviation (e.g., 'km', 'nm').\n */\nexport type DistanceUnitAbbreviation =\n (typeof DISTANCE_UNIT_ABBREVIATIONS)[DistanceUnit];\n\n/**\n * Default distance units for geographic measurements.\n */\nexport const DEFAULT_DISTANCE_UNITS: DistanceUnit = 'kilometers';\n\n/**\n * Get the full Turf.js unit name from an abbreviation.\n *\n * @param abbrev - The abbreviation (e.g., 'km', 'nm')\n * @returns The full unit name (e.g., 'kilometers', 'nauticalmiles') or undefined\n *\n * @example\n * ```typescript\n * import { getDistanceUnitFromAbbreviation } from '@accelint/map-toolkit/shared';\n *\n * const unit = getDistanceUnitFromAbbreviation('km');\n * console.log(unit); // 'kilometers'\n *\n * const unknown = getDistanceUnitFromAbbreviation('invalid');\n * console.log(unknown); // undefined\n * ```\n */\nexport function getDistanceUnitFromAbbreviation(\n abbrev: string,\n): DistanceUnit | undefined {\n const entry = Object.entries(DISTANCE_UNIT_ABBREVIATIONS).find(\n ([, a]) => a === abbrev,\n );\n return entry?.[0] as DistanceUnit | undefined;\n}\n\n/**\n * Get the abbreviation for a Turf.js unit name.\n *\n * @param unit - The full unit name (e.g., 'kilometers')\n * @returns The abbreviation (e.g., 'km') or the input if not found\n *\n * @example\n * ```typescript\n * import { getDistanceUnitAbbreviation } from '@accelint/map-toolkit/shared';\n *\n * const abbrev = getDistanceUnitAbbreviation('kilometers');\n * console.log(abbrev); // 'km'\n *\n * const fallback = getDistanceUnitAbbreviation('unknown');\n * console.log(fallback); // 'unknown'\n * ```\n */\nexport function getDistanceUnitAbbreviation(unit: string): string {\n return DISTANCE_UNIT_ABBREVIATIONS[unit as DistanceUnit] ?? unit;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,8BAA8B;CACzC,YAAY;CACZ,QAAQ;CACR,eAAe;CACf,OAAO;CACP,MAAM;CACP;;;;AAgBD,MAAaA,yBAAuC;;;;;;;;;;;;;;;;;;AAmBpD,SAAgB,gCACd,QAC0B;AAI1B,QAHc,OAAO,QAAQ,4BAA4B,CAAC,MACvD,GAAG,OAAO,MAAM,OAClB,GACc;;;;;;;;;;;;;;;;;;;AAoBjB,SAAgB,4BAA4B,MAAsB;AAChE,QAAO,4BAA4B,SAAyB"}
|
package/dist/viewport/store.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ declare function getViewport(mapId: UniqueId): ViewportState;
|
|
|
62
62
|
* This is typically not needed as cleanup happens automatically when all subscribers unmount.
|
|
63
63
|
*
|
|
64
64
|
* @param mapId - The unique identifier for the map instance to clear
|
|
65
|
+
* @returns void
|
|
65
66
|
*/
|
|
66
67
|
declare function clearViewportState(mapId: UniqueId): void;
|
|
67
68
|
//#endregion
|
package/dist/viewport/store.js
CHANGED
|
@@ -40,6 +40,9 @@ const bus = Broadcast.getInstance();
|
|
|
40
40
|
/**
|
|
41
41
|
* Create default state for a given mapId.
|
|
42
42
|
* Returns uninitialized values that indicate no viewport data yet.
|
|
43
|
+
*
|
|
44
|
+
* @param mapId - Unique identifier for the map instance
|
|
45
|
+
* @returns Uninitialized viewport state with NaN values and undefined bounds
|
|
43
46
|
*/
|
|
44
47
|
function createDefaultState(mapId) {
|
|
45
48
|
return {
|
|
@@ -115,6 +118,7 @@ function getViewport(mapId) {
|
|
|
115
118
|
* This is typically not needed as cleanup happens automatically when all subscribers unmount.
|
|
116
119
|
*
|
|
117
120
|
* @param mapId - The unique identifier for the map instance to clear
|
|
121
|
+
* @returns void
|
|
118
122
|
*/
|
|
119
123
|
function clearViewportState(mapId) {
|
|
120
124
|
viewportStore.clear(mapId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":[],"sources":["../../src/viewport/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\n/**\n * Viewport Store\n *\n * Manages viewport state (bounds, lat/lon, zoom, dimensions) per map instance.\n * State is updated automatically from MapEvents.viewport bus events.\n *\n * @example\n * ```tsx\n * import { viewportStore } from '@accelint/map-toolkit/viewport';\n *\n * function MapInfo({ mapId }) {\n * const { state } = viewportStore.use(mapId);\n * return (\n * <div>\n * Lat: {state.latitude.toFixed(2)}, Lon: {state.longitude.toFixed(2)}\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapEvents } from '../deckgl/base-map/events';\nimport { createMapStore } from '../shared/create-map-store';\nimport type { UniqueId } from '@accelint/core';\nimport type {\n MapEventType,\n MapViewportPayload,\n} from '../deckgl/base-map/types';\n\nconst bus = Broadcast.getInstance<MapEventType>();\n\n/**\n * State shape for viewport - mirrors MapViewportPayload but with\n * a placeholder id that gets replaced per-instance\n */\ntype ViewportState = MapViewportPayload;\n\n/**\n * No actions needed - viewport state is read-only from bus events\n */\ntype ViewportActions = Record<string, never>;\n\n/**\n * Create default state for a given mapId.\n * Returns uninitialized values that indicate no viewport data yet.\n */\nfunction createDefaultState(mapId: UniqueId): ViewportState {\n return {\n id: mapId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n };\n}\n\n/**\n * Viewport store instance.\n *\n * Note: The defaultState uses a placeholder id that gets replaced\n * when instances are created. The actual id comes from the bus event payload.\n */\nexport const viewportStore = createMapStore<ViewportState, ViewportActions>({\n // Placeholder default state - actual instances get proper id from bus events\n defaultState: {\n id: '' as UniqueId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n },\n\n // No actions - state is read-only from bus events\n actions: () => ({}) as ViewportActions,\n\n bus: (mapId, { replace }) => {\n const unsub = bus.on(MapEvents.viewport, ({ payload }) => {\n if (payload.id === mapId) {\n // Replace entire state with the new payload\n replace(payload);\n }\n });\n\n return unsub;\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Hook to subscribe to viewport state changes for a specific map.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state including bounds, latitude, longitude, zoom, width, height\n *\n * @example\n * ```tsx\n * function MapInfo({ mapId }) {\n * const viewport = useMapViewport(mapId);\n * return (\n * <div>\n * Lat: {viewport.latitude?.toFixed(2)}, Lon: {viewport.longitude?.toFixed(2)}, Zoom: {viewport.zoom}\n * </div>\n * );\n * }\n * ```\n */\nexport function useMapViewport(mapId: UniqueId): ViewportState {\n return viewportStore.useSelector(mapId, (state) => {\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n });\n}\n\n/**\n * Get current viewport state (non-reactive, for imperative code).\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state\n */\nexport function getViewport(mapId: UniqueId): ViewportState {\n const state = viewportStore.get(mapId);\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n}\n\n/**\n * Manually clear viewport state for a specific map instance.\n * This is typically not needed as cleanup happens automatically when all subscribers unmount.\n *\n * @param mapId - The unique identifier for the map instance to clear\n */\nexport function clearViewportState(mapId: UniqueId): void {\n viewportStore.clear(mapId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,MAAM,UAAU,aAA2B
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../src/viewport/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\n/**\n * Viewport Store\n *\n * Manages viewport state (bounds, lat/lon, zoom, dimensions) per map instance.\n * State is updated automatically from MapEvents.viewport bus events.\n *\n * @example\n * ```tsx\n * import { viewportStore } from '@accelint/map-toolkit/viewport';\n *\n * function MapInfo({ mapId }) {\n * const { state } = viewportStore.use(mapId);\n * return (\n * <div>\n * Lat: {state.latitude.toFixed(2)}, Lon: {state.longitude.toFixed(2)}\n * </div>\n * );\n * }\n * ```\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { MapEvents } from '../deckgl/base-map/events';\nimport { createMapStore } from '../shared/create-map-store';\nimport type { UniqueId } from '@accelint/core';\nimport type {\n MapEventType,\n MapViewportPayload,\n} from '../deckgl/base-map/types';\n\nconst bus = Broadcast.getInstance<MapEventType>();\n\n/**\n * State shape for viewport - mirrors MapViewportPayload but with\n * a placeholder id that gets replaced per-instance\n */\ntype ViewportState = MapViewportPayload;\n\n/**\n * No actions needed - viewport state is read-only from bus events\n */\ntype ViewportActions = Record<string, never>;\n\n/**\n * Create default state for a given mapId.\n * Returns uninitialized values that indicate no viewport data yet.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Uninitialized viewport state with NaN values and undefined bounds\n */\nfunction createDefaultState(mapId: UniqueId): ViewportState {\n return {\n id: mapId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n };\n}\n\n/**\n * Viewport store instance.\n *\n * Note: The defaultState uses a placeholder id that gets replaced\n * when instances are created. The actual id comes from the bus event payload.\n */\nexport const viewportStore = createMapStore<ViewportState, ViewportActions>({\n // Placeholder default state - actual instances get proper id from bus events\n defaultState: {\n id: '' as UniqueId,\n bounds: undefined,\n latitude: Number.NaN,\n longitude: Number.NaN,\n zoom: Number.NaN,\n width: 0,\n height: 0,\n },\n\n // No actions - state is read-only from bus events\n actions: () => ({}) as ViewportActions,\n\n bus: (mapId, { replace }) => {\n const unsub = bus.on(MapEvents.viewport, ({ payload }) => {\n if (payload.id === mapId) {\n // Replace entire state with the new payload\n replace(payload);\n }\n });\n\n return unsub;\n },\n});\n\n// =============================================================================\n// Convenience exports\n// =============================================================================\n\n/**\n * Hook to subscribe to viewport state changes for a specific map.\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state including bounds, latitude, longitude, zoom, width, height\n *\n * @example\n * ```tsx\n * function MapInfo({ mapId }) {\n * const viewport = useMapViewport(mapId);\n * return (\n * <div>\n * Lat: {viewport.latitude?.toFixed(2)}, Lon: {viewport.longitude?.toFixed(2)}, Zoom: {viewport.zoom}\n * </div>\n * );\n * }\n * ```\n */\nexport function useMapViewport(mapId: UniqueId): ViewportState {\n return viewportStore.useSelector(mapId, (state) => {\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n });\n}\n\n/**\n * Get current viewport state (non-reactive, for imperative code).\n *\n * @param mapId - Unique identifier for the map instance\n * @returns Current viewport state\n */\nexport function getViewport(mapId: UniqueId): ViewportState {\n const state = viewportStore.get(mapId);\n // Return proper default state with correct mapId if uninitialized\n if (state.id === '') {\n return createDefaultState(mapId);\n }\n return state;\n}\n\n/**\n * Manually clear viewport state for a specific map instance.\n * This is typically not needed as cleanup happens automatically when all subscribers unmount.\n *\n * @param mapId - The unique identifier for the map instance to clear\n * @returns void\n */\nexport function clearViewportState(mapId: UniqueId): void {\n viewportStore.clear(mapId);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,MAAM,UAAU,aAA2B;;;;;;;;AAoBjD,SAAS,mBAAmB,OAAgC;AAC1D,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,UAAU;EACV,WAAW;EACX,MAAM;EACN,OAAO;EACP,QAAQ;EACT;;;;;;;;AASH,MAAa,gBAAgB,eAA+C;CAE1E,cAAc;EACZ,IAAI;EACJ,QAAQ;EACR,UAAU;EACV,WAAW;EACX,MAAM;EACN,OAAO;EACP,QAAQ;EACT;CAGD,gBAAgB,EAAE;CAElB,MAAM,OAAO,EAAE,cAAc;AAQ3B,SAPc,IAAI,GAAG,UAAU,WAAW,EAAE,cAAc;AACxD,OAAI,QAAQ,OAAO,MAEjB,SAAQ,QAAQ;IAElB;;CAIL,CAAC;;;;;;;;;;;;;;;;;;;AAwBF,SAAgB,eAAe,OAAgC;AAC7D,QAAO,cAAc,YAAY,QAAQ,UAAU;AAEjD,MAAI,MAAM,OAAO,GACf,QAAO,mBAAmB,MAAM;AAElC,SAAO;GACP;;;;;;;;AASJ,SAAgB,YAAY,OAAgC;CAC1D,MAAM,QAAQ,cAAc,IAAI,MAAM;AAEtC,KAAI,MAAM,OAAO,GACf,QAAO,mBAAmB,MAAM;AAElC,QAAO;;;;;;;;;AAUT,SAAgB,mBAAmB,OAAuB;AACxD,eAAc,MAAM,MAAM"}
|
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.3.0",
|
|
5
5
|
"author": "https://hypergiant.com",
|
|
6
6
|
"$schema": "https://json.schemastore.org/package",
|
|
7
7
|
"devDependencies": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@accelint/bus": "3.0.2",
|
|
47
47
|
"@accelint/core": "0.5.2",
|
|
48
48
|
"@accelint/design-foundation": "2.1.0",
|
|
49
|
-
"@accelint/design-toolkit": "9.
|
|
49
|
+
"@accelint/design-toolkit": "9.5.0",
|
|
50
50
|
"@accelint/geo": "0.5.1",
|
|
51
51
|
"@accelint/logger": "0.1.5",
|
|
52
52
|
"@accelint/postcss-tailwind-css-modules": "1.0.1",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"milsymbol": "^3.0.2",
|
|
152
152
|
"mjolnir.js": "^3.0.0",
|
|
153
153
|
"react-map-gl": "^8.1.0",
|
|
154
|
-
"@accelint/hotkey-manager": "1.0.
|
|
154
|
+
"@accelint/hotkey-manager": "1.0.1"
|
|
155
155
|
},
|
|
156
156
|
"owner": "default/pathfinder",
|
|
157
157
|
"peerDependencies": {
|