@12-apps/ui 6.98.0 → 6.99.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 (210) hide show
  1. package/dist/_internal/components/data-display/Alert/Alert.content.js +4 -2
  2. package/dist/_internal/components/data-display/Alert/Alert.ink.js +27 -0
  3. package/dist/_internal/components/data-display/Alert/Alert.js +6 -5
  4. package/dist/_internal/components/data-display/Alert/Alert.metrics.js +5 -1
  5. package/dist/_internal/components/data-display/Alert/Alert.styles.js +2 -2
  6. package/dist/_internal/components/data-display/Banner/Banner.js +3 -3
  7. package/dist/_internal/components/data-display/Banner/Banner.styles.js +2 -2
  8. package/dist/_internal/components/data-display/CardKit/card-actions-context.js +8 -7
  9. package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +9 -8
  10. package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
  11. package/dist/_internal/components/data-display/Chart/SpecChart.js +3 -3
  12. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +1 -1
  13. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +17 -17
  14. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +26 -25
  15. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +1 -1
  16. package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js +1 -1
  17. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +1 -1
  18. package/dist/_internal/components/data-display/DataViews/base-list-card.js +4 -4
  19. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +2 -2
  20. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +14 -14
  21. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
  22. package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +2 -2
  23. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +3 -3
  24. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +8 -7
  25. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +1 -1
  26. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +2 -2
  27. package/dist/_internal/components/data-display/DayRuler/DayRuler.footer.js +2 -2
  28. package/dist/_internal/components/data-display/DayRuler/DayRuler.js +7 -7
  29. package/dist/_internal/components/data-display/DayRuler/DayRuler.keyboard.js +2 -2
  30. package/dist/_internal/components/data-display/DayRuler/DayRuler.parts.js +3 -3
  31. package/dist/_internal/components/data-display/DayRuler/useRulerDrag.js +2 -2
  32. package/dist/_internal/components/data-display/FleetMap/FleetBody.js +6 -6
  33. package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js +5 -5
  34. package/dist/_internal/components/data-display/FleetMap/FleetMap.js +7 -7
  35. package/dist/_internal/components/data-display/HoverCard/HoverCard.js +2 -2
  36. package/dist/_internal/components/data-display/Lightbox/Lightbox.js +3 -3
  37. package/dist/_internal/components/data-display/MapPreview/MapLayers.js +2 -2
  38. package/dist/_internal/components/data-display/MapPreview/MapPreview.js +4 -4
  39. package/dist/_internal/components/data-display/Progress/Progress.js +2 -2
  40. package/dist/_internal/components/data-display/RegionMap/RegionMap.fixtures.js +1 -1
  41. package/dist/_internal/components/data-display/RegionMap/RegionMap.helpers.js +3 -1
  42. package/dist/_internal/components/data-display/RegionMap/RegionMap.hooks.js +3 -3
  43. package/dist/_internal/components/data-display/RegionMap/RegionMap.js +6 -5
  44. package/dist/_internal/components/data-display/RegionMap/RegionMap.view.js +2 -2
  45. package/dist/_internal/components/data-display/RegionMap/RegionMapDrawing.js +3 -2
  46. package/dist/_internal/components/data-display/RegionMap/RegionMapPin.js +26 -0
  47. package/dist/_internal/components/data-display/index.js +60 -50
  48. package/dist/_internal/components/feedback/NotificationStatusNotice/NotificationStatusNotice.js +7 -6
  49. package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.frame.js +4 -4
  50. package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.header.js +3 -3
  51. package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.js +14 -13
  52. package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.overview.js +9 -8
  53. package/dist/_internal/components/feedback/SequenceResolver/SequenceResolver.parts.js +8 -7
  54. package/dist/_internal/components/feedback/index.js +15 -14
  55. package/dist/_internal/components/index.js +66 -56
  56. package/dist/charts.js +11 -11
  57. package/dist/{chunk-WYNGH2WL.js → chunk-255WTBZ5.js} +8 -8
  58. package/dist/{chunk-QIP37Y2C.js → chunk-2RC6EAGD.js} +2 -2
  59. package/dist/{chunk-ZKVSFVIA.js → chunk-37MJZLQU.js} +2 -2
  60. package/dist/{chunk-BQBEN44R.js → chunk-3I7KE2N6.js} +2 -2
  61. package/dist/{chunk-L4FWEI2P.js → chunk-3V3Z4EML.js} +5 -5
  62. package/dist/{chunk-E4MJA52O.js → chunk-3V5V44UP.js} +16 -18
  63. package/dist/chunk-3V5V44UP.js.map +1 -0
  64. package/dist/{chunk-6RYSSNOO.js → chunk-54DOLCJ3.js} +2 -2
  65. package/dist/{chunk-74IH3ZGD.js → chunk-643YFWNK.js} +2 -2
  66. package/dist/{chunk-FRQGZIYU.js → chunk-6WAP7BJS.js} +2 -2
  67. package/dist/chunk-AG4F6NVT.js +1 -0
  68. package/dist/chunk-AG4F6NVT.js.map +1 -0
  69. package/dist/{chunk-3CXRHRGE.js → chunk-AYDVNXGR.js} +7 -3
  70. package/dist/chunk-AYDVNXGR.js.map +1 -0
  71. package/dist/{chunk-JO6D7YGN.js → chunk-CWRQGA2T.js} +2 -2
  72. package/dist/chunk-DRYBA76G.js +293 -0
  73. package/dist/chunk-DRYBA76G.js.map +1 -0
  74. package/dist/{chunk-ZW7Q3IRL.js → chunk-FYJL432J.js} +4 -4
  75. package/dist/{chunk-G6TKWNF2.js → chunk-GZITNT7O.js} +11 -11
  76. package/dist/{chunk-RFMQBIE6.js → chunk-HBHBRRSW.js} +4 -4
  77. package/dist/{chunk-U4Z6GZDW.js → chunk-HN36UVOE.js} +4 -4
  78. package/dist/{chunk-WK577LQL.js → chunk-J2IQ6FJ5.js} +4 -4
  79. package/dist/{chunk-HCTHY66U.js → chunk-KAGXV3ID.js} +4 -4
  80. package/dist/{chunk-FWAY2SAI.js → chunk-KAZWBDIH.js} +2 -2
  81. package/dist/{chunk-DKPELKGL.js → chunk-KV7LTOWC.js} +5 -5
  82. package/dist/{chunk-UC7DLGMP.js → chunk-KVP5A3YK.js} +2 -2
  83. package/dist/chunk-LNZJJFZQ.js +41 -0
  84. package/dist/chunk-LNZJJFZQ.js.map +1 -0
  85. package/dist/{chunk-EGBQR5GH.js → chunk-LOLYBFYY.js} +5 -5
  86. package/dist/{chunk-SOXO2VN3.js → chunk-MEMVZW64.js} +3 -3
  87. package/dist/{chunk-COOSJKZL.js → chunk-MNJZPJ5S.js} +2 -1
  88. package/dist/chunk-MNJZPJ5S.js.map +1 -0
  89. package/dist/{chunk-G2NVBCF7.js → chunk-NWUMGE6T.js} +3 -3
  90. package/dist/{chunk-NQCTANF3.js → chunk-Q5SCBQLY.js} +18 -11
  91. package/dist/chunk-Q5SCBQLY.js.map +1 -0
  92. package/dist/{chunk-64LNNC54.js → chunk-QLKUWDMQ.js} +4 -4
  93. package/dist/{chunk-MPVH2GM5.js → chunk-RAQD5QCM.js} +4 -4
  94. package/dist/{chunk-KXDLLS62.js → chunk-REHGWN5M.js} +4 -4
  95. package/dist/{chunk-Z4ZFJ565.js → chunk-SRAIV6U6.js} +3 -3
  96. package/dist/{chunk-5ZY3IIIG.js → chunk-TBQR5VCN.js} +11 -1
  97. package/dist/chunk-TBQR5VCN.js.map +1 -0
  98. package/dist/{chunk-3N2HWNKT.js → chunk-UIJLDAV7.js} +2 -2
  99. package/dist/{chunk-7HZ36QDH.js → chunk-UN5WU2OX.js} +9 -3
  100. package/dist/chunk-UN5WU2OX.js.map +1 -0
  101. package/dist/{chunk-A27J4CIZ.js → chunk-UTIYB6VE.js} +11 -11
  102. package/dist/{chunk-5V7IZBRY.js → chunk-VRSW7PIO.js} +5 -5
  103. package/dist/{chunk-U4PV2ZKK.js → chunk-WDEVKND3.js} +3 -3
  104. package/dist/chunk-WGKLBVDZ.js +1 -0
  105. package/dist/chunk-WGKLBVDZ.js.map +1 -0
  106. package/dist/{chunk-AP7GEUJS.js → chunk-XFOKLWJZ.js} +2 -2
  107. package/dist/{chunk-6D5AMANO.js → chunk-XQAFSC66.js} +2 -2
  108. package/dist/{chunk-YNZUYZ2O.js → chunk-XR474YYX.js} +2 -2
  109. package/dist/{chunk-L5RS6QAT.js → chunk-YBF2WUGQ.js} +2 -2
  110. package/dist/{chunk-RCTHCXS6.js → chunk-YQ5GQ32T.js} +2 -2
  111. package/dist/{chunk-YLZXIR6B.js → chunk-Z5V5E3JW.js} +4 -4
  112. package/dist/data-display/Alert.js +10 -6
  113. package/dist/data-display/Banner.js +3 -3
  114. package/dist/data-display/CardKit.js +9 -8
  115. package/dist/data-display/Carousel.js +2 -2
  116. package/dist/data-display/Chart.js +11 -11
  117. package/dist/data-display/DataGrid.js +1 -1
  118. package/dist/data-display/DataViews.js +33 -32
  119. package/dist/data-display/DayRuler.js +7 -7
  120. package/dist/data-display/FleetMap.js +7 -7
  121. package/dist/data-display/HoverCard.js +2 -2
  122. package/dist/data-display/Lightbox.js +3 -3
  123. package/dist/data-display/MapPreview.js +4 -4
  124. package/dist/data-display/Progress.js +2 -2
  125. package/dist/data-display/RegionMap.js +14 -8
  126. package/dist/feedback/NotificationStatusNotice.js +7 -6
  127. package/dist/feedback/SequenceResolver.js +14 -13
  128. package/dist/native/data-display/Alert.js +10 -0
  129. package/dist/native/data-display/Alert.js.map +1 -1
  130. package/dist/types/components/data-display/Alert/Alert.content.d.ts.map +1 -1
  131. package/dist/types/components/data-display/Alert/Alert.d.ts.map +1 -1
  132. package/dist/types/components/data-display/Alert/Alert.ink.d.ts +65 -0
  133. package/dist/types/components/data-display/Alert/Alert.ink.d.ts.map +1 -0
  134. package/dist/types/components/data-display/Alert/Alert.metrics.d.ts +73 -5
  135. package/dist/types/components/data-display/Alert/Alert.metrics.d.ts.map +1 -1
  136. package/dist/types/components/data-display/Alert/index.d.ts +1 -0
  137. package/dist/types/components/data-display/Alert/index.d.ts.map +1 -1
  138. package/dist/types/components/data-display/RegionMap/RegionMap.d.ts +1 -1
  139. package/dist/types/components/data-display/RegionMap/RegionMap.d.ts.map +1 -1
  140. package/dist/types/components/data-display/RegionMap/RegionMap.helpers.d.ts +9 -0
  141. package/dist/types/components/data-display/RegionMap/RegionMap.helpers.d.ts.map +1 -1
  142. package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts +45 -1
  143. package/dist/types/components/data-display/RegionMap/RegionMap.types.d.ts.map +1 -1
  144. package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts +4 -1
  145. package/dist/types/components/data-display/RegionMap/RegionMapDrawing.d.ts.map +1 -1
  146. package/dist/types/components/data-display/RegionMap/RegionMapPin.d.ts +102 -0
  147. package/dist/types/components/data-display/RegionMap/RegionMapPin.d.ts.map +1 -0
  148. package/dist/types/components/data-display/RegionMap/index.d.ts +2 -1
  149. package/dist/types/components/data-display/RegionMap/index.d.ts.map +1 -1
  150. package/dist/types-native/components/data-display/Alert/Alert.metrics.d.ts +73 -5
  151. package/dist/types-native/components/data-display/Alert/Alert.metrics.d.ts.map +1 -1
  152. package/package.json +1 -1
  153. package/src/components/data-display/Alert/Alert.content.tsx +16 -1
  154. package/src/components/data-display/Alert/Alert.ink.ts +106 -0
  155. package/src/components/data-display/Alert/Alert.md +40 -0
  156. package/src/components/data-display/Alert/Alert.metrics.ts +57 -5
  157. package/src/components/data-display/Alert/Alert.tsx +18 -7
  158. package/src/components/data-display/Alert/index.ts +1 -0
  159. package/src/components/data-display/RegionMap/RegionMap.helpers.ts +1 -1
  160. package/src/components/data-display/RegionMap/RegionMap.md +19 -2
  161. package/src/components/data-display/RegionMap/RegionMap.tsx +4 -0
  162. package/src/components/data-display/RegionMap/RegionMap.types.ts +49 -1
  163. package/src/components/data-display/RegionMap/RegionMapDrawing.tsx +11 -13
  164. package/src/components/data-display/RegionMap/RegionMapPin.tsx +388 -0
  165. package/src/components/data-display/RegionMap/index.ts +5 -0
  166. package/dist/chunk-3CXRHRGE.js.map +0 -1
  167. package/dist/chunk-5ZY3IIIG.js.map +0 -1
  168. package/dist/chunk-7HZ36QDH.js.map +0 -1
  169. package/dist/chunk-COOSJKZL.js.map +0 -1
  170. package/dist/chunk-DY2I6LMX.js +0 -1
  171. package/dist/chunk-E4MJA52O.js.map +0 -1
  172. package/dist/chunk-NQCTANF3.js.map +0 -1
  173. package/dist/chunk-NUGT7LMR.js +0 -1
  174. /package/dist/{chunk-DY2I6LMX.js.map → _internal/components/data-display/Alert/Alert.ink.js.map} +0 -0
  175. /package/dist/{chunk-NUGT7LMR.js.map → _internal/components/data-display/RegionMap/RegionMapPin.js.map} +0 -0
  176. /package/dist/{chunk-WYNGH2WL.js.map → chunk-255WTBZ5.js.map} +0 -0
  177. /package/dist/{chunk-QIP37Y2C.js.map → chunk-2RC6EAGD.js.map} +0 -0
  178. /package/dist/{chunk-ZKVSFVIA.js.map → chunk-37MJZLQU.js.map} +0 -0
  179. /package/dist/{chunk-BQBEN44R.js.map → chunk-3I7KE2N6.js.map} +0 -0
  180. /package/dist/{chunk-L4FWEI2P.js.map → chunk-3V3Z4EML.js.map} +0 -0
  181. /package/dist/{chunk-6RYSSNOO.js.map → chunk-54DOLCJ3.js.map} +0 -0
  182. /package/dist/{chunk-74IH3ZGD.js.map → chunk-643YFWNK.js.map} +0 -0
  183. /package/dist/{chunk-FRQGZIYU.js.map → chunk-6WAP7BJS.js.map} +0 -0
  184. /package/dist/{chunk-JO6D7YGN.js.map → chunk-CWRQGA2T.js.map} +0 -0
  185. /package/dist/{chunk-ZW7Q3IRL.js.map → chunk-FYJL432J.js.map} +0 -0
  186. /package/dist/{chunk-G6TKWNF2.js.map → chunk-GZITNT7O.js.map} +0 -0
  187. /package/dist/{chunk-RFMQBIE6.js.map → chunk-HBHBRRSW.js.map} +0 -0
  188. /package/dist/{chunk-U4Z6GZDW.js.map → chunk-HN36UVOE.js.map} +0 -0
  189. /package/dist/{chunk-WK577LQL.js.map → chunk-J2IQ6FJ5.js.map} +0 -0
  190. /package/dist/{chunk-HCTHY66U.js.map → chunk-KAGXV3ID.js.map} +0 -0
  191. /package/dist/{chunk-FWAY2SAI.js.map → chunk-KAZWBDIH.js.map} +0 -0
  192. /package/dist/{chunk-DKPELKGL.js.map → chunk-KV7LTOWC.js.map} +0 -0
  193. /package/dist/{chunk-UC7DLGMP.js.map → chunk-KVP5A3YK.js.map} +0 -0
  194. /package/dist/{chunk-EGBQR5GH.js.map → chunk-LOLYBFYY.js.map} +0 -0
  195. /package/dist/{chunk-SOXO2VN3.js.map → chunk-MEMVZW64.js.map} +0 -0
  196. /package/dist/{chunk-G2NVBCF7.js.map → chunk-NWUMGE6T.js.map} +0 -0
  197. /package/dist/{chunk-64LNNC54.js.map → chunk-QLKUWDMQ.js.map} +0 -0
  198. /package/dist/{chunk-MPVH2GM5.js.map → chunk-RAQD5QCM.js.map} +0 -0
  199. /package/dist/{chunk-KXDLLS62.js.map → chunk-REHGWN5M.js.map} +0 -0
  200. /package/dist/{chunk-Z4ZFJ565.js.map → chunk-SRAIV6U6.js.map} +0 -0
  201. /package/dist/{chunk-3N2HWNKT.js.map → chunk-UIJLDAV7.js.map} +0 -0
  202. /package/dist/{chunk-A27J4CIZ.js.map → chunk-UTIYB6VE.js.map} +0 -0
  203. /package/dist/{chunk-5V7IZBRY.js.map → chunk-VRSW7PIO.js.map} +0 -0
  204. /package/dist/{chunk-U4PV2ZKK.js.map → chunk-WDEVKND3.js.map} +0 -0
  205. /package/dist/{chunk-AP7GEUJS.js.map → chunk-XFOKLWJZ.js.map} +0 -0
  206. /package/dist/{chunk-6D5AMANO.js.map → chunk-XQAFSC66.js.map} +0 -0
  207. /package/dist/{chunk-YNZUYZ2O.js.map → chunk-XR474YYX.js.map} +0 -0
  208. /package/dist/{chunk-L5RS6QAT.js.map → chunk-YBF2WUGQ.js.map} +0 -0
  209. /package/dist/{chunk-RCTHCXS6.js.map → chunk-YQ5GQ32T.js.map} +0 -0
  210. /package/dist/{chunk-YLZXIR6B.js.map → chunk-Z5V5E3JW.js.map} +0 -0
