@defra/interactive-map 0.0.21-alpha → 0.0.23-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 (218) 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/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 +39 -0
  8. package/docs/api/context.md +4 -4
  9. package/docs/api/map-style-config.md +11 -2
  10. package/docs/api/marker-config.md +38 -7
  11. package/docs/api/panel-definition.md +7 -3
  12. package/docs/api/symbol-config.md +10 -26
  13. package/docs/api/symbol-registry.md +28 -13
  14. package/docs/api.md +65 -4
  15. package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
  16. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  17. package/docs/examples/index.mdx +12 -0
  18. package/docs/examples/toggle-marker-label.mdx +89 -0
  19. package/docs/plugins/datasets.md +39 -1
  20. package/docs/plugins/interact.md +4 -11
  21. package/docusaurus.config.cjs +2 -1
  22. package/package.json +10 -12
  23. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -0
  24. package/plugins/beta/datasets/dist/css/index.css +2 -2
  25. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  26. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +2 -0
  27. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +2 -0
  28. package/plugins/beta/datasets/dist/umd/maplibre/index.js +2 -0
  29. package/plugins/beta/datasets/src/adapters/maplibre/index.js +1 -1
  30. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
  31. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
  32. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
  33. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
  34. package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
  35. package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
  36. package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
  37. package/plugins/beta/datasets/src/index.maplibre.umd.js +23 -0
  38. package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
  39. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  40. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  41. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  42. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  43. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  44. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  45. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  46. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  47. package/plugins/beta/use-location/src/manifest.js +1 -0
  48. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  49. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  50. package/plugins/interact/dist/umd/index.js +1 -1
  51. package/plugins/interact/src/InteractInit.jsx +23 -55
  52. package/plugins/interact/src/InteractInit.test.js +58 -80
  53. package/plugins/interact/src/defaults.js +1 -3
  54. package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
  55. package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
  56. package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
  57. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
  58. package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
  59. package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
  60. package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
  61. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
  62. package/plugins/interact/src/hooks/useMapItemList.js +187 -0
  63. package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
  64. package/plugins/interact/src/index.js +0 -2
  65. package/plugins/interact/src/index.test.js +0 -2
  66. package/plugins/interact/src/manifest.js +37 -6
  67. package/plugins/interact/src/reducer.js +8 -4
  68. package/plugins/interact/src/reducer.test.js +18 -3
  69. package/plugins/interact/src/utils/buildStylesMap.js +21 -10
  70. package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
  71. package/plugins/interact/src/utils/spatial.js +0 -15
  72. package/plugins/interact/src/utils/spatial.test.js +1 -13
  73. package/plugins/search/dist/css/index.css +1 -1
  74. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  75. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  76. package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
  77. package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
  78. package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
  79. package/plugins/search/src/search.scss +1 -0
  80. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  81. package/providers/beta/esri/src/esriProvider.js +4 -0
  82. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  83. package/providers/maplibre/dist/esm/index.js +1 -1
  84. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  85. package/providers/maplibre/dist/umd/index.js +1 -1
  86. package/providers/maplibre/src/index.js +7 -1
  87. package/providers/maplibre/src/index.test.js +15 -2
  88. package/providers/maplibre/src/mapEvents.js +11 -5
  89. package/providers/maplibre/src/mapEvents.test.js +14 -3
  90. package/providers/maplibre/src/maplibreProvider.js +18 -11
  91. package/providers/maplibre/src/maplibreProvider.test.js +43 -36
  92. package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
  93. package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
  94. package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
  95. package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
  96. package/providers/maplibre/src/utils/patternImages.js +10 -12
  97. package/providers/maplibre/src/utils/patternImages.test.js +145 -167
  98. package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
  99. package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
  100. package/providers/maplibre/src/utils/symbolImages.js +23 -86
  101. package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
  102. package/rollup.esm.mjs +1 -1
  103. package/src/App/components/Actions/Actions.module.scss +1 -0
  104. package/src/App/components/Actions/Actions.test.jsx +17 -4
  105. package/src/App/components/Hints/Hints.jsx +52 -0
  106. package/src/App/components/Hints/Hints.module.scss +37 -0
  107. package/src/App/components/Hints/Hints.test.jsx +104 -0
  108. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
  109. package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
  110. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
  111. package/src/App/components/Markers/LabelMarker.jsx +14 -0
  112. package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
  113. package/src/App/components/Markers/Markers.jsx +44 -75
  114. package/src/App/components/Markers/Markers.module.scss +58 -0
  115. package/src/App/components/Markers/Markers.test.jsx +90 -113
  116. package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
  117. package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
  118. package/src/App/components/Markers/SymbolMarker.jsx +31 -0
  119. package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
  120. package/src/App/components/Panel/Panel.jsx +1 -1
  121. package/src/App/components/Panel/Panel.module.scss +1 -0
  122. package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
  123. package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
  124. package/src/App/components/Tabs/Tabs.jsx +68 -0
  125. package/src/App/components/Tabs/Tabs.module.scss +57 -0
  126. package/src/App/components/Tabs/Tabs.test.jsx +121 -0
  127. package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
  128. package/src/App/components/Viewport/Features.jsx +35 -0
  129. package/src/App/components/Viewport/Features.module.scss +31 -0
  130. package/src/App/components/Viewport/Features.test.jsx +124 -0
  131. package/src/App/components/Viewport/MapController.jsx +1 -1
  132. package/src/App/components/Viewport/MapStatus.jsx +0 -4
  133. package/src/App/components/Viewport/Viewport.jsx +35 -29
  134. package/src/App/components/Viewport/Viewport.module.scss +7 -21
  135. package/src/App/components/Viewport/Viewport.test.jsx +108 -65
  136. package/src/App/controls/keyboardActions.js +9 -3
  137. package/src/App/controls/keyboardActions.test.js +122 -87
  138. package/src/App/controls/keyboardShortcuts.js +1 -14
  139. package/src/App/hooks/useFeatureFocus.js +199 -0
  140. package/src/App/hooks/useFeatureFocus.test.js +635 -0
  141. package/src/App/hooks/useFeatureItems.js +30 -0
  142. package/src/App/hooks/useFeatureItems.test.js +97 -0
  143. package/src/App/hooks/useFocusVisible.js +4 -7
  144. package/src/App/hooks/useFocusVisible.test.js +6 -6
  145. package/src/App/hooks/useKeyboardHint.js +9 -34
  146. package/src/App/hooks/useKeyboardHint.test.js +34 -84
  147. package/src/App/hooks/useKeyboardShortcuts.js +8 -5
  148. package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
  149. package/src/App/hooks/useLayoutMeasurements.js +10 -4
  150. package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
  151. package/src/App/hooks/useMapURLSync.js +3 -3
  152. package/src/App/hooks/useMapURLSync.test.js +20 -3
  153. package/src/App/hooks/useMarkerCursor.js +33 -0
  154. package/src/App/hooks/useMarkerCursor.test.js +87 -0
  155. package/src/App/hooks/useMarkersAPI.js +32 -9
  156. package/src/App/hooks/useMarkersAPI.test.js +162 -94
  157. package/src/App/layout/Layout.jsx +2 -0
  158. package/src/App/layout/Layout.test.jsx +3 -0
  159. package/src/App/registry/keyboardShortcutRegistry.js +3 -2
  160. package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
  161. package/src/App/renderer/mapButtons.js +5 -1
  162. package/src/App/renderer/mapButtons.test.js +37 -1
  163. package/src/App/store/ServiceProvider.jsx +4 -1
  164. package/src/App/store/ServiceProvider.test.jsx +11 -0
  165. package/src/App/store/appActionsMap.js +5 -1
  166. package/src/App/store/appActionsMap.test.js +13 -0
  167. package/src/App/store/appReducer.js +1 -0
  168. package/src/App/store/mapActionsMap.js +2 -1
  169. package/src/InteractiveMap/InteractiveMap.js +13 -1
  170. package/src/InteractiveMap/InteractiveMap.test.js +6 -4
  171. package/src/InteractiveMap/behaviourController.js +1 -2
  172. package/src/InteractiveMap/buttonManager.js +1 -3
  173. package/src/InteractiveMap/buttonManager.test.js +16 -8
  174. package/src/InteractiveMap/domStateManager.js +2 -3
  175. package/src/InteractiveMap/historyManager.js +4 -3
  176. package/src/config/appConfig.js +3 -1
  177. package/src/config/appConfig.test.js +4 -0
  178. package/src/config/defaults.js +3 -2
  179. package/src/config/events.js +8 -0
  180. package/src/config/mapTheme.js +8 -4
  181. package/src/config/mergeConfig.js +8 -2
  182. package/src/config/mergeConfig.test.js +19 -0
  183. package/src/config/symbolConfig.js +23 -16
  184. package/src/scss/main.scss +3 -0
  185. package/src/scss/tools/_border-focus.scss +0 -1
  186. package/src/services/eventBus.js +15 -0
  187. package/src/services/eventBus.test.js +11 -0
  188. package/src/services/hints.js +47 -0
  189. package/src/services/hints.test.js +110 -0
  190. package/src/services/patternRegistry.js +69 -1
  191. package/src/services/patternRegistry.test.js +112 -1
  192. package/src/services/reverseGeocode.js +4 -0
  193. package/src/services/reverseGeocode.test.js +11 -1
  194. package/src/services/symbolRegistry.js +152 -17
  195. package/src/services/symbolRegistry.test.js +141 -31
  196. package/src/types.js +93 -16
  197. package/src/utils/constrainKeyboardFocus.js +5 -5
  198. package/src/utils/detectInterfaceType.js +27 -5
  199. package/src/utils/detectInterfaceType.test.js +29 -3
  200. package/src/utils/mapStateSync.js +2 -2
  201. package/src/utils/mapStateSync.test.js +18 -0
  202. package/src/utils/patternUtils.js +4 -67
  203. package/src/utils/patternUtils.test.js +1 -123
  204. package/src/utils/symbolUtils.js +11 -13
  205. package/src/utils/symbolUtils.test.js +18 -26
  206. package/webpack.dev.mjs +4 -0
  207. package/webpack.umd.mjs +9 -6
  208. package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +0 -1
  209. package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +0 -2
  210. package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +0 -1
  211. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +0 -2
  212. package/plugins/beta/datasets/dist/umd/index.js +0 -2
  213. package/plugins/interact/dist/css/index.css +0 -1
  214. package/plugins/interact/src/interact.scss +0 -24
  215. /package/plugins/beta/datasets/{adapters/maplibre/dist → dist/adapters/maplibre}/esm/index.js +0 -0
  216. /package/plugins/beta/datasets/{adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt → dist/umd/maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt} +0 -0
  217. /package/plugins/beta/datasets/dist/umd/{im-datasets-plugin.js.LICENSE.txt → maplibre/im-datasets-plugin.js.LICENSE.txt} +0 -0
  218. /package/plugins/beta/datasets/dist/umd/{index.js.LICENSE.txt → maplibre/index.js.LICENSE.txt} +0 -0
