@defra/interactive-map 0.0.40-alpha → 0.0.41-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 (351) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/assets/images/slot-map.svg +61 -262
  9. package/docs/examples/draw-tools.mdx +3 -3
  10. package/docs/plugins/draw.md +79 -16
  11. package/docs/plugins/search.md +1 -1
  12. package/govuk-prototype-kit.config.json +2 -0
  13. package/jest.setup.js +5 -0
  14. package/package.json +9 -1
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  19. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  21. package/plugins/datasets/dist/css/index.css +1 -1
  22. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  23. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  24. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  25. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  26. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  28. package/plugins/datasets/dist/umd/index.js +1 -1
  29. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  30. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  31. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  32. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  33. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  34. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  35. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  36. package/plugins/datasets/src/datasets.scss +0 -1
  37. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  38. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  39. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  40. package/plugins/datasets/src/index.test.js +36 -0
  41. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  42. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  43. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  44. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  45. package/plugins/datasets/src/manifest.js +0 -38
  46. package/plugins/datasets/src/manifest.test.js +49 -0
  47. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  48. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  49. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  50. package/plugins/datasets/src/registry/dataset.js +13 -1
  51. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  52. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  53. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  54. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  55. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  56. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  57. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  58. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  59. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  60. package/plugins/draw/dist/esm/index.js +1 -1
  61. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  62. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  63. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  64. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  65. package/plugins/draw/dist/umd/index.js +1 -1
  66. package/plugins/draw/src/DrawInit.jsx +10 -2
  67. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  68. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  69. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  70. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  71. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  72. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  74. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  75. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  91. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  92. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  93. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  94. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  95. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  96. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  97. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  98. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  101. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  102. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  103. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  104. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  105. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  106. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  107. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  108. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  109. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  110. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  111. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  112. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  113. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  114. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  115. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  116. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  117. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  121. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  122. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  123. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  124. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  125. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  126. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  127. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  128. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  129. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  130. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  132. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  134. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  136. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  139. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  140. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  141. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  142. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  143. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  144. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  145. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  146. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  147. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  148. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  149. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  150. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  151. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  154. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  155. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  156. package/plugins/draw/src/api/addFeature.js +16 -1
  157. package/plugins/draw/src/api/addFeature.test.js +62 -5
  158. package/plugins/draw/src/api/editFeature.js +7 -4
  159. package/plugins/draw/src/api/editFeature.test.js +16 -0
  160. package/plugins/draw/src/api/newPoint.js +42 -0
  161. package/plugins/draw/src/api/newPoint.test.js +117 -0
  162. package/plugins/draw/src/api/setStyle.js +43 -0
  163. package/plugins/draw/src/api/setStyle.test.js +75 -0
  164. package/plugins/draw/src/events.js +17 -10
  165. package/plugins/draw/src/events.test.js +14 -0
  166. package/plugins/draw/src/manifest.js +18 -6
  167. package/plugins/draw/src/manifest.test.js +32 -1
  168. package/plugins/draw/src/reducer.test.js +7 -0
  169. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  170. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  171. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  172. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  173. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  174. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  175. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  176. package/plugins/interact/dist/umd/index.js +1 -1
  177. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  178. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  179. package/plugins/interact/src/utils/featureQueries.js +18 -0
  180. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  181. package/plugins/interact/src/utils/spatial.js +9 -3
  182. package/plugins/interact/src/utils/spatial.test.js +12 -1
  183. package/plugins/map-key/dist/css/index.css +62 -0
  184. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  185. package/plugins/map-key/dist/esm/index.js +1 -0
  186. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  187. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  188. package/plugins/map-key/dist/umd/index.js +2 -0
  189. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  190. package/plugins/map-key/package.json +28 -0
  191. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  192. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  193. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  194. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  195. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  196. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  197. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  198. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  199. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  200. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  201. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  202. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  203. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  204. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  205. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  206. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  208. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  210. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  211. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  212. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  213. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  214. package/plugins/map-key/src/index.js +13 -0
  215. package/plugins/map-key/src/index.test.js +31 -0
  216. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  217. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  218. package/plugins/map-key/src/manifest.js +30 -0
  219. package/plugins/map-key/src/manifest.test.js +63 -0
  220. package/plugins/map-key/src/mapKey.scss +4 -0
  221. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  222. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  223. package/plugins/map-key/src/registry/index.js +3 -0
  224. package/plugins/map-key/src/registry/index.test.js +15 -0
  225. package/plugins/menu/dist/css/index.css +80 -0
  226. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  227. package/plugins/menu/dist/esm/index.js +1 -0
  228. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  229. package/plugins/menu/dist/umd/index.js +2 -0
  230. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  231. package/plugins/menu/package.json +28 -0
  232. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  233. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  234. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  235. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  236. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  237. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  238. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  239. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  240. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  241. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  242. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  243. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  244. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  245. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  246. package/plugins/menu/src/index.js +13 -0
  247. package/plugins/menu/src/index.test.js +31 -0
  248. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  249. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  250. package/plugins/menu/src/initialise/createMenu.js +10 -0
  251. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  252. package/plugins/menu/src/manifest.js +35 -0
  253. package/plugins/menu/src/manifest.test.js +63 -0
  254. package/plugins/menu/src/menu.scss +4 -0
  255. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  256. package/plugins/menu/src/reducers/pluginState.js +35 -0
  257. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  258. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  259. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  260. package/plugins/menu/src/registry/index.js +3 -0
  261. package/plugins/menu/src/registry/index.test.js +15 -0
  262. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  263. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  264. package/plugins/search/dist/css/index.css +1 -1
  265. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  266. package/plugins/search/dist/esm/index.js +1 -1
  267. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  268. package/plugins/search/dist/umd/index.js +1 -1
  269. package/plugins/search/src/Search.jsx +2 -3
  270. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  271. package/plugins/search/src/index.js +0 -1
  272. package/plugins/search/src/index.test.js +2 -4
  273. package/plugins/search/src/manifest.js +1 -1
  274. package/plugins/search/src/search.scss +17 -16
  275. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  276. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  277. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  278. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  279. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  280. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  281. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  282. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  283. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  284. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  285. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  286. package/providers/maplibre/dist/umd/index.js +1 -1
  287. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  288. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  289. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  290. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  291. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  292. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  293. package/providers/maplibre/src/utils/patternImages.js +1 -1
  294. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  295. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  296. package/rollup.esm.mjs +11 -1
  297. package/src/App/components/Actions/Actions.test.jsx +47 -0
  298. package/src/App/components/Hints/Hints.test.jsx +13 -0
  299. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  300. package/src/App/components/Panel/Panel.jsx +18 -3
  301. package/src/App/components/Panel/Panel.module.scss +18 -8
  302. package/src/App/components/Panel/Panel.test.jsx +34 -0
  303. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  304. package/src/App/hooks/useFeatureItems.js +16 -7
  305. package/src/App/hooks/useFeatureItems.test.js +57 -0
  306. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  307. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  308. package/src/App/layout/Layout.jsx +9 -10
  309. package/src/App/layout/Layout.test.jsx +2 -0
  310. package/src/App/layout/layout.module.scss +31 -17
  311. package/src/App/registry/pluginRegistry.js +1 -1
  312. package/src/App/registry/pluginRegistry.test.js +12 -0
  313. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  314. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  315. package/src/App/renderer/mapButtons.js +28 -12
  316. package/src/App/renderer/mapButtons.test.js +17 -4
  317. package/src/App/renderer/mapControls.js +3 -1
  318. package/src/App/renderer/mapControls.test.js +16 -0
  319. package/src/App/renderer/mapPanels.js +44 -9
  320. package/src/App/renderer/mapPanels.test.js +51 -2
  321. package/src/App/renderer/orderItems.js +28 -0
  322. package/src/App/renderer/orderItems.test.js +50 -0
  323. package/src/App/renderer/slotAggregator.js +2 -18
  324. package/src/App/renderer/slotHelpers.js +3 -1
  325. package/src/App/renderer/slotHelpers.test.js +5 -0
  326. package/src/App/renderer/slots.js +3 -0
  327. package/src/App/store/AppProvider.jsx +1 -0
  328. package/src/App/store/appDispatchMiddleware.js +1 -1
  329. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  330. package/src/config/events.js +2 -0
  331. package/src/scss/settings/_colors.scss +1 -0
  332. package/src/scss/settings/_dimensions.scss +2 -3
  333. package/src/scss/settings/_transition.scss +1 -1
  334. package/src/services/eventBus.js +44 -0
  335. package/src/services/eventBus.test.js +83 -0
  336. package/src/services/symbolRegistry.js +1 -1
  337. package/src/types.js +11 -1
  338. package/webpack.umd.mjs +2 -0
  339. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  340. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  341. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  342. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  343. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  344. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  345. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  346. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  347. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  348. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  349. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  350. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  351. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -49,8 +49,7 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