@@ -102,7 +102,10 @@ drawing means copying its view math and losing its frame. So:
102
102
  |---|---|---|---|
103
103
  | `regions` | `readonly RegionMapRegion[]` | — | the areas; drawn when any part is in view |
104
104
  | `center` | `{ latitude, longitude } \| null` | — | centre, dot and circles; `null` centres on the areas, no dot |
105
- | `circles` | `readonly { radiusMetres }[]` | `[]` | dashed circles; the view fits the largest |
105
+ | `circles` | `readonly { radiusMetres }[]` | `[]` | dashed circles; the view fits the largest (solid, around the pin, when there is a `pin`) |
106
+ | `pin` | `{ at, label, ariaLabel, onMove?, onDrop, stepMetres?, disabled? }` | — | the centre as a pin the viewer drags or nudges with the arrow keys; the view stays on `center` |
107
+ | `ghost` | `{ at, radiusMetres }` | — | a dashed ring and dot where the circles stood, a dashed line to the pin |
108
+ | `dots` | `readonly { id, at, filled }[]` | — | small points, filled or hollow, not tappable |
106
109
  | `reach` | `{ radiusMetres } \| { bounds: { south, west, north, east } }` | — | an extent the view must also fit (margin on top) |
107
110
  | `overlay` | `(layer: { view, projection }) => ReactNode` | — | a host layer inside the SVG, under the circles, tap-transparent |
