@defra/interactive-map 0.0.3-alpha → 0.0.5-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 (407) hide show
  1. package/README.md +16 -57
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/button-definition.md +176 -0
  8. package/docs/api/context.md +124 -0
  9. package/docs/api/control-definition.md +72 -0
  10. package/docs/api/icon-definition.md +28 -0
  11. package/docs/api/map-style-config.md +76 -0
  12. package/docs/api/marker-config.md +51 -0
  13. package/docs/api/panel-definition.md +117 -0
  14. package/docs/api/slots.md +25 -0
  15. package/docs/api.md +721 -0
  16. package/docs/architecture.md +139 -0
  17. package/docs/building-a-plugin.md +149 -0
  18. package/docs/getting-started.md +46 -0
  19. package/docs/govuk-prototype.md +0 -0
  20. package/docs/plugins/interact.md +205 -0
  21. package/docs/plugins/map-styles.md +84 -0
  22. package/docs/plugins/plugin-context.md +67 -0
  23. package/docs/plugins/plugin-descriptor.md +71 -0
  24. package/docs/plugins/plugin-manifest.md +118 -0
  25. package/docs/plugins/scale-bar.md +62 -0
  26. package/docs/plugins/search.md +162 -0
  27. package/docs/plugins.md +116 -0
  28. package/govuk-prototype-kit.config.json +7 -7
  29. package/package.json +16 -2
  30. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -0
  31. package/plugins/beta/datasets/dist/esm/index.js +2 -0
  32. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  33. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  34. package/plugins/beta/datasets/src/datasets.js +113 -0
  35. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
  36. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
  37. package/plugins/{datasets → beta/datasets}/src/handleSetMapStyle.js +8 -1
  38. package/plugins/{datasets → beta/datasets}/src/manifest.js +1 -1
  39. package/plugins/{datasets → beta/datasets}/src/mapLayers.js +42 -4
  40. package/plugins/{datasets → beta/datasets}/src/panels/Key.jsx +1 -1
  41. package/plugins/beta/datasets/src/utils/bbox.js +115 -0
  42. package/plugins/beta/draw-ml/dist/css/index.css +1 -0
  43. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -0
  44. package/plugins/beta/draw-ml/dist/esm/index.js +2 -0
  45. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -0
  46. package/plugins/beta/draw-ml/dist/umd/index.js +2 -0
  47. package/plugins/beta/draw-ml/src/api/addFeature.js +27 -0
  48. package/plugins/{draw-ml → beta/draw-ml}/src/api/editFeature.js +17 -6
  49. package/plugins/beta/draw-ml/src/api/merge.js +9 -0
  50. package/plugins/beta/draw-ml/src/api/newLine.js +68 -0
  51. package/plugins/beta/draw-ml/src/api/newPolygon.js +68 -0
  52. package/plugins/beta/draw-ml/src/api/split.js +81 -0
  53. package/plugins/{draw-ml → beta/draw-ml}/src/defaults.js +3 -0
  54. package/plugins/beta/draw-ml/src/draw.scss +32 -0
  55. package/plugins/beta/draw-ml/src/events.js +171 -0
  56. package/plugins/beta/draw-ml/src/manifest.js +132 -0
  57. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxDraw.js +51 -2
  58. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxSnap.js +18 -12
  59. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +497 -0
  60. package/plugins/beta/draw-ml/src/modes/drawLineMode.js +18 -0
  61. package/plugins/beta/draw-ml/src/modes/drawPolygonMode.js +16 -0
  62. package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
  63. package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
  64. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
  65. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
  66. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
  67. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
  68. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +437 -0
  69. package/plugins/{draw-ml → beta/draw-ml}/src/reducer.js +30 -2
  70. package/plugins/{draw-ml → beta/draw-ml}/src/styles.js +66 -36
  71. package/plugins/beta/draw-ml/src/undoStack.js +50 -0
  72. package/plugins/beta/draw-ml/src/utils/debounce.js +16 -0
  73. package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
  74. package/plugins/beta/draw-ml/src/utils/spatial.js +258 -0
  75. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -0
  76. package/plugins/beta/frame/dist/esm/index.js +2 -0
  77. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -0
  78. package/plugins/beta/frame/dist/umd/index.js +2 -0
  79. package/plugins/{frame → beta/frame}/src/Frame.jsx +1 -1
  80. package/plugins/{map-styles → beta/map-styles}/dist/css/index.css +1 -1
  81. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -0
  82. package/plugins/beta/map-styles/dist/esm/index.js +2 -0
  83. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -0
  84. package/plugins/beta/map-styles/dist/umd/index.js +2 -0
  85. package/plugins/{map-styles → beta/map-styles}/src/MapStyles.jsx +1 -1
  86. package/plugins/{map-styles → beta/map-styles}/src/manifest.js +8 -10
  87. package/plugins/{map-styles → beta/map-styles}/src/mapStyles.scss +4 -1
  88. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -0
  89. package/plugins/beta/scale-bar/dist/esm/index.js +2 -0
  90. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -0
  91. package/plugins/beta/scale-bar/dist/umd/index.js +2 -0
  92. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -0
  93. package/plugins/beta/use-location/dist/esm/index.js +2 -0
  94. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -0
  95. package/plugins/beta/use-location/dist/umd/index.js +2 -0
  96. package/plugins/interact/dist/css/index.css +1 -1
  97. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  98. package/plugins/interact/dist/esm/index.js +1 -1
  99. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  100. package/plugins/interact/dist/umd/index.js +1 -1
  101. package/plugins/interact/src/InteractInit.jsx +1 -1
  102. package/plugins/interact/src/InteractInit.test.js +97 -0
  103. package/plugins/interact/src/api/clear.test.js +18 -0
  104. package/plugins/interact/src/api/disable.test.js +12 -0
  105. package/plugins/interact/src/api/enable.test.js +48 -0
  106. package/plugins/interact/src/api/selectFeature.test.js +33 -0
  107. package/plugins/interact/src/api/unselectFeature.test.js +33 -0
  108. package/plugins/interact/src/defaults.js +2 -0
  109. package/plugins/interact/src/events.js +37 -52
  110. package/plugins/interact/src/events.test.js +186 -0
  111. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  112. package/plugins/interact/src/hooks/useHighlightSync.test.js +212 -0
  113. package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -41
  114. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +316 -0
  115. package/plugins/interact/src/index.test.js +22 -0
  116. package/plugins/interact/src/interact.scss +6 -0
  117. package/plugins/interact/src/manifest.js +1 -1
  118. package/plugins/interact/src/manifest.test.js +94 -0
  119. package/plugins/interact/src/reducer.js +28 -21
  120. package/plugins/interact/src/reducer.test.js +155 -0
  121. package/plugins/interact/src/utils/buildStylesMap.test.js +57 -0
  122. package/plugins/interact/src/utils/featureQueries.js +2 -2
  123. package/plugins/interact/src/utils/featureQueries.test.js +56 -0
  124. package/plugins/interact/src/utils/spatial.js +95 -0
  125. package/plugins/interact/src/utils/spatial.test.js +93 -0
  126. package/plugins/search/dist/css/index.css +1 -1
  127. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  128. package/plugins/search/dist/esm/index.js +1 -1
  129. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  130. package/plugins/search/dist/umd/index.js +1 -1
  131. package/plugins/search/src/Search.jsx +2 -2
  132. package/plugins/search/src/components/Form/Form.jsx +1 -1
  133. package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
  134. package/plugins/search/src/datasets.js +1 -1
  135. package/plugins/search/src/defaults.js +3 -0
  136. package/plugins/search/src/events/fetchSuggestions.js +48 -53
  137. package/plugins/search/src/events/formHandlers.js +3 -2
  138. package/plugins/search/src/events/index.js +1 -1
  139. package/plugins/search/src/events/inputHandlers.js +3 -1
  140. package/plugins/search/src/events/suggestionHandlers.js +12 -3
  141. package/plugins/search/src/search.scss +4 -1
  142. package/plugins/search/src/utils/parseOsNamesResults.js +15 -19
  143. package/providers/{esri → beta/esri}/src/esriProvider.js +1 -1
  144. package/providers/{esri → beta/esri}/src/index.js +1 -1
  145. package/providers/{esri → beta/esri}/src/mapEvents.js +2 -2
  146. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +2 -0
  147. package/providers/beta/open-names/dist/esm/index.js +2 -0
  148. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js +2 -0
  149. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt +1 -0
  150. package/providers/beta/open-names/dist/umd/index.js +2 -0
  151. package/providers/beta/open-names/dist/umd/index.js.LICENSE.txt +1 -0
  152. package/providers/maplibre/dist/esm/im-maplibre-framework.js +1 -1
  153. package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +1 -1
  154. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  155. package/providers/maplibre/dist/esm/index.js +1 -1
  156. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  157. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  158. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  159. package/providers/maplibre/dist/umd/index.js +1 -1
  160. package/providers/maplibre/src/defaults.js +3 -2
  161. package/providers/maplibre/src/index.js +36 -20
  162. package/providers/maplibre/src/maplibreProvider.js +140 -11
  163. package/providers/maplibre/src/utils/detectWebgl.js +1 -2
  164. package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
  165. package/providers/maplibre/src/utils/labels.js +174 -120
  166. package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
  167. package/providers/maplibre/src/utils/queryFeatures.js +146 -0
  168. package/providers/maplibre/src/utils/spatial.js +14 -17
  169. package/sonar-project.properties +40 -3
  170. package/src/App/components/Actions/Actions.jsx +2 -2
  171. package/src/App/components/Actions/Actions.module.scss +2 -2
  172. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
  173. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
  174. package/src/App/components/Logo/Logo.jsx +1 -1
  175. package/src/App/components/MapButton/MapButton.jsx +217 -41
  176. package/src/App/components/MapButton/MapButton.module.scss +32 -21
  177. package/src/App/components/MapButton/MapButton.test.jsx +128 -84
  178. package/src/App/components/Markers/Markers.jsx +1 -1
  179. package/src/App/components/Markers/Markers.module.scss +0 -5
  180. package/src/App/components/Panel/Panel.jsx +75 -45
  181. package/src/App/components/Panel/Panel.module.scss +18 -24
  182. package/src/App/components/Panel/Panel.test.jsx +18 -0
  183. package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
  184. package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
  185. package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
  186. package/src/App/components/Tooltip/Tooltip.jsx +4 -2
  187. package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
  188. package/src/App/components/Viewport/MapController.jsx +1 -1
  189. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  190. package/src/App/components/Viewport/Viewport.jsx +2 -2
  191. package/src/App/components/Viewport/Viewport.module.scss +2 -2
  192. package/src/App/controls/keyboardActions.js +3 -2
  193. package/src/App/controls/keyboardShortcuts.js +4 -2
  194. package/src/App/hooks/useButtonStateEvaluator.js +58 -45
  195. package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
  196. package/src/App/hooks/useCrossHairAPI.js +49 -47
  197. package/src/App/hooks/useFocusVisible.js +2 -2
  198. package/src/App/hooks/useInterfaceAPI.js +36 -1
  199. package/src/App/hooks/useKeyboardHint.js +1 -1
  200. package/src/App/hooks/useKeyboardShortcuts.js +4 -2
  201. package/src/App/hooks/useMapAnnouncements.js +34 -32
  202. package/src/App/hooks/useMapEvents.js +12 -1
  203. package/src/App/hooks/useMapProviderOverrides.js +3 -3
  204. package/src/App/hooks/useMapStateSync.js +7 -1
  205. package/src/App/hooks/useMapURLSync.js +6 -1
  206. package/src/App/hooks/useMarkersAPI.js +88 -50
  207. package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
  208. package/src/App/hooks/useModalPanelBehaviour.js +46 -40
  209. package/src/App/hooks/useResizeObserver.js +2 -2
  210. package/src/App/initialiseApp.js +45 -38
  211. package/src/App/layout/Layout.jsx +6 -4
  212. package/src/App/layout/layout.module.scss +13 -17
  213. package/src/App/registry/keyboardShortcutRegistry.js +12 -2
  214. package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
  215. package/src/App/registry/mergeManifests.js +1 -1
  216. package/src/App/registry/panelRegistry.js +1 -1
  217. package/src/App/registry/panelRegistry.test.js +30 -1
  218. package/src/App/registry/pluginRegistry.js +14 -51
  219. package/src/App/registry/pluginRegistry.test.js +54 -6
  220. package/src/App/renderer/HtmlElementHost.jsx +186 -0
  221. package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
  222. package/src/App/renderer/mapButtons.js +12 -9
  223. package/src/App/renderer/mapButtons.test.js +6 -4
  224. package/src/App/renderer/mapControls.js +6 -0
  225. package/src/App/renderer/mapControls.test.js +10 -2
  226. package/src/App/renderer/mapPanels.js +39 -26
  227. package/src/App/renderer/mapPanels.test.js +29 -0
  228. package/src/App/renderer/pluginWrapper.test.js +28 -0
  229. package/src/App/renderer/slotHelpers.js +60 -0
  230. package/src/App/renderer/slotHelpers.test.js +82 -0
  231. package/src/App/store/AppProvider.jsx +3 -0
  232. package/src/App/store/AppProvider.test.jsx +40 -0
  233. package/src/App/store/PluginProvider.jsx +7 -5
  234. package/src/App/store/appActionsMap.js +46 -1
  235. package/src/App/store/appActionsMap.test.js +21 -0
  236. package/src/App/store/appReducer.js +1 -0
  237. package/src/App/store/mapActionsMap.js +12 -24
  238. package/src/App/store/mapReducer.js +1 -1
  239. package/src/InteractiveMap/InteractiveMap.js +194 -13
  240. package/src/InteractiveMap/InteractiveMap.test.js +164 -2
  241. package/src/InteractiveMap/behaviourController.js +12 -2
  242. package/src/InteractiveMap/behaviourController.test.js +82 -1
  243. package/src/InteractiveMap/deviceChecker.js +1 -1
  244. package/src/InteractiveMap/deviceChecker.test.js +2 -2
  245. package/src/InteractiveMap/historyManager.js +41 -5
  246. package/src/InteractiveMap/historyManager.test.js +37 -6
  247. package/src/InteractiveMap/polyfills.js +39 -0
  248. package/src/InteractiveMap/polyfills.test.js +127 -0
  249. package/src/config/appConfig.js +5 -5
  250. package/src/config/appConfig.test.js +107 -42
  251. package/src/config/defaults.js +30 -20
  252. package/src/config/events.js +110 -4
  253. package/src/index.js +5 -2
  254. package/src/index.umd.js +1 -1
  255. package/src/scss/main.scss +1 -0
  256. package/src/scss/settings/_dimensions.scss +6 -2
  257. package/src/scss/tools/_border-focus.scss +20 -4
  258. package/src/services/eventBus.test.js +24 -1
  259. package/src/test-utils.js +1 -0
  260. package/src/types.js +590 -0
  261. package/src/utils/detectBreakpoint.js +78 -77
  262. package/src/utils/detectBreakpoint.test.js +50 -4
  263. package/src/utils/detectInterfaceType.js +4 -4
  264. package/src/utils/getIsFullscreen.js +3 -1
  265. package/src/utils/getSafeZoneInset.js +23 -7
  266. package/src/utils/getSafeZoneInset.test.js +18 -0
  267. package/src/utils/mapStateSync.js +3 -3
  268. package/src/utils/queryString.js +1 -1
  269. package/src/utils/stringToKebab.js +1 -1
  270. package/src/utils/toggleInertElements.js +37 -12
  271. package/webpack.dev.mjs +3 -4
  272. package/webpack.esm.mjs +8 -8
  273. package/webpack.umd.mjs +8 -8
  274. package/plugins/datasets/dist/esm/im-datasets-plugin.js +0 -1
  275. package/plugins/datasets/dist/esm/index.js +0 -2
  276. package/plugins/datasets/dist/umd/im-datasets-plugin.js +0 -1
  277. package/plugins/datasets/dist/umd/index.js +0 -2
  278. package/plugins/datasets/src/datasets.js +0 -60
  279. package/plugins/datasets/src/utils/bbox.js +0 -4
  280. package/plugins/draw-ml/dist/css/index.css +0 -1
  281. package/plugins/draw-ml/dist/esm/im-draw-ml-plugin.js +0 -1
  282. package/plugins/draw-ml/dist/esm/index.js +0 -2
  283. package/plugins/draw-ml/dist/umd/im-draw-ml-plugin.js +0 -1
  284. package/plugins/draw-ml/dist/umd/index.js +0 -2
  285. package/plugins/draw-ml/src/api/addFeature.js +0 -15
  286. package/plugins/draw-ml/src/api/newPolygon.js +0 -43
  287. package/plugins/draw-ml/src/draw.scss +0 -9
  288. package/plugins/draw-ml/src/events.js +0 -127
  289. package/plugins/draw-ml/src/manifest.js +0 -100
  290. package/plugins/draw-ml/src/modes/drawVertexMode.js +0 -275
  291. package/plugins/draw-ml/src/modes/editVertexMode.js +0 -426
  292. package/plugins/draw-ml/src/utils.js +0 -102
  293. package/plugins/frame/dist/esm/im-frame-plugin.js +0 -1
  294. package/plugins/frame/dist/esm/index.js +0 -2
  295. package/plugins/frame/dist/umd/im-frame-plugin.js +0 -1
  296. package/plugins/frame/dist/umd/index.js +0 -2
  297. package/plugins/map-styles/dist/esm/im-map-styles-plugin.js +0 -1
  298. package/plugins/map-styles/dist/esm/index.js +0 -2
  299. package/plugins/map-styles/dist/umd/im-map-styles-plugin.js +0 -1
  300. package/plugins/map-styles/dist/umd/index.js +0 -2
  301. package/plugins/scale-bar/dist/esm/im-scale-bar-plugin.js +0 -1
  302. package/plugins/scale-bar/dist/esm/index.js +0 -2
  303. package/plugins/scale-bar/dist/umd/im-scale-bar-plugin.js +0 -1
  304. package/plugins/scale-bar/dist/umd/index.js +0 -2
  305. package/plugins/search/src/utils/fetchDataset.js +0 -23
  306. package/plugins/use-location/dist/esm/im-use-location-plugin.js +0 -1
  307. package/plugins/use-location/dist/esm/index.js +0 -2
  308. package/plugins/use-location/dist/umd/im-use-location-plugin.js +0 -1
  309. package/plugins/use-location/dist/umd/index.js +0 -2
  310. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
  311. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
  312. package/providers/open-names/dist/esm/im-reverse-geocode.js +0 -2
  313. package/providers/open-names/dist/esm/index.js +0 -2
  314. package/providers/open-names/dist/umd/im-reverse-geocode.js +0 -2
  315. package/providers/open-names/dist/umd/index.js +0 -2
  316. /package/docs/architecture/{ARCHITECTURE_DIAGRAMS.md → architecture-diagrams.md} +0 -0
  317. /package/plugins/{datasets → beta/datasets}/dist/css/index.css +0 -0
  318. /package/plugins/{datasets/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt} +0 -0
  319. /package/plugins/{datasets/dist/umd → beta/datasets/dist/esm}/index.js.LICENSE.txt +0 -0
  320. /package/plugins/{draw-ml/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt} +0 -0
  321. /package/plugins/{draw-ml → beta/datasets}/dist/umd/index.js.LICENSE.txt +0 -0
  322. /package/plugins/{datasets → beta/datasets}/src/DatasetsInit.jsx +0 -0
  323. /package/plugins/{datasets → beta/datasets}/src/api/addDataset.js +0 -0
  324. /package/plugins/{datasets → beta/datasets}/src/api/hideDataset.js +0 -0
  325. /package/plugins/{datasets → beta/datasets}/src/api/hideFeatures.js +0 -0
  326. /package/plugins/{datasets → beta/datasets}/src/api/removeDataset.js +0 -0
  327. /package/plugins/{datasets → beta/datasets}/src/api/showDataset.js +0 -0
  328. /package/plugins/{datasets → beta/datasets}/src/api/showFeatures.js +0 -0
  329. /package/plugins/{datasets → beta/datasets}/src/datasets.scss +0 -0
  330. /package/plugins/{datasets → beta/datasets}/src/defaults.js +0 -0
  331. /package/plugins/{datasets → beta/datasets}/src/index.js +0 -0
  332. /package/plugins/{datasets → beta/datasets}/src/panels/Key.module.scss +0 -0
  333. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.jsx +0 -0
  334. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.module.scss +0 -0
  335. /package/plugins/{datasets → beta/datasets}/src/reducer.js +0 -0
  336. /package/plugins/{datasets → beta/datasets}/src/utils/debounce.js +0 -0
  337. /package/plugins/{datasets → beta/datasets}/src/utils/filters.js +0 -0
  338. /package/plugins/{draw-es → beta/draw-es}/src/DrawInit.jsx +0 -0
  339. /package/plugins/{draw-es → beta/draw-es}/src/api/addFeature.js +0 -0
  340. /package/plugins/{draw-es → beta/draw-es}/src/api/deleteFeature.js +0 -0
  341. /package/plugins/{draw-es → beta/draw-es}/src/api/editFeature.js +0 -0
  342. /package/plugins/{draw-es → beta/draw-es}/src/api/newPolygon.js +0 -0
  343. /package/plugins/{draw-es → beta/draw-es}/src/events.js +0 -0
  344. /package/plugins/{draw-es → beta/draw-es}/src/graphic.js +0 -0
  345. /package/plugins/{draw-es → beta/draw-es}/src/index.js +0 -0
  346. /package/plugins/{draw-es → beta/draw-es}/src/manifest.js +0 -0
  347. /package/plugins/{draw-es → beta/draw-es}/src/reducer.js +0 -0
  348. /package/plugins/{draw-es → beta/draw-es}/src/sketchViewModel.js +0 -0
  349. /package/plugins/{frame → beta/draw-ml}/dist/esm/index.js.LICENSE.txt +0 -0
  350. /package/plugins/{frame → beta/draw-ml}/dist/umd/index.js.LICENSE.txt +0 -0
  351. /package/plugins/{draw-ml → beta/draw-ml}/src/DrawInit.jsx +0 -0
  352. /package/plugins/{draw-ml → beta/draw-ml}/src/api/deleteFeature.js +0 -0
  353. /package/plugins/{draw-ml → beta/draw-ml}/src/index.js +0 -0
  354. /package/plugins/{draw-ml → beta/draw-ml}/src/modes/disabledMode.js +0 -0
  355. /package/plugins/{draw-ml/src → beta/draw-ml/src/utils}/snapHelpers.js +0 -0
  356. /package/plugins/{frame → beta/frame}/dist/css/index.css +0 -0
  357. /package/plugins/{map-styles → beta/frame}/dist/esm/index.js.LICENSE.txt +0 -0
  358. /package/plugins/{map-styles → beta/frame}/dist/umd/index.js.LICENSE.txt +0 -0
  359. /package/plugins/{frame → beta/frame}/package.json +0 -0
  360. /package/plugins/{frame → beta/frame}/src/FrameInit.jsx +0 -0
  361. /package/plugins/{frame → beta/frame}/src/api/addFrame.js +0 -0
  362. /package/plugins/{frame → beta/frame}/src/api/editFeature.js +0 -0
  363. /package/plugins/{frame → beta/frame}/src/config.js +0 -0
  364. /package/plugins/{frame → beta/frame}/src/frame.scss +0 -0
  365. /package/plugins/{frame → beta/frame}/src/index.js +0 -0
  366. /package/plugins/{frame → beta/frame}/src/manifest.js +0 -0
  367. /package/plugins/{frame → beta/frame}/src/reducer.js +0 -0
  368. /package/plugins/{frame → beta/frame}/src/utils.js +0 -0
  369. /package/plugins/{scale-bar → beta/map-styles}/dist/esm/index.js.LICENSE.txt +0 -0
  370. /package/plugins/{scale-bar → beta/map-styles}/dist/umd/index.js.LICENSE.txt +0 -0
  371. /package/plugins/{map-styles → beta/map-styles}/src/MapStylesInit.jsx +0 -0
  372. /package/plugins/{map-styles → beta/map-styles}/src/config.js +0 -0
  373. /package/plugins/{map-styles → beta/map-styles}/src/index.js +0 -0
  374. /package/plugins/{scale-bar → beta/scale-bar}/dist/css/index.css +0 -0
  375. /package/plugins/{use-location → beta/scale-bar}/dist/esm/index.js.LICENSE.txt +0 -0
  376. /package/plugins/{use-location → beta/scale-bar}/dist/umd/index.js.LICENSE.txt +0 -0
  377. /package/plugins/{scale-bar → beta/scale-bar}/package.json +0 -0
  378. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.jsx +0 -0
  379. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.test.jsx +0 -0
  380. /package/plugins/{scale-bar → beta/scale-bar}/src/index.js +0 -0
  381. /package/plugins/{scale-bar → beta/scale-bar}/src/index.test.js +0 -0
  382. /package/plugins/{scale-bar → beta/scale-bar}/src/manifest.js +0 -0
  383. /package/plugins/{scale-bar → beta/scale-bar}/src/scaleBar.scss +0 -0
  384. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.js +0 -0
  385. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.test.js +0 -0
  386. /package/{providers/open-names → plugins/beta/use-location}/dist/esm/index.js.LICENSE.txt +0 -0
  387. /package/{providers/open-names → plugins/beta/use-location}/dist/umd/index.js.LICENSE.txt +0 -0
  388. /package/plugins/{use-location → beta/use-location}/src/UseLocation.jsx +0 -0
  389. /package/plugins/{use-location → beta/use-location}/src/UseLocationInit.jsx +0 -0
  390. /package/plugins/{use-location → beta/use-location}/src/defaults.js +0 -0
  391. /package/plugins/{use-location → beta/use-location}/src/events.js +0 -0
  392. /package/plugins/{use-location → beta/use-location}/src/index.js +0 -0
  393. /package/plugins/{use-location → beta/use-location}/src/manifest.js +0 -0
  394. /package/plugins/{use-location → beta/use-location}/src/reducer.js +0 -0
  395. /package/providers/{esri → beta/esri}/src/appEvents.js +0 -0
  396. /package/providers/{esri → beta/esri}/src/defaults.js +0 -0
  397. /package/providers/{esri → beta/esri}/src/esriProvider.scss +0 -0
  398. /package/providers/{esri → beta/esri}/src/utils/coords.js +0 -0
  399. /package/providers/{esri → beta/esri}/src/utils/detectWebGL.js +0 -0
  400. /package/providers/{esri → beta/esri}/src/utils/esriFixes.js +0 -0
  401. /package/providers/{esri → beta/esri}/src/utils/query.js +0 -0
  402. /package/providers/{esri → beta/esri}/src/utils/spatial.js +0 -0
  403. /package/providers/{open-names → beta/open-names}/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -0
  404. /package/providers/{open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt → beta/open-names/dist/esm/index.js.LICENSE.txt} +0 -0
  405. /package/providers/{open-names → beta/open-names}/src/index.js +0 -0
  406. /package/providers/{open-names → beta/open-names}/src/reverseGeocode.js +0 -0
  407. /package/providers/{open-names → beta/open-names}/src/utils/mapToLocationModel.js +0 -0