@@ -38,7 +38,8 @@ describe('mapButtons module', () => {
38
38
  pressedButtons: new Set(),
39
39
  expandedButtons: new Set(),
40
40
  buttonConfig: {},
41
- panelConfig: {}
41
+ panelConfig: {},
42
+ layoutRefs: { viewportRef: { current: { focus: jest.fn() } } }
42
43
  }
43
44
  appState.buttonConfig = ({})
44
45
  getPanelConfig.mockReturnValue({})
@@ -227,6 +228,41 @@ describe('mapButtons module', () => {
227
228
  // dispatch should NOT be called because panelId is missing
228
229
  expect(appState.dispatch).not.toHaveBeenCalled()
229
230
  })
231
+
232
+ it('focuses viewport via requestAnimationFrame after onClick when not toggle and no keepFocus', () => {
233
+ const rafSpy = jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => { cb(); return 1 })
234
+ const viewportFocusSpy = jest.spyOn(appState.layoutRefs.viewportRef.current, 'focus')
235
+ const onClick = jest.fn()
236
+ render({ ...baseBtn, onClick }).props.onClick({})
237
+ expect(viewportFocusSpy).toHaveBeenCalled()
238
+ rafSpy.mockRestore()
239
+ viewportFocusSpy.mockRestore()
240
+ })
241
+
242
+ it('does not call requestAnimationFrame when keepFocus is true', () => {
243
+ const rafSpy = jest.spyOn(global, 'requestAnimationFrame')
244
+ const onClick = jest.fn()
245
+ render({ ...baseBtn, onClick, keepFocus: true }).props.onClick({})
246
+ expect(rafSpy).not.toHaveBeenCalled()
247
+ rafSpy.mockRestore()
248
+ })
249
+
250
+ it('does not call requestAnimationFrame when button is a toggle (pressedWhen set)', () => {
251
+ const rafSpy = jest.spyOn(global, 'requestAnimationFrame')
252
+ const onClick = jest.fn()
253
+ render({ ...baseBtn, onClick, pressedWhen: jest.fn() }).props.onClick({})
254
+ expect(rafSpy).not.toHaveBeenCalled()
255
+ rafSpy.mockRestore()
256
+ })
257
+
258
+ it('includes focusOnOpen: false in OPEN_PANEL payload when keepFocus is true', () => {
259
+ const mockButtonEl = document.createElement('button')
260
+ render({ ...baseBtn, panelId: 'p1', keepFocus: true }).props.onClick({ currentTarget: mockButtonEl })
261
+ expect(appState.dispatch).toHaveBeenCalledWith({
262
+ type: 'OPEN_PANEL',
263
+ payload: { panelId: 'p1', props: { triggeringElement: mockButtonEl }, focusOnOpen: false }
264
+ })
265
+ })
230
266
  })