108
111
  | `onToggle` | `(id: string) => void` | — | a tap or a list press; omit for read-only |
@@ -132,7 +135,21 @@ drawing means copying its view math and losing its frame. So:
132
135
 
133
136
  `mapLabel`, `legendExcluded`, `legendCircle(radiusMetres)`, `legendCenter`,
134
137
  `listLabel`, `showList(count)` (the count of LISTED areas), `hideList`, and
135
- `rowState: { excluded, outside }` — a row's state in words, for a screen reader.
138
+ `rowState: { excluded, outside }` — a row's state in words, for a screen reader;
139
+ optional `scale(metres)` — a scale bar's length ("1 km"), shown only when given.
140
+
141
+ ### The movable centre (`pin`)
142
+
143
+ A store's point placed 200 m off by a geocoder is corrected by dragging it
144
+ where the circles are measured from. So the pin moves and the VIEW does not:
145
+ the areas never slide under a dragging finger. The pin is HTML over the
146
+ drawing (a 44×48 button, `touch-action: none`), carrying `data-region-pin` so
147
+ a host that zooms the drawing's layers can move it with them. A drag is
148
+ captured, clamped inside the view, and kept from the host's own listeners on
149
+ the drawing; each arrow key moves it `stepMetres` (50). Moves are reported
150
+ (`onMove`, `onDrop`), never applied: the host passes the new `at` back.
151
+ `latLngAt(center, planePoint)` is the projection's inverse for a host that
152
+ needs it.
136
153
 
137
154
  ## Helpers
138
155
 