49
49
  })
50
50
  }
51
51
 
52
- function createButtonClickHandler (btn, appState, evaluateProp) {
53
- const [, config] = btn
52
+ function createButtonClickHandler (config, appState, evaluateProp) {
54
53
  const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
55
54
  const isToggle = config.isPressed !== undefined || !!config.pressedWhen
56
55
 
@@ -134,15 +133,33 @@ function applySlotExclusivity (matching, appState) {
134
133
  return matching.filter(([_, config]) => config.pluginId === exclusivePluginId)
135
134
  }
136
135
 
137
- function renderButton ({ btn, appState, appConfig, evaluateProp }) {
138
- const [buttonId, config] = btn
136
+ /**
137
+ * Builds the props for a <SlotButton>. isHidden/variant are included here (not just derived
138
+ * inside SlotButton) because Actions.jsx also reads them directly off the 'actions' slot's
139
+ * immediate children via React.Children.toArray — they need to be top-level props on whatever
140
+ * element ends up there, not just used internally to build MapButton.
141
+ */
142
+ const slotButtonProps = ({ buttonId, config, appState, appConfig, evaluateProp }) => ({
143
+ buttonId,
144
+ config,
145
+ appState,
146
+ appConfig,
147
+ evaluateProp,
148
+ isHidden: appState.hiddenButtons.has(buttonId),
149
+ variant: config.variant
150
+ })
151
+
152
+ /**
153
+ * Renders a single button's MapButton element for its slot, computing its click handler
154
+ * and derived state (panel-open, showLabel fallback, etc.) from its config and appState.
155
+ */
156
+ function SlotButton ({ buttonId, config, appState, appConfig, evaluateProp }) {
139
157
  const bpConfig = config[appState.breakpoint] ?? {}
140
- const handleClick = createButtonClickHandler(btn, appState, evaluateProp)
158
+ const handleClick = createButtonClickHandler(config, appState, evaluateProp)
141
159
  const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
142
160
 
143
161
  return (
144
162
  <MapButton
145
- key={buttonId}
146
163
  buttonId={buttonId}
147
164
  iconId={evaluateProp(config.iconId, config.pluginId)}
148
165
  iconSvgContent={evaluateProp(config.iconSvgContent, config.pluginId)}
@@ -219,21 +236,20 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
219
236
  id: buttonId,
220
237
  type: 'button',
221
238
  order,
222
- element: renderButton({ btn, appState, appConfig, evaluateProp })
239
+ element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
223
240
  })
224
241
  }
225
242
 
226
243
  for (const [groupName, { label, order: groupOrder, members }] of groupMap) {
227
244
  if (members.length < 2) {
228
245
  // Singleton group: degrade to a regular button using the group's slot order
229
- const btn = members[0]
230
- const [buttonId, config] = btn
246
+ const [buttonId, config] = members[0]
231
247
  const order = groupOrder || config[breakpoint]?.order || 0
232
248
  result.push({
233
249
  id: buttonId,
234
250
  type: 'button',
235
251
  order,
236
- element: renderButton({ btn, appState, appConfig, evaluateProp })
252
+ element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
237
253
  })
238
254
  continue
239
255
  }
@@ -251,7 +267,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
251
267
  order: groupOrder,
252
268
  element: (
253
269
  <div key={`group-${groupName}`} role='group' aria-label={label} className='im-c-button-group'>{/* NOSONAR - div with role="group" is correct for a button group */}
254
- {sorted.map(btn => renderButton({ btn, appState, appConfig, evaluateProp }))}
270
+ {sorted.map(([buttonId, config]) => <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />)}
255
271
  </div>
256
272
  )
257
273
  })
@@ -264,7 +280,7 @@ export {
264
280
  mapButtons,
265
281
  getMatchingButtons,
266
282
  applySlotExclusivity,
267
- renderButton,
283
+ SlotButton,
268
284
  resolveGroupName,
269
285
  resolveGroupLabel,
270
286
  resolveGroupOrder
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { mapButtons, getMatchingButtons, applySlotExclusivity, renderButton, resolveGroupName, resolveGroupLabel, resolveGroupOrder } from './mapButtons.js'
2
+ import { mapButtons, getMatchingButtons, applySlotExclusivity, SlotButton, resolveGroupName, resolveGroupLabel, resolveGroupOrder } from './mapButtons.js'
3
3
  import { logger } from '../../services/logger.js'
4
4
  import { getPanelConfig } from '../registry/panelRegistry.js'
5
5
 
@@ -156,11 +156,11 @@ describe('mapButtons module', () => {
156
156
  })
157
157
 
158
158
  // -------------------------
159
- // renderButton tests
159
+ // SlotButton tests
160
160
  // -------------------------
161
- describe('renderButton', () => {
161
+ describe('SlotButton', () => {
162
162
  const render = (config, state = appState) =>
163
- renderButton({ btn: ['id', config], appState: state, appConfig, evaluateProp })
163
+ SlotButton({ buttonId: 'id', config, appState: state, appConfig, evaluateProp })
164
164
 
165
165
  it('renders a MapButton with correct basic props', () => {
166
166
  const result = render(baseBtn)
@@ -404,5 +404,18 @@ describe('mapButtons module', () => {
404
404
  expect(result).toHaveLength(1)
405
405
  expect(result[0].id).toBe('parent')
406
406
  })
407
+
408
+ // Actions.jsx (the 'actions' slot wrapper) reads isHidden/variant directly off each slot
409
+ // item's top-level element props via React.Children.toArray — these two guard that contract.
410
+ it('carries isHidden as a top-level prop on the slot element, not just inside MapButton', () => {
411
+ appState.buttonConfig = ({ b1: baseBtn })
412
+ appState.hiddenButtons = new Set(['b1'])
413
+ expect(map()[0].element.props.isHidden).toBe(true)
414
+ })
415
+
416
+ it('carries variant as a top-level prop on the slot element, not just inside MapButton', () => {
417
+ appState.buttonConfig = ({ b1: { ...baseBtn, variant: 'touch' } })
418
+ expect(map()[0].element.props.variant).toBe('touch')
419
+ })
407
420
  })
408
421
  })
@@ -28,7 +28,9 @@ export function mapControls ({ slot, appState, evaluateProp }) {
28
28
  return false
29
29
  }
30
30
 
31
- const slotAllowed = allowedSlots.control.includes(bpConfig.slot)
31
+ // A control may also target a panel's body directly via the `<panelId>-panel`
32
+ // slot convention (mirrors the `<buttonId>-button` convention panels already use).
33
+ const slotAllowed = allowedSlots.control.includes(bpConfig.slot) || bpConfig.slot?.endsWith('-panel')
32
34
  const inModeWhitelist = control.includeModes?.includes(mode) ?? true
33
35
  const inExcludeModes = control.excludeModes?.includes(mode) ?? false
34
36
 
@@ -164,4 +164,20 @@ describe('mapControls', () => {
164
164
  const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
165
165
  expect(result.map(c => c.id)).toEqual(['ctrl1'])
166
166
  })
167
+
168
+ it('matches a control targeting a panel-body slot via the <panelId>-panel convention', () => {
169
+ defaultAppState.controlConfig = ({
170
+ ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
171
+ })
172
+ const result = mapControls({ slot: 'map-styles-panel', appState: defaultAppState, evaluateProp: (p) => p })
173
+ expect(result.map(c => c.id)).toEqual(['ctrl1'])
174
+ })
175
+
176
+ it('does not match a panel-body-targeting control against an unrelated slot', () => {
177
+ defaultAppState.controlConfig = ({
178
+ ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
179
+ })
180
+ const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
181
+ expect(result).toEqual([])
182
+ })
167
183
  })
@@ -4,6 +4,10 @@ import { withPluginContexts } from './pluginWrapper.js'
4
4
  import { Panel } from '../components/Panel/Panel.jsx'
5
5
  import { allowedSlots } from './slots.js'
6
6
  import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
7
+ import { mapControls } from './mapControls.js'
8
+ import { orderItems } from './orderItems.js'
9
+ import { stringToKebab } from '../../utils/stringToKebab.js'
10
+ import { logger } from '../../services/logger.js'
7
11
 
8
12
  /**
9
13
  * Determines whether a panel should be rendered in the given slot.
@@ -73,13 +77,7 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
73
77
  const plugin = pluginRegistry.registeredPlugins.find(p => p.id === config.pluginId)
74
78
  const pluginId = plugin?.id
75
79
 
76
- const WrappedChild = config.render
77
- ? withPluginContexts(config.render, {
78
- ...props,
79
- pluginId,
80
- pluginConfig: plugin?.config
81
- })
82
- : null
80
+ const html = pluginId ? evaluateProp(config.html, pluginId) : config.html
83
81
 
84
82
  return {
85
83
  id: panelId,
@@ -92,12 +90,49 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
92
90
  panelConfig={config}
93
91
  props={props}
94
92
  focusOnOpen={focusOnOpen}
95
- WrappedChild={WrappedChild}
93
+ items={buildPanelBodyItems({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp })}
96
94
  label={evaluateProp(config.label, pluginId)}
97
- html={pluginId ? evaluateProp(config.html, pluginId) : config.html}
95
+ html={html}
98
96
  />
99
97
  )
100
98
  }
101
99
  })
102
100
  .filter(Boolean)
103
101
  }
102
+
103
+ /**
104
+ * Builds the ordered list of body items for a panel: its own render content (if any) plus
105
+ * any controls registered against its `<panelId>-panel` slot by other plugins. Static-html
106
+ * panels don't build an items list — dangerouslySetInnerHTML can't host injected controls,
107
+ * so any controls targeting one are silently skipped (with a dev warning).
108
+ */
109
+ function buildPanelBodyItems ({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp }) {
110
+ const injectedItems = mapControls({
111
+ slot: `${stringToKebab(panelId)}-panel`,
112
+ appState,
113
+ evaluateProp
114
+ })
115
+
116
+ if (html) {
117
+ /* istanbul ignore next */
118
+ if (process.env.NODE_ENV !== 'production' && injectedItems.length > 0) {
119
+ logger.warn(`Panel "${panelId}" uses static html — controls targeting its slot are not rendered.`)
120
+ }
121
+ return undefined
122
+ }
123
+
124
+ if (!config.render) {
125
+ return injectedItems
126
+ }
127
+
128
+ const WrappedChild = withPluginContexts(config.render, {
129
+ ...props,
130
+ pluginId,
131
+ pluginConfig: plugin?.config
132
+ })
133
+
134
+ return orderItems([
135
+ { id: panelId, order: 0, element: <WrappedChild {...props} /> },
136
+ ...injectedItems
137
+ ])
138
+ }
@@ -2,10 +2,14 @@ import React from 'react'
2
2
  import { mapPanels } from './mapPanels.js'
3
3
  import { registeredPlugins } from '../registry/pluginRegistry.js'
4
4
  import { withPluginContexts } from './pluginWrapper.js'
5
+ import { mapControls } from './mapControls.js'
6
+ import { logger } from '../../services/logger.js'
5
7
 
8
+ jest.mock('../../services/logger.js')
6
9
  jest.mock('../registry/panelRegistry.js')
7
10
  jest.mock('../registry/pluginRegistry.js', () => ({ registeredPlugins: [] }))
8
11
  jest.mock('./pluginWrapper.js', () => ({ withPluginContexts: jest.fn((c) => c) }))
12
+ jest.mock('./mapControls.js', () => ({ mapControls: jest.fn(() => []) }))
9
13
  jest.mock('../components/Panel/Panel.jsx', () => ({ Panel: (props) => <div data-testid='panel' {...props} /> }))
10
14
  jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'left-top'] } }))