231
267
 
232
268
  // -------------------------
@@ -1,6 +1,7 @@
1
1
  // src/App/store/ServiceProvider.jsx
2
2
  import React, { createContext, useMemo, useRef } from 'react'
3
3
  import { createAnnouncer } from '../../services/announcer.js'
4
+ import { createHints } from '../../services/hints.js'
4
5
  import { reverseGeocode } from '../../services/reverseGeocode.js'
5
6
  import { useConfig } from '../store/configContext.js'
6
7
  import { closeApp } from '../../services/closeApp.js'
@@ -13,18 +14,20 @@ export const ServiceProvider = ({ eventBus, children }) => {
13
14
  const { id, handleExitClick, symbolDefaults: constructorSymbolDefaults } = useConfig()
14
15
  const mapStatusRef = useRef(null)
15
16
  const announce = useMemo(() => createAnnouncer(mapStatusRef), [])
17
+ const hints = useMemo(() => createHints(announce), [announce])
16
18
 
17
19
  symbolRegistry.setDefaults(constructorSymbolDefaults || {})
18
20
 
19
21
  const services = useMemo(() => ({
20
22
  announce,
23
+ hints,
21
24
  reverseGeocode: (zoom, center) => reverseGeocode(zoom, center),
22
25
  eventBus,
23
26
  mapStatusRef,
24
27
  closeApp: () => closeApp(id, handleExitClick, eventBus),
25
28
  symbolRegistry,
26
29
  patternRegistry
27
- }), [announce])
30
+ }), [announce, hints])
28
31
 
