@12-apps/ui 6.90.0 → 6.91.0

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 (205) hide show
  1. package/components-catalog.md +1 -1
  2. package/dist/_internal/components/data-display/AsyncStateContainer/AsyncStateContainer.js +3 -3
  3. package/dist/_internal/components/data-display/Avatar/Avatar.js +2 -2
  4. package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
  5. package/dist/_internal/components/data-display/Chart/Chart.js +4 -4
  6. package/dist/_internal/components/data-display/Chart/SpecChart.js +5 -5
  7. package/dist/_internal/components/data-display/Chart/chart-context.js +2 -2
  8. package/dist/_internal/components/data-display/Chart/chart-renderers.js +3 -3
  9. package/dist/_internal/components/data-display/Chip/Chip.js +3 -3
  10. package/dist/_internal/components/data-display/Chip/Chip.styles.js +2 -2
  11. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +1 -1
  12. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +21 -21
  13. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +25 -25
  14. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +6 -6
  15. package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js +7 -7
  16. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +5 -5
  17. package/dist/_internal/components/data-display/DataViews/base-list-card.js +8 -8
  18. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +6 -6
  19. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +7 -7
  20. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +18 -18
  21. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +8 -8
  22. package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +6 -6
  23. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +7 -7
  24. package/dist/_internal/components/data-display/DataViews/data-views-range-bounds.js +4 -4
  25. package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +5 -5
  26. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +2 -2
  27. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +4 -4
  28. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +2 -2
  29. package/dist/_internal/components/data-display/EmptyState/EmptyState.js +2 -2
  30. package/dist/_internal/components/data-display/FleetMap/FleetBody.js +9 -9
  31. package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js +8 -8
  32. package/dist/_internal/components/data-display/FleetMap/FleetMap.js +12 -12
  33. package/dist/_internal/components/data-display/InteractiveTooltip/InteractiveTooltip.js +2 -2
  34. package/dist/_internal/components/data-display/Lightbox/Lightbox.js +7 -7
  35. package/dist/_internal/components/data-display/Lightbox/LightboxControls.js +2 -2
  36. package/dist/_internal/components/data-display/Lightbox/LightboxOverlay.js +3 -3
  37. package/dist/_internal/components/data-display/Lightbox/LightboxThumbnails.js +2 -2
  38. package/dist/_internal/components/data-display/MapPreview/MapLayers.js +2 -2
  39. package/dist/_internal/components/data-display/MapPreview/MapMarkers.js +2 -2
  40. package/dist/_internal/components/data-display/MapPreview/MapPreview.js +7 -7
  41. package/dist/_internal/components/data-display/RegionMap/RegionMap.hooks.js +2 -1
  42. package/dist/_internal/components/data-display/RegionMap/RegionMap.js +4 -3
  43. package/dist/_internal/components/data-display/RegionMap/RegionMap.view.js +19 -0
  44. package/dist/_internal/components/data-display/RegionMap/RegionMapDrawing.js +1 -1
  45. package/dist/_internal/components/data-display/SectionOnboarding/SectionOnboarding.js +3 -3
  46. package/dist/_internal/components/data-display/index.js +50 -44
  47. package/dist/_internal/components/index.js +57 -51
  48. package/dist/_internal/components/layout/SettingCard/SettingCard.js +8 -8
  49. package/dist/_internal/components/layout/SettingCard/SettingCard.parts.js +4 -4
  50. package/dist/_internal/components/layout/SettingCard/SettingCard.summary.js +7 -7
  51. package/dist/_internal/components/layout/SettingCard/SettingGrid.js +5 -5
  52. package/dist/_internal/components/layout/SettingCard/SettingGroup.js +9 -9
  53. package/dist/_internal/components/layout/SettingCard/SettingSwitch.js +8 -8
  54. package/dist/_internal/components/layout/SettingCard/SettingToggle.js +9 -9
  55. package/dist/_internal/components/layout/index.js +12 -12
  56. package/dist/_internal/user-avatar/user-avatar.js +3 -3
  57. package/dist/charts.js +5 -5
  58. package/dist/{chunk-K2VDRODL.js → chunk-3IDL7NK7.js} +2 -2
  59. package/dist/{chunk-QADTI63U.js → chunk-3JZ3KEQQ.js} +2 -2
  60. package/dist/{chunk-EOIRU62W.js → chunk-42D7KBDH.js} +2 -2
  61. package/dist/{chunk-LXIPBHNJ.js → chunk-42TOCPXB.js} +5 -5
  62. package/dist/{chunk-KANCE4MS.js → chunk-4KWDNGMA.js} +3 -3
  63. package/dist/{chunk-7YJYIMWP.js → chunk-4QR7LM5F.js} +6 -6
  64. package/dist/{chunk-SCXVTNLE.js → chunk-5E2CRRD4.js} +4 -4
  65. package/dist/{chunk-ZDAAHCIS.js → chunk-5HADIIWN.js} +3 -3
  66. package/dist/{chunk-BP3ZKIPE.js → chunk-64UNVW6D.js} +4 -4
  67. package/dist/{chunk-7F6D73NL.js → chunk-76DX2IMH.js} +2 -2
  68. package/dist/{chunk-FI2CQOGC.js → chunk-7NP2JHGM.js} +2 -2
  69. package/dist/{chunk-BMTIBQQC.js → chunk-BBBHPK2Q.js} +6 -6
  70. package/dist/{chunk-EUR2ORTN.js → chunk-CSJ7WNXU.js} +2 -2
  71. package/dist/{chunk-WWR6APJR.js → chunk-DA3D5X5Z.js} +5 -5
  72. package/dist/chunk-DY2I6LMX.js +1 -0
  73. package/dist/chunk-DY2I6LMX.js.map +1 -0
  74. package/dist/{chunk-QKOJ4637.js → chunk-E6K2UZGR.js} +3 -3
  75. package/dist/{chunk-VIMMEADQ.js → chunk-FORAVDJF.js} +9 -9
  76. package/dist/chunk-FWAY2SAI.js +72 -0
  77. package/dist/chunk-FWAY2SAI.js.map +1 -0
  78. package/dist/{chunk-GMRQPNG3.js → chunk-G2NVBCF7.js} +20 -25
  79. package/dist/chunk-G2NVBCF7.js.map +1 -0
  80. package/dist/{chunk-Z5WUSI47.js → chunk-G3JFESKD.js} +6 -6
  81. package/dist/{chunk-3EIRRLCQ.js → chunk-HC6OZ7QK.js} +2 -2
  82. package/dist/{chunk-SKIAWOUC.js → chunk-HOPRZEBM.js} +3 -3
  83. package/dist/{chunk-JHWEL4NX.js → chunk-HS662RLW.js} +2 -2
  84. package/dist/{chunk-BZBLPXV7.js → chunk-IWSTMVCA.js} +4 -4
  85. package/dist/{chunk-TZC6KIK2.js → chunk-JG733RMP.js} +5 -5
  86. package/dist/{chunk-CW5Z67NA.js → chunk-JH2XLVWM.js} +2 -2
  87. package/dist/{chunk-3OTGREQG.js → chunk-JKZPGCN7.js} +11 -11
  88. package/dist/{chunk-DSBC6O46.js → chunk-JN2IANXD.js} +3 -3
  89. package/dist/{chunk-GBB23QLL.js → chunk-KJKD6Y6V.js} +3 -3
  90. package/dist/{chunk-KRBPWTXW.js → chunk-L3PDY7CJ.js} +2 -2
  91. package/dist/{chunk-HI7JPDD6.js → chunk-M3XJVD5G.js} +2 -2
  92. package/dist/{chunk-EEO7M5GU.js → chunk-MYTV353T.js} +4 -4
  93. package/dist/{chunk-3HZKUN5J.js → chunk-MZAYIG2I.js} +2 -2
  94. package/dist/{chunk-AKE6KVQG.js → chunk-NGXL4R54.js} +5 -1
  95. package/dist/chunk-NGXL4R54.js.map +1 -0
  96. package/dist/{chunk-3X2MA47R.js → chunk-NKEFH3RS.js} +5 -5
  97. package/dist/{chunk-NI67M453.js → chunk-PGANINQD.js} +5 -5
  98. package/dist/{chunk-BV2LOTMC.js → chunk-Q3SILZTQ.js} +2 -2
  99. package/dist/{chunk-TIM4PXWN.js → chunk-QHIWV55K.js} +2 -2
  100. package/dist/{chunk-DHAXUVMR.js → chunk-QN7FVZQD.js} +4 -4
  101. package/dist/{chunk-X2GWR75Q.js → chunk-TSEBMVYY.js} +3 -3
  102. package/dist/{chunk-CJ6XXM4X.js → chunk-U2AEM3K4.js} +6 -6
  103. package/dist/{chunk-ZH6NYB6Y.js → chunk-U2QXQ7D4.js} +4 -4
  104. package/dist/{chunk-SK2CTB3O.js → chunk-U2SAW6DH.js} +5 -5
  105. package/dist/{chunk-DAHSKQBV.js → chunk-UMOHHWXY.js} +2 -2
  106. package/dist/{chunk-P772HCHI.js → chunk-W676GY42.js} +4 -4
  107. package/dist/{chunk-JFD742C3.js → chunk-WCF56ZCD.js} +2 -2
  108. package/dist/{chunk-T2G4LDU5.js → chunk-WLLUARTC.js} +2 -2
  109. package/dist/{chunk-NK6O4NE4.js → chunk-XILMDEGZ.js} +3 -3
  110. package/dist/{chunk-QAEO52Z4.js → chunk-YGQM6GFQ.js} +2 -2
  111. package/dist/{chunk-WNNA6YYE.js → chunk-Z7T4EFEC.js} +7 -4
  112. package/dist/chunk-Z7T4EFEC.js.map +1 -0
  113. package/dist/{chunk-LOLYBFYY.js → chunk-ZCNR4DN5.js} +5 -5
  114. package/dist/{chunk-COKPPU42.js → chunk-ZIHSCBMN.js} +12 -12
  115. package/dist/data-display/AsyncStateContainer.js +3 -3
  116. package/dist/data-display/Avatar.js +2 -2
  117. package/dist/data-display/CardKit.js +7 -7
  118. package/dist/data-display/Carousel.js +2 -2
  119. package/dist/data-display/Chart.js +5 -5
  120. package/dist/data-display/Chip.js +3 -3
  121. package/dist/data-display/DataGrid.js +1 -1
  122. package/dist/data-display/DataViews.js +34 -34
  123. package/dist/data-display/DayRuler.js +4 -4
  124. package/dist/data-display/EmptyState.js +2 -2
  125. package/dist/data-display/FleetMap.js +12 -12
  126. package/dist/data-display/InteractiveTooltip.js +2 -2
  127. package/dist/data-display/Lightbox.js +7 -7
  128. package/dist/data-display/MapPreview.js +7 -7
  129. package/dist/data-display/RegionMap.js +11 -5
  130. package/dist/data-display/SectionOnboarding.js +3 -3
  131. package/dist/layout/SettingCard.js +12 -12
  132. package/dist/types/components/data-display/RegionMap/RegionMap.d.ts +4 -1
  133. package/dist/types/components/data-display/RegionMap/RegionMap.d.ts.map +1 -1
  134. package/dist/types/components/data-display/RegionMap/RegionMap.hooks.d.ts +4 -2
  135. package/dist/types/components/data-display/RegionMap/RegionMap.hooks.d.ts.map +1 -1
  136. package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts +40 -0
  137. package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts.map +1 -1
  138. package/dist/types/components/data-display/RegionMap/RegionMap.view.d.ts +36 -0
  139. package/dist/types/components/data-display/RegionMap/RegionMap.view.d.ts.map +1 -0
  140. package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts +4 -1
  141. package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts.map +1 -1
  142. package/dist/types/components/data-display/RegionMap/index.d.ts +3 -2
  143. package/dist/types/components/data-display/RegionMap/index.d.ts.map +1 -1
  144. package/dist/user-avatar.js +3 -3
  145. package/package.json +1 -1
  146. package/src/components/data-display/RegionMap/RegionMap.hooks.ts +24 -33
  147. package/src/components/data-display/RegionMap/RegionMap.md +27 -0
  148. package/src/components/data-display/RegionMap/RegionMap.tsx +7 -1
  149. package/src/components/data-display/RegionMap/RegionMap.types.ts +41 -0
  150. package/src/components/data-display/RegionMap/RegionMap.view.ts +105 -0
  151. package/src/components/data-display/RegionMap/RegionMapDrawing.tsx +11 -0
  152. package/src/components/data-display/RegionMap/index.ts +5 -1
  153. package/src/components/data-display/RegionMap/tests.md +10 -2
  154. package/dist/chunk-ACFKPE2T.js +0 -1
  155. package/dist/chunk-AKE6KVQG.js.map +0 -1
  156. package/dist/chunk-GMRQPNG3.js.map +0 -1
  157. package/dist/chunk-WNNA6YYE.js.map +0 -1
  158. /package/dist/{chunk-ACFKPE2T.js.map → _internal/components/data-display/RegionMap/RegionMap.view.js.map} +0 -0
  159. /package/dist/{chunk-K2VDRODL.js.map → chunk-3IDL7NK7.js.map} +0 -0
  160. /package/dist/{chunk-QADTI63U.js.map → chunk-3JZ3KEQQ.js.map} +0 -0
  161. /package/dist/{chunk-EOIRU62W.js.map → chunk-42D7KBDH.js.map} +0 -0
  162. /package/dist/{chunk-LXIPBHNJ.js.map → chunk-42TOCPXB.js.map} +0 -0
  163. /package/dist/{chunk-KANCE4MS.js.map → chunk-4KWDNGMA.js.map} +0 -0
  164. /package/dist/{chunk-7YJYIMWP.js.map → chunk-4QR7LM5F.js.map} +0 -0
  165. /package/dist/{chunk-SCXVTNLE.js.map → chunk-5E2CRRD4.js.map} +0 -0
  166. /package/dist/{chunk-ZDAAHCIS.js.map → chunk-5HADIIWN.js.map} +0 -0
  167. /package/dist/{chunk-BP3ZKIPE.js.map → chunk-64UNVW6D.js.map} +0 -0
  168. /package/dist/{chunk-7F6D73NL.js.map → chunk-76DX2IMH.js.map} +0 -0
  169. /package/dist/{chunk-FI2CQOGC.js.map → chunk-7NP2JHGM.js.map} +0 -0
  170. /package/dist/{chunk-BMTIBQQC.js.map → chunk-BBBHPK2Q.js.map} +0 -0
  171. /package/dist/{chunk-EUR2ORTN.js.map → chunk-CSJ7WNXU.js.map} +0 -0
  172. /package/dist/{chunk-WWR6APJR.js.map → chunk-DA3D5X5Z.js.map} +0 -0
  173. /package/dist/{chunk-QKOJ4637.js.map → chunk-E6K2UZGR.js.map} +0 -0
  174. /package/dist/{chunk-VIMMEADQ.js.map → chunk-FORAVDJF.js.map} +0 -0
  175. /package/dist/{chunk-Z5WUSI47.js.map → chunk-G3JFESKD.js.map} +0 -0
  176. /package/dist/{chunk-3EIRRLCQ.js.map → chunk-HC6OZ7QK.js.map} +0 -0
  177. /package/dist/{chunk-SKIAWOUC.js.map → chunk-HOPRZEBM.js.map} +0 -0
  178. /package/dist/{chunk-JHWEL4NX.js.map → chunk-HS662RLW.js.map} +0 -0
  179. /package/dist/{chunk-BZBLPXV7.js.map → chunk-IWSTMVCA.js.map} +0 -0
  180. /package/dist/{chunk-TZC6KIK2.js.map → chunk-JG733RMP.js.map} +0 -0
  181. /package/dist/{chunk-CW5Z67NA.js.map → chunk-JH2XLVWM.js.map} +0 -0
  182. /package/dist/{chunk-3OTGREQG.js.map → chunk-JKZPGCN7.js.map} +0 -0
  183. /package/dist/{chunk-DSBC6O46.js.map → chunk-JN2IANXD.js.map} +0 -0
  184. /package/dist/{chunk-GBB23QLL.js.map → chunk-KJKD6Y6V.js.map} +0 -0
  185. /package/dist/{chunk-KRBPWTXW.js.map → chunk-L3PDY7CJ.js.map} +0 -0
  186. /package/dist/{chunk-HI7JPDD6.js.map → chunk-M3XJVD5G.js.map} +0 -0
  187. /package/dist/{chunk-EEO7M5GU.js.map → chunk-MYTV353T.js.map} +0 -0
  188. /package/dist/{chunk-3HZKUN5J.js.map → chunk-MZAYIG2I.js.map} +0 -0
  189. /package/dist/{chunk-3X2MA47R.js.map → chunk-NKEFH3RS.js.map} +0 -0
  190. /package/dist/{chunk-NI67M453.js.map → chunk-PGANINQD.js.map} +0 -0
  191. /package/dist/{chunk-BV2LOTMC.js.map → chunk-Q3SILZTQ.js.map} +0 -0
  192. /package/dist/{chunk-TIM4PXWN.js.map → chunk-QHIWV55K.js.map} +0 -0
  193. /package/dist/{chunk-DHAXUVMR.js.map → chunk-QN7FVZQD.js.map} +0 -0
  194. /package/dist/{chunk-X2GWR75Q.js.map → chunk-TSEBMVYY.js.map} +0 -0
  195. /package/dist/{chunk-CJ6XXM4X.js.map → chunk-U2AEM3K4.js.map} +0 -0
  196. /package/dist/{chunk-ZH6NYB6Y.js.map → chunk-U2QXQ7D4.js.map} +0 -0
  197. /package/dist/{chunk-SK2CTB3O.js.map → chunk-U2SAW6DH.js.map} +0 -0
  198. /package/dist/{chunk-DAHSKQBV.js.map → chunk-UMOHHWXY.js.map} +0 -0
  199. /package/dist/{chunk-P772HCHI.js.map → chunk-W676GY42.js.map} +0 -0
  200. /package/dist/{chunk-JFD742C3.js.map → chunk-WCF56ZCD.js.map} +0 -0
  201. /package/dist/{chunk-T2G4LDU5.js.map → chunk-WLLUARTC.js.map} +0 -0
  202. /package/dist/{chunk-NK6O4NE4.js.map → chunk-XILMDEGZ.js.map} +0 -0
  203. /package/dist/{chunk-QAEO52Z4.js.map → chunk-YGQM6GFQ.js.map} +0 -0
  204. /package/dist/{chunk-LOLYBFYY.js.map → chunk-ZCNR4DN5.js.map} +0 -0
  205. /package/dist/{chunk-COKPPU42.js.map → chunk-ZIHSCBMN.js.map} +0 -0