11
15
 
@@ -23,6 +27,7 @@ describe('mapPanels', () => {
23
27
 
24
28
  beforeEach(() => {
25
29
  jest.clearAllMocks()
30
+ mapControls.mockReturnValue([])
26
31
  registeredPlugins.length = 0
27
32
  defaultAppState = {
28
33
  breakpoint: 'desktop',
@@ -116,9 +121,53 @@ describe('mapPanels', () => {
116
121
  )
117
122
  })
118
123
 
119
- it('sets WrappedChild null if no render function', () => {
124
+ it('builds an empty items list if there is no render function and no injected controls', () => {
120
125
  const result = map()
121
- expect(result[0].element.props.WrappedChild).toBeNull()
126
+ expect(result[0].element.props.items).toEqual([])
127
+ })
128
+
129
+ it('builds a single-item items list for a panel with its own render content', () => {
130
+ const renderFn = () => <div>child</div>
131
+ defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
132
+ const result = map()
133
+ expect(result[0].element.props.items).toEqual([
134
+ { id: 'p1', order: 0, element: expect.anything() }
135
+ ])
136
+ })
137
+
138
+ it('requests injected controls using the <panelId>-panel slot convention', () => {
139
+ map()
140
+ expect(mapControls).toHaveBeenCalledWith(expect.objectContaining({ slot: 'p1-panel' }))
141
+ })
142
+
143
+ it('merges and orders the panel\'s own content with controls injected via mapControls', () => {
144
+ const renderFn = () => <div>child</div>
145
+ mapControls.mockReturnValue([{ id: 'injected1', order: 1, element: <span>injected</span> }])
146
+ defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
147
+ const result = map()
148
+ // injected1 has order 1, so it's spliced ahead of the panel's own (unordered) content
149
+ expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1', 'p1'])
150
+ })
151
+
152
+ it('returns just the injected controls when the panel has neither render nor html', () => {
153
+ mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
154
+ defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'] } })
155
+ const result = map()
156
+ expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1'])
157
+ })
158
+
159
+ it('does not build an items list for a static-html panel (dangerouslySetInnerHTML can\'t host injected controls)', () => {
160
+ defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
161
+ const result = map()
162
+ expect(result[0].element.props.items).toBeUndefined()
163
+ expect(result[0].element.props.html).toBe('<p>Hi</p>')
164
+ })
165
+
166
+ it('warns in dev when controls target a static-html panel', () => {
167
+ mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
168
+ defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
169
+ map()
170
+ expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('p1'))
122
171
  })
123
172
 
124
173
  it('returns correct structure and defaults', () => {
@@ -0,0 +1,28 @@
1
+ // src/core/renderers/orderItems.js
2
+
3
+ /**
4
+ * Orders a list of `{ id, order, element }` items for a single render target
5
+ * (a layout slot, or a panel body).
6
+ *
7
+ * Items without an explicit `order` (or `order === 0`) keep their natural/registration
8
+ * sequence. Items with an explicit `order` are spliced into that 1-based position among
9
+ * the unordered items, clamped to the valid range.
10
+ */
11
+ export function orderItems (items) {
12
+ // Start with items that have no explicit order (in their natural sequence)
13
+ const withoutOrder = items.filter(item => item.order == null || item.order === 0)
14
+ const withOrder = items.filter(item => item.order != null && item.order !== 0)
15
+
16
+ // Sort items with order by their order value (stable sort preserves ties)
17
+ withOrder.sort((a, b) => a.order - b.order)
18
+
19
+ // Insert each ordered item at its specified position
20
+ const result = [...withoutOrder]
21
+ for (const item of withOrder) {
22
+ // Clamp position to valid range (1-based, so subtract 1 for array index)
23
+ const pos = Math.min(Math.max(item.order - 1, 0), result.length)
24
+ result.splice(pos, 0, item)
25
+ }
26
+
27
+ return result
28
+ }
@@ -0,0 +1,50 @@
1
+ import { orderItems } from './orderItems.js'
2
+
3
+ describe('orderItems', () => {
4
+ it('keeps unordered items in their natural sequence', () => {
5
+ const items = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
6
+ expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
7
+ })
8
+
9
+ it('treats order 0 the same as no order', () => {
10
+ const items = [{ id: 'a', order: 0 }, { id: 'b' }]
11
+ expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b'])
12
+ })
13
+
14
+ it('inserts an ordered item at its 1-based position among unordered items', () => {
15
+ const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 1 }]
16
+ expect(orderItems(items).map(i => i.id)).toEqual(['c', 'a', 'b'])
17
+ })
18
+
19
+ it('splices ordered items in ascending order, each relative to the growing result', () => {
20
+ const items = [
21
+ { id: 'a' },
22
+ { id: 'b', order: 2 },
23
+ { id: 'c', order: 1 }
24
+ ]
25
+ // c (order 1) is spliced in first, ahead of 'a': [c, a]
26
+ // b (order 2) is then spliced at index 1 of that 2-item result: [c, b, a]
27
+ expect(orderItems(items).map(i => i.id)).toEqual(['c', 'b', 'a'])
28
+ })
29
+
30
+ it('clamps an order larger than the list to the end', () => {
31
+ const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 99 }]
32
+ expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
33
+ })
34
+
35
+ it('splices equal-order items each at position 0, reversing their relative order', () => {
36
+ // The stable sort keeps a before b, but with no unordered items to anchor against,
37
+ // each splice(0, 0, item) inserts ahead of the previous one.
38
+ const items = [{ id: 'a', order: 1 }, { id: 'b', order: 1 }]
39
+ expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
40
+ })
41
+
42
+ it('handles an all-ordered list', () => {
43
+ const items = [{ id: 'a', order: 2 }, { id: 'b', order: 1 }]
44
+ expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
45
+ })
46
+
47
+ it('handles an empty list', () => {
48
+ expect(orderItems([])).toEqual([])
49
+ })
50
+ })
@@ -2,28 +2,12 @@
2
2
  import { mapControls } from './mapControls.js'
3
3
  import { mapPanels } from './mapPanels.js'
4
4
  import { mapButtons } from './mapButtons.js'
5
+ import { orderItems } from './orderItems.js'
5
6
 
6
7
  export function getSlotItems ({ evaluateProp, ...args }) {
7
8
  const controls = mapControls({ evaluateProp, ...args })
8
9
  const panels = mapPanels({ evaluateProp, ...args })
9
10
  const buttons = mapButtons({ evaluateProp, ...args })
10
11
 
11
- const items = [...controls, ...panels, ...buttons]
12
-
13
- // Start with items that have no explicit order (in their natural sequence)
14
- const withoutOrder = items.filter(item => item.order == null || item.order === 0)
15
- const withOrder = items.filter(item => item.order != null && item.order !== 0)
16
-
17
- // Sort items with order by their order value (stable sort preserves ties)
18
- withOrder.sort((a, b) => a.order - b.order)
19
-
20
- // Insert each ordered item at its specified position
21
- const result = [...withoutOrder]
22
- for (const item of withOrder) {
23
- // Clamp position to valid range (1-based, so subtract 1 for array index)
24
- const pos = Math.min(Math.max(item.order - 1, 0), result.length)
25
- result.splice(pos, 0, item)
26
- }
27
-
28
- return result
12
+ return orderItems([...controls, ...panels, ...buttons])
29
13
  }
@@ -39,7 +39,9 @@ export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) =>
39
39
  if (!bpConfig) {
40
40
  return false
41
41
  }
42
- if (!allowedSlots.control.includes(bpConfig.slot)) {
42
+ // A control may also target a panel's body directly via the `<panelId>-panel`
43
+ // slot convention (mirrors the `<buttonId>-button` convention panels already use).
44
+ if (!allowedSlots.control.includes(bpConfig.slot) && !bpConfig.slot?.endsWith('-panel')) {
43
45
  return false
44
46
  }
45
47
  if (!isModeAllowed(control, mode)) {
@@ -65,6 +65,11 @@ describe('isControlVisible', () => {
65
65
  it('returns true when inline:false and fullscreen', () => {
66
66
  expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: true })).toBe(true)
67
67
  })
68
+
69
+ it('returns true for a control targeting a panel-body slot via the <panelId>-panel convention', () => {
70
+ const panelTargeting = { desktop: { slot: 'map-styles-panel' } }
71
+ expect(isControlVisible(panelTargeting, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(true)
72
+ })
68
73
  })
69
74
 
70
75
  describe('isConsumerHtml', () => {
@@ -1,6 +1,7 @@
1
1
  // src/core/renderers/slots.js
2
2
  export const layoutSlots = Object.freeze({
3
3
  SIDE: 'side',
4
+ HEADER: 'header',
4
5
  BANNER: 'banner',
5
6
  TOP_LEFT: 'top-left',
6
7
  TOP_MIDDLE: 'top-middle',
@@ -19,6 +20,7 @@ export const layoutSlots = Object.freeze({
19
20
 
20
21
  export const allowedSlots = Object.freeze({
21
22
  control: [
23
+ layoutSlots.HEADER,
22
24
  layoutSlots.BANNER,
23
25
  layoutSlots.TOP_LEFT,
24
26
  layoutSlots.TOP_RIGHT,
@@ -31,6 +33,7 @@ export const allowedSlots = Object.freeze({
31
33
  ],
32
34
  panel: [
33
35
  layoutSlots.SIDE,
36
+ layoutSlots.HEADER,
34
37
  layoutSlots.BANNER,
35
38
  layoutSlots.LEFT_TOP,
36
39
  layoutSlots.LEFT_BOTTOM,
@@ -16,6 +16,7 @@ export const AppProvider = ({ options, children }) => {
16
16
  appContainerRef: useRef(null),
17
17
  sideRef: useRef(null),
18
18
  mainRef: useRef(null),
19
+ headerRef: useRef(null),
19
20
  topRef: useRef(null),
20
21
  topLeftColRef: useRef(null),
21
22
  topRightColRef: useRef(null),
@@ -101,7 +101,7 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
101
101
  const mergedConfig = deepMerge(defaultControlConfig, config)
102
102
  BREAKPOINTS.forEach(bp => {
103
103
  const slot = mergedConfig[bp]?.slot
104
- if (slot && !allowedSlots.control.includes(slot)) {
104
+ if (slot && !allowedSlots.control.includes(slot) && !slot.endsWith('-panel')) {
105
105
  logger.warn(`control "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.control.join(', ')}.`)
106
106
  }
107
107
  })
@@ -185,6 +185,14 @@ describe('ADD_CONTROL', () => {
185
185
  )
186
186
  expect(console.warn).not.toHaveBeenCalled()
187
187
  })
188
+
189
+ it('does not warn for a control targeting a panel-body slot', () => {
190
+ run(
191
+ { type: 'ADD_CONTROL', payload: { id: 'myCtrl', config: { desktop: { slot: 'map-styles-panel' } } } },
192
+ { breakpoint: 'desktop' }
193
+ )
194
+ expect(console.warn).not.toHaveBeenCalled()
195
+ })
188
196
  })
189
197
 
190
198
  describe('ADD_PANEL', () => {
@@ -147,6 +147,8 @@ export const EVENTS = {
147
147
  MAP_SET_SIZE: 'map:setsize',
148
148
  /** @internal Set the accessible features list. Payload: { items: { id: string, label: string }[] } */
149
149
  MAP_SET_FEATURES: 'map:setfeatures',
150
+ /** @internal Suppress/restore the accessible features list. Payload: { suppressed: boolean } */
151
+ MAP_SET_FEATURES_SUPPRESSED: 'map:setfeaturessuppressed',
150
152
  /** @internal Set the active feature in the accessible features list. Payload: { id: string | null } */
151
153
  MAP_SET_ACTIVE_FEATURE: 'map:setactivefeature',
152
154
  /** @internal Select the active listbox feature as the real selection (Enter key). Payload: none */
@@ -26,6 +26,7 @@
26
26
  // Tooltips
27
27
  --tooltip-background-color: #383F43;
28
28
  --tooltip-foreground-color: #ffffff;
29
+ --tooltip-hover-color: #505a5f;
29
30
 
30
31
  // Modals
31
32
  --modal-background-color: #0b0c0c;
@@ -39,9 +39,8 @@
39
39
  --panel-close-button-gap: 4px;
40
40
  --panel-heading-margin-top: 12px;
41
41
  --panel-margin: 15px;
42
- --banner-close-button-gap: 0px;
43
- --banner-max-desktop-width: 400px;
44
- --banner-collapse-margin: 14px;
42
+ --banner-preferred-width: 280px; // Docked max-width, and the gutter threshold JS docks/stacks against
43
+ --banner-border-radius: 0px; // Styled like a hint, not a panel
45
44
 
46
45
  // Popup menu
47
46
  --popup-menu-min-width: 200px;
@@ -1,3 +1,3 @@
1
1
  :root {
2
- --duration: 0.2s;
2
+ --duration: 0s;
3
3
  }
@@ -108,6 +108,50 @@ class EventBus {
108
108
  return this.emit(eventName, ...args)
109
109
  }
110
110
 
111
+ /**
112
+ * Like `emit`, but also registers a listener for the corresponding `Required` event.
113
+ *
114
+ * Use this for events that may be requested before or after the emitter side is ready
115
+ * e.g. called from plugins that require a state reference from a different plugin like this:
116
+ * const removeRegistryListener = eventBus.emitWhenRequested('datasets:registry', datasetRegistry)
117
+ *
118
+ * @param {string} eventName
119
+ * @param {...*} args
120
+ * @returns {Function} A function to remove the listener for the `Required` event.
121
+ */
122
+ emitWhenRequested (eventName, ...args) {
123
+ const eventRequestedName = `${eventName}Requested`
124
+ const eventReadyName = `${eventName}Ready`
125
+ const handleOnRequired = () => this.emit(eventReadyName, ...args)
126
+ // Add a listener that will emit the event whenever requested
127
+ this.on(eventRequestedName, handleOnRequired)
128
+ // Also emit the event immediately, in case a listener is already waiting for it
129
+ handleOnRequired()
130
+ return () => this.off(eventRequestedName, handleOnRequired)
131
+ }
132
+
133
+ /**
134
+ * Like `emit`, but appends `Requested` to the event.
135
+ * used for events that require a response from another plugin,
136
+ * e.g. `datasets:registryRequested` will be emitted by the menu plugin,
137
+ * via `eventBus.requestOnce('datasets:registry')`, and the datasets plugin will respond with `datasets:registryReady`.
138
+ * and the datasets plugin will respond with `datasets:registryReady`.
139
+ * via `eventBus.emitWhenRequested('datasets:registry', datasetRegistry)`.
140
+ *
141
+ * @param {string} eventName
142
+ * @param {Function} eventHandler
143
+ * @returns {this}
144
+ */
145
+ requestOnce (eventName, eventHandler) {
146
+ const eventRequestedName = `${eventName}Requested`
147
+ const eventReadyName = `${eventName}Ready`
148
+ // Note - must register the once listener before emitting the requested event,
149
+ // otherwise the event won't emit, as there are no listeners yet.
150
+ this.once(eventReadyName, eventHandler)
151
+ this.emit(eventRequestedName, eventName)
152
+ return this
153
+ }
154
+
111
155
  /**
112
156
  * Remove all handlers and clear any queued events.
113
157
  */