29
32
  return (
30
33
  <ServiceContext.Provider value={services}>
@@ -11,6 +11,11 @@ jest.mock('../../services/announcer.js', () => ({
11
11
  createAnnouncer: jest.fn(() => jest.fn())
12
12
  }))
13
13
 
14
+ const mockShow = jest.fn()
15
+ jest.mock('../../services/hints.js', () => ({
16
+ createHints: jest.fn(() => ({ show: mockShow, dismiss: jest.fn(), subscribe: jest.fn() }))
17
+ }))
18
+
14
19
  jest.mock('../../services/reverseGeocode.js', () => ({
15
20
  reverseGeocode: jest.fn(() => 'mockedReverseGeocode')
16
21
  }))
@@ -57,6 +62,12 @@ describe('ServiceProvider', () => {
57
62
  expect(result.current).toBeTruthy()
58
63
  })
59
64
 
65
+ test('hints.show() delegates to createHints show', () => {
66
+ const { result } = renderHook(() => React.useContext(ServiceContext), { wrapper })
67
+ result.current.hints.show('<b>test</b>', { duration: 2000 })
68
+ expect(mockShow).toHaveBeenCalledWith('<b>test</b>', { duration: 2000 })
69
+ })
70
+
60
71
  test('closeApp calls closeApp service with id and handleExitClick', () => {
61
72
  const mockHandleExitClick = jest.fn()
62
73
  const { useConfig } = require('../store/configContext.js')
@@ -23,7 +23,7 @@ function buildOpenPanels (state, panelId, breakpoint, props, focusOnOpen) {
23
23
  return {
24
24
  ...(isExclusiveNonModal ? {} : filteredPanels),
25
25
  ...(isModal ? state.openPanels : {}),
26
- [panelId]: { props, ...(focusOnOpen && { focusOnOpen: true }) }
26
+ [panelId]: { props, ...(focusOnOpen !== undefined && { focusOnOpen }) }
27
27
  }
28
28
  }
29
29
 
@@ -158,6 +158,9 @@ const setPluginsEvaluated = (state) =>
158
158
  const clearPluginsEvaluated = (state) =>
159
159
  state.arePluginsEvaluated ? { ...state, arePluginsEvaluated: false } : state
160
160
 
161
+ const setListboxActive = (state) =>
162
+ state.listboxIsActive ? state : { ...state, listboxIsActive: true }
163
+
161
164
  const setSafeZoneInset = (state, { safeZoneInset, syncMapPadding = true }) => {
162
165
  return shallowEqual(state.safeZoneInset, safeZoneInset)
163
166
  ? state
@@ -373,6 +376,7 @@ export const actionsMap = {
373
376
  SET_MODE: setMode,
374
377
  PLUGINS_EVALUATED: setPluginsEvaluated,
375
378
  CLEAR_PLUGINS_EVALUATED: clearPluginsEvaluated,
379
+ SET_LISTBOX_ACTIVE: setListboxActive,
376
380
  SET_SAFE_ZONE_INSET: setSafeZoneInset,
377
381
  REVERT_MODE: revertMode,
378
382
  OPEN_PANEL: openPanel,
@@ -425,4 +425,17 @@ describe('actionsMap full coverage', () => {
425
425
  expect(res.openPanels).toHaveProperty('p1')
426
426
  })
427
427
  })
428
+
429
+ describe('SET_LISTBOX_ACTIVE', () => {
430
+ test('sets listboxIsActive to true when currently false', () => {
431
+ const res = actionsMap.SET_LISTBOX_ACTIVE({ ...state, listboxIsActive: false })
432
+ expect(res.listboxIsActive).toBe(true)
433
+ })
434
+
435
+ test('returns same state reference when listboxIsActive is already true', () => {
436
+ const activeState = { ...state, listboxIsActive: true }
437
+ const res = actionsMap.SET_LISTBOX_ACTIVE(activeState)
438
+ expect(res).toBe(activeState)
439
+ })
440
+ })
428
441
  })
@@ -47,6 +47,7 @@ export const initialState = (config) => {
47
47
  hasExclusiveControl: false,
48
48
  openPanels,
49
49
  previousOpenPanels: {},
50
+ listboxIsActive: false,
50
51
  syncMapPadding: true,
51
52
  pluginRegistry,
52
53
  buttonRegistry,
@@ -36,7 +36,8 @@ const updateCrossHair = (state, payload) => {
36
36
 
37
37
  const upsertMarker = (state, payload) => {
38
38
  const map = new Map(state.markers.items.map(item => [item.id, item]))
39
- map.set(payload.id, payload)
39
+ const existing = map.get(payload.id) || {}
40
+ map.set(payload.id, { ...existing, ...payload })
40
41
  const newItems = Array.from(map.values())
41
42
 
42
43
  return {
@@ -144,7 +144,7 @@ export default class InteractiveMap {
144
144
  if (history.state?.isBack) {
145
145
  history.back()
146
146
  } else {
147
- const key = this.config.mapViewParamKey
147
+ const key = this.config.mapViewQueryParam
148
148
  const newUrl = this._removeMapParamFromUrl(location.href, key)
149
149
  history.replaceState(history.state, '', newUrl)
150
150
  }
@@ -336,6 +336,18 @@ export default class InteractiveMap {
336
336
  this.eventBus.emit(events.APP_ADD_MARKER, { id, coords, options })
337
337
  }
338
338
 
339
+ /**
340
+ * Update an existing marker's options without replacing it. No-op if the marker id is not found.
341
+ * Merges the provided options into the existing marker — unspecified properties are preserved.
342
+ * Pass `coords` inside options to move the marker to new coordinates.
343
+ *
344
+ * @param {string} id - Marker identifier to update.
345
+ * @param {MarkerOptions & { coords?: [number, number] }} [options] - Properties to merge into the marker.
346
+ */
347
+ updateMarker (id, options) {
348
+ this.eventBus.emit(events.APP_UPDATE_MARKER, { id, options })
349
+ }
350
+
339
351
  /**
340
352
  * Remove a marker from the map.
341
353
  *
@@ -274,7 +274,7 @@ describe('InteractiveMap Core Functionality', () => {
274
274
  const map = new InteractiveMap('map', {
275
275
  behaviour: 'buttonFirst',
276
276
  mapProvider: mapProviderMock,
277
- mapViewParamKey: 'mv',
277
+ mapViewQueryParam: 'mv',
278
278
  manageHistoryState: true,
279
279
  preserveStateOnClose: false
280
280
  })
@@ -303,7 +303,7 @@ describe('InteractiveMap Core Functionality', () => {
303
303
  const map = new InteractiveMap('map', {
304
304
  behaviour: 'buttonFirst',
305
305
  mapProvider: mapProviderMock,
306
- mapViewParamKey: 'mv',
306
+ mapViewQueryParam: 'mv',
307
307
  manageHistoryState: true,
308
308
  preserveStateOnClose: true
309
309
  })
@@ -360,7 +360,7 @@ describe('InteractiveMap Core Functionality', () => {
360
360
  const map = new InteractiveMap('map', {
361
361
  behaviour: 'buttonFirst',
362
362
  mapProvider: mapProviderMock,
363
- mapViewParamKey: 'mv',
363
+ mapViewQueryParam: 'mv',
364
364
  manageHistoryState: true,
365
365
  preserveStateOnClose: false
366
366
  })
@@ -380,7 +380,7 @@ describe('InteractiveMap Core Functionality', () => {
380
380
  const map = new InteractiveMap('map', {
381
381
  behaviour: 'buttonFirst',
382
382
  mapProvider: mapProviderMock,
383
- mapViewParamKey: 'mv',
383
+ mapViewQueryParam: 'mv',
384
384
  manageHistoryState: false,
385
385
  preserveStateOnClose: false
386
386
  })
@@ -586,6 +586,7 @@ describe('InteractiveMap Public API Methods', () => {
586
586
  map.off('testEvent', cb)
587
587
  map.emit('customEvent', 123)
588
588
  map.addMarker('marker-1', coords, options)
589
+ map.updateMarker('marker-1', options)
589
590
  map.removeMarker('marker-1')
590
591
  map.setMode('test-mode')
591
592
 
@@ -593,6 +594,7 @@ describe('InteractiveMap Public API Methods', () => {
593
594
  expect(map.eventBus.off).toHaveBeenCalledWith('testEvent', cb)
594
595
  expect(map.eventBus.emit).toHaveBeenCalledWith('customEvent', 123)
595
596
  expect(map.eventBus.emit).toHaveBeenCalledWith('app:addmarker', { id: 'marker-1', coords, options })
597
+ expect(map.eventBus.emit).toHaveBeenCalledWith('app:updatemarker', { id: 'marker-1', options })
596
598
  expect(map.eventBus.emit).toHaveBeenCalledWith('app:removemarker', 'marker-1')
597
599
  expect(map.eventBus.emit).toHaveBeenCalledWith('app:setmode', 'test-mode')
598
600
  })
@@ -1,7 +1,6 @@
1
1
  import { getQueryParam } from '../utils/queryString.js'
2
2
  import { isHybridFullscreen } from '../utils/getIsFullscreen.js'
3
3
  import { updateDOMState } from './domStateManager.js'
4
- import defaults from '../config/defaults.js'
5
4
 
6
5
  // -----------------------------------------------------------------------------
7
6
  // Public API
@@ -18,7 +17,7 @@ import defaults from '../config/defaults.js'
18
17
  */
19
18
  function shouldLoadComponent (config) {
20
19
  const { id, behaviour } = config
21
- const hasViewParam = getQueryParam(defaults.mapViewParamKey) === id
20
+ const hasViewParam = getQueryParam(config.mapViewQueryParam) === id
22
21
 
23
22
  return ['mapOnly', 'inline'].includes(behaviour) ||
24
23
  (behaviour === 'hybrid' && !isHybridFullscreen(config)) ||
@@ -12,8 +12,6 @@
12
12
  * @returns {HTMLAnchorElement} The newly created button element.
13
13
  */
14
14
 
15
- import defaults from '../config/defaults.js'
16
-
17
15
  export function createButton (config, rootEl, handleClick) {
18
16
  const { id, buttonText, buttonClass } = config
19
17
 
@@ -33,7 +31,7 @@ export function createButton (config, rootEl, handleClick) {
33
31
 
34
32
  // Build URL keeping existing params
35
33
  const url = new URL(window.location.href)
36
- url.searchParams.set(defaults.mapViewParamKey, id)
34
+ url.searchParams.set(config.mapViewQueryParam, id)
37
35
  button.setAttribute('href', url.toString())
38
36
 
39
37
  button.addEventListener('click', (e) => {
@@ -1,5 +1,4 @@
1
1
  import { createButton } from './buttonManager.js'
2
- import defaults from '../config/defaults.js'
3
2
 
4
3
  describe('createButton', () => {
5
4
  let rootEl, handleClick, button
@@ -19,11 +18,11 @@ describe('createButton', () => {
19
18
  })
20
19
 
21
20
  it('creates button with correct attributes and inserts before rootEl', () => {
22
- const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn' }
21
+ const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn', mapViewQueryParam: 'mv' }
23
22
  button = createButton(config, rootEl, handleClick)
24
23
 
25
24
  const expectedHref = new URL(window.location.href)
26
- expectedHref.searchParams.set(defaults.mapViewParamKey, 'map')
25
+ expectedHref.searchParams.set('mv', 'map')
27
26
 
28
27
  expect(button).toBeInstanceOf(HTMLAnchorElement)
29
28
  expect(button.getAttribute('href')).toBe(expectedHref.toString())
@@ -34,7 +33,7 @@ describe('createButton', () => {
34
33
  })
35
34
 
36
35
  it('prevents default and calls handleClick on click', () => {
37
- const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn' }
36
+ const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn', mapViewQueryParam: 'mv' }
38
37
  button = createButton(config, rootEl, handleClick)
39
38
 
40
39
  const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true })
@@ -47,21 +46,30 @@ describe('createButton', () => {
47
46
  })
48
47
 
49
48
  it('builds href with map param and preserves existing query params', () => {
50
- const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn' }
49
+ const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn', mapViewQueryParam: 'mv' }
51
50
  button = createButton(config, rootEl, handleClick)
52
51
 
53
52
  const url = new URL(button.getAttribute('href'))
54
53
  expect(url.searchParams.get('foo')).toBe('bar') // existing param preserved
55
- expect(url.searchParams.get(defaults.mapViewParamKey)).toBe('map') // new param added
54
+ expect(url.searchParams.get('mv')).toBe('map') // new param added
56
55
  })
57
56
 
58
57
  it('sets correct href even when no existing query params', () => {
59
58
  window.history.replaceState({}, '', '/test') // no query
60
- const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn' }
59
+ const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn', mapViewQueryParam: 'mv' }
61
60
  button = createButton(config, rootEl, handleClick)
62
61
 
63
62
  const url = new URL(button.getAttribute('href'))
64
63
  expect(url.pathname).toBe('/test')
65
- expect(url.searchParams.get(defaults.mapViewParamKey)).toBe('map')
64
+ expect(url.searchParams.get('mv')).toBe('map')
65
+ })
66
+
67
+ it('uses a custom mapViewQueryParam in the button href', () => {
68
+ const config = { id: 'map', buttonText: 'View Map', buttonClass: 'custom-btn', mapViewQueryParam: 'view' }
69
+ button = createButton(config, rootEl, handleClick)
70
+
71
+ const url = new URL(button.getAttribute('href'))
72
+ expect(url.searchParams.get('view')).toBe('map')
73
+ expect(url.searchParams.has('mv')).toBe(false)
66
74
  })
67
75
  })
@@ -1,7 +1,6 @@
1
1
  import { getQueryParam } from '../utils/queryString.js'
2
2
  import { toggleInertElements } from '../utils/toggleInertElements.js'
3
3
  import { isHybridFullscreen } from '../utils/getIsFullscreen.js'
4
- import defaults from '../config/defaults.js'
5
4
 
6
5
  // -----------------------------------------------------------------------------
7
6
  // Internal (not exported)
@@ -25,10 +24,10 @@ function getIsFullscreen (config) {
25
24
  if (manageHistoryState === false) {
26
25
  return true
27
26
  }
28
- return getQueryParam(defaults.mapViewParamKey) === id
27
+ return getQueryParam(config.mapViewQueryParam) === id
29
28
  }
30
29
 
31
- return isHybridFullscreen(config) && getQueryParam(defaults.mapViewParamKey) === id
30
+ return isHybridFullscreen(config) && getQueryParam(config.mapViewQueryParam) === id
32
31
  }
33
32
 
34
33
  // -----------------------------------------------------------------------------
@@ -1,6 +1,5 @@
1
1
  import { getQueryParam } from '../utils/queryString.js'
2
2
  import { isHybridFullscreen } from '../utils/getIsFullscreen.js'
3
- import defaults from '../config/defaults.js'
4
3
 
5
4
  // -----------------------------------------------------------------------------
6
5
  // Internal helpers
@@ -81,8 +80,10 @@ function syncMapInstance (mapInstance, viewId) {
81
80
  * @returns {void}
82
81
  */
83
82
  function handlePopstate () {
84
- const viewId = getQueryParam(defaults.mapViewParamKey)
85
- components.forEach(mapInstance => syncMapInstance(mapInstance, viewId))
83
+ components.forEach(mapInstance => {
84
+ const viewId = getQueryParam(mapInstance.config.mapViewQueryParam)
85
+ syncMapInstance(mapInstance, viewId)
86
+ })
86
87
  }
87
88
 
88
89
  // -----------------------------------------------------------------------------
@@ -45,6 +45,7 @@ export const defaultAppConfig = {
45
45
  group: { name: 'zoom', label: 'Zoom controls', order: 0 },
46
46
  label: 'Zoom in',
47
47
  iconId: 'plus',
48
+ keepFocus: true,
48
49
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
49
50
  excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
50
51
  enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
@@ -56,6 +57,7 @@ export const defaultAppConfig = {
56
57
  group: { name: 'zoom', label: 'Zoom controls', order: 0 },
57
58
  label: 'Zoom out',
58
59
  iconId: 'minus',
60
+ keepFocus: true,
59
61
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
60
62
  excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
61
63
  enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
@@ -78,7 +80,7 @@ export const defaultAppConfig = {
78
80
  ...keyboardBasePanelSlots,
79
81
  width: '500px'
80
82
  },
81
- render: () => <KeyboardHelp />
83
+ render: (props) => <KeyboardHelp context={props?.context} />
82
84
  }],
83
85
 
84
86
  icons: [{
@@ -1,6 +1,10 @@
1
1
  import { render } from '@testing-library/react'
2
2
  import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
3
3
 
4
+ jest.mock('../App/store/appContext.js', () => ({
5
+ useApp: () => ({ listboxIsActive: false })
6
+ }))
7
+
4
8
  describe('defaultAppConfig', () => {
5
9
  const appState = {
6
10
  layoutRefs: { appContainerRef: { current: document.createElement('div') } },
@@ -21,11 +21,12 @@ const defaults = {
21
21
  hasExitButton: false,
22
22
  hybridWidth: null, // Defaults to maxMobileWidth if not set
23
23
  keyboardHintText: '<span class="im-u-visually-hidden">Press </span><kbd>Alt</kbd> + <kbd>K</kbd> <span class="im-u-visually-hidden">to view </span>keyboard shortcuts',
24
- mapLabel: 'Interactive map',
24
+ mapLabel: 'Interactive map application',
25
25
  mapProvider: null,
26
26
  mapSize: 'small',
27
27
  manageHistoryState: true,
28
- mapViewParamKey: 'mv',
28
+ mapViewQueryParam: 'mv',
29
+ urlPosition: 'sync',
29
30
  maxMobileWidth: 640,
30
31
  minDesktopWidth: 835,
31
32
  nudgePanDelta: 5,
@@ -25,6 +25,8 @@ export const EVENTS = {
25
25
 
26
26
  /** @internal Add a marker. Payload: { id, coords, options } */
27
27
  APP_ADD_MARKER: 'app:addmarker',
28
+ /** @internal Update an existing marker's options. No-op if id not found. Payload: { id, options } */
29
+ APP_UPDATE_MARKER: 'app:updatemarker',
28
30
  /** @internal Remove a marker. Payload: id */
29
31
  APP_REMOVE_MARKER: 'app:removemarker',
30
32
  /** @internal Set application mode. Payload: mode */
@@ -121,6 +123,12 @@ export const EVENTS = {
121
123
  MAP_SET_STYLE: 'map:setstyle',
122
124
  /** @internal Set map size. Payload: { width, height } */
123
125
  MAP_SET_SIZE: 'map:setsize',
126
+ /** @internal Set the accessible features list. Payload: { items: { id: string, label: string }[] } */
127
+ MAP_SET_FEATURES: 'map:setfeatures',
128
+ /** @internal Set the active feature in the accessible features list. Payload: { id: string | null } */
129
+ MAP_SET_ACTIVE_FEATURE: 'map:setactivefeature',
130
+ /** @internal Select the active listbox feature as the real selection (Enter key). Payload: none */
131
+ MAP_SELECT_FEATURE: 'map:selectfeature',
124
132
  /** @internal Set pixel ratio. Payload: pixelRatio */
125
133
  MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
126
134
  /** @internal Fit the map to a bounding box. Payload: [west, south, east, north] */
@@ -4,18 +4,20 @@
4
4
  * used both by the symbol SVG renderer (JS canvas) and injected as
5
5
  * CSS custom properties onto the app container for CSS-rendered elements.
6
6
  *
7
- * Per-style overrides take precedence: set `haloColor`, `selectedColor`, or
7
+ * Per-style overrides take precedence: set `haloColor`, `selectedColor`, `activeColor`, or
8
8
  * `foregroundColor` directly on a `MapStyleConfig` to override these defaults.
9
9
  */
10
- export const SCHEME_COLORS = {
10
+ export const THEME_COLORS = {
11
11
  light: {
12
12
  haloColor: '#ffffff',
13
13
  selectedColor: '#0b0c0c',
14
+ activeColor: '#ffdd00',
14
15
  foregroundColor: '#0b0c0c'
15
16
  },
16
17
  dark: {
17
18
  haloColor: '#0b0c0c',
18
19
  selectedColor: '#ffffff',
20
+ activeColor: '#ffdd00',
19
21
  foregroundColor: '#ffffff'
20
22
  }
21
23
  }
@@ -30,10 +32,11 @@ export const SCHEME_COLORS = {
30
32
  * @returns {{ haloColor: string, selectedColor: string, foregroundColor: string }}
31
33
  */
32
34
  export const resolveMapTheme = (mapStyle) => {
33
- const scheme = SCHEME_COLORS[mapStyle?.mapColorScheme] ?? SCHEME_COLORS.light
35
+ const scheme = THEME_COLORS[mapStyle?.mapColorScheme] ?? THEME_COLORS.light
34
36
  return {
35
37
  haloColor: mapStyle?.haloColor ?? scheme.haloColor,
36
38
  selectedColor: mapStyle?.selectedColor ?? scheme.selectedColor,
39
+ activeColor: mapStyle?.activeColor ?? scheme.activeColor,
37
40
  foregroundColor: mapStyle?.foregroundColor ?? scheme.foregroundColor
38
41
  }
39
42
  }
@@ -47,10 +50,11 @@ export const resolveMapTheme = (mapStyle) => {
47
50
  * @returns {Object} CSS custom property object
48
51
  */
49
52
  export const getMapThemeVars = (mapStyle) => {
50
- const { haloColor, selectedColor, foregroundColor } = resolveMapTheme(mapStyle)
53
+ const { haloColor, selectedColor, activeColor, foregroundColor } = resolveMapTheme(mapStyle)
51
54
  return {
52
55
  '--map-overlay-halo-color': haloColor,
53
56
  '--map-overlay-selected-color': selectedColor,
57
+ '--map-overlay-active-color': activeColor,
54
58
  '--map-overlay-foreground-color': foregroundColor
55
59
  }
56
60
  }
@@ -1,10 +1,16 @@
1
1
  import defaults from './defaults.js'
2
2
 
3
3
  export const mergeConfig = (userConfig = {}) => {
4
- // Merge defaultConfig with userConfig
4
+ const { mapViewParamKey, ...restUser } = userConfig
5
+
5
6
  const config = {
6
7
  ...defaults,
7
- ...userConfig
8
+ ...restUser
9
+ }
10
+
11
+ if (mapViewParamKey !== undefined) {
12
+ console.warn('[InteractiveMap] mapViewParamKey is deprecated — use mapViewQueryParam instead.')
13
+ config.mapViewQueryParam = mapViewParamKey
8
14
  }
9
15
 
10
16
  return config
@@ -21,4 +21,23 @@ describe('mergeConfig', () => {
21
21
  const result = mergeConfig(userConfig)
22
22
  expect(result.theme).toBe('dark')
23
23
  })
24
+
25
+ it('maps deprecated mapViewParamKey to mapViewQueryParam', () => {
26
+ const result = mergeConfig({ mapViewParamKey: 'view' })
27
+ expect(result.mapViewQueryParam).toBe('view')
28
+ })
29
+
30
+ it('warns when deprecated mapViewParamKey is used', () => {
31
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
32
+ mergeConfig({ mapViewParamKey: 'view' })
33
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('mapViewParamKey is deprecated'))
34
+ warnSpy.mockRestore()
35
+ })
36
+
37
+ it('does not warn when mapViewParamKey is not used', () => {
38
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
39
+ mergeConfig({ mapViewQueryParam: 'view' })
40
+ expect(warnSpy).not.toHaveBeenCalled()
41
+ warnSpy.mockRestore()
42
+ })
24
43
  })
@@ -7,11 +7,18 @@
7
7
  export const symbolDefaults = {
8
8
  symbol: 'pin',
9
9
  backgroundColor: '#ca3535',
10
- foregroundColor: '#ffffff',
11
- haloWidth: '1',
12
- selectedWidth: '6'
10
+ foregroundColor: '#ffffff'
13
11
  }
14
12
 
13
+ /** Stroke width for the halo (background shape outline) in SVG units */
14
+ export const HALO_STROKE_WIDTH = 2
15
+
16
+ /** Stroke width for the selected state — symbol rings and feature highlight lines */
17
+ export const SELECTED_STROKE_WIDTH = 3
18
+
19
+ /** Stroke width for the active (keyboard cursor) state — double selected, extends 1× each side */
20
+ export const ACTIVE_STROKE_WIDTH = SELECTED_STROKE_WIDTH * 2
21
+
15
22
  /**
16
23
  * Built-in graphic path data strings for use with the `graphic` token.
17
24
  *
@@ -46,35 +53,35 @@ export const graphics = {
46
53
  }
47
54
 
48
55
  // ─── Built-in symbol definitions ─────────────────────────────────────────────
49
- // Each symbol uses a 38×38 viewBox. SVG templates use {{token}} placeholders
56
+ // Each symbol uses a 44×44 viewBox. SVG templates use {{token}} placeholders
50
57
  // resolved at render time by the symbolRegistry.
51
58
 
52
59
  export const pin = {
53
60
  id: 'pin',
54
- viewBox: '0 0 38 38',
61
+ viewBox: '0 0 44 44',
55
62
  anchor: [0.5, 0.9], // NOSONAR
56
63
  graphic: graphics.dot,
57
- svg: `<path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
58
- <path d="M19 33.499c-5.318-5-12-9.509-12-16.998 0-6.583 5.417-12 12-12s12 5.417 12 12c0 7.489-6.682 11.998-12 16.998z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
59
- <g transform="translate(19, 16) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
64
+ svg: `<path d="M22 40.999c-3.621 0-8.306-5.864-10.258-8.3C9.02 29.302 6 23.66 6 19.002a16.01 16.01 0 0 1 16-16 16.01 16.01 0 0 1 16 16c0 4.658-3.02 10.3-5.742 13.697-1.952 2.437-6.637 8.3-10.258 8.3z" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
65
+ <path d="M22 7.001a12.01 12.01 0 0 1 12 12c0 7.623-10.377 17.998-12 17.998S10 26.624 10 19.001a12.01 12.01 0 0 1 12-12z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
66
+ <g transform="translate(22, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
60
67
  }
61
68
 
62
69
  export const circle = {
63
70
  id: 'circle',
64
- viewBox: '0 0 38 38',
71
+ viewBox: '0 0 44 44',
65
72
  anchor: [0.5, 0.5],
66
73
  graphic: graphics.dot,
67
- svg: `<path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
68
- <path d="M19 7C12.376 7 7 12.376 7 19s5.376 12 12 12a12.01 12.01 0 0 0 12-12A12.01 12.01 0 0 0 19 7z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
69
- <g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
74
+ svg: `<circle cx="22" cy="22" r="16" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
75
+ <circle cx="22" cy="22" r="12" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
76
+ <g transform="translate(22, 22) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
70
77
  }
71
78
 
72
79
  export const square = {
73
80
  id: 'square',
74
- viewBox: '0 0 38 38',
81
+ viewBox: '0 0 44 44',
75
82
  anchor: [0.5, 0.5],
76
83
  graphic: graphics.dot,
77
- svg: `<path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="none" stroke="{{selectedColor}}" stroke-width="{{selectedWidth}}"/>
78
- <path d="M28 7a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H10a3 3 0 0 1-3-3V10a3 3 0 0 1 3-3h18z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="{{haloWidth}}"/>
79
- <g transform="translate(19, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
84
+ svg: `<path d="M13 6h18c3.863 0 7 3.137 7 7v18c0 3.863-3.137 7-7 7H13c-3.863 0-7-3.137-7-7V13c0-3.863 3.137-7 7-7" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
85
+ <path d="M13 34a3 3 0 0 1-3-3V13a3 3 0 0 1 3-3h18a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H13z" fill-rule="nonzero" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
86
+ <g transform="translate(22, 22) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
80
87
  }