@@ -7,6 +7,10 @@
7
7
  * every word arrives through `copy`.
8
8
  */
9
9
 
10
+ import type React from 'react';
11
+
12
+ import type { PlaneBox, Projection } from './RegionMap.helpers';
13
+
10
14
  /** A latitude/longitude pair, in that order. */
11
15
  export type RegionMapPoint = readonly [latitude: number, longitude: number];
12
16
 
@@ -50,6 +54,29 @@ export interface RegionMapCircle {
50
54
  radiusMetres: number;
51
55
  }
52
56
 
57
+ /** A latitude/longitude box, by its four edges in degrees. */
58
+ export interface RegionMapBounds {
59
+ south: number;
60
+ west: number;
61
+ north: number;
62
+ east: number;
63
+ }
64
+
65
+ /**
66
+ * An extent the view must fit besides its circles: a radius around the centre,
67
+ * or a box — e.g. the bounds of some outlines (`boundsOfRings`), for a reach
68
+ * no single radius describes.
69
+ */
70
+ export type RegionMapReach = { radiusMetres: number } | { bounds: RegionMapBounds };
71
+
72
+ /** What a host overlay is drawn with: the map's own view and projection. */
73
+ export interface RegionMapLayer {
74
+ /** The SVG viewBox, in projected metres. */
75
+ view: PlaneBox;
76
+ /** The projection the map draws with — `projectionAt(center)`. */
77
+ projection: Projection;
78
+ }
79
+
53
80
  /** Every word the map prints or announces. */