@@ -39,6 +39,9 @@ export function RegionMap({
39
39
  regions,
40
40
  center,
41
41
  circles,
42
+ pin,
43
+ ghost,
44
+ dots,
42
45
  reach,
43
46
  overlay,
44
47
  onToggle,
@@ -62,6 +65,7 @@ export function RegionMap({
62
65
  model={{ ...model, view }}
63
66
  overlay={overlay}
64
67
  showCenter={center !== null}
68
+ moving={center === null ? undefined : { center, pin, ghost, dots, copy }}
65
69
  highlightedIds={highlightedIds}
66
70
  maxHeight={maxHeight}
67
71
  label={copy.mapLabel}
@@ -49,6 +49,46 @@ export interface RegionMapRegion {
49
49
  detail?: string;
50
50
  }
51
51
 
52
+ /** A latitude/longitude pair, by name. */
53
+ export interface RegionMapCenter {
54
+ latitude: number;
55
+ longitude: number;
56
+ }
57
+
58
+ /**
59
+ * The centre as a pin the viewer moves (a store's door). The VIEW stays on
60
+ * `center`; the pin, and the circles with it, are drawn at `at`. Moves are
61
+ * reported, never applied: the host passes the new `at` back.
62
+ */
63
+ export interface RegionMapPin {
64
+ at: RegionMapCenter;
65
+ /** Printed beside the pin, e.g. "Store". */
66
+ label: string;
67
+ /** The pin's accessible name, e.g. "Store pin. Drag it, or use the arrow keys, to move it". */
68
+ ariaLabel: string;
69
+ /** Every point while a pointer drags it. */
70
+ onMove?: (at: RegionMapCenter) => void;
71
+ /** Where it came to rest: the pointer let go, or an arrow key moved it. */
72
+ onDrop: (at: RegionMapCenter) => void;
73
+ /** Metres one arrow key moves it. Default 50. */
74
+ stepMetres?: number;
75
+ /** Held in place (a save in flight). */
76
+ disabled?: boolean;
77
+ }
78
+
79
+ /** Where the pin's outermost circle stood: a dashed ring of `radiusMetres` and a dashed dot at `at`. */
80
+ export interface RegionMapGhost {
81
+ at: RegionMapCenter;
82
+ radiusMetres: number;
83
+ }
84
+
85
+ /** A small point on the map (an address), filled or hollow. Not tappable. */
86
+ export interface RegionMapDot {
87
+ id: string;
88
+ at: RegionMapCenter;
89
+ filled: boolean;
90
+ }
91
+
52
92
  /** A circle drawn around the centre, e.g. a delivery ring. */
53
93
  export interface RegionMapCircle {
54
94
  radiusMetres: number;
@@ -99,6 +139,8 @@ export interface RegionMapCopy {
99
139
  * `outside` one (faded). Neither is shown; the row's look carries it.
100
140
  */
101
141
  rowState: { excluded: string; outside: string };
142
+ /** A scale bar's length, e.g. "1 km". Omit for no scale bar. */
143
+ scale?: (metres: number) => string;
102
144
  }
103
145
 
104
146
  /** The order of the list: as given (e.g. nearest first), or by name. */
@@ -120,8 +162,14 @@ export interface RegionMapProps {
120
162
  * `null` with no regions renders NOTHING rather than a map of `0, 0`.
121
163
  */
122
164
  center: { latitude: number; longitude: number } | null;
123
- /** Circles around the centre; the view fits the largest. */
165
+ /** Circles around the centre (around `pin.at` when there is a pin); the view fits the largest. */
124
166
  circles?: readonly RegionMapCircle[];
167
+ /** The centre as a draggable pin; it replaces the centre's dot, and the circles follow it. */
168
+ pin?: RegionMapPin;
169
+ /** A dashed ring and dot where the circles stood (drawn with a `pin`). */
170
+ ghost?: RegionMapGhost;
171
+ /** Small points, filled or hollow, over the areas and under the pin. */
172
+ dots?: readonly RegionMapDot[];
125
173
  /**
126
174
  * An extent the view must ALSO fit, before its margin — something the host
127
175
  * draws past the largest circle. With circles the view stays centred on the
@@ -9,12 +9,16 @@ import { labelPosition, type AreaGeometry, type DrawnRegion, type PlaneBox } fro
9
9
  import type { RegionMapModel } from './RegionMap.hooks';
10
10
  import { labelOrder, placeLabels } from './RegionMap.labels';
11
11
  import type { RegionMapLayer } from './RegionMap.types';
12
+ import type { Moving } from './RegionMapPin';
13
+ import { PinOverlay, PinShapes } from './RegionMapPin';
12
14
 
13
15
  export interface RegionMapDrawingProps {
14
16
  model: RegionMapModel & { view: PlaneBox };
15
17
  /** The host's layer, drawn inside the SVG under the circles (`RegionMapProps.overlay`). */
16
18
  overlay?: (layer: RegionMapLayer) => React.ReactNode;
17
19
  showCenter: boolean;
20
+ /** The movable centre and what is drawn with it (`RegionMapProps.pin`, `ghost`, `dots`). */
21
+ moving?: Moving;
18
22
  highlightedIds: readonly string[];
19
23
  maxHeight: string | undefined;
20
24
  label: string;
@@ -33,17 +37,8 @@ export interface RegionMapDrawingProps {
33
37
  * The base areas are a memoised layer: a toggle changes `excluded`, not the
34
38
  * geometry, so only the hatch, highlight and label layers re-render.
35
39
  */
36
- export function RegionMapDrawing({
37
- model,
38
- overlay,
39
- showCenter,
40
- highlightedIds,
41
- maxHeight,
42
- label,
43
- tappable,
44
- testId,
45
- theme,
46
- }: RegionMapDrawingProps): React.JSX.Element {
40
+ export function RegionMapDrawing(props: RegionMapDrawingProps): React.JSX.Element {
41
+ const { model, overlay, showCenter, moving, highlightedIds, maxHeight, label, tappable, testId, theme } = props;
47
42
  const hatchId = `${React.useId().replace(/:/g, '')}-hatch`;
48
43
  const { view, drawn } = model;
49
44
  const wanted = new Set(highlightedIds);
@@ -52,6 +47,7 @@ export function RegionMapDrawing({
52
47
  // claim their place in, so a collision hides the less important one.
53
48
  const labelled = labelOrder(drawn.filter((d) => d.excluded || wanted.has(d.id)), wanted);
54
49
  const boxRef = React.useRef<HTMLDivElement>(null);
50
+ const svgRef = React.useRef<SVGSVGElement>(null);
55
51
  useLabelPlacement(boxRef, labelled.map((d) => d.id).join('|'), remPx(theme, 4));
56
52
  const base = drawn.map((d) => ({ id: d.id, name: d.name, geometry: d.geometry, outside: d.outside === true }));
57
53
  return (
@@ -72,6 +68,7 @@ export function RegionMapDrawing({
72
68
  ref={boxRef}
73
69
  >
74
70
  <svg
71
+ ref={svgRef}
75
72
  viewBox={`${view.minX} ${view.minY} ${view.spanX} ${view.spanY}`}
76
73
  role="img"
77
74
  aria-label={label}
@@ -89,9 +86,9 @@ export function RegionMapDrawing({
89
86
  {overlay({ view, projection: model.projection })}
90
87
  </g>
91
88
  )}
92
- <Circles radii={model.radii} theme={theme} />
89
+ <PinShapes model={model} moving={moving} testId={testId} theme={theme} fallback={<Circles radii={model.radii} theme={theme} />} />
93
90
  <Highlights areas={highlighted} testId={testId} theme={theme} />
94
- {showCenter && (
91
+ {showCenter && !moving?.pin && (
95
92
  <circle
96
93
  cx={0}
97
94
  cy={0}
@@ -108,6 +105,7 @@ export function RegionMapDrawing({
108
105
  {labelled.map((d) => (
109
106
  <AreaLabel key={`l-${d.id}`} area={d} view={view} testId={testId} theme={theme} />
110
107
  ))}
108
+ <PinOverlay model={model} moving={moving} svgRef={svgRef} testId={testId} theme={theme} />
111
109
  </Box>
112
110
  );
113
111
  }
@@ -0,0 +1,388 @@
1
+ import Box from '@mui/material/Box/index.js';
2
+ import { alpha, type Theme } from '@mui/material/styles/index.js';
3
+ import React from 'react';
4
+
5
+ import { rem } from '../../../tokens/relative';
6
+
7
+ import { METRES_PER_DEGREE, type PlaneBox, type PlanePoint, type Projection } from './RegionMap.helpers';
8
+ import type { RegionMapModel } from './RegionMap.hooks';
9
+ import type { RegionMapCenter, RegionMapCopy, RegionMapDot, RegionMapGhost, RegionMapPin } from './RegionMap.types';
10
+
11
+ /**
12
+ * The movable centre of a `RegionMap` (`pin`, `ghost`, `dots`): a pin the
13
+ * viewer drags with a mouse or one finger, or focuses and nudges with the
14
+ * arrow keys, while the VIEW stays where `center` put it — so the areas never
15
+ * slide under a dragging finger. The circles follow the pin; a dashed ring
16
+ * and dot stay at `ghost`; `dots` are small points (filled or hollow).
17
+ *
18
+ * The pin is HTML over the drawing, at its point by percent like the labels,
19
+ * carrying `data-region-pin` so a host that zooms the drawing's layers can
20
+ * move it with them. Its moves are reported, never applied: the host passes
21
+ * the new point back.
22
+ */
23
+
24
+ /** The movable centre and what is drawn with it, as the drawing receives it. */
25
+ export interface Moving {
26
+ center: RegionMapCenter;
27
+ pin?: RegionMapPin;
28
+ ghost?: RegionMapGhost;
29
+ dots?: readonly RegionMapDot[];
30
+ copy: RegionMapCopy;
31
+ }
32
+
33
+ type DrawnModel = Pick<RegionMapModel, 'projection' | 'radii'> & { view: PlaneBox };
34
+
35
+ /**
36
+ * What the movable centre draws inside the SVG: the circles around the pin
37
+ * (else `fallback`, the centre's own circles), the ghost, and the dots.
38
+ */
39
+ export function PinShapes({ model, moving, testId, theme, fallback }: { model: DrawnModel; moving: Moving | undefined; testId: string; theme: Theme; fallback: React.ReactNode }): React.JSX.Element {
40
+ const { projection, view } = model;
41
+ if (!moving || !projection) return <>{fallback}</>;
42
+ const pinAt = moving.pin ? projection.project([moving.pin.at.latitude, moving.pin.at.longitude]) : null;
43
+ return (
44
+ <>
45
+ {pinAt ? <PinCircles at={pinAt} radii={model.radii} theme={theme} /> : fallback}
46
+ {pinAt && moving.ghost ? <GhostShapes ghost={moving.ghost} pinAt={pinAt} projection={projection} view={view} testId={testId} theme={theme} /> : null}
47
+ {moving.dots ? <Dots dots={moving.dots} projection={projection} view={view} testId={testId} theme={theme} /> : null}
48
+ </>
49
+ );
50
+ }
51
+
52
+ /** What the movable centre draws over the SVG, in HTML: the pin and the scale bar. */
53
+ export function PinOverlay({
54
+ model,
55
+ moving,
56
+ svgRef,
57
+ testId,
58
+ theme,
59
+ }: {
60
+ model: DrawnModel;
61
+ moving: Moving | undefined;
62
+ svgRef: React.RefObject<SVGSVGElement | null>;
63
+ testId: string;
64
+ theme: Theme;
65
+ }): React.JSX.Element | null {
66
+ if (!moving || !model.projection) return null;
67
+ return (
68
+ <>
69
+ {moving.pin ? <PinMarker pin={moving.pin} center={moving.center} projection={model.projection} view={model.view} svgRef={svgRef} testId={testId} theme={theme} /> : null}
70
+ <ScaleBar view={model.view} copy={moving.copy} svgRef={svgRef} testId={testId} theme={theme} />
71
+ </>
72
+ );
73
+ }
74
+
75
+ /** The flat projection's inverse around `center` — it is linear. */
76
+ export function latLngAt(center: RegionMapCenter, point: PlanePoint): RegionMapCenter {
77
+ const kx = METRES_PER_DEGREE * Math.cos((center.latitude * Math.PI) / 180);
78
+ return { latitude: center.latitude - point.y / METRES_PER_DEGREE, longitude: center.longitude + point.x / kx };
79
+ }
80
+
81
+ /** A point of the plane as fractions of the view (0..1 across, 0..1 down). */
82
+ function fractionOf(view: PlaneBox, point: PlanePoint): { x: number; y: number } {
83
+ return { x: (point.x - view.minX) / view.spanX, y: (point.y - view.minY) / view.spanY };
84
+ }
85
+
86
+ /** Keep a fraction inside the view, a hair in from its edge (the mock's clamp). */
87
+ const clampFraction = (value: number): number => Math.min(0.995, Math.max(0.005, value));
88
+
89
+ /** A plane point kept inside the view by the same clamp a drag gets. */
90
+ function clampToView(view: PlaneBox, point: PlanePoint): PlanePoint {
91
+ const at = fractionOf(view, point);
92
+ return { x: view.minX + clampFraction(at.x) * view.spanX, y: view.minY + clampFraction(at.y) * view.spanY };
93
+ }
94
+
95
+ /** Arrow keys, as metres east and north per press. */
96
+ const ARROWS: Record<string, [number, number]> = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] };
97
+
98
+ /** One arrow key: 50 m unless the host says otherwise. */
99
+ const DEFAULT_STEP_METRES = 50;
100
+
101
+ interface PinLayerProps {
102
+ pin: RegionMapPin;
103
+ center: RegionMapCenter;
104
+ projection: Projection;
105
+ view: PlaneBox;
106
+ /** The drawing's SVG, whose on-screen box maps a pointer to the plane (zoomed or not). */
107
+ svgRef: React.RefObject<SVGSVGElement | null>;
108
+ testId: string;
109
+ theme: Theme;
110
+ }
111
+
112
+ /** The draggable pin, as HTML over the drawing. */
113
+ export function PinMarker({ pin, center, projection, view, svgRef, testId, theme }: PinLayerProps): React.JSX.Element {
114
+ const ref = React.useRef<HTMLDivElement>(null);
115
+ const live = React.useRef({ pin, center, view });
116
+ live.current = { pin, center, view };
117
+ usePinDrag(ref, svgRef, live);
118
+ const at = fractionOf(view, projection.project([pin.at.latitude, pin.at.longitude]));
119
+ const onKeyDown = (event: React.KeyboardEvent): void => {
120
+ const arrow = ARROWS[event.key];
121
+ if (!arrow || pin.disabled) return;
122
+ event.preventDefault();
123
+ const step = pin.stepMetres ?? DEFAULT_STEP_METRES;
124
+ const plane = projection.project([pin.at.latitude, pin.at.longitude]);
125
+ const next = latLngAt(center, clampToView(view, { x: plane.x + arrow[0] * step, y: plane.y - arrow[1] * step }));
126
+ pin.onMove?.(next);
127
+ pin.onDrop(next);
128
+ };
129
+ return (
130
+ <Box
131
+ ref={ref}
132
+ role="button"
133
+ tabIndex={pin.disabled ? -1 : 0}
134
+ aria-label={pin.ariaLabel}
135
+ aria-disabled={pin.disabled || undefined}
136
+ onKeyDown={onKeyDown}
137
+ data-testid={`${testId}-pin`}
138
+ data-region-pin=""
139
+ style={{ left: `${(at.x * 100).toFixed(3)}%`, top: `${(at.y * 100).toFixed(3)}%` }}
140
+ sx={{
141
+ position: 'absolute',
142
+ transform: 'translate(-50%, -100%)',
143
+ width: rem(theme, 44),
144
+ height: rem(theme, 48),
145
+ display: 'flex',
146
+ alignItems: 'flex-end',
147
+ justifyContent: 'center',
148
+ cursor: pin.disabled ? 'default' : 'grab',
149
+ touchAction: 'none',
150
+ zIndex: 2,
151
+ borderRadius: 1,
152
+ '&:focus-visible': { outline: `${rem(theme, 2)} solid`, outlineColor: 'primary.main', outlineOffset: rem(theme, 2) },
153
+ }}
154
+ >
155
+ <Box component="svg" viewBox="0 0 32 42" aria-hidden="true" sx={{ width: rem(theme, 32), height: rem(theme, 42) }}>
156
+ <path
157
+ d="M16 41C16 41 3 25 3 15.5A13 13 0 1 1 29 15.5C29 25 16 41 16 41Z"
158
+ fill={theme.palette.primary.main}
159
+ stroke={theme.palette.background.paper}
160
+ strokeWidth={2}
161
+ />
162
+ <circle cx="16" cy="15.5" r="5" fill={theme.palette.background.paper} />
163
+ </Box>
164
+ <Box
165
+ component="span"
166
+ aria-hidden
167
+ sx={{
168
+ position: 'absolute',
169
+ left: '88%',
170
+ top: 0,
171
+ px: 0.75,
172
+ borderRadius: 0.5,
173
+ border: 1,
174
+ borderColor: 'divider',
175
+ bgcolor: 'background.paper',
176
+ color: 'text.primary',
177
+ fontSize: rem(theme, 12),
178
+ fontWeight: 600,
179
+ whiteSpace: 'nowrap',
180
+ pointerEvents: 'none',
181
+ }}
182
+ >
183
+ {pin.label}
184
+ </Box>
185
+ </Box>
186
+ );
187
+ }
188
+
189
+ /**
190
+ * The pointer drag: native listeners on the pin, so a host's own listeners on
191
+ * the drawing (a pan, a tap) never see the press; captured, so a finger that
192
+ * leaves the pin keeps dragging it; clamped inside the view.
193
+ */
194
+ function usePinDrag(
195
+ ref: React.RefObject<HTMLDivElement | null>,
196
+ svgRef: React.RefObject<SVGSVGElement | null>,
197
+ live: React.RefObject<{ pin: RegionMapPin; center: RegionMapCenter; view: PlaneBox }>,
198
+ ): void {
199
+ React.useEffect(() => {
200
+ const element = ref.current;
201
+ if (!element) return undefined;
202
+ let dragging: number | null = null;
203
+ // Where the last move put the pin: a cancelled gesture (a browser may
204
+ // report 0,0 for it) rests there rather than jumping to the corner.
205
+ let last: RegionMapCenter | null = null;
206
+ const pointAt = (event: PointerEvent): RegionMapCenter | null => {
207
+ const box = svgRef.current?.getBoundingClientRect();
208
+ if (!box || box.width === 0 || box.height === 0) return null;
209
+ const { view, center } = live.current;
210
+ if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return null;
211
+ const fx = clampFraction((event.clientX - box.left) / box.width);
212
+ const fy = clampFraction((event.clientY - box.top) / box.height);
213
+ return latLngAt(center, { x: view.minX + fx * view.spanX, y: view.minY + fy * view.spanY });
214
+ };
215
+ const down = (event: PointerEvent): void => {
216
+ event.stopPropagation();
217
+ if (live.current.pin.disabled) return;
218
+ event.preventDefault();
219
+ dragging = event.pointerId;
220
+ last = null;
221
+ element.setPointerCapture?.(event.pointerId);
222
+ };
223
+ const move = (event: PointerEvent): void => {
224
+ if (dragging !== event.pointerId) return;
225
+ event.stopPropagation();
226
+ const at = pointAt(event);
227
+ if (!at) return;
228
+ last = at;
229
+ live.current.pin.onMove?.(at);
230
+ };
231
+ const up = (event: PointerEvent): void => {
232
+ if (dragging !== event.pointerId) return;
233
+ event.stopPropagation();
234
+ dragging = null;
235
+ const at = event.type === 'pointercancel' ? last : (pointAt(event) ?? last);
236
+ if (at) live.current.pin.onDrop(at);
237
+ };
238
+ // A click that ends a drag is the drag's, not a tap on the area under it.
239
+ const click = (event: MouseEvent): void => event.stopPropagation();
240
+ element.addEventListener('pointerdown', down);
241
+ element.addEventListener('pointermove', move);
242
+ element.addEventListener('pointerup', up);
243
+ element.addEventListener('pointercancel', up);
244
+ element.addEventListener('click', click);
245
+ return () => {
246
+ element.removeEventListener('pointerdown', down);
247
+ element.removeEventListener('pointermove', move);
248
+ element.removeEventListener('pointerup', up);
249
+ element.removeEventListener('pointercancel', up);
250
+ element.removeEventListener('click', click);
251
+ };
252
+ }, [ref, svgRef, live]);
253
+ }
254
+
255
+ /** The circles around the pin: solid in the primary colour, faintly filled. */
256
+ export function PinCircles({ at, radii, theme }: { at: PlanePoint; radii: readonly number[]; theme: Theme }): React.JSX.Element {
257
+ return (
258
+ <>
259
+ {radii.map((radius) => (
260
+ <circle
261
+ key={radius}
262
+ cx={at.x}
263
+ cy={at.y}
264
+ r={radius}
265
+ fill={alpha(theme.palette.primary.main, 0.06)}
266
+ stroke={theme.palette.primary.main}
267
+ strokeWidth={2.25}
268
+ vectorEffect="non-scaling-stroke"
269
+ pointerEvents="none"
270
+ />
271
+ ))}
272
+ </>
273
+ );
274
+ }
275
+
276
+ /** Where the circles stood: a dashed ring and dot at `ghost`, and a dashed line from it to the pin. */
277
+ export function GhostShapes({
278
+ ghost,
279
+ pinAt,
280
+ projection,
281
+ view,
282
+ testId,
283
+ theme,
284
+ }: {
285
+ ghost: RegionMapGhost;
286
+ pinAt: PlanePoint;
287
+ projection: Projection;
288
+ view: PlaneBox;
289
+ testId: string;
290
+ theme: Theme;
291
+ }): React.JSX.Element {
292
+ const at = projection.project([ghost.at.latitude, ghost.at.longitude]);
293
+ const stroke = theme.palette.text.secondary;
294
+ const common = { fill: 'none', stroke, vectorEffect: 'non-scaling-stroke', pointerEvents: 'none' } as const;
295
+ return (
296
+ <g data-testid={`${testId}-ghost`}>
297
+ <circle cx={at.x} cy={at.y} r={ghost.radiusMetres} strokeWidth={1.5} strokeDasharray="6 5" {...common} />
298
+ <line x1={at.x} y1={at.y} x2={pinAt.x} y2={pinAt.y} strokeWidth={1.5} strokeDasharray="4 4" {...common} />
299
+ <circle cx={at.x} cy={at.y} r={view.spanX / 110} strokeWidth={2} strokeDasharray="3 2" {...common} fill={alpha(theme.palette.background.paper, 0.7)} />
300
+ </g>
301
+ );
302
+ }
303
+
304
+ /** The dots: filled with the ink, or hollow (paper with an ink ring). */
305
+ export function Dots({ dots, projection, view, testId, theme }: { dots: readonly RegionMapDot[]; projection: Projection; view: PlaneBox; testId: string; theme: Theme }): React.JSX.Element {
306
+ return (
307
+ <g data-testid={`${testId}-dots`}>
308
+ {dots.map((dot) => {
309
+ const at = projection.project([dot.at.latitude, dot.at.longitude]);
310
+ return (
311
+ <circle
312
+ key={dot.id}
313
+ cx={at.x}
314
+ cy={at.y}
315
+ r={view.spanX / 150}
316
+ fill={dot.filled ? theme.palette.text.primary : theme.palette.background.paper}
317
+ stroke={theme.palette.text.primary}
318
+ strokeWidth={2}
319
+ vectorEffect="non-scaling-stroke"
320
+ pointerEvents="none"
321
+ data-filled={dot.filled ? 'true' : 'false'}
322
+ />
323
+ );
324
+ })}
325
+ </g>
326
+ );
327
+ }
328
+
329
+ /** Nice lengths a scale bar may take, in metres. */
330
+ const SCALE_STEPS = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
331
+
332
+ /** The longest nice length that fits in a quarter of the drawing's width. */
333
+ export function scaleMetres(view: PlaneBox): number {
334
+ const quarter = view.spanX / 4;
335
+ return SCALE_STEPS.filter((step) => step <= quarter).at(-1) ?? SCALE_STEPS[0]!;
336
+ }
337
+
338
+ /**
339
+ * A scale bar, bottom-left, when the host gives `copy.scale`. It reads what is
340
+ * on screen: a host that zooms the drawing's layers (a CSS scale on the SVG)
341
+ * shows fewer metres per pixel, so after every render the bar compares the
342
+ * SVG's drawn width with its laid-out width — both off the SVG itself — and
343
+ * spans the drawing's own width, so its share of it is a share of the view.
344
+ */
345
+ export function ScaleBar({
346
+ view,
347
+ copy,
348
+ svgRef,
349
+ testId,
350
+ theme,
351
+ }: {
352
+ view: PlaneBox;
353
+ copy: RegionMapCopy;
354
+ svgRef: React.RefObject<SVGSVGElement | null>;
355
+ testId: string;
356
+ theme: Theme;
357
+ }): React.JSX.Element | null {
358
+ // What the drawing shows on screen: its laid-out width, and its width as
359
+ // drawn (a host's CSS scale on the SVG), both read off the SVG itself.
360
+ const [screen, setScreen] = React.useState({ laidOut: 0, drawn: 0 });
361
+ const wanted = copy.scale !== undefined;
362
+ React.useLayoutEffect(() => {
363
+ if (!wanted) return;
364
+ const svg = svgRef.current;
365
+ const next = { laidOut: svg?.clientWidth ?? 0, drawn: svg?.getBoundingClientRect().width ?? 0 };
366
+ if (next.laidOut !== screen.laidOut || Math.abs(next.drawn - screen.drawn) > 0.5) setScreen(next);
367
+ });
368
+ if (!copy.scale) return null;
369
+ const zoom = screen.laidOut > 0 && screen.drawn > 0 ? screen.drawn / screen.laidOut : 1;
370
+ const metres = scaleMetres({ ...view, spanX: view.spanX / zoom });
371
+ // The bar's share of the drawing's width: metres on screen over the metres the width shows.
372
+ const share = (metres * zoom) / view.spanX;
373
+ return (
374
+ <Box
375
+ data-testid={`${testId}-scale`}
376
+ aria-hidden
377
+ sx={{ position: 'absolute', left: 0, right: 0, bottom: rem(theme, 10), pointerEvents: 'none', zIndex: 1 }}
378
+ >
379
+ <Box component="span" sx={{ display: 'block', ml: rem(theme, 10), fontSize: rem(theme, 11), color: 'text.secondary' }}>
380
+ {copy.scale(metres)}
381
+ </Box>
382
+ <Box
383
+ component="span"
384
+ sx={{ display: 'block', height: rem(theme, 4), ml: rem(theme, 10), width: `${(share * 100).toFixed(2)}%`, borderStyle: 'solid', borderWidth: `0 ${rem(theme, 1.5)} ${rem(theme, 1.5)}`, borderColor: 'text.secondary' }}
385
+ />
386
+ </Box>
387
+ );
388
+ }
@@ -1,14 +1,19 @@
1
1
  export { RegionMap } from './RegionMap';
2
2
  export { decodePolyline, hitRegion, projectionAt } from './RegionMap.helpers';
3
+ export { latLngAt, scaleMetres } from './RegionMapPin';
3
4
  export { boundsOfRings, regionMapView } from './RegionMap.view';
4
5
  export type { AreaGeometry, PlaneBox, PlanePoint, PlaneRegion, Projection } from './RegionMap.helpers';
5
6
  export type {
6
7
  RegionMapBounds,
7
8
  RegionMapCircle,
9
+ RegionMapCenter,
8
10
  RegionMapCopy,
11
+ RegionMapDot,
12
+ RegionMapGhost,
9
13
  RegionMapLayer,
10
14
  RegionMapListMode,
11
15
  RegionMapListSort,
16
+ RegionMapPin,
12
17
  RegionMapPoint,
13
18
  RegionMapProps,
14
19
  RegionMapReach,
@@ -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 * - **The host can draw inside it.** `reach` widens the view past the largest\n * circle and `overlay` draws a layer in the SVG at the map's own projection,\n * tap-transparent; `regionMapView` gives the same view to HTML laid over it.\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 reach,\n overlay,\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, reach, 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 overlay={overlay}\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;AA0DjB,mBACE,KADF;AAhDR,IAAM,SAA4B,CAAC;AA0B5B,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;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,OAAO,SAAS,CAAC;AACxE,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;AAAA,UACA,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":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/Alert/Alert.metrics.ts"],"sourcesContent":["import type { AlertVariant } from './Alert.base';\nimport { alpha, darken, lighten } from '../../../tokens/color';\nimport { NEUTRAL_RAMP } from '../../../tokens/ink.core';\nimport { contrastText, type UiGreyStep, type UiTheme } from '../../../tokens/theme';\n\n/**\n * THE NUMBERS BOTH `Alert` RENDERERS DRAW WITH.\n *\n * `Alert.styles.tsx` / `Alert.tsx` (MUI) and `Alert.native.tsx` (React\n * Native) read this one table. Spacing is in SPACING UNITS where the web\n * writes `theme.spacing(n)`; durations are ms (the web turns them into `s`\n * through {@link seconds} so its emitted CSS is byte-for-byte what it was);\n * everything else is px, a ratio or an alpha. The hover, active and focus\n * numbers are here too although only the DOM can honour them — a number the\n * web draws with belongs in the table whether or not native can reach it.\n */\n\n/** The root: 12px corners, 14px 16px of padding, one 300ms curve for everything. */\nexport const ALERT_RADIUS_UNITS = 1.5;\nexport const ALERT_PADDING_UNITS = { vertical: 1.75, horizontal: 2 } as const;\nexport const ALERT_TRANSITION_MS = 300;\nexport const ALERT_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';\n\n/** Mount: the web's `fadeInScale` keyframes and the icon's `iconRotate`. */\nexport const FADE_IN = { ms: 300, scale: 0.95, lift: 10 } as const;\nexport const ICON_SPIN = { ms: 600, from: 0.8, mid: 1.1 } as const;\n/** Dismiss: MUI `Collapse` runs 300ms; `onClose` fires 200ms in. */\nexport const COLLAPSE_MS = 300;\nexport const CLOSE_DELAY_MS = 200;\n\n/** The message column: 8px between title, description and children; 0.95rem body on 1.5. */\nexport const MESSAGE_GAP_UNITS = 1;\nexport const MESSAGE_FONT_SIZE = 15.2;\nexport const MESSAGE_LINE_HEIGHT = 1.5;\n/** A control inside the message gets an edge: 4px above, a hairline at 0.45 of the hue. */\nexport const MESSAGE_BUTTON = { marginTopUnits: 0.5, borderAlpha: 0.45 } as const;\n\n/** The title: 1.05rem at 600; MUI's `AlertTitle` lifts it 2px; 4px below it when a description follows. */\nexport const TITLE = { fontSize: 16.8, fontWeight: 600, marginTop: -2, marginBottomUnits: 0.5 } as const;\n/** The description: 0.925rem, slightly faded. */\nexport const DESCRIPTION = { fontSize: 14.8, opacity: 0.9 } as const;\n\n/** The icon on the title's line (`iconPlacement: 'title'`): 10px from the title, centred on its first line (1.5). */\nexport const TITLE_ICON = { gapUnits: 1.25, lineHeight: 1.5 } as const;\n/** The icon slot: MUI's 22px glyph at 0.9, 14px from the words, 2px down (MUI's 7px stays below). */\nexport const ICON_SLOT = { size: 22, opacity: 0.9, marginRightUnits: 1.75, paddingTopUnits: 0.25, paddingBottom: 7 } as const;\n/** The action slot: MUI's `4px 0 0 16px` with the 16px ours, pushed right, 8px into the padding. */\nexport const ACTION_SLOT = { paddingLeftUnits: 2, paddingTop: 4, marginRight: -8 } as const;\n/** The close button: MUI's small `IconButton` (5px padding, 18px glyph) at 0.7. */\nexport const CLOSE_BUTTON = { padding: 5, iconSize: 18, opacity: 0.7, washAlpha: 0.1 } as const;\n\n/**\n * A semantic Alert's surface and ink: an OPAQUE tint of the hue with a dark\n * ink of the same hue — MUI's own `standard` recipe, at its ratios. See\n * `semanticSurface` in `Alert.styles.tsx` for why not `alpha(main, 0.1)`.\n */\nexport const SEMANTIC_SURFACE = { tint: { light: 0.9, dark: 0.8 }, ink: 0.6, borderAlpha: 0.35 } as const;\n/** `glass`: 85% paper under a 20px blur, a 0.4 divider hairline. */\nexport const GLASS = { backgroundAlpha: 0.85, borderAlpha: 0.4, blur: 20, saturatePercent: 180 } as const;\n/**\n * `gradient`: light→dark at 0.9 along 135°, a 0.2 white shimmer sweeping 1000px every 3s.\n *\n * It carries no hover of its own, and that is load-bearing rather than an\n * omission. `alertVariantStyles` is spread AFTER the root's `'&:hover'` in the\n * same object literal, so a second `'&:hover'` key does not merge with it — it\n * REPLACES it. While this variant had one, `gradient` was the only alert that\n * brightened on hover while every other variant darkened, and the only one to\n * lose the shimmer reveal the root's hover performs.\n */\nexport const GRADIENT = {\n angleDeg: 135,\n stopAlpha: 0.9,\n shimmerAlpha: 0.2,\n shimmerMs: 3000,\n shimmerTravel: 1000,\n} as const;\n\n/** Emphasis: the glow's shadow and brightness, the pulse's wash. */\nexport const GLOW = { blur: 20, spread: 5, alpha: 0.3, brightness: 1.05 } as const;\nexport const PULSE = { alpha: 0.2, ms: 2000, spread: 10 } as const;\n\n/**\n * Pointer states — DOM only, kept so the web derives them.\n *\n * HOVER and ACTIVE carry NO geometry, and that is the point rather than an\n * omission. They used to lift the card 3px, scale it 1.01, grow a 20px shadow\n * and spin the icon 10deg at 1.15 — four things moving at once on a surface\n * that is not a control, which made a page of alerts twitch under the pointer.\n *\n * Both are a TINT now: a flat inset layer over the surface. Two mechanisms\n * came before it and each failed on a case the next had to fix. Dropping the\n * element's `opacity` composites toward the PAGE, so it inverted the hover on\n * a light tint (L* 95.0 idle, 89.1 hovered, 91.1 pressed) and vanished on a\n * dark one. A `filter: brightness()` fixed the direction but is MULTIPLICATIVE,\n * so it moved a near-black `glass` by four counts (ΔE76 1.71, under the JND)\n * and Banner's 10%-alpha wash by ΔE76 0.44. A layer is additive: it reads the\n * same on an opaque tint, a translucent wash and a gradient, and because it\n * paints above the background and below the content the ink keeps its own\n * contrast.\n */\nexport const HOVER = { tintAlpha: 0.06 } as const;\n\n/**\n * The press: a 20% tint LAYER, eased back fast.\n *\n * The layer is the point. The first attempt dropped the whole element's\n * opacity, which composites toward the PAGE and so behaves differently on\n * every surface it is asked about. Measured: over a light page it made a light\n * tint LIGHTER (L* 95.0 idle, 89.1 hovered, 91.1 pressed), so pushing the\n * surface looked like the pointer leaving; over a dark page it landed back on\n * the idle surface (ΔE76 0.82 on `glass`); on `gradient` it washed white body\n * copy to 2.31:1, under AA; and it diluted the ink during a text-selection\n * drag. A `brightness()` step instead of it fixed the direction but not the\n * reach — a multiplier does nothing to a near-black `glass` in dark mode\n * (ΔE76 1.71) and nothing to Banner's 10%-alpha wash (ΔE76 0.44), because it\n * scales only what the element already paints.\n *\n * An inset shadow at 100vmax is a flat tint over the whole surface: ADDITIVE,\n * so it reads the same on an opaque tint, a translucent wash and a gradient;\n * painted above the background and below the content, so the ink keeps its\n * own contrast; and animatable, which `background-image` is not.\n *\n * `ms` is 250 and not the 1000 first asked for, and the reason is that ONE\n * property carries both states. CSS runs a transition from the DESTINATION\n * state's declaration, and entering hover and releasing a press share that\n * destination — so they cannot have different durations without a second\n * animatable property, and both free pseudo-elements are already spent on the\n * `gradient` shimmer and the `pulse` wash. At 1000ms the hover inherited the\n * second: crossing an alert in 300ms peaked at a ≈ 0.024, under the JND, and\n * for ~360ms the alert you had just LEFT was darker than the one under the\n * pointer. A fast press-return is the smaller loss, and it is the call the\n * product owner made when shown both.\n */\nexport const ACTIVE = { tintAlpha: 0.2, ms: 250 } as const;\n\nexport const FOCUS = { ringWidth: 2, ringAlpha: 1, offset: 2 } as const;\n\n/** `neutral` has no palette slot; the web has always drawn it from three greys. */\nexport const NEUTRAL_GREY: Record<'main' | 'light' | 'dark', UiGreyStep> = { main: 500, light: 300, dark: 700 };\n\n/** A duration as the CSS the web has always emitted: `2000` → `2s`, `300` → `0.3s`. */\nexport const seconds = (ms: number): string => `${ms / 1000}s`;\n\n/** The palette shape the web's `getColorFromTheme` returns — a main plus optional shades. */\nexport interface AlertPalette {\n main: string;\n light?: string;\n dark?: string;\n}\n\n/**\n * The renderer-free twin of the web's `getColorFromTheme`: a variant or\n * colour name to its palette, `neutral` from the greys, anything unknown\n * (`glass`, `gradient`) to `info`.\n */\nexport function alertPalette(theme: UiTheme, key: string | undefined): AlertPalette {\n switch (key) {\n case 'info':\n case 'success':\n case 'warning':\n case 'danger':\n case 'primary':\n case 'secondary':\n return theme.palette[key];\n case 'neutral':\n return {\n main: NEUTRAL_RAMP[NEUTRAL_GREY.main],\n light: NEUTRAL_RAMP[NEUTRAL_GREY.light],\n dark: NEUTRAL_RAMP[NEUTRAL_GREY.dark],\n };\n default:\n return theme.palette.info;\n }\n}\n\n/** What a variant paints: its fill, its ink, its hairline if any, and the colour of its glyph. */\nexport interface AlertSurface {\n backgroundColor: string;\n color: string;\n borderColor?: string;\n iconColor: string;\n}\n\nexport const SEMANTIC_VARIANTS: ReadonlySet<AlertVariant> = new Set(['info', 'success', 'warning', 'danger']);\n\n/** The opaque tint and same-hue ink, inverted for dark mode; the hairline keeps its alpha. */\nexport function semanticSurface(theme: UiTheme, palette: AlertPalette): AlertSurface {\n const dark = theme.mode === 'dark';\n return {\n backgroundColor: dark\n ? darken(palette.main, SEMANTIC_SURFACE.tint.dark)\n : lighten(palette.main, SEMANTIC_SURFACE.tint.light),\n color: dark ? lighten(palette.main, SEMANTIC_SURFACE.ink) : darken(palette.main, SEMANTIC_SURFACE.ink),\n borderColor: alpha(palette.main, SEMANTIC_SURFACE.borderAlpha),\n iconColor: palette.main,\n };\n}\n\nexport function glassSurface(theme: UiTheme): AlertSurface {\n return {\n backgroundColor: alpha(theme.palette.background.paper, GLASS.backgroundAlpha),\n color: theme.palette.text.primary,\n borderColor: alpha(theme.palette.divider, GLASS.borderAlpha),\n iconColor: theme.palette.primary.main,\n };\n}\n\n/** The gradient's two stops; a renderer with no gradient fill paints `from`. */\nexport function gradientStops(palette: AlertPalette): { from: string; to: string } {\n return {\n from: alpha(palette.light || palette.main, GRADIENT.stopAlpha),\n to: alpha(palette.dark || palette.main, GRADIENT.stopAlpha),\n };\n}\n\nexport function gradientSurface(palette: AlertPalette): AlertSurface {\n const ink = contrastText(palette.main);\n return { backgroundColor: gradientStops(palette).from, color: ink, iconColor: ink };\n}\n\n/** The surface for a variant, `glass` and `gradient` included. */\nexport function alertSurface(theme: UiTheme, variant: AlertVariant, palette: AlertPalette): AlertSurface {\n if (variant === 'glass') return glassSurface(theme);\n if (variant === 'gradient') return gradientSurface(palette);\n return semanticSurface(theme, palette);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/Alert/Alert.content.tsx"],"sourcesContent":["import AlertTitle from '@mui/material/AlertTitle/index.js';\nimport Box from '@mui/material/Box/index.js';\nimport React from 'react';\n\nimport { sxRem } from '../../../tokens/relative';\n\nimport { testIdFor } from './Alert.helpers';\nimport { DESCRIPTION, ICON_SLOT, TITLE, TITLE_ICON } from './Alert.metrics';\nimport type { AlertProps } from './Alert.types';\n\nconst BREAK_WORDS = {\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n wordBreak: 'break-word',\n} as const;\n\n/**\n * The message column: title, description, then whatever the caller put inside.\n *\n * `titleIcon` is the icon when it sits on the title's line\n * (`iconPlacement: 'title'`, see `Alert.placement.ts`). The title then shares\n * a row with it, and the description below is a sibling of that row — so it\n * starts at the column's own edge rather than under the title's first letter.\n */\nexport const AlertContent: React.FC<{\n title?: AlertProps['title'];\n description?: AlertProps['description'];\n titleIcon?: React.ReactNode;\n dataTestId?: string;\n children?: React.ReactNode;\n}> = ({ title, description, titleIcon, dataTestId, children }) => {\n const heading = title ? (\n <AlertTitle\n data-testid={testIdFor(dataTestId, 'title')}\n sx={{\n fontWeight: TITLE.fontWeight,\n fontSize: sxRem(TITLE.fontSize),\n // On the icon's row MUI's 2px lift would push the title off the glyph,\n // and the line height is stated so the glyph can be centred on it.\n ...(titleIcon\n ? { margin: 0, lineHeight: TITLE_ICON.lineHeight }\n : { marginBottom: description ? TITLE.marginBottomUnits : 0 }),\n ...BREAK_WORDS,\n }}\n >\n {title}\n </AlertTitle>\n ) : null;\n\n return (\n <>\n {titleIcon && heading ? (\n <Box\n data-testid={testIdFor(dataTestId, 'title-row')}\n sx={{\n display: 'flex',\n // The FIRST line, not the block: a title that wraps to three lines\n // on a phone would otherwise float its icon beside the second.\n alignItems: 'flex-start',\n gap: TITLE_ICON.gapUnits,\n marginBottom: description ? TITLE.marginBottomUnits : 0,\n '& > span': {\n display: 'flex',\n flex: 'none',\n alignItems: 'center',\n fontSize: sxRem(ICON_SLOT.size),\n // One title line tall, so the glyph is centred on that line.\n height: sxRem(TITLE.fontSize * TITLE_ICON.lineHeight),\n },\n }}\n >\n {titleIcon}\n {heading}\n </Box>\n ) : (\n heading\n )}\n {description && (\n <Box\n component=\"div\"\n data-testid={testIdFor(dataTestId, 'message')}\n sx={{ opacity: DESCRIPTION.opacity, fontSize: sxRem(DESCRIPTION.fontSize), ...BREAK_WORDS }}\n >\n {description}\n </Box>\n )}\n {children}\n </>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,gBAAgB;AACvB,OAAO,SAAS;AA+BZ,SAkBA,UAlBA,KAoBI,YApBJ;AAtBJ,IAAM,cAAc;AAAA,EAClB,UAAU;AAAA,EACV,cAAc;AAAA,EACd,WAAW;AACb;AAUO,IAAM,eAMR,CAAC,EAAE,OAAO,aAAa,WAAW,YAAY,SAAS,MAAM;AAChE,QAAM,UAAU,QACd;AAAA,IAAC;AAAA;AAAA,MACC,eAAa,UAAU,YAAY,OAAO;AAAA,MAC1C,IAAI;AAAA,QACF,YAAY,MAAM;AAAA,QAClB,UAAU,MAAM,MAAM,QAAQ;AAAA;AAAA;AAAA,QAG9B,GAAI,YACA,EAAE,QAAQ,GAAG,YAAY,WAAW,WAAW,IAC/C,EAAE,cAAc,cAAc,MAAM,oBAAoB,EAAE;AAAA,QAC9D,GAAG;AAAA,MACL;AAAA,MAEC;AAAA;AAAA,EACH,IACE;AAEJ,SACE,iCACG;AAAA,iBAAa,UACZ;AAAA,MAAC;AAAA;AAAA,QACC,eAAa,UAAU,YAAY,WAAW;AAAA,QAC9C,IAAI;AAAA,UACF,SAAS;AAAA;AAAA;AAAA,UAGT,YAAY;AAAA,UACZ,KAAK,WAAW;AAAA,UAChB,cAAc,cAAc,MAAM,oBAAoB;AAAA,UACtD,YAAY;AAAA,YACV,SAAS;AAAA,YACT,MAAM;AAAA,YACN,YAAY;AAAA,YACZ,UAAU,MAAM,UAAU,IAAI;AAAA;AAAA,YAE9B,QAAQ,MAAM,MAAM,WAAW,WAAW,UAAU;AAAA,UACtD;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,UACA;AAAA;AAAA;AAAA,IACH,IAEA;AAAA,IAED,eACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,eAAa,UAAU,YAAY,SAAS;AAAA,QAC5C,IAAI,EAAE,SAAS,YAAY,SAAS,UAAU,MAAM,YAAY,QAAQ,GAAG,GAAG,YAAY;AAAA,QAEzF;AAAA;AAAA,IACH;AAAA,IAED;AAAA,KACH;AAEJ;","names":[]}