@defra/interactive-map 0.0.15-alpha → 0.0.16-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.
Files changed (176) hide show
  1. package/assets/css/docusaurus.css +104 -0
  2. package/assets/images/favicon.svg +1 -0
  3. package/assets/images/hero.png +0 -0
  4. package/dist/esm/im-core.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/slot-map.svg +1 -0
  8. package/docs/api/slots.md +89 -6
  9. package/docs/api.md +1 -1
  10. package/docs/architecture.md +3 -1
  11. package/docs/{demo.mdx → examples.mdx} +1 -1
  12. package/docs/getting-started.md +1 -3
  13. package/docs/index.mdx +42 -0
  14. package/docs/plugins/interact.md +176 -55
  15. package/docs/plugins/map-styles.md +64 -7
  16. package/docs/plugins/search.md +207 -63
  17. package/docs/plugins.md +7 -15
  18. package/docusaurus.config.cjs +34 -34
  19. package/jest.setup.js +1 -1
  20. package/package.json +5 -4
  21. package/plugins/beta/datasets/src/DatasetsInit.jsx +1 -1
  22. package/plugins/beta/datasets/src/api/addDataset.js +1 -1
  23. package/plugins/beta/datasets/src/api/hideDataset.js +1 -1
  24. package/plugins/beta/datasets/src/api/hideFeatures.js +1 -1
  25. package/plugins/beta/datasets/src/api/removeDataset.js +1 -1
  26. package/plugins/beta/datasets/src/api/showDataset.js +1 -1
  27. package/plugins/beta/datasets/src/api/showFeatures.js +1 -1
  28. package/plugins/beta/datasets/src/datasets.js +4 -4
  29. package/plugins/beta/datasets/src/defaults.js +1 -1
  30. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +5 -5
  31. package/plugins/beta/datasets/src/handleSetMapStyle.js +1 -1
  32. package/plugins/beta/datasets/src/manifest.js +3 -3
  33. package/plugins/beta/datasets/src/mapLayers.js +2 -3
  34. package/plugins/beta/datasets/src/panels/Key.jsx +31 -29
  35. package/plugins/beta/datasets/src/panels/Layers.jsx +8 -9
  36. package/plugins/beta/datasets/src/utils/bbox.js +4 -4
  37. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  38. package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
  39. package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
  40. package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
  41. package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
  42. package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
  43. package/plugins/beta/draw-es/src/events.js +52 -20
  44. package/plugins/beta/draw-es/src/events.test.js +301 -0
  45. package/plugins/beta/draw-es/src/graphic.js +1 -1
  46. package/plugins/beta/draw-es/src/manifest.js +4 -4
  47. package/plugins/beta/draw-es/src/reducer.js +1 -1
  48. package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
  49. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  50. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  51. package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
  52. package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
  53. package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
  54. package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
  55. package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
  56. package/plugins/beta/draw-ml/src/api/split.js +4 -4
  57. package/plugins/beta/draw-ml/src/defaults.js +1 -1
  58. package/plugins/beta/draw-ml/src/events.js +8 -6
  59. package/plugins/beta/draw-ml/src/manifest.js +15 -15
  60. package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
  61. package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
  62. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
  63. package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
  64. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
  65. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
  66. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
  67. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
  68. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
  69. package/plugins/beta/draw-ml/src/reducer.js +1 -1
  70. package/plugins/beta/draw-ml/src/undoStack.js +4 -4
  71. package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
  72. package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
  73. package/plugins/beta/frame/src/Frame.jsx +4 -4
  74. package/plugins/beta/frame/src/FrameInit.jsx +4 -4
  75. package/plugins/beta/frame/src/api/addFrame.js +1 -1
  76. package/plugins/beta/frame/src/api/editFeature.js +1 -1
  77. package/plugins/beta/frame/src/config.js +1 -1
  78. package/plugins/beta/frame/src/manifest.js +3 -3
  79. package/plugins/beta/frame/src/reducer.js +1 -1
  80. package/plugins/beta/frame/src/utils.js +1 -1
  81. package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
  82. package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
  83. package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
  84. package/plugins/beta/use-location/src/defaults.js +1 -1
  85. package/plugins/beta/use-location/src/events.js +3 -3
  86. package/plugins/interact/src/InteractInit.jsx +1 -2
  87. package/plugins/interact/src/api/enable.js +8 -5
  88. package/plugins/interact/src/api/enable.test.js +2 -2
  89. package/plugins/interact/src/api/selectFeature.js +4 -4
  90. package/plugins/interact/src/api/unselectFeature.js +5 -5
  91. package/plugins/interact/src/defaults.js +0 -1
  92. package/plugins/interact/src/events.test.js +15 -15
  93. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  94. package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
  95. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
  96. package/plugins/interact/src/manifest.js +2 -2
  97. package/plugins/interact/src/manifest.test.js +3 -4
  98. package/plugins/interact/src/reducer.js +3 -3
  99. package/plugins/interact/src/reducer.test.js +0 -1
  100. package/plugins/interact/src/utils/spatial.js +10 -10
  101. package/plugins/interact/src/utils/spatial.test.js +14 -14
  102. package/plugins/search/dist/css/index.css +1 -1
  103. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  104. package/plugins/search/dist/esm/index.js +1 -1
  105. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  106. package/plugins/search/dist/umd/index.js +1 -1
  107. package/plugins/search/src/Search.jsx +7 -6
  108. package/plugins/search/src/Search.test.jsx +23 -23
  109. package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
  110. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
  111. package/plugins/search/src/components/Form/Form.jsx +14 -14
  112. package/plugins/search/src/components/Form/Form.test.jsx +11 -11
  113. package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
  114. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
  115. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
  116. package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
  117. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
  118. package/plugins/search/src/datasets.js +12 -13
  119. package/plugins/search/src/datasets.test.js +1 -1
  120. package/plugins/search/src/defaults.js +1 -1
  121. package/plugins/search/src/events/fetchSuggestions.js +3 -3
  122. package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
  123. package/plugins/search/src/events/formHandlers.js +3 -3
  124. package/plugins/search/src/events/formHandlers.test.js +1 -1
  125. package/plugins/search/src/events/index.js +2 -2
  126. package/plugins/search/src/events/index.test.js +2 -2
  127. package/plugins/search/src/events/inputHandlers.js +4 -4
  128. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  129. package/plugins/search/src/events/suggestionHandlers.js +2 -2
  130. package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
  131. package/plugins/search/src/index.js +2 -1
  132. package/plugins/search/src/index.test.js +3 -3
  133. package/plugins/search/src/manifest.js +6 -4
  134. package/plugins/search/src/reducer.js +1 -2
  135. package/plugins/search/src/reducer.test.js +2 -2
  136. package/plugins/search/src/search.scss +10 -3
  137. package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
  138. package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
  139. package/plugins/search/src/utils/updateMap.js +1 -1
  140. package/plugins/search/src/utils/updateMap.test.js +5 -5
  141. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  142. package/providers/beta/esri/src/esriProvider.js +5 -5
  143. package/providers/beta/esri/src/utils/coords.js +1 -1
  144. package/providers/beta/esri/src/utils/esriFixes.js +1 -1
  145. package/providers/beta/esri/src/utils/query.js +4 -4
  146. package/providers/beta/esri/src/utils/spatial.js +1 -2
  147. package/providers/beta/esri/src/utils/spatial.test.js +4 -1
  148. package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
  149. package/providers/maplibre/src/appEvents.test.js +1 -1
  150. package/providers/maplibre/src/index.js +1 -1
  151. package/providers/maplibre/src/index.test.js +3 -5
  152. package/providers/maplibre/src/mapEvents.test.js +15 -5
  153. package/providers/maplibre/src/maplibreProvider.test.js +6 -2
  154. package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
  155. package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
  156. package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
  157. package/providers/maplibre/src/utils/highlightFeatures.js +2 -2
  158. package/providers/maplibre/src/utils/highlightFeatures.test.js +12 -6
  159. package/providers/maplibre/src/utils/labels.js +19 -20
  160. package/providers/maplibre/src/utils/labels.test.js +15 -13
  161. package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
  162. package/providers/maplibre/src/utils/queryFeatures.js +6 -6
  163. package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
  164. package/providers/maplibre/src/utils/spatial.js +0 -1
  165. package/providers/maplibre/src/utils/spatial.test.js +26 -27
  166. package/src/App/registry/pluginRegistry.js +17 -0
  167. package/src/App/registry/pluginRegistry.test.js +33 -0
  168. package/src/App/renderer/mapButtons.js +3 -2
  169. package/src/App/store/appDispatchMiddleware.js +33 -1
  170. package/src/App/store/appDispatchMiddleware.test.js +250 -222
  171. package/src/config/appConfig.js +2 -2
  172. package/src/utils/logger.js +6 -0
  173. package/src/utils/logger.test.js +32 -0
  174. package/webpack.dev.mjs +22 -18
  175. package/docs/govuk-prototype.md +0 -23
  176. package/docs/index.md +0 -19