54
81
  export interface RegionMapCopy {
55
82
  /** The drawing's accessible name (`role="img"`). */
@@ -95,6 +122,20 @@ export interface RegionMapProps {
95
122
  center: { latitude: number; longitude: number } | null;
96
123
  /** Circles around the centre; the view fits the largest. */
97
124
  circles?: readonly RegionMapCircle[];
125
+ /**
126
+ * An extent the view must ALSO fit, before its margin — something the host
127
+ * draws past the largest circle. With circles the view stays centred on the
128
+ * centre and grows to the reach's farthest edge; without, it fits the areas
129
+ * and the reach together. Omit it and the view is the circles' (or areas').
130
+ */
131
+ reach?: RegionMapReach;
132
+ /**
133
+ * A host layer drawn INSIDE the drawing's SVG, over the excluded hatch and
134
+ * under the circles, highlights and centre, so it shares the map's view,
135
+ * projection and frame. It is wrapped in `pointer-events: none`: taps go
136
+ * through it to the areas. Return SVG elements, in projected metres.
137
+ */
138
+ overlay?: (layer: RegionMapLayer) => React.ReactNode;
98
139
  /**
99
140
  * A tap on an area, or a press of its list button. Omit it and the map is
100
141
  * read-only: nothing is tappable and the list rows are not buttons.
@@ -0,0 +1,105 @@
1
+ import {
2
+ extent,
3
+ planeBounds,
4
+ projectionAt,
5
+ projectRings,
6
+ viewBoxFor,
7
+ type PlaneBox,
8
+ type Projection,
9
+ } from './RegionMap.helpers';
10
+ import type {
11
+ RegionMapBounds,
12
+ RegionMapCircle,
13
+ RegionMapReach,
14
+ RegionMapRegion,
15
+ RegionMapRing,
16
+ } from './RegionMap.types';
17
+
18
+ // What `RegionMap` shows: its view, given the circles, the areas and a host's
19
+ // `reach` — pure, so a host laying HTML over the drawing computes the same one.
20
+
21
+ /** The smallest box holding every given box, or null with none. */
22
+ export function unionBox(boxes: readonly (PlaneBox | null)[]): PlaneBox | null {
23
+ const real = boxes.filter((box): box is PlaneBox => box !== null);
24
+ const xs = extent(real.flatMap((b) => [b.minX, b.minX + b.spanX]));
25
+ const ys = extent(real.flatMap((b) => [b.minY, b.minY + b.spanY]));
26
+ if (xs === null || ys === null) return null;
27
+ return { minX: xs.min, minY: ys.min, spanX: xs.max - xs.min, spanY: ys.max - ys.min };
28
+ }
29
+
30
+ /** The latitude/longitude box around some outlines — a `reach` no radius describes. */
31
+ export function boundsOfRings(rings: readonly RegionMapRing[]): RegionMapBounds | null {
32
+ const points = rings.flat();
33
+ const lats = extent(points.map(([latitude]) => latitude));
34
+ const lngs = extent(points.map(([, longitude]) => longitude));
35
+ if (lats === null || lngs === null) return null;
36
+ return { south: lats.min, west: lngs.min, north: lats.max, east: lngs.max };
37
+ }
38
+
39
+ /** A `reach` as a box in projected metres (a radius is a square around the centre). */
40
+ export function reachBox(reach: RegionMapReach, projection: Projection): PlaneBox | null {
41
+ if ('radiusMetres' in reach) {
42
+ const r = reach.radiusMetres;
43
+ return Number.isFinite(r) && r > 0 ? { minX: -r, minY: -r, spanX: r * 2, spanY: r * 2 } : null;
44
+ }
45
+ const { south, west, north, east } = reach.bounds;
46
+ const a = projection.project([north, west]);
47
+ const b = projection.project([south, east]);
48
+ return unionBox([{ minX: a.x, minY: a.y, spanX: 0, spanY: 0 }, { minX: b.x, minY: b.y, spanX: 0, spanY: 0 }]);
49
+ }
50
+
51
+ /** How far the box reaches from the centre along either axis — the half-side of a centred square holding it. */
52
+ function farthestEdge(box: PlaneBox): number {
53
+ return Math.max(Math.abs(box.minX), Math.abs(box.minX + box.spanX), Math.abs(box.minY), Math.abs(box.minY + box.spanY));
54
+ }
55
+
56
+ /**
57
+ * The view, given what it must fit: the outermost circle and the `reach` with
58
+ * the margin on top, still centred on the centre; else every area and the
59
+ * reach together. Without a reach it is exactly `viewBoxFor`'s.
60
+ */
61
+ export function viewWithin(
62
+ outermostMetres: number | null,
63
+ reach: PlaneBox | null,
64
+ areaBoxes: () => readonly (PlaneBox | null)[],
65
+ ): PlaneBox | null {
66
+ if (outermostMetres !== null) {
67
+ return viewBoxFor(Math.max(outermostMetres, reach === null ? 0 : farthestEdge(reach)), null);
68
+ }
69
+ return viewBoxFor(null, unionBox([...areaBoxes(), reach]));
70
+ }
71
+
72
+ /** The middle of every area, for a map with no centre of its own. */
73
+ export function centreOf(regions: readonly RegionMapRegion[]): { latitude: number; longitude: number } | null {
74
+ const bounds = boundsOfRings(regions.flatMap((r) => r.rings));
75
+ if (bounds === null) return null;
76
+ return { latitude: (bounds.south + bounds.north) / 2, longitude: (bounds.west + bounds.east) / 2 };
77
+ }
78
+
79
+ /**
80
+ * The square `RegionMap` shows for these props, in projected metres around
81
+ * `center` (or the areas' middle) — for a host that lays its own HTML over the
82
+ * drawing at the same scale: a point `p` sits at
83
+ * `(p.x - view.minX) / view.spanX` across. With circles the view is centred,
84
+ * so its half-side is `view.spanX / 2`.
85
+ */
86
+ export function regionMapView({
87
+ regions,
88
+ center,
89
+ circles = [],
90
+ reach,
91
+ }: {
92
+ regions: readonly RegionMapRegion[];
93
+ center: { latitude: number; longitude: number } | null;
94
+ circles?: readonly RegionMapCircle[];
95
+ reach?: RegionMapReach;
96
+ }): PlaneBox | null {
97
+ const origin = center ?? centreOf(regions);
98
+ if (origin === null) return null;
99
+ const projection = projectionAt(origin);
100
+ return viewWithin(
101
+ extent(circles.map((c) => c.radiusMetres))?.max ?? null,
102
+ reach === undefined ? null : reachBox(reach, projection),
103
+ () => regions.map((r) => planeBounds(projectRings(r.rings, projection))),
104
+ );
105
+ }
@@ -8,9 +8,12 @@ import { rem, remPx } from '../../../tokens/relative';
8
8
  import { labelPosition, type AreaGeometry, type DrawnRegion, type PlaneBox } from './RegionMap.helpers';
9
9
  import type { RegionMapModel } from './RegionMap.hooks';
10
10
  import { labelOrder, placeLabels } from './RegionMap.labels';
11
+ import type { RegionMapLayer } from './RegionMap.types';
11
12
 
12
13
  export interface RegionMapDrawingProps {
13
14
  model: RegionMapModel & { view: PlaneBox };
15
+ /** The host's layer, drawn inside the SVG under the circles (`RegionMapProps.overlay`). */
16
+ overlay?: (layer: RegionMapLayer) => React.ReactNode;
14
17
  showCenter: boolean;
15
18
  highlightedIds: readonly string[];
16
19
  maxHeight: string | undefined;
@@ -32,6 +35,7 @@ export interface RegionMapDrawingProps {
32
35
  */
33
36
  export function RegionMapDrawing({
34
37
  model,
38
+ overlay,
35
39
  showCenter,
36
40
  highlightedIds,
37
41
  maxHeight,
@@ -78,6 +82,13 @@ export function RegionMapDrawing({
78
82
  <Hatch id={hatchId} pitch={view.spanX / 70} theme={theme} />
79
83
  <BaseAreas areas={base} testId={testId} theme={theme} />
80
84
  <ExcludedAreas drawn={drawn} hatchId={hatchId} testId={testId} theme={theme} />
85
+ {overlay && model.projection && (
86
+ // `pointer-events` is inherited in SVG: nothing the host draws here
87
+ // takes a tap from the areas beneath.
88
+ <g pointerEvents="none" data-testid={`${testId}-overlay`}>
89
+ {overlay({ view, projection: model.projection })}
90
+ </g>
91
+ )}
81
92
  <Circles radii={model.radii} theme={theme} />
82
93
  <Highlights areas={highlighted} testId={testId} theme={theme} />
83
94
  {showCenter && (
@@ -1,13 +1,17 @@
1
1
  export { RegionMap } from './RegionMap';
2
2
  export { decodePolyline, hitRegion, projectionAt } from './RegionMap.helpers';
3
- export type { AreaGeometry, PlanePoint, PlaneRegion } from './RegionMap.helpers';
3
+ export { boundsOfRings, regionMapView } from './RegionMap.view';
4
+ export type { AreaGeometry, PlaneBox, PlanePoint, PlaneRegion, Projection } from './RegionMap.helpers';
4
5
  export type {
6
+ RegionMapBounds,
5
7
  RegionMapCircle,
6
8
  RegionMapCopy,
9
+ RegionMapLayer,
7
10
  RegionMapListMode,
8
11
  RegionMapListSort,
9
12
  RegionMapPoint,
10
13
  RegionMapProps,
14
+ RegionMapReach,
11
15
  RegionMapRegion,
12
16
  RegionMapRing,
13
17
  } from './RegionMap.types';
@@ -6,12 +6,20 @@
6
6
  - [x] `__tests__/region-map-helpers.test.ts` — the geometry: even-odd with a
7
7
  hole, distance, which area a tap means (hole, smallest, sliver within the
8
8
  tolerance even from inside a big neighbour, nothing beyond it), the view,
9
- label placement, `extent` on 200k values, the projection and the decoder
9
+ label placement, `extent` on 200k values, the projection and the decoder;
10
+ the view with a reach (none is today's view, a radius past the circle
11
+ grows it centred, a smaller one never shrinks it, a box off to one side
12
+ by its farthest edge, areas and reach together with no circle, a
13
+ non-positive radius ignored) and `boundsOfRings`
10
14
  - [x] `__tests__/region-map.test.tsx` — what is drawn (areas, hatch + name for
11
15
  excluded only, several highlights, nothing with no centre and no areas),
12
16
  what a tap reports, read-only, and the list (collapsed by default with its
13
17
  count, `aria-expanded`, `aria-pressed`, state in words, outside areas off
14
- the list and out of the count, `listed`, sort by name, detail, `none`)
18
+ the list and out of the count, `listed`, sort by name, detail, `none`),
19
+ and the reach and overlay (viewBox unchanged without a reach, widened as
20
+ `regionMapView` says with one, the overlay inside the SVG in a
21
+ `pointer-events: none` group with the map's view and projection, a tap
22
+ still reaching the area beneath, no group without an overlay)
15
23
 
16
24
  ## Storybook Tests Status
17
25
 
@@ -1 +0,0 @@
1
- //# sourceMappingURL=chunk-ACFKPE2T.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/RegionMap/RegionMapDrawing.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport Typography from '@mui/material/Typography/index.js';\nimport { alpha, type Theme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { rem, remPx } from '../../../tokens/relative';\n\nimport { labelPosition, type AreaGeometry, type DrawnRegion, type PlaneBox } from './RegionMap.helpers';\nimport type { RegionMapModel } from './RegionMap.hooks';\nimport { labelOrder, placeLabels } from './RegionMap.labels';\n\nexport interface RegionMapDrawingProps {\n model: RegionMapModel & { view: PlaneBox };\n showCenter: boolean;\n highlightedIds: readonly string[];\n maxHeight: string | undefined;\n label: string;\n tappable: boolean;\n testId: string;\n theme: Theme;\n}\n\n/**\n * The drawing: one `role=\"img\"`, square, with HTML labels laid over it.\n *\n * Strokes are `non-scaling-stroke` and labels are HTML at theme sizes, so the\n * picture reads the same at 320 px and 1600 px; `maxHeight` caps it on a wide\n * screen. Where it sits in a wider box is the host's call.\n *\n * The base areas are a memoised layer: a toggle changes `excluded`, not the\n * geometry, so only the hatch, highlight and label layers re-render.\n */\nexport function RegionMapDrawing({\n model,\n showCenter,\n highlightedIds,\n maxHeight,\n label,\n tappable,\n testId,\n theme,\n}: RegionMapDrawingProps): React.JSX.Element {\n const hatchId = `${React.useId().replace(/:/g, '')}-hatch`;\n const { view, drawn } = model;\n const wanted = new Set(highlightedIds);\n const highlighted = drawn.filter((d) => wanted.has(d.id));\n // Highlighted, then excluded, then larger, then by name: the order labels\n // claim their place in, so a collision hides the less important one.\n const labelled = labelOrder(drawn.filter((d) => d.excluded || wanted.has(d.id)), wanted);\n const boxRef = React.useRef<HTMLDivElement>(null);\n useLabelPlacement(boxRef, labelled.map((d) => d.id).join('|'), remPx(theme, 4));\n const base = drawn.map((d) => ({ id: d.id, name: d.name, geometry: d.geometry, outside: d.outside === true }));\n return (\n <Box\n sx={{\n position: 'relative',\n width: '100%',\n maxWidth: maxHeight,\n maxHeight,\n aspectRatio: '1 / 1',\n border: 1,\n borderColor: 'divider',\n borderRadius: 1,\n overflow: 'hidden',\n bgcolor: 'background.paper',\n }}\n data-testid={`${testId}-canvas`}\n ref={boxRef}\n >\n <svg\n viewBox={`${view.minX} ${view.minY} ${view.spanX} ${view.spanY}`}\n role=\"img\"\n aria-label={label}\n onPointerDown={model.onPointerDown}\n onClick={model.onClick}\n style={{ display: 'block', width: '100%', height: '100%', cursor: tappable ? 'pointer' : 'default' }}\n >\n <Hatch id={hatchId} pitch={view.spanX / 70} theme={theme} />\n <BaseAreas areas={base} testId={testId} theme={theme} />\n <ExcludedAreas drawn={drawn} hatchId={hatchId} testId={testId} theme={theme} />\n <Circles radii={model.radii} theme={theme} />\n <Highlights areas={highlighted} testId={testId} theme={theme} />\n {showCenter && (\n <circle\n cx={0}\n cy={0}\n r={view.spanX / 90}\n fill={theme.palette.primary.main}\n stroke={theme.palette.background.paper}\n strokeWidth={2}\n vectorEffect=\"non-scaling-stroke\"\n pointerEvents=\"none\"\n data-testid={`${testId}-center`}\n />\n )}\n </svg>\n {labelled.map((d) => (\n <AreaLabel key={`l-${d.id}`} area={d} view={view} testId={testId} theme={theme} />\n ))}\n </Box>\n );\n}\n\n/** The highlighted areas, drawn over everything but the centre. */\nfunction Highlights({\n areas,\n testId,\n theme,\n}: {\n areas: readonly DrawnRegion[];\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n return (\n <>\n {areas.map((d) => (\n <path\n key={`h-${d.id}`}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={alpha(theme.palette.primary.main, 0.3)}\n stroke={theme.palette.primary.main}\n strokeWidth={2.5}\n pointerEvents=\"none\"\n data-testid={`${testId}-highlight-${d.id}`}\n />\n ))}\n </>\n );\n}\n\n/** The hatch an excluded area is filled with: lines, not a colour. */\nfunction Hatch({ id, pitch, theme }: { id: string; pitch: number; theme: Theme }): React.JSX.Element {\n return (\n <defs>\n <pattern id={id} width={pitch} height={pitch} patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\">\n <rect width={pitch} height={pitch} fill={alpha(theme.palette.text.primary, 0.1)} />\n <rect width={pitch / 3} height={pitch} fill={theme.palette.text.secondary} />\n </pattern>\n </defs>\n );\n}\n\ninterface BaseArea {\n id: string;\n name: string;\n geometry: AreaGeometry;\n outside: boolean;\n}\n\ninterface BaseAreasProps {\n areas: readonly BaseArea[];\n testId: string;\n theme: Theme;\n}\n\n/** Same areas, same geometry objects, same look: nothing to re-render. */\nfunction sameBase(a: BaseAreasProps, b: BaseAreasProps): boolean {\n if (a.theme !== b.theme || a.testId !== b.testId || a.areas.length !== b.areas.length) return false;\n return a.areas.every((area, i) => {\n const other = b.areas[i]!;\n return (\n area.id === other.id &&\n area.geometry === other.geometry &&\n area.outside === other.outside &&\n area.name === other.name\n );\n });\n}\n\n/** Every area's fill and border — the tap targets. Memoised: see `sameBase`. */\nconst BaseAreas = React.memo(function BaseAreas({ areas, testId, theme }: BaseAreasProps): React.JSX.Element {\n const { palette } = theme;\n return (\n <>\n {areas.map((d) => (\n <path\n key={d.id}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={d.outside ? palette.background.paper : palette.action.hover}\n stroke={palette.divider}\n strokeWidth={1}\n data-region-id={d.id}\n data-testid={`${testId}-area-${d.id}`}\n >\n <title>{d.name}</title>\n </path>\n ))}\n </>\n );\n}, sameBase);\n\n/** The hatch over every excluded area. */\nfunction ExcludedAreas({\n drawn,\n hatchId,\n testId,\n theme,\n}: {\n drawn: readonly DrawnRegion[];\n hatchId: string;\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n return (\n <>\n {drawn\n .filter((d) => d.excluded)\n .map((d) => (\n <path\n key={`x-${d.id}`}\n d={d.geometry.path}\n fillRule=\"evenodd\"\n vectorEffect=\"non-scaling-stroke\"\n fill={`url(#${hatchId})`}\n stroke={theme.palette.text.secondary}\n strokeWidth={1.5}\n pointerEvents=\"none\"\n data-testid={`${testId}-excluded-${d.id}`}\n />\n ))}\n </>\n );\n}\n\n/** The dashed circles around the centre. */\nfunction Circles({ radii, theme }: { radii: readonly number[]; theme: Theme }): React.JSX.Element {\n return (\n <>\n {radii.map((radius) => (\n <circle\n key={radius}\n cx={0}\n cy={0}\n r={radius}\n fill=\"none\"\n stroke={theme.palette.info.main}\n strokeWidth={1.75}\n strokeDasharray=\"7 5\"\n vectorEffect=\"non-scaling-stroke\"\n pointerEvents=\"none\"\n />\n ))}\n </>\n );\n}\n\n/**\n * Places every label the drawing carries, in their DOM order (the priority\n * order), and hides one that would overlap a label already drawn\n * (`placeLabels`). Measured by a ResizeObserver, never in a layout effect: its\n * first call comes after the browser's own layout, so mounting a label on a\n * tap forces no synchronous reflow of the drawing.\n */\nfunction useLabelPlacement(boxRef: React.RefObject<HTMLDivElement | null>, key: string, gap: number): void {\n React.useEffect(() => {\n const box = boxRef.current;\n if (!box || typeof ResizeObserver === 'undefined') return undefined;\n const labels = Array.from(box.querySelectorAll<HTMLElement>('[data-region-label]'));\n const place = (): void => {\n const spanX = box.clientWidth;\n const spanY = box.clientHeight;\n if (spanX === 0 || spanY === 0) return;\n const placed = placeLabels(\n labels.map((label) => ({\n fractionX: Number(label.dataset.fractionX),\n fractionY: Number(label.dataset.fractionY),\n spanX: label.offsetWidth,\n spanY: label.offsetHeight,\n })),\n { spanX, spanY },\n gap,\n );\n placed.forEach(({ x, y, visible }, index) => {\n const label = labels[index]!;\n label.style.left = `${(x / spanX) * 100}%`;\n label.style.top = `${(y / spanY) * 100}%`;\n label.style.visibility = visible ? 'visible' : 'hidden';\n label.dataset.hidden = visible ? 'false' : 'true';\n });\n };\n const observer = new ResizeObserver(place);\n observer.observe(box);\n for (const label of labels) observer.observe(label);\n return () => observer.disconnect();\n }, [boxRef, key, gap]);\n}\n\n/**\n * An area's name over the drawing, at the theme's type size whatever the\n * width. Placed — and hidden on a collision — by {@link useLabelPlacement}.\n * Decorative (`aria-hidden`): the area's name is its `<title>` and its row in\n * the list.\n */\nfunction AreaLabel({\n area,\n view,\n testId,\n theme,\n}: {\n area: DrawnRegion;\n view: PlaneBox;\n testId: string;\n theme: Theme;\n}): React.JSX.Element {\n const at = labelPosition(area.geometry.rings, view);\n const halo = `0 0 ${rem(theme, 3)} ${theme.palette.background.paper}`;\n return (\n <Typography\n component=\"span\"\n aria-hidden\n data-testid={`${testId}-label-${area.id}`}\n data-region-label=\"\"\n data-fraction-x={at.fractionX}\n data-fraction-y={at.fractionY}\n // Position as an inline style, not `sx`: a per-label class would insert\n // a new CSS rule (and a style recalculation) on every tap.\n style={{\n left: `${(at.fractionX * 100).toFixed(2)}%`,\n top: `${(at.fractionY * 100).toFixed(2)}%`,\n }}\n sx={{\n position: 'absolute',\n transform: 'translate(-50%, -50%)',\n fontSize: rem(theme, 12),\n fontWeight: 700,\n lineHeight: 1.1,\n textAlign: 'center',\n maxWidth: '45%',\n color: 'text.primary',\n pointerEvents: 'none',\n textShadow: `${halo}, ${halo}`,\n }}\n >\n {area.name}\n </Typography>\n );\n}\n"],"mappings":"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","names":["BaseAreas"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/RegionMap/RegionMap.hooks.ts"],"sourcesContent":["import React from 'react';\n\nimport {\n drawOrder,\n extent,\n geometryCache,\n hitRegion,\n projectionAt,\n viewBoxFor,\n type DrawnRegion,\n type PlaneBox,\n} from './RegionMap.helpers';\nimport type { RegionMapProps, RegionMapRegion } from './RegionMap.types';\n\n/** What the drawing needs, derived from the props. */\nexport interface RegionMapModel {\n /** The square shown, or null when there is nothing to centre on. */\n view: PlaneBox | null;\n drawn: DrawnRegion[];\n /** Radii drawn, in metres; empty without circles. */\n radii: number[];\n outermostMetres: number | null;\n /** The tap handlers for the drawing's `<svg>`. */\n onPointerDown: (event: React.PointerEvent<SVGSVGElement>) => void;\n onClick: (event: React.MouseEvent<SVGSVGElement>) => void;\n}\n\n/** A tap reaches an area this close: a share of the view (~8 px on a phone). */\nconst TOLERANCE_SHARE = 1 / 45;\n\n/** The middle of every area, for a map with no centre of its own. */\nfunction centreOf(regions: readonly RegionMapRegion[]): { latitude: number; longitude: number } | null {\n const points = regions.flatMap((r) => r.rings.flat());\n const lats = extent(points.map(([lat]) => lat));\n const lngs = extent(points.map(([, lng]) => lng));\n if (lats === null || lngs === null) return null;\n return { latitude: (lats.min + lats.max) / 2, longitude: (lngs.min + lngs.max) / 2 };\n}\n\n/** The click, in the drawing's own coordinates — or null where it cannot be read. */\nfunction svgPoint(event: React.MouseEvent<SVGSVGElement>): { x: number; y: number } | null {\n const svg = event.currentTarget;\n const matrix = typeof svg.getScreenCTM === 'function' ? svg.getScreenCTM() : null;\n if (!matrix || typeof DOMPoint === 'undefined') return null;\n const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());\n return { x: point.x, y: point.y };\n}\n\n/** The view: the outermost circle when there is one, else every area's box. */\nfunction viewOf(outermostMetres: number | null, drawable: () => (PlaneBox | null)[]): PlaneBox | null {\n if (outermostMetres !== null) return viewBoxFor(outermostMetres, null);\n const boxes = drawable().filter((box): box is PlaneBox => box !== null);\n const xs = extent(boxes.flatMap((b) => [b.minX, b.minX + b.spanX]));\n const ys = extent(boxes.flatMap((b) => [b.minY, b.minY + b.spanY]));\n if (xs === null || ys === null) return null;\n return viewBoxFor(null, { minX: xs.min, minY: ys.min, spanX: xs.max - xs.min, spanY: ys.max - ys.min });\n}\n\n/** Which area a click or tap means — see `useRegionMap`. */\nfunction tappedRegion(\n event: React.MouseEvent<SVGSVGElement>,\n pointer: string,\n drawn: readonly DrawnRegion[],\n view: PlaneBox,\n): string | null {\n const targetId = (event.target as Element).getAttribute('data-region-id');\n if (pointer === 'mouse' && targetId) return targetId;\n const point = svgPoint(event);\n if (point === null) return targetId;\n return hitRegion(\n drawn.map((d) => ({ id: d.id, geometry: d.geometry })),\n point,\n view.spanX * TOLERANCE_SHARE,\n );\n}\n\n/** The geometry cache for a centre, or null without one. */\nfunction useGeometry(\n origin: { latitude: number; longitude: number } | null,\n): ReturnType<typeof geometryCache> | null {\n const lat = origin?.latitude ?? null;\n const lng = origin?.longitude ?? null;\n return React.useMemo(\n () => (lat === null || lng === null ? null : geometryCache(projectionAt({ latitude: lat, longitude: lng }))),\n [lat, lng],\n );\n}\n\n/**\n * The projection, the view, the areas in draw order and the tap handlers.\n *\n * Geometry is projected once per `rings` identity and centre\n * (`geometryCache`), so a toggle — which changes `excluded`, not `rings` —\n * re-projects nothing.\n *\n * A MOUSE click on a path is that path's area: a pointer is precise. Any\n * other tap — a finger, a pen — goes to `hitRegion`: the smallest area within\n * a few pixels, so a sliver beside a big area is reachable from the map.\n */\nexport function useRegionMap({\n regions,\n center,\n circles = [],\n onToggle,\n}: Pick<RegionMapProps, 'regions' | 'center' | 'circles' | 'onToggle'>): RegionMapModel {\n const geometryOf = useGeometry(center ?? centreOf(regions));\n const radii = circles.map((c) => c.radiusMetres);\n const outermostMetres = extent(radii)?.max ?? null;\n const view =\n geometryOf === null ? null : viewOf(outermostMetres, () => regions.map((r) => geometryOf(r.rings).bounds));\n const drawn = geometryOf && view ? drawOrder(regions, geometryOf, view) : [];\n const pointerType = React.useRef<string>('mouse');\n\n const onPointerDown = (event: React.PointerEvent<SVGSVGElement>): void => {\n pointerType.current = event.pointerType || 'mouse';\n };\n\n const onClick = (event: React.MouseEvent<SVGSVGElement>): void => {\n if (!onToggle || view === null) return;\n const hit = tappedRegion(event, pointerType.current, drawn, view);\n if (hit) onToggle(hit);\n };\n\n return { view, drawn, radii, outermostMetres, onPointerDown, onClick };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/RegionMap/RegionMap.tsx"],"sourcesContent":["import Box from '@mui/material/Box/index.js';\nimport { useTheme } from '@mui/material/styles/index.js';\nimport React from 'react';\n\nimport { useRegionMap } from './RegionMap.hooks';\nimport type { RegionMapProps } from './RegionMap.types';\nimport { RegionMapDrawing } from './RegionMapDrawing';\nimport { RegionMapLegend } from './RegionMapLegend';\nimport { RegionMapList } from './RegionMapList';\n\n/** One empty list, so the default does not change identity per render. */\nconst NO_IDS: readonly string[] = [];\n\n/**\n * A hand-drawn map of named areas around a centre; a tap toggles an area.\n *\n * Drawn as inline SVG over a plain background — no tile server, no API key,\n * nothing fetched — because the question it answers is \"which of THESE areas\",\n * not \"what is the street called\". Four choices shape it:\n *\n * - **The list is the accessible way in.** The drawing is one `role=\"img\"`\n * with a name; its areas are not tab stops (dozens of them, many too small to\n * tap on a phone). Every area is also a toggle button in the list beneath,\n * with `aria-pressed`, which is what a keyboard and a screen reader use —\n * and what reaches a sliver no finger can.\n * - **Excluded is hatched AND named, never only coloured,** and never in the\n * error colour: leaving an area out is a choice, not a failure.\n * - **The hit test is ours, not only the browser's.** A mouse click on a path\n * is that path. A finger's tap takes the SMALLEST area within a few pixels'\n * worth of metres (`hitRegion`), so a sliver beside a big area can be hit\n * from the map; one too thin even for that is still on the list.\n * - **A toggle is cheap.** Geometry is projected once per `rings` identity,\n * and the base layer is memoised, so a tap re-renders the hatch and labels.\n */\nexport function RegionMap({\n regions,\n center,\n circles,\n onToggle,\n highlightedIds = NO_IDS,\n copy,\n listMode = 'collapsed',\n listSort = 'given',\n maxHeight,\n className,\n dataTestId = 'region-map',\n}: RegionMapProps): React.JSX.Element | null {\n const theme = useTheme();\n const model = useRegionMap({ regions, center, circles, onToggle });\n const { view } = model;\n if (view === null && regions.length === 0) return null;\n return (\n <Box className={className} data-testid={dataTestId}>\n {view && (\n <>\n <RegionMapDrawing\n model={{ ...model, view }}\n showCenter={center !== null}\n highlightedIds={highlightedIds}\n maxHeight={maxHeight}\n label={copy.mapLabel}\n tappable={onToggle !== undefined}\n testId={dataTestId}\n theme={theme}\n />\n <RegionMapLegend\n copy={copy}\n outermostMetres={model.outermostMetres}\n showCenter={center !== null}\n theme={theme}\n testId={dataTestId}\n />\n </>\n )}\n {listMode !== 'none' && (\n <RegionMapList\n regions={regions}\n copy={copy}\n mode={listMode}\n sort={listSort}\n onToggle={onToggle}\n testId={dataTestId}\n theme={theme}\n />\n )}\n </Box>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,SAAS;AAChB,SAAS,gBAAgB;AAqDjB,mBACE,KADF;AA3CR,IAAM,SAA4B,CAAC;AAuB5B,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,aAAa;AACf,GAA6C;AAC3C,QAAM,QAAQ,SAAS;AACvB,QAAM,QAAQ,aAAa,EAAE,SAAS,QAAQ,SAAS,SAAS,CAAC;AACjE,QAAM,EAAE,KAAK,IAAI;AACjB,MAAI,SAAS,QAAQ,QAAQ,WAAW,EAAG,QAAO;AAClD,SACE,qBAAC,OAAI,WAAsB,eAAa,YACrC;AAAA,YACC,iCACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,EAAE,GAAG,OAAO,KAAK;AAAA,UACxB,YAAY,WAAW;AAAA,UACvB;AAAA,UACA;AAAA,UACA,OAAO,KAAK;AAAA,UACZ,UAAU,aAAa;AAAA,UACvB,QAAQ;AAAA,UACR;AAAA;AAAA,MACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,iBAAiB,MAAM;AAAA,UACvB,YAAY,WAAW;AAAA,UACvB;AAAA,UACA,QAAQ;AAAA;AAAA,MACV;AAAA,OACF;AAAA,IAED,aAAa,UACZ;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR;AAAA;AAAA,IACF;AAAA,KAEJ;AAEJ;","names":[]}