@@ -36,18 +36,19 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
36
36
 
37
37
  function createButtonClickHandler (btn, appState, evaluateProp) {
38
38
  const [, config] = btn
39
- const isOpen = !!(config.panelId && appState.openPanels[config.panelId])
39
+ const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
40
40
 
41
41
  return (e) => {
42
42
  if (typeof config.onClick === 'function') {
43
- return config.onClick(e, evaluateProp(ctx => ctx, config.pluginId))
43
+ config.onClick(e, evaluateProp(ctx => ctx, config.pluginId))
44
+ return
44
45
  }
45
46
 
46
47
  if (config.panelId) {
47
48
  const triggeringElement = e.currentTarget
48
49
  appState.dispatch({
49
- type: isOpen ? 'CLOSE_PANEL' : 'OPEN_PANEL',
50
- payload: isOpen
50
+ type: isPanelOpen ? 'CLOSE_PANEL' : 'OPEN_PANEL',
51
+ payload: isPanelOpen
51
52
  ? config.panelId
52
53
  : { panelId: config.panelId, props: { triggeringElement } }
53
54
  })
@@ -59,7 +60,7 @@ function renderButton ({ btn, appState, appConfig, evaluateProp, groupStart, gro
59
60
  const [buttonId, config] = btn
60
61
  const bpConfig = config[appState.breakpoint] ?? {}
61
62
  const handleClick = createButtonClickHandler(btn, appState, evaluateProp)
62
- const isOpen = !!(config.panelId && appState.openPanels[config.panelId])
63
+ const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
63
64
 
64
65
  return (
65
66
  <MapButton
@@ -73,10 +74,12 @@ function renderButton ({ btn, appState, appConfig, evaluateProp, groupStart, gro
73
74
  showLabel={bpConfig.showLabel}
74
75
  isDisabled={appState.disabledButtons.has(buttonId)}
75
76
  isHidden={appState.hiddenButtons.has(buttonId)}
76
- isPressed={config.pressedWhen ? appState.pressedButtons.has(buttonId) : undefined}
77
- isOpen={isOpen}
77
+ isPressed={(config.isPressed !== undefined || config.pressedWhen) ? appState.pressedButtons.has(buttonId) : undefined}
78
+ isExpanded={(config.isExpanded !== undefined || config.expandedWhen) ? appState.expandedButtons.has(buttonId) : undefined}
79
+ isPanelOpen={isPanelOpen}
78
80
  onClick={handleClick}
79
81
  panelId={config.panelId}
82
+ menuItems={config.menuItems}
80
83
  idPrefix={appConfig.id}
81
84
  groupStart={groupStart}
82
85
  groupMiddle={groupMiddle}
@@ -115,10 +118,10 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
115
118
  return matching.map((btn, idx) => {
116
119
  const [buttonId, config] = btn
117
120
  const key = config.group
118
- const indices = key != null ? groupMap.get(key) : null
121
+ const indices = key == null ? null : groupMap.get(key)
119
122
  const groupStart = indices ? idx === indices[0] : false
120
123
  const groupEnd = indices ? idx === indices[indices.length - 1] : false
121
- const groupMiddle = indices && indices.length >= 3 && !groupStart && !groupEnd
124
+ const groupMiddle = indices && indices.length >= 3 && !groupStart && !groupEnd // NOSONAR: 3 = minimum for a start/middle/end group
122
125
  const order = config[breakpoint]?.order ?? 0
123
126
 
124
127
  return {
@@ -36,6 +36,7 @@ describe('mapButtons module', () => {
36
36
  disabledButtons: new Set(),
37
37
  hiddenButtons: new Set(),
38
38
  pressedButtons: new Set(),
39
+ expandedButtons: new Set(),
39
40
  buttonConfig: {},
40
41
  panelConfig: {}
41
42
  }
@@ -137,15 +138,16 @@ describe('mapButtons module', () => {
137
138
  })
138
139
  })
139
140
 
140
- it('renders correct state flags for disabled, hidden, and pressed buttons', () => {
141
+ it('renders correct state flags for disabled, hidden, pressed and expanded buttons', () => {
141
142
  const state = {
142
143
  ...appState,
143
144
  disabledButtons: new Set(['id']),
144
145
  hiddenButtons: new Set(['id']),
145
- pressedButtons: new Set(['id'])
146
+ pressedButtons: new Set(['id']),
147
+ expandedButtons: new Set(['id'])
146
148
  }
147
- const result = render({ ...baseBtn, pressedWhen: jest.fn() }, state)
148
- expect(result.props).toMatchObject({ isDisabled: true, isHidden: true, isPressed: true })
149
+ const result = render({ ...baseBtn, pressedWhen: jest.fn(), expandedWhen: jest.fn() }, state)
150
+ expect(result.props).toMatchObject({ isDisabled: true, isHidden: true, isPressed: true, isExpanded: true })
149
151
  })
150
152
 
151
153
  it('uses empty object fallback for missing breakpoint config', () => {
@@ -2,6 +2,7 @@
2
2
  import React from 'react'
3
3
  import { withPluginContexts } from './pluginWrapper.js'
4
4
  import { allowedSlots } from './slots.js'
5
+ import { isConsumerHtml } from './slotHelpers.js'
5
6
 
6
7
  /**
7
8
  * Map controls for a given slot and app state.
@@ -12,6 +13,11 @@ export function mapControls ({ slot, appState, evaluateProp }) {
12
13
 
13
14
  return Object.values(controlConfig)
14
15
  .filter(control => {
16
+ // Consumer HTML controls are managed by HtmlElementHost
17
+ if (isConsumerHtml(control)) {
18
+ return false
19
+ }
20
+
15
21
  const bpConfig = control[breakpoint]
16
22
  if (!bpConfig) {
17
23
  return false
@@ -90,14 +90,22 @@ describe('mapControls', () => {
90
90
  expect(result[0].order).toBe(0)
91
91
  })
92
92
 
93
- it('renders HTML controls with dangerouslySetInnerHTML', () => {
93
+ it('renders plugin HTML controls with dangerouslySetInnerHTML', () => {
94
94
  defaultAppState.controlConfig = ({
95
- ctrlHtml: { id: 'ctrlHtml', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
95
+ ctrlHtml: { id: 'ctrlHtml', pluginId: 'plugin1', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
96
96
  })
97
97
  const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
98
98
  expect(result[0].element.props.dangerouslySetInnerHTML).toEqual({ __html: '<p>Hi</p>' })
99
99
  })
100
100
 
101
+ it('filters out consumer HTML controls (handled by HtmlElementHost)', () => {
102
+ defaultAppState.controlConfig = ({
103
+ ctrlHtml: { id: 'ctrlHtml', desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
104
+ })
105
+ const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
106
+ expect(result).toEqual([])
107
+ })
108
+
101
109
  it('handles plugin-less controls gracefully', () => {
102
110
  defaultAppState.controlConfig = ({
103
111
  ctrl2: { id: 'ctrl2', desktop: { slot: 'header' }, render: () => <div />, includeModes: ['view'] }
@@ -4,7 +4,38 @@ import { stringToKebab } from '../../utils/stringToKebab.js'
4
4
  import { withPluginContexts } from './pluginWrapper.js'
5
5
  import { Panel } from '../components/Panel/Panel.jsx'
6
6
  import { allowedSlots } from './slots.js'
7
+ import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
8
+
9
+ /**
10
+ * Determines whether a panel should be rendered in the given slot.
11
+ * Checks slot eligibility, mode restrictions, inline/fullscreen constraints,
12
+ * and ensures only the topmost modal panel is shown.
13
+ */
14
+ const isPanelVisible = (panelId, config, bpConfig, { targetSlot, slot, mode, isFullscreen, allowedModalPanelId }) => {
15
+ const isNextToButton = `${stringToKebab(panelId)}-button` === targetSlot
16
+ if (!allowedSlots.panel.includes(targetSlot) && !isNextToButton) {
17
+ return false
18
+ }
19
+ if (!isModeAllowed(config, mode)) {
20
+ return false
21
+ }
22
+ if (config.inline === false && !isFullscreen) {
23
+ return false
24
+ }
25
+ if (targetSlot !== slot) {
26
+ return false
27
+ }
28
+ if (bpConfig.modal && panelId !== allowedModalPanelId) {
29
+ return false
30
+ }
31
+ return true
32
+ }
7
33
 
34
+ /**
35
+ * Maps open panels to renderable entries for a given layout slot.
36
+ * Filters panels by slot, breakpoint, mode, and modal state, then wraps
37
+ * each panel's render function with the appropriate plugin contexts.
38
+ */
8
39
  export function mapPanels ({ slot, appState, evaluateProp }) {
9
40
  const { breakpoint, pluginRegistry, panelConfig, mode, openPanels } = appState
10
41
 
@@ -18,43 +49,25 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
18
49
 
19
50
  return openPanelEntries.map(([panelId, { props }]) => {
20
51
  const config = panelConfig[panelId]
21
-
22
52
  if (!config) {
23
53
  return null
24
54
  }
25
55
 
26
- const bpConfig = config[breakpoint]
27
- if (!bpConfig) {
56
+ // Consumer HTML panels are managed by HtmlElementHost
57
+ if (isConsumerHtml(config)) {
28
58
  return null
29
59
  }
30
60
 
31
- // Slot constriant: modal panels have a dedicated slot
32
- let targetSlot = bpConfig.modal ? 'modal' : bpConfig.slot
33
-
34
- // Slot constraint: bottom slot only permitted for mobile, revert to inset
35
- if (targetSlot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
36
- targetSlot = 'inset'
37
- }
38
-
39
- const isNextToButton = `${stringToKebab(panelId)}-button` === targetSlot
40
- const slotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
41
- const inModeWhitelist = config.includeModes?.includes(mode) ?? true
42
- const inExcludeModes = config.excludeModes?.includes(mode) ?? false
43
-
44
- if (!slotAllowed || !inModeWhitelist || inExcludeModes) {
45
- return null
46
- }
47
-
48
- // Skip panels marked as inline:false when not in fullscreen mode
49
- if (config.inline === false && !appState.isFullscreen) {
61
+ const bpConfig = config[breakpoint]
62
+ if (!bpConfig) {
50
63
  return null
51
64
  }
52
65
 
53
- if (targetSlot !== slot) {
54
- return null
55
- }
66
+ const targetSlot = resolveTargetSlot(bpConfig, breakpoint)
56
67
 
57
- if (bpConfig.modal && panelId !== allowedModalPanelId) {
68
+ if (!isPanelVisible(panelId, config, bpConfig, {
69
+ targetSlot, slot, mode, isFullscreen: appState.isFullscreen, allowedModalPanelId
70
+ })) {
58
71
  return null
59
72
  }
60
73
 
@@ -65,6 +65,28 @@ describe('mapPanels', () => {
65
65
  expect(map()).toEqual([])
66
66
  })
67
67
 
68
+ it('skips panel if mode is not allowed (isModeAllowed returns false)', () => {
69
+ // 1. Define config that only allows 'edit' mode
70
+ const panelConfig = {
71
+ p1: {
72
+ desktop: { slot: 'header' },
73
+ includeModes: ['edit']
74
+ }
75
+ }
76
+
77
+ // 2. Mock appState with 'view' mode
78
+ const state = {
79
+ ...defaultAppState,
80
+ mode: 'view',
81
+ panelConfig,
82
+ openPanels: { p1: { props: {} } }
83
+ }
84
+
85
+ // 3. Verify it's filtered out even though it's the right slot
86
+ const result = map(state, 'header')
87
+ expect(result).toEqual([])
88
+ })
89
+
68
90
  it('only allows last opened modal panel', () => {
69
91
  defaultAppState.panelConfig = ({
70
92
  p1: { desktop: { modal: true }, includeModes: ['view'] },
@@ -159,4 +181,11 @@ describe('mapPanels', () => {
159
181
  })
160
182
  expect(map().map(p => p.id)).toEqual(['p1'])
161
183
  })
184
+
185
+ it('filters out consumer HTML panels (handled by HtmlElementHost)', () => {
186
+ defaultAppState.panelConfig = ({
187
+ p1: { desktop: { slot: 'header' }, html: '<p>Hi</p>', includeModes: ['view'] }
188
+ })
189
+ expect(map()).toEqual([])
190
+ })
162
191
  })
@@ -44,4 +44,32 @@ describe('withPluginContexts', () => {
44
44
 
45
45
  expect(Wrapped2).toBe(Wrapped1)
46
46
  })
47
+
48
+ it('filters buttonConfig by pluginId', () => {
49
+ const Inner = jest.fn(() => <div>Inner</div>)
50
+
51
+ // Provide a buttonConfig with buttons for multiple plugins
52
+ const appStateMock = {
53
+ user: 'testUser',
54
+ buttonConfig: {
55
+ btn1: { label: 'A', pluginId: 'plugin1' },
56
+ btn2: { label: 'B', pluginId: 'plugin2' }
57
+ }
58
+ }
59
+
60
+ // Override the useApp hook for this test
61
+ const { useApp } = require('../store/appContext.js')
62
+ useApp.mockImplementation(() => appStateMock)
63
+
64
+ const Wrapped = withPluginContexts(Inner, { pluginId: 'plugin1', pluginConfig: { foo: 'bar' } })
65
+
66
+ render(<Wrapped customProp='xyz' />)
67
+
68
+ const props = Inner.mock.calls[0][0]
69
+
70
+ // buttonConfig should include only buttons belonging to plugin1
71
+ expect(props.buttonConfig).toEqual({
72
+ btn1: { label: 'A', pluginId: 'plugin1' }
73
+ })
74
+ })
47
75
  })
@@ -0,0 +1,60 @@
1
+ // src/App/renderer/slotHelpers.js
2
+ import { allowedSlots } from './slots.js'
3
+
4
+ /**
5
+ * Resolves the target slot for a panel based on its breakpoint config.
6
+ * Modal panels always render in the 'modal' slot, and the bottom slot
7
+ * is only available on mobile — tablet and desktop fall back to 'inset'.
8
+ */
9
+ export const resolveTargetSlot = (bpConfig, breakpoint) => {
10
+ if (bpConfig.modal) {
11
+ return 'modal'
12
+ }
13
+ if (bpConfig.slot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
14
+ return 'inset'
15
+ }
16
+ return bpConfig.slot
17
+ }
18
+
19
+ /**
20
+ * Checks whether the current application mode permits an item to be shown,
21
+ * based on its includeModes and excludModes configuration.
22
+ */
23
+ export const isModeAllowed = (config, mode) => {
24
+ if (config.includeModes && !config.includeModes.includes(mode)) {
25
+ return false
26
+ }
27
+ if (config.excludeModes?.includes(mode)) {
28
+ return false
29
+ }
30
+ return true
31
+ }
32
+
33
+ /**
34
+ * Checks whether a control should be visible based on breakpoint,
35
+ * mode, fullscreen, and slot constraints.
36
+ */
37
+ export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) => {
38
+ const bpConfig = control[breakpoint]
39
+ if (!bpConfig) {
40
+ return false
41
+ }
42
+ if (!allowedSlots.control.includes(bpConfig.slot)) {
43
+ return false
44
+ }
45
+ if (!isModeAllowed(control, mode)) {
46
+ return false
47
+ }
48
+ if (control.inline === false && !isFullscreen) {
49
+ return false
50
+ }
51
+ return true
52
+ }
53
+
54
+ /**
55
+ * Returns true if a panel/control was added via the consumer API with static HTML
56
+ * (i.e. not a plugin component).
57
+ */
58
+ export const isConsumerHtml = (config) => {
59
+ return typeof config.html === 'string' && !config.pluginId
60
+ }
@@ -0,0 +1,82 @@
1
+ import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
2
+
3
+ jest.mock('./slots.js', () => ({ allowedSlots: { control: ['inset', 'banner', 'actions'] } }))
4
+
5
+ describe('resolveTargetSlot', () => {
6
+ it('returns modal for modal panels', () => {
7
+ expect(resolveTargetSlot({ modal: true, slot: 'side' }, 'desktop')).toBe('modal')
8
+ })
9
+
10
+ it('replaces bottom with inset on tablet and desktop', () => {
11
+ expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('inset')
12
+ expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('inset')
13
+ })
14
+
15
+ it('keeps bottom on mobile', () => {
16
+ expect(resolveTargetSlot({ slot: 'bottom' }, 'mobile')).toBe('bottom')
17
+ })
18
+
19
+ it('returns slot as-is otherwise', () => {
20
+ expect(resolveTargetSlot({ slot: 'side' }, 'desktop')).toBe('side')
21
+ })
22
+ })
23
+
24
+ describe('isModeAllowed', () => {
25
+ it('returns true when no mode restrictions', () => {
26
+ expect(isModeAllowed({}, 'view')).toBe(true)
27
+ })
28
+
29
+ it('rejects when mode not in includeModes', () => {
30
+ expect(isModeAllowed({ includeModes: ['edit'] }, 'view')).toBe(false)
31
+ })
32
+
33
+ it('rejects when mode in excludeModes', () => {
34
+ expect(isModeAllowed({ excludeModes: ['view'] }, 'view')).toBe(false)
35
+ })
36
+
37
+ it('allows when mode matches includeModes', () => {
38
+ expect(isModeAllowed({ includeModes: ['view'] }, 'view')).toBe(true)
39
+ })
40
+ })
41
+
42
+ describe('isControlVisible', () => {
43
+ const base = { desktop: { slot: 'inset' } }
44
+
45
+ it('returns true for valid control', () => {
46
+ expect(isControlVisible(base, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(true)
47
+ })
48
+
49
+ it('returns false when breakpoint config missing', () => {
50
+ expect(isControlVisible(base, { breakpoint: 'mobile', mode: 'view', isFullscreen: false })).toBe(false)
51
+ })
52
+
53
+ it('returns false when slot not allowed', () => {
54
+ expect(isControlVisible({ desktop: { slot: 'invalid' } }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
55
+ })
56
+
57
+ it('returns false when mode not allowed', () => {
58
+ expect(isControlVisible({ ...base, includeModes: ['edit'] }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
59
+ })
60
+
61
+ it('returns false when inline:false and not fullscreen', () => {
62
+ expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(false)
63
+ })
64
+
65
+ it('returns true when inline:false and fullscreen', () => {
66
+ expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: true })).toBe(true)
67
+ })
68
+ })
69
+
70
+ describe('isConsumerHtml', () => {
71
+ it('returns true for consumer HTML config', () => {
72
+ expect(isConsumerHtml({ html: '<p>Hi</p>' })).toBe(true)
73
+ })
74
+
75
+ it('returns false when pluginId present', () => {
76
+ expect(isConsumerHtml({ html: '<p>Hi</p>', pluginId: 'p1' })).toBe(false)
77
+ })
78
+
79
+ it('returns false when no html', () => {
80
+ expect(isConsumerHtml({ render: () => {} })).toBe(false)
81
+ })
82
+ })
@@ -21,9 +21,12 @@ export const AppProvider = ({ options, children }) => {
21
21
  topRightColRef: useRef(null),
22
22
  insetRef: useRef(null),
23
23
  rightRef: useRef(null),
24
+ middleRef: useRef(null),
25
+ bottomRef: useRef(null),
24
26
  footerRef: useRef(null),
25
27
  actionsRef: useRef(null),
26
28
  bannerRef: useRef(null),
29
+ modalRef: useRef(null),
27
30
  viewportRef: useRef(null)
28
31
  }
29
32
 
@@ -4,6 +4,7 @@ import { AppProvider, AppContext } from './AppProvider.jsx'
4
4
  import { createMockRegistries } from '../../test-utils.js'
5
5
  import * as mediaHook from '../hooks/useMediaQueryDispatch.js'
6
6
  import * as detectInterface from '../../utils/detectInterfaceType.js'
7
+ import * as appReducerModule from './appReducer.js'
7
8
 
8
9
  jest.mock('../hooks/useMediaQueryDispatch.js')
9
10
  jest.mock('../../utils/detectInterfaceType.js')
@@ -111,4 +112,43 @@ describe('AppProvider', () => {
111
112
  expect(contextValue.layoutRefs).toHaveProperty('mainRef')
112
113
  expect(contextValue.layoutRefs).toHaveProperty('footerRef')
113
114
  })
115
+
116
+ test('dispatch fallback uses options.panelRegistry.getPanelConfig() when state.panelConfig missing', () => {
117
+ const getPanelConfigMock = jest.fn(() => ({ panel1: {} }))
118
+
119
+ // Mock initialState to return state without panelConfig but with panelRegistry
120
+ jest.spyOn(appReducerModule, 'initialState').mockImplementation(() => ({
121
+ mode: 'view',
122
+ previousMode: 'edit',
123
+ openPanels: {},
124
+ previousOpenPanels: {},
125
+ interfaceType: 'default',
126
+ isFullscreen: false,
127
+ hasExclusiveControl: false,
128
+ panelRegistry: { getPanelConfig: getPanelConfigMock } // <-- provide it here!
129
+ }))
130
+
131
+ const mockEventBus = { on: jest.fn(), off: jest.fn() }
132
+ const mockBreakpointDetector = { subscribe: jest.fn(() => jest.fn()) }
133
+ const mockOptions = {
134
+ ...createMockRegistries({ panelConfig: undefined }),
135
+ eventBus: mockEventBus,
136
+ breakpointDetector: mockBreakpointDetector
137
+ }
138
+
139
+ render(
140
+ <AppProvider options={mockOptions}>
141
+ <div>Child</div>
142
+ </AppProvider>
143
+ )
144
+
145
+ // Trigger a dispatch via eventBus to hit the dispatch wrapper
146
+ act(() => {
147
+ mockEventBus.on.mock.calls.forEach(([event, handler]) => {
148
+ if (event === 'app:setmode') handler('newMode')
149
+ })
150
+ })
151
+
152
+ expect(getPanelConfigMock).toHaveBeenCalled()
153
+ })
114
154
  })
@@ -27,19 +27,21 @@ export const PluginProvider = ({ children }) => {
27
27
  }
28
28
 
29
29
  // Initialize ref registry for each plugin
30
- if (!refs.current[plugin.id]) refs.current[plugin.id] = {}
30
+ if (!refs.current[plugin.id]) {
31
+ refs.current[plugin.id] = {}
32
+ }
31
33
  })
32
34
 
33
35
  // Combined reducer
34
- const combinedReducer = (state, action) => {
36
+ const combinedReducer = (pluginState, action) => {
35
37
  const { pluginId } = action
36
38
  if (pluginId && pluginReducers[pluginId]) {
37
39
  return {
38
- ...state,
39
- [pluginId]: pluginReducers[pluginId](state[pluginId], action)
40
+ ...pluginState,
41
+ [pluginId]: pluginReducers[pluginId](pluginState[pluginId], action)
40
42
  }
41
43
  }
42
- return state
44
+ return pluginState
43
45
  }
44
46
 
45
47
  const [state, dispatch] = useReducer(combinedReducer, initialState)
@@ -200,6 +200,22 @@ const toggleButtonPressed = (state, payload) => {
200
200
  }
201
201
  }
202
202
 
203
+ const toggleButtonExpanded = (state, payload) => {
204
+ const { id, isExpanded } = payload
205
+ const updated = new Set(state.expandedButtons)
206
+
207
+ if (isExpanded) {
208
+ updated.add(id)
209
+ } else {
210
+ updated.delete(id)
211
+ }
212
+
213
+ return {
214
+ ...state,
215
+ expandedButtons: updated
216
+ }
217
+ }
218
+
203
219
  // Registry mutation actions
204
220
  const registerButton = (state, payload) => {
205
221
  return {
@@ -212,6 +228,30 @@ const addButton = (state, payload) => {
212
228
  const { id, config } = payload
213
229
  const newButtonConfig = addButtonFn(state.buttonConfig, id, config)
214
230
 
231
+ // Set hidden state
232
+ const hiddenButtons = new Set(state.hiddenButtons)
233
+ if (config.isHidden) {
234
+ hiddenButtons.add(id)
235
+ }
236
+
237
+ // Set disabled state
238
+ const disabledButtons = new Set(state.disabledButtons)
239
+ if (config.isDisabled) {
240
+ disabledButtons.add(id)
241
+ }
242
+
243
+ // Set pressed state
244
+ const pressedButtons = new Set(state.pressedButtons)
245
+ if (config.isPressed) {
246
+ pressedButtons.add(id)
247
+ }
248
+
249
+ // Set expanded state
250
+ const expandedButtons = new Set(state.expandedButtons)
251
+ if (config.isExpanded) {
252
+ expandedButtons.add(id)
253
+ }
254
+
215
255
  // Also update the registry instance for persistence across app lifecycle
216
256
  if (state.buttonRegistry?.addButton) {
217
257
  state.buttonRegistry.addButton(id, config)
@@ -219,7 +259,11 @@ const addButton = (state, payload) => {
219
259
 
220
260
  return {
221
261
  ...state,
222
- buttonConfig: newButtonConfig
262
+ buttonConfig: newButtonConfig,
263
+ hiddenButtons,
264
+ disabledButtons,
265
+ pressedButtons,
266
+ expandedButtons
223
267
  }
224
268
  }
225
269
 
@@ -313,6 +357,7 @@ export const actionsMap = {
313
357
  TOGGLE_BUTTON_DISABLED: toggleButtonDisabled,
314
358
  TOGGLE_BUTTON_HIDDEN: toggleButtonHidden,
315
359
  TOGGLE_BUTTON_PRESSED: toggleButtonPressed,
360
+ TOGGLE_BUTTON_EXPANDED: toggleButtonExpanded,
316
361
  // Registry actions
317
362
  REGISTER_BUTTON: registerButton,
318
363
  ADD_BUTTON: addButton,
@@ -30,6 +30,7 @@ describe('actionsMap full coverage', () => {
30
30
  disabledButtons: new Set(['btn1']),
31
31
  hiddenButtons: new Set(['btn3']),
32
32
  pressedButtons: new Set(['btn5']),
33
+ expandedButtons: new Set(['btn7']),
33
34
  panelConfig: mockPanelConfig,
34
35
  panelRegistry: { addPanel: jest.fn(), removePanel: jest.fn(), getPanelConfig: jest.fn(() => mockPanelConfig) },
35
36
  buttonConfig: {},
@@ -152,6 +153,13 @@ describe('actionsMap full coverage', () => {
152
153
  expect(r2.pressedButtons.has('btn5')).toBe(false)
153
154
  })
154
155
 
156
+ test('TOGGLE_BUTTON_EXPANDED adds/removes button', () => {
157
+ const r1 = actionsMap.TOGGLE_BUTTON_EXPANDED(state, { id: 'btn8', isExpanded: true })
158
+ expect(r1.expandedButtons.has('btn8')).toBe(true)
159
+ const r2 = actionsMap.TOGGLE_BUTTON_EXPANDED(state, { id: 'btn7', isExpanded: false })
160
+ expect(r2.expandedButtons.has('btn7')).toBe(false)
161
+ })
162
+
155
163
  test('REGISTER_PANEL updates panelConfig', () => {
156
164
  const payload = { id: 'panelX', config: { desktop: { slot: 'side' } } }
157
165
  const result = actionsMap.REGISTER_PANEL(state, payload)
@@ -204,6 +212,19 @@ describe('actionsMap full coverage', () => {
204
212
  expect(state.buttonRegistry.addButton).toHaveBeenCalled()
205
213
  })
206
214
 
215
+ test('ADD_BUTTON sets hidden, disabled, pressed and expanded state when config flags are true', () => {
216
+ const payload = {
217
+ id: 'btnSpecial',
218
+ config: { isHidden: true, isDisabled: true, isPressed: true, isExpanded: true }
219
+ }
220
+ const result = actionsMap.ADD_BUTTON(state, payload)
221
+ expect(result.buttonConfig.btnSpecial).toBeDefined()
222
+ expect(result.hiddenButtons.has('btnSpecial')).toBe(true)
223
+ expect(result.disabledButtons.has('btnSpecial')).toBe(true)
224
+ expect(result.pressedButtons.has('btnSpecial')).toBe(true)
225
+ expect(result.expandedButtons.has('btnSpecial')).toBe(true)
226
+ })
227
+
207
228
  // ---------------------- FALLBACK / OPTIONAL BRANCHES ----------------------
208
229
  test('SET_MODE uses panelRegistry.getPanelConfig() when panelConfig missing', () => {
209
230
  const tmp = { ...state, panelConfig: undefined }
@@ -41,6 +41,7 @@ export const initialState = (config) => {
41
41
  disabledButtons: new Set(),
42
42
  hiddenButtons: new Set(),
43
43
  pressedButtons: new Set(),
44
+ expandedButtons: new Set(),
44
45
  hasExclusiveControl: false,
45
46
  openPanels,
46
47
  previousOpenPanels: {},