@@ -30,7 +30,7 @@ const isPointInPolygon = (point, ring) => {
30
30
  const intersectX = ((xj - xi) * (py - yi)) / (yj - yi) + xi
31
31
 
32
32
  if (px < intersectX) {
33
- inside = !inside;
33
+ inside = !inside
34
34
  }
35
35
  }
36
36
  return inside
@@ -43,7 +43,7 @@ const getMinDistToGeometry = (map, point, geometry) => {
43
43
  const { coordinates: coords, type } = geometry
44
44
  let minSqDist = Infinity
45
45
  const getScreenPt = (lngLat) => map.project(lngLat)
46
-
46
+
47
47
  const processLine = (lineCoords) => {
48
48
  for (let i = 0; i < lineCoords.length - 1; i++) {
49
49
  const d2 = distToSegmentSquared(point, getScreenPt(lineCoords[i]), getScreenPt(lineCoords[i + 1]))
@@ -52,7 +52,7 @@ const getMinDistToGeometry = (map, point, geometry) => {
52
52
  }
53
53
  }
54
54
  }
55
-
55
+
56
56
  if (type === 'Point') {
57
57
  const p = getScreenPt(coords)
58
58
  minSqDist = (point.x - p.x) ** 2 + (point.y - p.y) ** 2
@@ -117,7 +117,7 @@ export const queryFeatures = (map, point, options = {}) => {
117
117
  let score = 0
118
118
  const type = f.geometry.type
119
119
  const pixelDistSq = getMinDistToGeometry(map, point, f.geometry)
120
-
120
+
121
121
  // PRIORITY 1: LAYER ORDER
122
122
  const layerRank = layerStack.indexOf(f.layer.id)
123
123
  score += (layerRank * 1000000)
@@ -126,7 +126,7 @@ export const queryFeatures = (map, point, options = {}) => {
126
126
  if (type.includes('Polygon')) {
127
127
  const polys = type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates
128
128
  const isInside = polys.some((ring) => isPointInPolygon(clickPt, ring[0]))
129
-
129
+
130
130
  if (isInside === true) {
131
131
  // Massive boost for polygons if we are actually inside them
132
132
  score -= 500000 // NOSONAR - tolerance used only here
@@ -143,4 +143,4 @@ export const queryFeatures = (map, point, options = {}) => {
143
143
  })
144
144
  .sort((a, b) => a.score - b.score)
145
145
  .map(({ f }) => f)
146
- }
146
+ }
@@ -15,12 +15,12 @@ describe('queryFeatures coverage', () => {
15
15
  const cases = [
16
16
  { type: 'Point', coords: [0, 0], p: { x: 3, y: 4 } },
17
17
  { type: 'LineString', coords: [[0, 0], [10, 0]], p: { x: 5, y: 5 } }, // t=0.5
18
- { type: 'LineString', coords: [[0, 0], [0, 0]], p: { x: 1, y: 1 } }, // l2=0
18
+ { type: 'LineString', coords: [[0, 0], [0, 0]], p: { x: 1, y: 1 } }, // l2=0
19
19
  { type: 'LineString', coords: [[0, 0], [10, 0]], p: { x: -5, y: 0 } }, // t<0
20
20
  { type: 'MultiPoint', coords: [[0, 0], [10, 10]], p: { x: 1, y: 0 } },
21
21
  { type: 'MultiLineString', coords: [[[0, 0], [10, 0]]], p: { x: 5, y: 1 } },
22
22
  { type: 'Polygon', coords: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], p: { x: 5, y: 5 } }, // Inside
23
- { type: 'MultiPolygon', coords: [[[[0, 0], [10, 0], [10, 10], [0, 0]]]], p: { x: 20, y: 20 } }, // Outside
23
+ { type: 'MultiPolygon', coords: [[[[0, 0], [10, 0], [10, 10], [0, 0]]]], p: { x: 20, y: 20 } }, // Outside
24
24
  { type: 'Unknown', coords: [], p: { x: 0, y: 0 } }
25
25
  ]
26
26
 
@@ -31,21 +31,21 @@ describe('queryFeatures coverage', () => {
31
31
  })
32
32
 
33
33
  // 3. Hits Line 144 (.sort) and property-based ID fallback
34
- const f1 = {
35
- properties: { key: 'a' },
36
- layer: { id: 'layer-A' },
37
- geometry: { type: 'Point', coordinates: [10, 10] }
34
+ const f1 = {
35
+ properties: { key: 'a' },
36
+ layer: { id: 'layer-A' },
37
+ geometry: { type: 'Point', coordinates: [10, 10] }
38
38
  }
39
- const f2 = {
40
- id: 'b',
41
- layer: { id: 'layer-B' },
42
- geometry: { type: 'Point', coordinates: [0, 0] }
39
+ const f2 = {
40
+ id: 'b',
41
+ layer: { id: 'layer-B' },
42
+ geometry: { type: 'Point', coordinates: [0, 0] }
43
43
  }
44
-
44
+
45
45
  // map.queryRenderedFeatures returns multiple items to trigger .sort()
46
46
  const sortMap = { ...mockMap, queryRenderedFeatures: () => [f1, f2] }
47
47
  const result = queryFeatures(sortMap, { x: 0, y: 0 })
48
-
48
+
49
49
  expect(result.length).toBe(2)
50
50
  expect(result[0].layer.id).toBe('layer-A') // Sorted by layerStack index
51
51
 
@@ -57,4 +57,4 @@ describe('queryFeatures coverage', () => {
57
57
  const rayMap = { ...mockMap, queryRenderedFeatures: () => [polyFeat] }
58
58
  expect(queryFeatures(rayMap, { x: -1, y: 5 }).length).toBe(1)
59
59
  })
60
- })
60
+ })
@@ -186,7 +186,6 @@ const getPaddedBounds = (LngLatBounds, map) => {
186
186
  return new LngLatBounds(swLngLat, neLngLat)
187
187
  }
188
188
 
189
-
190
189
  /**
191
190
  * Get a flat bbox [west, south, east, north] from any GeoJSON object
192
191
  * (Feature, FeatureCollection, or geometry).
@@ -10,7 +10,6 @@ jest.mock('geodesy/latlon-spherical.js', () =>
10
10
  jest.mock('@turf/bbox', () => jest.fn(() => [-1, 50, 1, 52]))
11
11
 
12
12
  describe('spatial utils', () => {
13
-
14
13
  test('formatDimension hits all branches', () => {
15
14
  // < 0.5 miles
16
15
  expect(spatial.formatDimension(500)).toMatch(/m$/)
@@ -43,55 +42,55 @@ describe('spatial utils', () => {
43
42
  })
44
43
 
45
44
  test('north/south/east/west moves', () => {
46
- expect(spatial.getCardinalMove([0,0],[0,0.5])).toMatch(/north/)
47
- expect(spatial.getCardinalMove([0,0],[0,-0.5])).toMatch(/south/)
48
- expect(spatial.getCardinalMove([0,0],[0.5,0])).toMatch(/east/)
49
- expect(spatial.getCardinalMove([0,0],[-0.5,0])).toMatch(/west/)
50
- expect(spatial.getCardinalMove([0,0],[0.5,0.5])).toMatch(/north.*east|east.*north/)
51
- expect(spatial.getCardinalMove([0,0],[0.00001,0.00001])).toBe('')
45
+ expect(spatial.getCardinalMove([0, 0], [0, 0.5])).toMatch(/north/)
46
+ expect(spatial.getCardinalMove([0, 0], [0, -0.5])).toMatch(/south/)
47
+ expect(spatial.getCardinalMove([0, 0], [0.5, 0])).toMatch(/east/)
48
+ expect(spatial.getCardinalMove([0, 0], [-0.5, 0])).toMatch(/west/)
49
+ expect(spatial.getCardinalMove([0, 0], [0.5, 0.5])).toMatch(/north.*east|east.*north/)
50
+ expect(spatial.getCardinalMove([0, 0], [0.00001, 0.00001])).toBe('')
52
51
  })
53
52
 
54
53
  test('spatialNavigate all directions and fallback', () => {
55
- const pixels = [[0,0],[0,-1],[1,0],[0,1],[-1,0]]
56
- expect(spatial.spatialNavigate('ArrowUp',[0,0],pixels)).toBe(1)
57
- expect(spatial.spatialNavigate('ArrowDown',[0,0],pixels)).toBe(3)
58
- expect(spatial.spatialNavigate('ArrowLeft',[0,0],pixels)).toBe(4)
59
- expect(spatial.spatialNavigate('ArrowRight',[0,0],pixels)).toBe(2)
60
- expect(spatial.spatialNavigate('InvalidDir',[0,0],pixels)).toBe(0)
54
+ const pixels = [[0, 0], [0, -1], [1, 0], [0, 1], [-1, 0]]
55
+ expect(spatial.spatialNavigate('ArrowUp', [0, 0], pixels)).toBe(1)
56
+ expect(spatial.spatialNavigate('ArrowDown', [0, 0], pixels)).toBe(3)
57
+ expect(spatial.spatialNavigate('ArrowLeft', [0, 0], pixels)).toBe(4)
58
+ expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(2)
59
+ expect(spatial.spatialNavigate('InvalidDir', [0, 0], pixels)).toBe(0)
61
60
  })
62
61
 
63
62
  test('spatialNavigate finds closer candidates (hits dist < minDist)', () => {
64
- const start = [0,0]
65
- const pixels = [[0,0],[10,0],[2,0]]
63
+ const start = [0, 0]
64
+ const pixels = [[0, 0], [10, 0], [2, 0]]
66
65
  expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
67
66
  })
68
67
 
69
68
  test('spatialNavigate skips farther candidate (dist >= minDist false branch)', () => {
70
69
  // Closer candidate first → second candidate fails dist < minDist
71
- const pixels = [[0,0],[2,0],[10,0]]
72
- expect(spatial.spatialNavigate('ArrowRight', [0,0], pixels)).toBe(1)
70
+ const pixels = [[0, 0], [2, 0], [10, 0]]
71
+ expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(1)
73
72
  })
74
73
 
75
74
  test('spatialNavigate diagonal with dx>dy', () => {
76
- const start = [0,0]
77
- const pixels = [[0,0],[3,1],[1,0]] // dx>dy
75
+ const start = [0, 0]
76
+ const pixels = [[0, 0], [3, 1], [1, 0]] // dx>dy
78
77
  expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
79
78
  })
80
79
 
81
80
  test('getResolution returns positive value', () => {
82
- expect(spatial.getResolution({lat:0},1)).toBeGreaterThan(0)
81
+ expect(spatial.getResolution({ lat: 0 }, 1)).toBeGreaterThan(0)
83
82
  })
84
83
 
85
84
  test('getPaddedBounds returns bounds', () => {
86
85
  const map = {
87
- getContainer: () => ({ getBoundingClientRect: () => ({ width:100,height:200 }) }),
88
- getPadding: () => ({ top:1,right:2,bottom:3,left:4 }),
89
- unproject: p => ({ x:p[0], y:p[1] })
86
+ getContainer: () => ({ getBoundingClientRect: () => ({ width: 100, height: 200 }) }),
87
+ getPadding: () => ({ top: 1, right: 2, bottom: 3, left: 4 }),
88
+ unproject: p => ({ x: p[0], y: p[1] })
90
89
  }
91
- const LngLatBounds = function(sw,ne){
92
- return {sw,ne}
90
+ const LngLatBounds = function (sw, ne) {
91
+ return { sw, ne }
93
92
  }
94
- const bounds = spatial.getPaddedBounds(LngLatBounds,map)
93
+ const bounds = spatial.getPaddedBounds(LngLatBounds, map)
95
94
  expect(bounds.sw).toBeDefined()
96
95
  expect(bounds.ne).toBeDefined()
97
96
  })
@@ -140,4 +139,4 @@ describe('spatial utils', () => {
140
139
  expect(map.project).toHaveBeenCalledTimes(4)
141
140
  })
142
141
  })
143
- })
142
+ })
@@ -1,9 +1,23 @@
1
1
  // src/core/registry/pluginRegistry.js
2
2
  import { registerIcon } from './iconRegistry.js'
3
3
  import { registerKeyboardShortcut } from './keyboardShortcutRegistry.js'
4
+ import { allowedSlots } from '../renderer/slots.js'
5
+ import { logger } from '../../utils/logger.js'
4
6
 
5
7
  const asArray = (value) => Array.isArray(value) ? value : [value]
6
8
 
9
+ const BREAKPOINTS = ['mobile', 'tablet', 'desktop']
10
+
11
+ function validateSlots (item, type) {
12
+ const allowed = allowedSlots[type]
13
+ BREAKPOINTS.forEach(bp => {
14
+ const slot = item[bp]?.slot
15
+ if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button'))) {
16
+ logger.warn(`${type} "${item.id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowed.join(', ')}.`)
17
+ }
18
+ })
19
+ }
20
+
7
21
  export function createPluginRegistry ({ registerButton, registerPanel, registerControl }) {
8
22
  const registeredPlugins = []
9
23
 
@@ -18,6 +32,7 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
18
32
 
19
33
  if (manifest.buttons) {
20
34
  asArray(manifest.buttons).forEach(button => {
35
+ validateSlots(button, 'button')
21
36
  registerButton({ [button.id]: { ...pluginConfig, ...button } })
22
37
  // Flat button registry including any menu items (isMenuItem prevents slot rendering)
23
38
  button?.menuItems?.forEach(menuItem => {
@@ -28,12 +43,14 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
28
43
 
29
44
  if (manifest.panels) {
30
45
  asArray(manifest.panels).forEach(panel => {
46
+ validateSlots(panel, 'panel')
31
47
  registerPanel({ [panel.id]: { ...pluginConfig, ...panel } })
32
48
  })
33
49
  }
34
50
 
35
51
  if (manifest.controls) {
36
52
  asArray(manifest.controls).forEach(control => {
53
+ validateSlots(control, 'control')
37
54
  registerControl({ [control.id]: { ...pluginConfig, ...control } })
38
55
  })
39
56
  }
@@ -139,6 +139,39 @@ describe('pluginRegistry', () => {
139
139
  expect(pluginRegistry.registeredPlugins).toEqual([pluginA, pluginB])
140
140
  })
141
141
 
142
+ describe('slot validation', () => {
143
+ const INVALID_SLOT = 'invalid-slot'
144
+
145
+ beforeEach(() => jest.spyOn(console, 'warn').mockImplementation(() => {}))
146
+ afterEach(() => jest.restoreAllMocks())
147
+
148
+ it('warns when a manifest item has an invalid slot', () => {
149
+ const plugin = {
150
+ id: 'bad-plugin',
151
+ config: {},
152
+ manifest: {
153
+ buttons: [{ id: 'btn1', desktop: { slot: INVALID_SLOT } }],
154
+ panels: [{ id: 'panel1', desktop: { slot: INVALID_SLOT } }],
155
+ controls: [{ id: 'ctrl1', desktop: { slot: INVALID_SLOT } }]
156
+ }
157
+ }
158
+ pluginRegistry.registerPlugin(plugin)
159
+ expect(console.warn).toHaveBeenCalledWith('[interactive-map]', expect.stringContaining(INVALID_SLOT))
160
+ })
161
+
162
+ it('does not warn for a panel with a button-adjacent slot', () => {
163
+ const plugin = {
164
+ id: 'adj-plugin',
165
+ config: {},
166
+ manifest: {
167
+ panels: [{ id: 'panel1', desktop: { slot: 'left-top-button' } }]
168
+ }
169
+ }
170
+ pluginRegistry.registerPlugin(plugin)
171
+ expect(console.warn).not.toHaveBeenCalled()
172
+ })
173
+ })
174
+
142
175
  it('clears all registered plugins', () => {
143
176
  const pluginA = { id: 'A', config: {}, manifest: {} }
144
177
  const pluginB = { id: 'B', config: {}, manifest: {} }
@@ -1,6 +1,7 @@
1
1
  // src/core/renderers/mapButtons.js
2
2
  import { MapButton } from '../components/MapButton/MapButton.jsx'
3
3
  import { allowedSlots } from './slots.js'
4
+ import { logger } from '../../utils/logger.js'
4
5
 
5
6
  function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
6
7
  const { breakpoint, mode } = appState
@@ -163,7 +164,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
163
164
 
164
165
  /* istanbul ignore next */
165
166
  if (process.env.NODE_ENV !== 'production' && typeof group === 'string') {
166
- console.warn(`[interactive-map] Button "${buttonId}": group should be an object { name, label?, order? } — string groups are deprecated.`)
167
+ logger.warn(`Button "${buttonId}": group should be an object { name, label?, order? } — string groups are deprecated.`)
167
168
  }
168
169
 
169
170
  const name = resolveGroupName(group)
@@ -174,7 +175,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
174
175
  const existing = groupMap.get(name)
175
176
  /* istanbul ignore next */
176
177
  if (process.env.NODE_ENV !== 'production' && existing.order !== order) {
177
- console.warn(`[interactive-map] Group "${name}" has inconsistent order values (${existing.order} vs ${order}). Using the lower value.`)
178
+ logger.warn(`Group "${name}" has inconsistent order values (${existing.order} vs ${order}). Using the lower value.`)
178
179
  existing.order = Math.min(existing.order, order)
179
180
  }
180
181
  } else {
@@ -1,7 +1,11 @@
1
1
  // src/App/store/dispatchMiddleware.js
2
2
  import { EVENTS as events } from '../../config/events.js'
3
- import { defaultPanelConfig } from '../../config/appConfig.js'
3
+ import { defaultPanelConfig, defaultButtonConfig, defaultControlConfig } from '../../config/appConfig.js'
4
4
  import { deepMerge } from '../../utils/deepMerge.js'
5
+ import { allowedSlots } from '../renderer/slots.js'
6
+ import { logger } from '../../utils/logger.js'
7
+
8
+ const BREAKPOINTS = ['mobile', 'tablet', 'desktop']
5
9
 
6
10
  /**
7
11
  * Determines which panels were implicitly closed when opening a new panel
@@ -81,9 +85,37 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
81
85
  })
82
86
  }
83
87
 
88
+ if (type === 'ADD_BUTTON') {
89
+ const { id, config } = payload
90
+ const mergedConfig = deepMerge(defaultButtonConfig, config)
91
+ BREAKPOINTS.forEach(bp => {
92
+ const slot = mergedConfig[bp]?.slot
93
+ if (slot && !allowedSlots.button.includes(slot)) {
94
+ logger.warn(`button "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.button.join(', ')}.`)
95
+ }
96
+ })
97
+ }
98
+
99
+ if (type === 'ADD_CONTROL') {
100
+ const { id, config } = payload
101
+ const mergedConfig = deepMerge(defaultControlConfig, config)
102
+ BREAKPOINTS.forEach(bp => {
103
+ const slot = mergedConfig[bp]?.slot
104
+ if (slot && !allowedSlots.control.includes(slot)) {
105
+ logger.warn(`control "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.control.join(', ')}.`)
106
+ }
107
+ })
108
+ }
109
+
84
110
  if (type === 'ADD_PANEL') {
85
111
  const { id, config } = payload
86
112
  const mergedConfig = deepMerge(defaultPanelConfig, config)
113
+ BREAKPOINTS.forEach(bp => {
114
+ const slot = mergedConfig[bp]?.slot
115
+ if (slot && !allowedSlots.panel.includes(slot) && !slot.endsWith('-button')) {
116
+ logger.warn(`panel "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.panel.join(', ')}.`)
117
+ }
118
+ })
87
119
  const bpConfig = mergedConfig[previousState.breakpoint]
88
120
  if (bpConfig?.open) {
89
121
  queueMicrotask(() => {