@defra/interactive-map 0.0.17-alpha → 0.0.19-alpha
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/assets/css/docusaurus.css +58 -34
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/panel-definition.md +16 -0
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +50 -23
- package/docs/assets/basic-map.jpg +0 -0
- package/docs/assets/button-first.jpg +0 -0
- package/docs/assets/maker-panel.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +59 -0
- package/docs/examples/basic-map.mdx +24 -0
- package/docs/examples/button-map.mdx +24 -0
- package/docs/examples/index.mdx +49 -0
- package/docs/index.mdx +1 -1
- package/docs/plugins/datasets.md +105 -9
- package/docs/plugins/interact.md +100 -44
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/docusaurus.config.cjs +9 -1
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +32 -14
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +1 -1
- package/plugins/beta/datasets/src/api/setData.js +4 -2
- package/plugins/beta/datasets/src/api/setStyle.js +2 -2
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +13 -4
- package/plugins/beta/datasets/src/defaults.js +4 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +1 -1
- package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
- package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
- package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
- package/plugins/beta/datasets/src/reducer.js +6 -0
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +3 -0
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/src/scaleBar.scss +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +19 -8
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/clear.js +1 -1
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/api/selectMarker.js +14 -0
- package/plugins/interact/src/api/selectMarker.test.js +25 -0
- package/plugins/interact/src/api/unselectMarker.js +14 -0
- package/plugins/interact/src/api/unselectMarker.test.js +14 -0
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +27 -36
- package/plugins/interact/src/events.test.js +119 -90
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/manifest.js +10 -2
- package/plugins/interact/src/reducer.js +59 -5
- package/plugins/interact/src/reducer.test.js +100 -12
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- package/plugins/interact/src/utils/interactionModes.js +12 -0
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/Search.jsx +3 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +37 -0
- package/src/App/components/Viewport/Viewport.jsx +5 -15
- package/src/App/components/Viewport/Viewport.module.scss +2 -0
- package/src/App/components/Viewport/Viewport.test.jsx +16 -33
- package/src/App/hooks/useInterfaceAPI.js +7 -7
- package/src/App/hooks/useInterfaceAPI.test.js +162 -0
- package/src/App/hooks/useLayoutMeasurements.js +64 -72
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +3 -3
- package/src/App/layout/Layout.test.jsx +4 -2
- package/src/App/layout/layout.module.scss +1 -8
- package/src/App/renderer/HtmlElementHost.jsx +10 -5
- package/src/App/renderer/mapPanels.js +2 -1
- package/src/App/store/ServiceProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +4 -4
- package/src/App/store/appActionsMap.test.js +10 -0
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +59 -11
- package/src/InteractiveMap/InteractiveMap.test.js +126 -4
- package/src/InteractiveMap/domStateManager.js +18 -6
- package/src/InteractiveMap/domStateManager.test.js +21 -0
- package/src/InteractiveMap/historyManager.js +28 -16
- package/src/InteractiveMap/historyManager.test.js +17 -0
- package/src/config/appConfig.js +2 -7
- package/src/config/appConfig.test.js +4 -15
- package/src/config/defaults.js +2 -3
- package/src/config/events.js +20 -21
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/services/closeApp.js +1 -10
- package/src/services/closeApp.test.js +3 -43
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +99 -12
- package/src/utils/mapStateSync.js +48 -10
- package/src/utils/mapStateSync.test.js +29 -9
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/docs/examples.mdx +0 -70
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
- package/plugins/beta/datasets/src/styles/patterns.js +0 -157
|
@@ -69,6 +69,27 @@ describe('updateDOMState', () => {
|
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('isFullscreen override forces non-fullscreen even when URL param matches', () => {
|
|
73
|
+
mapInstance.config.behaviour = 'buttonFirst'
|
|
74
|
+
queryString.getQueryParam.mockReturnValue('map') // URL says fullscreen
|
|
75
|
+
|
|
76
|
+
updateDOMState(mapInstance, { isFullscreen: false }) // but override says no
|
|
77
|
+
|
|
78
|
+
expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(false)
|
|
79
|
+
expect(rootEl.style.height).toBe('auto')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('buttonFirst with manageHistoryState false is always fullscreen regardless of URL', () => {
|
|
83
|
+
mapInstance.config.behaviour = 'buttonFirst'
|
|
84
|
+
mapInstance.config.manageHistoryState = false
|
|
85
|
+
queryString.getQueryParam.mockReturnValue(null) // no mv param in URL
|
|
86
|
+
|
|
87
|
+
updateDOMState(mapInstance)
|
|
88
|
+
|
|
89
|
+
expect(document.documentElement.classList.contains('im-is-fullscreen')).toBe(true)
|
|
90
|
+
expect(mapInstance.rootEl.style.height).toBe('100%')
|
|
91
|
+
})
|
|
92
|
+
|
|
72
93
|
describe('hybrid behaviour', () => {
|
|
73
94
|
beforeEach(() => {
|
|
74
95
|
mapInstance.config.behaviour = 'hybrid'
|
|
@@ -40,6 +40,32 @@ function closeMap (mapInstance) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Syncs a single map instance against the current URL view parameter.
|
|
45
|
+
*
|
|
46
|
+
* @param {MapInstance} mapInstance
|
|
47
|
+
* @param {string|null} viewId - The current `mv` query param value.
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
function syncMapInstance (mapInstance, viewId) {
|
|
51
|
+
if (mapInstance.config.manageHistoryState === false) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const shouldBeOpen = mapInstance.id === viewId
|
|
56
|
+
const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
|
|
57
|
+
const isOpen = mapInstance.rootEl?.children.length
|
|
58
|
+
|
|
59
|
+
if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
|
|
60
|
+
openMap(mapInstance)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!shouldBeOpen && isOpen && !isHybridVisible) {
|
|
65
|
+
closeMap(mapInstance)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
/**
|
|
44
70
|
* Handles the `popstate` event triggered by browser back/forward navigation.
|
|
45
71
|
*
|
|
@@ -56,21 +82,7 @@ function closeMap (mapInstance) {
|
|
|
56
82
|
*/
|
|
57
83
|
function handlePopstate () {
|
|
58
84
|
const viewId = getQueryParam(defaults.mapViewParamKey)
|
|
59
|
-
|
|
60
|
-
for (const mapInstance of components.values()) {
|
|
61
|
-
const shouldBeOpen = mapInstance.id === viewId
|
|
62
|
-
const isHybridVisible = mapInstance.config.behaviour === 'hybrid' && !isHybridFullscreen(mapInstance.config)
|
|
63
|
-
const isOpen = mapInstance.rootEl?.children.length
|
|
64
|
-
|
|
65
|
-
if (shouldBeOpen && (!isOpen || mapInstance._isHidden)) {
|
|
66
|
-
openMap(mapInstance)
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!shouldBeOpen && isOpen && !isHybridVisible) {
|
|
71
|
-
closeMap(mapInstance)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
85
|
+
components.forEach(mapInstance => syncMapInstance(mapInstance, viewId))
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
// -----------------------------------------------------------------------------
|
|
@@ -105,7 +117,7 @@ let initialized = false
|
|
|
105
117
|
*/
|
|
106
118
|
function register (component) {
|
|
107
119
|
if (!initialized) {
|
|
108
|
-
|
|
120
|
+
globalThis.addEventListener('popstate', handlePopstate)
|
|
109
121
|
initialized = true
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -141,6 +141,23 @@ describe('historyManager', () => {
|
|
|
141
141
|
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 768px)')
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
it('skips component when manageHistoryState is false', () => {
|
|
145
|
+
const managedComponent = {
|
|
146
|
+
id: 'managed',
|
|
147
|
+
config: { behaviour: 'buttonFirst', hybridWidth: null, maxMobileWidth: 640, manageHistoryState: false },
|
|
148
|
+
rootEl: document.createElement('div'),
|
|
149
|
+
loadApp: jest.fn(),
|
|
150
|
+
removeApp: jest.fn(),
|
|
151
|
+
_isHidden: false
|
|
152
|
+
}
|
|
153
|
+
historyManager.register(managedComponent)
|
|
154
|
+
queryString.getQueryParam.mockReturnValue('managed')
|
|
155
|
+
|
|
156
|
+
window.dispatchEvent(popstateEvent)
|
|
157
|
+
|
|
158
|
+
expect(managedComponent.loadApp).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
144
161
|
it('unregisters component', () => {
|
|
145
162
|
historyManager.register(component1)
|
|
146
163
|
component1.rootEl.appendChild(document.createElement('div'))
|
package/src/config/appConfig.js
CHANGED
|
@@ -24,7 +24,7 @@ export const defaultAppConfig = {
|
|
|
24
24
|
label: 'Exit',
|
|
25
25
|
iconId: 'close',
|
|
26
26
|
onClick: (_e, { services }) => services.closeApp(),
|
|
27
|
-
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !
|
|
27
|
+
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !appState.isFullscreen,
|
|
28
28
|
mobile: exitButtonSlots,
|
|
29
29
|
tablet: exitButtonSlots,
|
|
30
30
|
desktop: exitButtonSlots
|
|
@@ -115,6 +115,7 @@ export const defaultButtonConfig = {
|
|
|
115
115
|
// Used by addPanel
|
|
116
116
|
export const defaultPanelConfig = {
|
|
117
117
|
label: 'Panel',
|
|
118
|
+
focus: true,
|
|
118
119
|
mobile: {
|
|
119
120
|
slot: 'drawer',
|
|
120
121
|
open: true,
|
|
@@ -159,9 +160,3 @@ export const scaleFactor = {
|
|
|
159
160
|
medium: 1.5,
|
|
160
161
|
large: 2
|
|
161
162
|
}
|
|
162
|
-
|
|
163
|
-
export const markerSvgPaths = [{
|
|
164
|
-
shape: 'pin',
|
|
165
|
-
backgroundPath: 'M31 16.001c0 7.489-8.308 15.289-11.098 17.698-.533.4-1.271.4-1.803 0C15.309 31.29 7 23.49 7 16.001c0-6.583 5.417-12 12-12s12 5.417 12 12z',
|
|
166
|
-
graphicPath: 'M19 11.001c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.241 5-5-2.24-5-5-5z'
|
|
167
|
-
}]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { render } from '@testing-library/react'
|
|
2
|
-
import { defaultAppConfig, defaultButtonConfig, scaleFactor
|
|
2
|
+
import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
|
|
3
3
|
|
|
4
4
|
describe('defaultAppConfig', () => {
|
|
5
5
|
const appState = {
|
|
@@ -23,28 +23,18 @@ describe('defaultAppConfig', () => {
|
|
|
23
23
|
|
|
24
24
|
// --- EXIT BUTTON (Line 27 Coverage) ---
|
|
25
25
|
it('covers all branches of exitBtn excludeWhen', () => {
|
|
26
|
-
const config = { hasExitButton: true, mapViewParamKey: 'view' }
|
|
27
|
-
|
|
28
26
|
expect(exitBtn.excludeWhen({
|
|
29
|
-
appConfig: {
|
|
27
|
+
appConfig: { hasExitButton: false },
|
|
30
28
|
appState: { isFullscreen: true }
|
|
31
29
|
})).toBe(true)
|
|
32
30
|
|
|
33
|
-
window.history.pushState({}, '', '?view=map')
|
|
34
31
|
expect(exitBtn.excludeWhen({
|
|
35
|
-
appConfig:
|
|
32
|
+
appConfig: { hasExitButton: true },
|
|
36
33
|
appState: { isFullscreen: false }
|
|
37
34
|
})).toBe(true)
|
|
38
35
|
|
|
39
|
-
window.history.pushState({}, '', '?wrong=param')
|
|
40
|
-
expect(exitBtn.excludeWhen({
|
|
41
|
-
appConfig: config,
|
|
42
|
-
appState: { isFullscreen: true }
|
|
43
|
-
})).toBe(true)
|
|
44
|
-
|
|
45
|
-
window.history.pushState({}, '', '?view=map')
|
|
46
36
|
expect(exitBtn.excludeWhen({
|
|
47
|
-
appConfig:
|
|
37
|
+
appConfig: { hasExitButton: true },
|
|
48
38
|
appState: { isFullscreen: true }
|
|
49
39
|
})).toBe(false)
|
|
50
40
|
})
|
|
@@ -135,6 +125,5 @@ describe('defaultAppConfig', () => {
|
|
|
135
125
|
it('exports supplementary configs and constants', () => {
|
|
136
126
|
expect(defaultButtonConfig.label).toBe('Button')
|
|
137
127
|
expect(scaleFactor.large).toBe(2)
|
|
138
|
-
expect(markerSvgPaths[0].shape).toBe('pin')
|
|
139
128
|
})
|
|
140
129
|
})
|
package/src/config/defaults.js
CHANGED
|
@@ -16,7 +16,7 @@ const defaults = {
|
|
|
16
16
|
containerHeight: '600px',
|
|
17
17
|
deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
|
|
18
18
|
enableFullscreen: false,
|
|
19
|
-
enableZoomControls:
|
|
19
|
+
enableZoomControls: true,
|
|
20
20
|
genericErrorText: 'There was a problem loading the map. Please try again later.',
|
|
21
21
|
hasExitButton: false,
|
|
22
22
|
hybridWidth: null, // Defaults to maxMobileWidth if not set
|
|
@@ -24,11 +24,10 @@ const defaults = {
|
|
|
24
24
|
mapLabel: 'Interactive map',
|
|
25
25
|
mapProvider: null,
|
|
26
26
|
mapSize: 'small',
|
|
27
|
+
manageHistoryState: true,
|
|
27
28
|
mapViewParamKey: 'mv',
|
|
28
29
|
maxMobileWidth: 640,
|
|
29
30
|
minDesktopWidth: 835,
|
|
30
|
-
markerColor: '#ff0000',
|
|
31
|
-
markerShape: 'pin',
|
|
32
31
|
nudgePanDelta: 5,
|
|
33
32
|
nudgeZoomDelta: 0.1,
|
|
34
33
|
panDelta: 100,
|
package/src/config/events.js
CHANGED
|
@@ -62,32 +62,34 @@ export const EVENTS = {
|
|
|
62
62
|
APP_READY: 'app:ready',
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Emitted when the map application
|
|
65
|
+
* Emitted when the map application has opened and is visible.
|
|
66
66
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* (e.g. resizing to mobile and opening the map).
|
|
67
|
+
* Fired after initial load (`loadApp`) and when the app is shown again after being hidden (`showApp`).
|
|
68
|
+
* Subscribe to this event to react whenever the map becomes visible to the user.
|
|
70
69
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* Payload: `{ statePreserved: boolean }` — `true` if the map state was preserved from a previous session.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* map.on(EVENTS.APP_OPENED, ({ statePreserved }) => {
|
|
74
|
+
* console.log('Map opened, state preserved:', statePreserved)
|
|
75
|
+
* })
|
|
75
76
|
*/
|
|
76
|
-
|
|
77
|
+
APP_OPENED: 'app:opened',
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Emitted when the map application
|
|
80
|
+
* Emitted when the map application has closed and is no longer visible.
|
|
81
|
+
*
|
|
82
|
+
* Fired when the app is hidden (`hideApp`) or removed (`removeApp`).
|
|
83
|
+
* Subscribe to this event to react whenever the map is closed.
|
|
80
84
|
*
|
|
81
|
-
*
|
|
82
|
-
* (e.g. visible on desktop) but then becomes hidden
|
|
83
|
-
* (e.g. resizing to mobile or closing the map view).
|
|
85
|
+
* Payload: `{ statePreserved: boolean }` — `true` if the map state was preserved (i.e. can be restored).
|
|
84
86
|
*
|
|
85
|
-
* @
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
87
|
+
* @example
|
|
88
|
+
* map.on(EVENTS.APP_CLOSED, ({ statePreserved }) => {
|
|
89
|
+
* console.log('Map closed, state preserved:', statePreserved)
|
|
90
|
+
* })
|
|
89
91
|
*/
|
|
90
|
-
|
|
92
|
+
APP_CLOSED: 'app:closed',
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* Emitted when a panel is opened.
|
|
@@ -202,9 +204,6 @@ export const EVENTS = {
|
|
|
202
204
|
*/
|
|
203
205
|
MAP_CLICK: 'map:click',
|
|
204
206
|
|
|
205
|
-
/** Emitted when the user exits the map (e.g., via the close button). */
|
|
206
|
-
MAP_EXIT: 'map:exit',
|
|
207
|
-
|
|
208
207
|
/** Emitted when the map is destroyed. Payload: { mapId: string } */
|
|
209
208
|
MAP_DESTROY: 'map:destroy'
|
|
210
209
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical colour values for light and dark map colour schemes.
|
|
3
|
+
* These are the single source of truth for map overlay colours —
|
|
4
|
+
* used both by the symbol SVG renderer (JS canvas) and injected as
|
|
5
|
+
* CSS custom properties onto the app container for CSS-rendered elements.
|
|
6
|
+
*
|
|
7
|
+
* Per-style overrides take precedence: set `haloColor`, `selectedColor`, or
|
|
8
|
+
* `foregroundColor` directly on a `MapStyleConfig` to override these defaults.
|
|
9
|
+
*/
|
|
10
|
+
export const SCHEME_COLORS = {
|
|
11
|
+
light: {
|
|
12
|
+
haloColor: '#ffffff',
|
|
13
|
+
selectedColor: '#0b0c0c',
|
|
14
|
+
foregroundColor: '#0b0c0c'
|
|
15
|
+
},
|
|
16
|
+
dark: {
|
|
17
|
+
haloColor: '#0b0c0c',
|
|
18
|
+
selectedColor: '#ffffff',
|
|
19
|
+
foregroundColor: '#ffffff'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the three map overlay colours for the given map style,
|
|
25
|
+
* falling back to the appropriate scheme defaults when not explicitly set.
|
|
26
|
+
* Stored in `mapState.mapTheme` so plugins can read resolved values without
|
|
27
|
+
* importing from core.
|
|
28
|
+
*
|
|
29
|
+
* @param {import('../types.js').MapStyleConfig|null} mapStyle
|
|
30
|
+
* @returns {{ haloColor: string, selectedColor: string, foregroundColor: string }}
|
|
31
|
+
*/
|
|
32
|
+
export const resolveMapTheme = (mapStyle) => {
|
|
33
|
+
const scheme = SCHEME_COLORS[mapStyle?.mapColorScheme] ?? SCHEME_COLORS.light
|
|
34
|
+
return {
|
|
35
|
+
haloColor: mapStyle?.haloColor ?? scheme.haloColor,
|
|
36
|
+
selectedColor: mapStyle?.selectedColor ?? scheme.selectedColor,
|
|
37
|
+
foregroundColor: mapStyle?.foregroundColor ?? scheme.foregroundColor
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Converts a mapStyle's overlay colours into CSS custom properties,
|
|
43
|
+
* falling back to the appropriate scheme defaults when not explicitly set.
|
|
44
|
+
* Suitable for spreading directly into a React `style` prop.
|
|
45
|
+
*
|
|
46
|
+
* @param {import('../types.js').MapStyleConfig|null} mapStyle
|
|
47
|
+
* @returns {Object} CSS custom property object
|
|
48
|
+
*/
|
|
49
|
+
export const getMapThemeVars = (mapStyle) => {
|
|
50
|
+
const { haloColor, selectedColor, foregroundColor } = resolveMapTheme(mapStyle)
|
|
51
|
+
return {
|
|
52
|
+
'--map-overlay-halo-color': haloColor,
|
|
53
|
+
'--map-overlay-selected-color': selectedColor,
|
|
54
|
+
'--map-overlay-foreground-color': foregroundColor
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in fill pattern SVG content.
|
|
3
|
+
* Each value is the inner SVG paths only (no wrapper element).
|
|
4
|
+
* Paths are authored in a 16×16 coordinate space and tile seamlessly.
|
|
5
|
+
* Use {{foregroundColor}} and {{backgroundColor}} tokens for colour injection.
|
|
6
|
+
*/
|
|
7
|
+
export const BUILT_IN_PATTERNS = {
|
|
8
|
+
'cross-hatch': '<path d="M0 4.486V3.485h3.5V.001h1v3.484h7.002V.001h1v3.484h3.5v1.001h-3.5v7h3.5v.999h-3.5v3.516h-1v-3.516H4.499v3.516h-1v-3.516H0v-.999h3.5v-7H0zm11.501 0H4.499v7h7.002v-7z" fill="{{foregroundColor}}"/>',
|
|
9
|
+
'diagonal-cross-hatch': '<path d="M0 8.707V7.293L7.293 0h1.414L16 7.293v1.414L8.707 16H7.293L0 8.707zM.707 8L8 15.293 15.293 8 8 .707.707 8z" fill="{{foregroundColor}}"/>',
|
|
10
|
+
'forward-diagonal-hatch': '<path d="M16 8.707V7.293L7.293 16h1.414L16 8.707zm-16 0L8.707 0H7.293L0 7.293v1.414z" fill="{{foregroundColor}}"/>',
|
|
11
|
+
'backward-diagonal-hatch': '<path d="M0 8.707V7.293L8.707 16H7.293L0 8.707zm16 0L7.293 0h1.414L16 7.293v1.414z" fill="{{foregroundColor}}"/>',
|
|
12
|
+
'horizontal-hatch': '<path d="M0 4.5V3.499h15.999V4.5H0zm0 7h15.999V12.5H0v-1.001z" fill="{{foregroundColor}}"/>',
|
|
13
|
+
'vertical-hatch': '<path d="M3.501 16.001V0h1v16.001h-1zm7.998 0V0h1v16.001h-1z" fill="{{foregroundColor}}"/>',
|
|
14
|
+
dot: '<path d="M3.999 2A2 2 0 0 1 6 3.999C6 5.103 5.103 6 3.999 6a2 2 0 0 1-1.999-2.001A2 2 0 0 1 3.999 2zm0 7.999C5.103 10 6 10.897 6 12.001A2 2 0 0 1 3.999 14a2 2 0 0 1-1.999-1.999A2 2 0 0 1 3.999 10zM11.999 2A2 2 0 0 1 14 3.999C14 5.103 13.103 6 11.999 6S10 5.103 10 3.999A2 2 0 0 1 11.999 2zm0 7.999c1.104 0 2.001.897 2.001 2.001A2 2 0 0 1 11.999 14 2 2 0 0 1 10 12.001c0-1.104.897-2.001 1.999-2.001z" fill="{{foregroundColor}}"/>',
|
|
15
|
+
diamond: '<path d="M4 .465L7.535 4 4 7.535.465 4 4 .465zm0 7.999l3.535 3.535L4 15.535.465 11.999 4 8.464zm8-8l3.535 3.535-3.536 3.536L8.464 4 12 .464zm0 8.001L15.536 12 12 15.536 8.465 12 12 8.465z" fill="{{foregroundColor}}"/>'
|
|
16
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default token values applied to all symbols unless overridden at the constructor,
|
|
3
|
+
* symbol registration, or marker creation level.
|
|
4
|
+
* Colour values may be a plain string or a map-style-keyed object,
|
|
5
|
+
* e.g. { outdoor: '#ffffff', dark: '#0b0c0c' }
|
|
6
|
+
*/
|
|
7
|
+
export const symbolDefaults = {
|
|
8
|
+
symbol: 'pin',
|
|
9
|
+
backgroundColor: '#ca3535',
|
|
10
|
+
foregroundColor: '#ffffff',
|
|
11
|
+
haloWidth: '1',
|
|
12
|
+
selectedWidth: '6'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Built-in graphic path data strings for use with the `graphic` token.
|
|
17
|
+
*
|
|
18
|
+
* Each value is an SVG `d` attribute string in a 16×16 coordinate space,
|
|
19
|
+
* centred at (8, 8). The built-in symbols (`pin`, `circle`, `square`) apply a
|
|
20
|
+
* `translate` transform to position this 16×16 area correctly within their
|
|
21
|
+
* 38×38 viewBox — so graphic path data does not need to account for symbol
|
|
22
|
+
* positioning.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* markers.add('id', coords, { symbol: 'pin', graphic: graphics.dot })
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Inline path data (16×16 space, centred at 8,8)
|
|
29
|
+
* markers.add('id', coords, { symbol: 'pin', graphic: 'M3 8 L8 3 L13 8 L8 13 Z' })
|
|
30
|
+
*/
|
|
31
|
+
export const graphics = {
|
|
32
|
+
/** Small filled circle — the default graphic for built-in symbols */
|
|
33
|
+
dot: 'M8 3c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.241 5-5-2.24-5-5-5z',
|
|
34
|
+
|
|
35
|
+
/** Filled plus / cross shape */
|
|
36
|
+
cross: 'M6 3H10V6H13V10H10V13H6V10H3V6H6Z',
|
|
37
|
+
|
|
38
|
+
/** Filled diamond / rotated square */
|
|
39
|
+
diamond: 'M8 2L14 8L8 14L2 8Z',
|
|
40
|
+
|
|
41
|
+
/** Filled upward-pointing triangle */
|
|
42
|
+
triangle: 'M8 2L14 14H2Z',
|
|
43
|
+
|
|
44
|
+
/** Filled square */
|
|
45
|
+
square: 'M3 3H13V13H3Z'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Built-in symbol definitions ─────────────────────────────────────────────
|
|
49
|
+
// Each symbol uses a 38×38 viewBox. SVG templates use {{token}} placeholders
|
|
50
|
+
// resolved at render time by the symbolRegistry.
|
|
51
|
+
|
|
52
|
+
export const pin = {
|
|
53
|
+
id: 'pin',
|
|
54
|
+
viewBox: '0 0 38 38',
|
|
55
|
+
anchor: [0.5, 0.9], // NOSONAR
|
|
56
|
+
graphic: graphics.dot,
|
|
57
|
+
svg: `<path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
|
|
58
|
+
<path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
|
|
59
|
+
<g transform="translate(19, 16) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const circle = {
|
|
63
|
+
id: 'circle',
|
|
64
|
+
viewBox: '0 0 38 38',
|
|
65
|
+
anchor: [0.5, 0.5],
|
|
66
|
+
graphic: graphics.dot,
|
|
67
|
+
svg: `<path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
|
|
68
|
+
<path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
|
|
69
|
+
<g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const square = {
|
|
73
|
+
id: 'square',
|
|
74
|
+
viewBox: '0 0 38 38',
|
|
75
|
+
anchor: [0.5, 0.5],
|
|
76
|
+
graphic: graphics.dot,
|
|
77
|
+
svg: `<path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
|
|
78
|
+
<path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
|
|
79
|
+
<g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
80
|
+
}
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
--attributions-foreground-color: #0b0c0c;
|
|
36
36
|
--attributions-background-color: #ffffff80;
|
|
37
37
|
|
|
38
|
-
// Map overlays, such as scale bar, target marker etc
|
|
39
|
-
--map-overlay-foreground-color: #0b0c0c;
|
|
40
|
-
--map-overlay-halo-color: #ffffff;
|
|
41
|
-
|
|
42
38
|
// Fixed colours (Dont chnage with dark mode)
|
|
43
39
|
--fixed-foreground-color: #0b0c0c;
|
|
44
40
|
|
|
@@ -78,8 +74,3 @@
|
|
|
78
74
|
--content-link-color: #ffffff;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
// Map color scheme specific overrides
|
|
82
|
-
:root .im-o-app--dark-map {
|
|
83
|
-
--map-overlay-foreground-color: #ffffff;
|
|
84
|
-
--map-overlay-halo-color: #0b0c0c;
|
|
85
|
-
}
|
package/src/services/closeApp.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
// src/services/closeApp.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function closeApp (mapId, handleExitClick, eventBus) {
|
|
5
|
-
eventBus.emit(events.MAP_EXIT, { mapId })
|
|
6
|
-
|
|
7
|
-
if (history.state?.isBack) {
|
|
8
|
-
history.back()
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
2
|
+
export function closeApp (_mapId, handleExitClick) {
|
|
12
3
|
handleExitClick()
|
|
13
4
|
}
|
|
@@ -1,49 +1,9 @@
|
|
|
1
1
|
import { closeApp } from './closeApp'
|
|
2
2
|
|
|
3
3
|
describe('closeApp', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
handleExitClickMock = jest.fn()
|
|
9
|
-
mockEventBus = {
|
|
10
|
-
emit: jest.fn(),
|
|
11
|
-
on: jest.fn(),
|
|
12
|
-
off: jest.fn()
|
|
13
|
-
}
|
|
14
|
-
jest.spyOn(history, 'back').mockImplementation(() => {})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
jest.restoreAllMocks()
|
|
19
|
-
jest.clearAllMocks()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('calls history.back() when history.state.isBack is true', () => {
|
|
23
|
-
Object.defineProperty(history, 'state', {
|
|
24
|
-
value: { isBack: true },
|
|
25
|
-
writable: true,
|
|
26
|
-
configurable: true
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
closeApp('map-123', handleExitClickMock, mockEventBus)
|
|
30
|
-
|
|
31
|
-
expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
|
|
32
|
-
expect(history.back).toHaveBeenCalled()
|
|
33
|
-
expect(handleExitClickMock).not.toHaveBeenCalled()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('calls handleExitClick when history.state.isBack is not true', () => {
|
|
37
|
-
Object.defineProperty(history, 'state', {
|
|
38
|
-
value: null,
|
|
39
|
-
writable: true,
|
|
40
|
-
configurable: true
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
closeApp('map-123', handleExitClickMock, mockEventBus)
|
|
44
|
-
|
|
45
|
-
expect(mockEventBus.emit).toHaveBeenCalledWith('map:exit', { mapId: 'map-123' })
|
|
46
|
-
expect(history.back).not.toHaveBeenCalled()
|
|
4
|
+
it('calls handleExitClick', () => {
|
|
5
|
+
const handleExitClickMock = jest.fn()
|
|
6
|
+
closeApp('map-123', handleExitClickMock)
|
|
47
7
|
expect(handleExitClickMock).toHaveBeenCalled()
|
|
48
8
|
})
|
|
49
9
|
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BUILT_IN_PATTERNS } from '../config/patternConfig.js'
|
|
2
|
+
|
|
3
|
+
const patterns = new Map()
|
|
4
|
+
|
|
5
|
+
export const patternRegistry = {
|
|
6
|
+
/**
|
|
7
|
+
* Register a named pattern.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} id - Unique pattern name (e.g. 'my-hatch')
|
|
10
|
+
* @param {string} svgContent - Inner SVG path content in a 16×16 coordinate space.
|
|
11
|
+
* Use {{foregroundColor}} and {{backgroundColor}} tokens for colour injection.
|
|
12
|
+
*/
|
|
13
|
+
register (id, svgContent) {
|
|
14
|
+
patterns.set(id, { id, svgContent })
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieve a registered pattern by name.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} id
|
|
21
|
+
* @returns {{ id: string, svgContent: string }|undefined}
|
|
22
|
+
*/
|
|
23
|
+
get (id) {
|
|
24
|
+
return patterns.get(id)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns all registered patterns.
|
|
29
|
+
*
|
|
30
|
+
* @returns {{ id: string, svgContent: string }[]}
|
|
31
|
+
*/
|
|
32
|
+
list () {
|
|
33
|
+
return [...patterns.values()]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Seed built-in patterns
|
|
38
|
+
Object.entries(BUILT_IN_PATTERNS).forEach(([id, svgContent]) => {
|
|
39
|
+
patternRegistry.register(id, svgContent)
|
|
40
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { patternRegistry } from './patternRegistry.js'
|
|
2
|
+
|
|
3
|
+
describe('patternRegistry', () => {
|
|
4
|
+
describe('built-in patterns', () => {
|
|
5
|
+
test.each([
|
|
6
|
+
'cross-hatch',
|
|
7
|
+
'diagonal-cross-hatch',
|
|
8
|
+
'forward-diagonal-hatch',
|
|
9
|
+
'backward-diagonal-hatch',
|
|
10
|
+
'horizontal-hatch',
|
|
11
|
+
'vertical-hatch',
|
|
12
|
+
'dot',
|
|
13
|
+
'diamond'
|
|
14
|
+
])('seeds built-in pattern: %s', (id) => {
|
|
15
|
+
const pattern = patternRegistry.get(id)
|
|
16
|
+
expect(pattern).toBeDefined()
|
|
17
|
+
expect(pattern.id).toBe(id)
|
|
18
|
+
expect(typeof pattern.svgContent).toBe('string')
|
|
19
|
+
expect(pattern.svgContent.length).toBeGreaterThan(0)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('register / get', () => {
|
|
24
|
+
test('registers a custom pattern and retrieves it by id', () => {
|
|
25
|
+
patternRegistry.register('test-hatch', '<path d="M0 0L8 8" stroke="{{foreground}}"/>')
|
|
26
|
+
const result = patternRegistry.get('test-hatch')
|
|
27
|
+
expect(result).toEqual({ id: 'test-hatch', svgContent: '<path d="M0 0L8 8" stroke="{{foreground}}"/>' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('returns undefined for an unregistered id', () => {
|
|
31
|
+
expect(patternRegistry.get('nonexistent-pattern')).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('overwrite an existing pattern by re-registering the same id', () => {
|
|
35
|
+
patternRegistry.register('overwrite-test', '<path d="M0 0"/>')
|
|
36
|
+
patternRegistry.register('overwrite-test', '<path d="M1 1"/>')
|
|
37
|
+
expect(patternRegistry.get('overwrite-test').svgContent).toBe('<path d="M1 1"/>')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('list', () => {
|
|
42
|
+
test('returns all registered patterns including built-ins', () => {
|
|
43
|
+
const all = patternRegistry.list()
|
|
44
|
+
expect(all.length).toBeGreaterThanOrEqual(8)
|
|
45
|
+
expect(all.every(p => p.id && p.svgContent)).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|