@mailwoman/react 9.4.0 → 10.1.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 (370) hide show
  1. package/README.md +17 -16
  2. package/fonts.css +97 -22
  3. package/lib/common/About.tsx +87 -71
  4. package/lib/common/ClientOnly.tsx +2 -14
  5. package/lib/common/CopyButton.tsx +3 -1
  6. package/lib/common/LoadingIndicator.tsx +4 -2
  7. package/lib/common/PresetChips.tsx +9 -8
  8. package/lib/common/confidence-tiers.ts +7 -6
  9. package/lib/common/cx.ts +3 -1
  10. package/lib/common/text-tokens.ts +7 -4
  11. package/lib/common/useBuildInfo.ts +54 -0
  12. package/lib/common/useClipboard.ts +2 -11
  13. package/lib/common.ts +22 -0
  14. package/lib/index.ts +27 -112
  15. package/lib/map/AppIdentity.tsx +85 -0
  16. package/lib/map/BackendControl.tsx +16 -13
  17. package/lib/map/CompareToggle.tsx +11 -33
  18. package/lib/map/DebugInfo.tsx +88 -0
  19. package/lib/map/Geocoder.tsx +92 -58
  20. package/lib/map/GeocoderControls.tsx +582 -127
  21. package/lib/map/GraticuleLayer.tsx +54 -0
  22. package/lib/map/MapCanvas.tsx +23 -12
  23. package/lib/map/MapChipRow.tsx +73 -0
  24. package/lib/map/MapCompass.tsx +68 -0
  25. package/lib/map/MapControlStack.tsx +97 -0
  26. package/lib/map/MapFooter.tsx +101 -0
  27. package/lib/map/MapProgressBar.tsx +55 -0
  28. package/lib/map/MapSearchBar.tsx +82 -0
  29. package/lib/map/MapSheet.tsx +71 -0
  30. package/lib/map/OverlayLayers.tsx +1 -1
  31. package/lib/map/PlaceAutocomplete.tsx +10 -21
  32. package/lib/map/PlaceMarker.tsx +4 -2
  33. package/lib/map/ResolvedPlaceLayers.tsx +5 -14
  34. package/lib/map/ResultCamera.tsx +8 -31
  35. package/lib/map/ResultOverlay.tsx +2 -12
  36. package/lib/map/ResultPanel.tsx +1 -20
  37. package/lib/map/VersionPicker.tsx +10 -19
  38. package/lib/map/fake-runtime.ts +10 -12
  39. package/lib/map/geometry.ts +14 -47
  40. package/lib/map/graticule.ts +80 -0
  41. package/lib/map/map-debug.ts +1 -1
  42. package/lib/map/place-render.ts +28 -60
  43. package/lib/map/types.ts +65 -97
  44. package/lib/map/useCompareState.ts +5 -4
  45. package/lib/map/useGeocode.ts +7 -5
  46. package/lib/map/useMapBearing.ts +56 -0
  47. package/lib/map/useMapLabelPick.ts +114 -0
  48. package/lib/map/useMapPlaceRender.ts +2 -6
  49. package/lib/map/usePlaceAutocomplete.ts +44 -50
  50. package/lib/map.ts +95 -0
  51. package/lib/pipeline/CandidatePicker.tsx +13 -5
  52. package/lib/pipeline/ConfidenceCell.tsx +1 -1
  53. package/lib/pipeline/FailureDiagnostic.tsx +8 -2
  54. package/lib/pipeline/QueryForm.tsx +5 -2
  55. package/lib/pipeline/ResolvedPlace.tsx +1 -1
  56. package/lib/pipeline/SpanHighlight.tsx +7 -5
  57. package/lib/pipeline/TimingPanel.tsx +1 -1
  58. package/lib/pipeline/TreeView.tsx +1 -1
  59. package/lib/pipeline/copy.ts +22 -25
  60. package/lib/pipeline/presets.ts +15 -13
  61. package/lib/pipeline/types.ts +21 -14
  62. package/lib/pipeline/useParsePipeline.ts +38 -24
  63. package/lib/pipeline.ts +36 -0
  64. package/lib/poi/AbstainPanel.tsx +1 -1
  65. package/lib/poi/LiveResultsBlock.tsx +2 -1
  66. package/lib/poi/POIExplorer.tsx +24 -15
  67. package/lib/poi/QueryInput.tsx +1 -1
  68. package/lib/poi/SubjectPanel.tsx +4 -3
  69. package/lib/poi/runtime.ts +5 -16
  70. package/lib/poi/types.ts +39 -27
  71. package/lib/poi/usePOISearch.ts +39 -44
  72. package/lib/poi.ts +39 -0
  73. package/lib/runtime/useReleaseRuntime.ts +125 -74
  74. package/lib/runtime.ts +16 -0
  75. package/lib/search/SearchModal.tsx +317 -0
  76. package/lib/search/search.css +144 -0
  77. package/lib/search/types.ts +67 -0
  78. package/lib/search.ts +11 -0
  79. package/out/common/About.d.ts +13 -4
  80. package/out/common/About.d.ts.map +1 -1
  81. package/out/common/About.js +5 -2
  82. package/out/common/About.js.map +1 -1
  83. package/out/common/ClientOnly.d.ts +2 -11
  84. package/out/common/ClientOnly.d.ts.map +1 -1
  85. package/out/common/ClientOnly.js +1 -13
  86. package/out/common/ClientOnly.js.map +1 -1
  87. package/out/common/CopyButton.d.ts +3 -1
  88. package/out/common/CopyButton.d.ts.map +1 -1
  89. package/out/common/CopyButton.js.map +1 -1
  90. package/out/common/LoadingIndicator.d.ts +4 -2
  91. package/out/common/LoadingIndicator.d.ts.map +1 -1
  92. package/out/common/LoadingIndicator.js.map +1 -1
  93. package/out/common/PresetChips.d.ts +9 -8
  94. package/out/common/PresetChips.d.ts.map +1 -1
  95. package/out/common/PresetChips.js.map +1 -1
  96. package/out/common/confidence-tiers.d.ts +7 -6
  97. package/out/common/confidence-tiers.d.ts.map +1 -1
  98. package/out/common/confidence-tiers.js +7 -6
  99. package/out/common/confidence-tiers.js.map +1 -1
  100. package/out/common/cx.d.ts +3 -1
  101. package/out/common/cx.d.ts.map +1 -1
  102. package/out/common/cx.js +3 -1
  103. package/out/common/cx.js.map +1 -1
  104. package/out/common/text-tokens.d.ts +4 -3
  105. package/out/common/text-tokens.d.ts.map +1 -1
  106. package/out/common/text-tokens.js +6 -3
  107. package/out/common/text-tokens.js.map +1 -1
  108. package/out/common/useBuildInfo.d.ts +27 -0
  109. package/out/common/useBuildInfo.d.ts.map +1 -0
  110. package/out/common/useBuildInfo.js +32 -0
  111. package/out/common/useBuildInfo.js.map +1 -0
  112. package/out/common/useClipboard.d.ts +1 -3
  113. package/out/common/useClipboard.d.ts.map +1 -1
  114. package/out/common/useClipboard.js +2 -11
  115. package/out/common/useClipboard.js.map +1 -1
  116. package/out/common.d.ts +22 -0
  117. package/out/common.d.ts.map +1 -0
  118. package/out/common.js +15 -0
  119. package/out/common.js.map +1 -0
  120. package/out/index.d.ts +26 -71
  121. package/out/index.d.ts.map +1 -1
  122. package/out/index.js +21 -47
  123. package/out/index.js.map +1 -1
  124. package/out/map/AppIdentity.d.ts +42 -0
  125. package/out/map/AppIdentity.d.ts.map +1 -0
  126. package/out/map/AppIdentity.js +16 -0
  127. package/out/map/AppIdentity.js.map +1 -0
  128. package/out/map/BackendControl.d.ts +9 -10
  129. package/out/map/BackendControl.d.ts.map +1 -1
  130. package/out/map/BackendControl.js +2 -2
  131. package/out/map/BackendControl.js.map +1 -1
  132. package/out/map/CompareToggle.d.ts +8 -31
  133. package/out/map/CompareToggle.d.ts.map +1 -1
  134. package/out/map/CompareToggle.js +5 -2
  135. package/out/map/CompareToggle.js.map +1 -1
  136. package/out/map/DebugInfo.d.ts +34 -0
  137. package/out/map/DebugInfo.d.ts.map +1 -0
  138. package/out/map/DebugInfo.js +39 -0
  139. package/out/map/DebugInfo.js.map +1 -0
  140. package/out/map/Geocoder.d.ts +47 -26
  141. package/out/map/Geocoder.d.ts.map +1 -1
  142. package/out/map/Geocoder.js +34 -50
  143. package/out/map/Geocoder.js.map +1 -1
  144. package/out/map/GeocoderControls.d.ts +23 -33
  145. package/out/map/GeocoderControls.d.ts.map +1 -1
  146. package/out/map/GeocoderControls.js +277 -16
  147. package/out/map/GeocoderControls.js.map +1 -1
  148. package/out/map/GraticuleLayer.d.ts +28 -0
  149. package/out/map/GraticuleLayer.d.ts.map +1 -0
  150. package/out/map/GraticuleLayer.js +25 -0
  151. package/out/map/GraticuleLayer.js.map +1 -0
  152. package/out/map/MapCanvas.d.ts +23 -12
  153. package/out/map/MapCanvas.d.ts.map +1 -1
  154. package/out/map/MapCanvas.js +5 -2
  155. package/out/map/MapCanvas.js.map +1 -1
  156. package/out/map/MapChipRow.d.ts +43 -0
  157. package/out/map/MapChipRow.d.ts.map +1 -0
  158. package/out/map/MapChipRow.js +12 -0
  159. package/out/map/MapChipRow.js.map +1 -0
  160. package/out/map/MapCompass.d.ts +33 -0
  161. package/out/map/MapCompass.d.ts.map +1 -0
  162. package/out/map/MapCompass.js +15 -0
  163. package/out/map/MapCompass.js.map +1 -0
  164. package/out/map/MapControlStack.d.ts +56 -0
  165. package/out/map/MapControlStack.d.ts.map +1 -0
  166. package/out/map/MapControlStack.js +18 -0
  167. package/out/map/MapControlStack.js.map +1 -0
  168. package/out/map/MapFooter.d.ts +27 -0
  169. package/out/map/MapFooter.d.ts.map +1 -0
  170. package/out/map/MapFooter.js +37 -0
  171. package/out/map/MapFooter.js.map +1 -0
  172. package/out/map/MapProgressBar.d.ts +31 -0
  173. package/out/map/MapProgressBar.d.ts.map +1 -0
  174. package/out/map/MapProgressBar.js +8 -0
  175. package/out/map/MapProgressBar.js.map +1 -0
  176. package/out/map/MapSearchBar.d.ts +48 -0
  177. package/out/map/MapSearchBar.d.ts.map +1 -0
  178. package/out/map/MapSearchBar.js +12 -0
  179. package/out/map/MapSearchBar.js.map +1 -0
  180. package/out/map/MapSheet.d.ts +35 -0
  181. package/out/map/MapSheet.d.ts.map +1 -0
  182. package/out/map/MapSheet.js +29 -0
  183. package/out/map/MapSheet.js.map +1 -0
  184. package/out/map/OverlayLayers.d.ts +1 -1
  185. package/out/map/OverlayLayers.js +2 -2
  186. package/out/map/PlaceAutocomplete.d.ts +10 -21
  187. package/out/map/PlaceAutocomplete.d.ts.map +1 -1
  188. package/out/map/PlaceAutocomplete.js +2 -1
  189. package/out/map/PlaceAutocomplete.js.map +1 -1
  190. package/out/map/PlaceMarker.d.ts +4 -2
  191. package/out/map/PlaceMarker.d.ts.map +1 -1
  192. package/out/map/PlaceMarker.js +3 -1
  193. package/out/map/PlaceMarker.js.map +1 -1
  194. package/out/map/ResolvedPlaceLayers.d.ts +5 -14
  195. package/out/map/ResolvedPlaceLayers.d.ts.map +1 -1
  196. package/out/map/ResolvedPlaceLayers.js +2 -2
  197. package/out/map/ResolvedPlaceLayers.js.map +1 -1
  198. package/out/map/ResultCamera.d.ts +8 -31
  199. package/out/map/ResultCamera.d.ts.map +1 -1
  200. package/out/map/ResultCamera.js +3 -28
  201. package/out/map/ResultCamera.js.map +1 -1
  202. package/out/map/ResultOverlay.d.ts +2 -12
  203. package/out/map/ResultOverlay.d.ts.map +1 -1
  204. package/out/map/ResultOverlay.js +1 -2
  205. package/out/map/ResultOverlay.js.map +1 -1
  206. package/out/map/ResultPanel.d.ts +1 -20
  207. package/out/map/ResultPanel.d.ts.map +1 -1
  208. package/out/map/ResultPanel.js +1 -1
  209. package/out/map/ResultPanel.js.map +1 -1
  210. package/out/map/VersionPicker.d.ts +7 -19
  211. package/out/map/VersionPicker.d.ts.map +1 -1
  212. package/out/map/VersionPicker.js +5 -2
  213. package/out/map/VersionPicker.js.map +1 -1
  214. package/out/map/fake-runtime.d.ts +9 -11
  215. package/out/map/fake-runtime.d.ts.map +1 -1
  216. package/out/map/fake-runtime.js +9 -12
  217. package/out/map/fake-runtime.js.map +1 -1
  218. package/out/map/geometry.d.ts +13 -34
  219. package/out/map/geometry.d.ts.map +1 -1
  220. package/out/map/geometry.js +9 -34
  221. package/out/map/geometry.js.map +1 -1
  222. package/out/map/graticule.d.ts +17 -0
  223. package/out/map/graticule.d.ts.map +1 -0
  224. package/out/map/graticule.js +67 -0
  225. package/out/map/graticule.js.map +1 -0
  226. package/out/map/map-debug.d.ts +1 -1
  227. package/out/map/map-debug.d.ts.map +1 -1
  228. package/out/map/map-debug.js +1 -1
  229. package/out/map/map-debug.js.map +1 -1
  230. package/out/map/place-render.d.ts +23 -43
  231. package/out/map/place-render.d.ts.map +1 -1
  232. package/out/map/place-render.js +4 -39
  233. package/out/map/place-render.js.map +1 -1
  234. package/out/map/types.d.ts +67 -95
  235. package/out/map/types.d.ts.map +1 -1
  236. package/out/map/types.js +1 -10
  237. package/out/map/types.js.map +1 -1
  238. package/out/map/useCompareState.d.ts +5 -4
  239. package/out/map/useCompareState.d.ts.map +1 -1
  240. package/out/map/useCompareState.js +3 -3
  241. package/out/map/useCompareState.js.map +1 -1
  242. package/out/map/useGeocode.d.ts +5 -3
  243. package/out/map/useGeocode.d.ts.map +1 -1
  244. package/out/map/useGeocode.js +4 -4
  245. package/out/map/useGeocode.js.map +1 -1
  246. package/out/map/useMapBearing.d.ts +28 -0
  247. package/out/map/useMapBearing.d.ts.map +1 -0
  248. package/out/map/useMapBearing.js +30 -0
  249. package/out/map/useMapBearing.js.map +1 -0
  250. package/out/map/useMapLabelPick.d.ts +13 -0
  251. package/out/map/useMapLabelPick.d.ts.map +1 -0
  252. package/out/map/useMapLabelPick.js +95 -0
  253. package/out/map/useMapLabelPick.js.map +1 -0
  254. package/out/map/useMapPlaceRender.d.ts +2 -6
  255. package/out/map/useMapPlaceRender.d.ts.map +1 -1
  256. package/out/map/useMapPlaceRender.js +2 -6
  257. package/out/map/useMapPlaceRender.js.map +1 -1
  258. package/out/map/usePlaceAutocomplete.d.ts +30 -28
  259. package/out/map/usePlaceAutocomplete.d.ts.map +1 -1
  260. package/out/map/usePlaceAutocomplete.js +6 -33
  261. package/out/map/usePlaceAutocomplete.js.map +1 -1
  262. package/out/map.d.ts +69 -0
  263. package/out/map.d.ts.map +1 -0
  264. package/out/map.js +46 -0
  265. package/out/map.js.map +1 -0
  266. package/out/pipeline/CandidatePicker.d.ts +12 -2
  267. package/out/pipeline/CandidatePicker.d.ts.map +1 -1
  268. package/out/pipeline/CandidatePicker.js +2 -2
  269. package/out/pipeline/CandidatePicker.js.map +1 -1
  270. package/out/pipeline/ConfidenceCell.d.ts +1 -1
  271. package/out/pipeline/ConfidenceCell.js +1 -1
  272. package/out/pipeline/FailureDiagnostic.d.ts +8 -2
  273. package/out/pipeline/FailureDiagnostic.d.ts.map +1 -1
  274. package/out/pipeline/FailureDiagnostic.js +3 -0
  275. package/out/pipeline/FailureDiagnostic.js.map +1 -1
  276. package/out/pipeline/PipelineExplorer.js +1 -1
  277. package/out/pipeline/QueryForm.d.ts +5 -2
  278. package/out/pipeline/QueryForm.d.ts.map +1 -1
  279. package/out/pipeline/QueryForm.js +1 -1
  280. package/out/pipeline/QueryForm.js.map +1 -1
  281. package/out/pipeline/ResolvedPlace.js +1 -1
  282. package/out/pipeline/SpanHighlight.d.ts +4 -3
  283. package/out/pipeline/SpanHighlight.d.ts.map +1 -1
  284. package/out/pipeline/SpanHighlight.js +3 -2
  285. package/out/pipeline/SpanHighlight.js.map +1 -1
  286. package/out/pipeline/TimingPanel.d.ts +1 -1
  287. package/out/pipeline/TimingPanel.js +1 -1
  288. package/out/pipeline/TreeView.d.ts +1 -1
  289. package/out/pipeline/copy.d.ts +1 -1
  290. package/out/pipeline/copy.d.ts.map +1 -1
  291. package/out/pipeline/copy.js +4 -3
  292. package/out/pipeline/copy.js.map +1 -1
  293. package/out/pipeline/presets.d.ts +5 -4
  294. package/out/pipeline/presets.d.ts.map +1 -1
  295. package/out/pipeline/presets.js +15 -13
  296. package/out/pipeline/presets.js.map +1 -1
  297. package/out/pipeline/types.d.ts +21 -14
  298. package/out/pipeline/types.d.ts.map +1 -1
  299. package/out/pipeline/types.js +2 -4
  300. package/out/pipeline/types.js.map +1 -1
  301. package/out/pipeline/useParsePipeline.d.ts +15 -7
  302. package/out/pipeline/useParsePipeline.d.ts.map +1 -1
  303. package/out/pipeline/useParsePipeline.js +10 -7
  304. package/out/pipeline/useParsePipeline.js.map +1 -1
  305. package/out/pipeline.d.ts +33 -0
  306. package/out/pipeline.d.ts.map +1 -0
  307. package/out/pipeline.js +21 -0
  308. package/out/pipeline.js.map +1 -0
  309. package/out/poi/AbstainPanel.d.ts +1 -1
  310. package/out/poi/LiveResultsBlock.d.ts +2 -1
  311. package/out/poi/LiveResultsBlock.d.ts.map +1 -1
  312. package/out/poi/LiveResultsBlock.js +1 -1
  313. package/out/poi/LiveResultsBlock.js.map +1 -1
  314. package/out/poi/POIExplorer.d.ts +22 -12
  315. package/out/poi/POIExplorer.d.ts.map +1 -1
  316. package/out/poi/POIExplorer.js +10 -9
  317. package/out/poi/POIExplorer.js.map +1 -1
  318. package/out/poi/QueryInput.d.ts +1 -1
  319. package/out/poi/SubjectPanel.d.ts +2 -2
  320. package/out/poi/SubjectPanel.d.ts.map +1 -1
  321. package/out/poi/SubjectPanel.js +3 -2
  322. package/out/poi/SubjectPanel.js.map +1 -1
  323. package/out/poi/runtime.d.ts +5 -14
  324. package/out/poi/runtime.d.ts.map +1 -1
  325. package/out/poi/runtime.js +5 -16
  326. package/out/poi/runtime.js.map +1 -1
  327. package/out/poi/types.d.ts +39 -27
  328. package/out/poi/types.d.ts.map +1 -1
  329. package/out/poi/types.js +2 -4
  330. package/out/poi/types.js.map +1 -1
  331. package/out/poi/usePOISearch.d.ts +28 -17
  332. package/out/poi/usePOISearch.d.ts.map +1 -1
  333. package/out/poi/usePOISearch.js +6 -32
  334. package/out/poi/usePOISearch.js.map +1 -1
  335. package/out/poi.d.ts +23 -0
  336. package/out/poi.d.ts.map +1 -0
  337. package/out/poi.js +15 -0
  338. package/out/poi.js.map +1 -0
  339. package/out/runtime/useReleaseRuntime.d.ts +64 -65
  340. package/out/runtime/useReleaseRuntime.d.ts.map +1 -1
  341. package/out/runtime/useReleaseRuntime.js +31 -26
  342. package/out/runtime/useReleaseRuntime.js.map +1 -1
  343. package/out/runtime.d.ts +9 -0
  344. package/out/runtime.d.ts.map +1 -0
  345. package/out/runtime.js +8 -0
  346. package/out/runtime.js.map +1 -0
  347. package/out/search/SearchModal.d.ts +33 -0
  348. package/out/search/SearchModal.d.ts.map +1 -0
  349. package/out/search/SearchModal.js +187 -0
  350. package/out/search/SearchModal.js.map +1 -0
  351. package/out/search/types.d.ts +64 -0
  352. package/out/search/types.d.ts.map +1 -0
  353. package/out/search/types.js +13 -0
  354. package/out/search/types.js.map +1 -0
  355. package/out/search.d.ts +11 -0
  356. package/out/search.d.ts.map +1 -0
  357. package/out/search.js +9 -0
  358. package/out/search.js.map +1 -0
  359. package/package.json +132 -133
  360. package/styles.css +1340 -202
  361. package/tokens/index.css +26 -7
  362. package/tokens/tokens.json +121 -6
  363. package/tokens/tokens.resolver.json +1 -1
  364. package/lib/map/index.ts +0 -76
  365. package/out/map/index.d.ts +0 -52
  366. package/out/map/index.d.ts.map +0 -1
  367. package/out/map/index.js +0 -36
  368. package/out/map/index.js.map +0 -1
  369. package/styleframe.config.ts +0 -321
  370. package/vitest.node.config.ts +0 -20
@@ -2,74 +2,91 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
- *
6
- * `<GeocoderControls>` — the geocoder's floating control panel, assembled from the geocoder-specific units
7
- * (VersionPicker / CompareToggle / BackendControl / PlaceAutocomplete / ResultPanel) and the REUSED
8
- * pipeline units (QueryForm / PresetChips / LoadingIndicator). It reads the injected {@link GeocoderRuntime}
9
- * for ready/version/backend/loading state and threads the {@link useGeocode} + {@link usePlaceAutocomplete}
10
- * + {@link useCompareState} hooks into the controls. Host-specific visualizers arrive via {@link GeocoderPanels}.
11
- *
12
- * NODE-SAFE: pure React + the shared/geocoder units, no maplibre. It rides `@mailwoman/react/map` as a
13
- * geocoder-specific control, not for WebGL.
14
5
  */
15
6
 
16
- import type { ReactNode } from "react"
7
+ import type React from "react"
8
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"
9
+ import type { MapInstance } from "react-map-gl/maplibre"
17
10
 
18
11
  import type { GeocoderPanels, GeocoderRuntime } from "#map/types"
19
12
  import type { UseCompareState } from "#map/useCompareState"
20
13
  import type { UseGeocode } from "#map/useGeocode"
14
+ import { useMapBearing } from "#map/useMapBearing"
15
+ import { useMapLabelPick } from "#map/useMapLabelPick"
21
16
  import type { UsePlaceAutocomplete } from "#map/usePlaceAutocomplete"
22
17
 
23
18
  import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
24
- import { PresetChips, type Preset } from "../common/PresetChips.tsx"
25
- import { QueryForm } from "../pipeline/QueryForm.tsx"
19
+ import type { Preset } from "../common/PresetChips.tsx"
26
20
  import { BackendControl } from "./BackendControl.tsx"
27
21
  import { CompareToggle } from "./CompareToggle.tsx"
22
+ import { DebugInfo } from "./DebugInfo.tsx"
23
+ import { MapChipRow } from "./MapChipRow.tsx"
24
+ import { MapCompass } from "./MapCompass.tsx"
25
+ import { MapControlButton, MapControlGroup, MapControlStack } from "./MapControlStack.tsx"
26
+ import { MapProgressBar } from "./MapProgressBar.tsx"
27
+ import { MapSearchBar, SearchGlyph } from "./MapSearchBar.tsx"
28
+ import { MapSheet, SheetClose } from "./MapSheet.tsx"
28
29
  import { PlaceAutocomplete } from "./PlaceAutocomplete.tsx"
29
30
  import { ResultPanel } from "./ResultPanel.tsx"
30
31
  import { VersionPicker } from "./VersionPicker.tsx"
31
32
 
33
+ /**
34
+ * Props for {@linkcode GeocoderControls}.
35
+ */
32
36
  export interface GeocoderControlsProps {
33
- /**
34
- * The injected geocoder runtime.
35
- */
36
37
  runtime: GeocoderRuntime
37
- /**
38
- * The parse+resolve state machine.
39
- */
40
38
  geocode: UseGeocode
41
- /**
42
- * The place-autocomplete combobox state.
43
- */
44
39
  autocomplete: UsePlaceAutocomplete
45
- /**
46
- * The compare-mode state.
47
- */
48
40
  compare: UseCompareState
49
41
  /**
50
- * Host-injected panels (about, release blurb, compare, permalink, extras, failure).
42
+ * A query from the URL to run once the runtime is ready.
51
43
  */
44
+ initialQuery?: string | null
52
45
  panels: GeocoderPanels
53
- /**
54
- * Example chips.
55
- */
56
46
  presets: ReadonlyArray<Preset>
57
47
  /**
58
- * Input placeholder.
48
+ * The input placeholder.
49
+ *
50
+ * The field also selects its value on focus while it still equals this text.
59
51
  */
60
52
  placeholder: string
53
+ map?: MapInstance | null
61
54
  /**
62
- * Select a model version (the composed geocoder also clears a now-colliding compare selection).
55
+ * Called before each user-initiated query, so the host can write the query to its URL.
63
56
  */
57
+ onSubmitQuery?: (query: string) => void
64
58
  onSelectVersion: (version: string) => void
59
+ onForceWASMChange: (forceWASM: boolean) => void
65
60
  /**
66
- * Toggle the forced WASM backend.
61
+ * Whether the developer panel starts open.
62
+ *
63
+ * @default false
67
64
  */
68
- onForceWASMChange: (forceWASM: boolean) => void
65
+ developer?: boolean
69
66
  }
70
67
 
68
+ type SheetName = "about" | "layers" | "developer" | null
69
+
71
70
  /**
72
- * The floating control panel.
71
+ * The media query for the bottom-drawer layout.
72
+ * It must match the breakpoint in `styles.css`.
73
+ */
74
+ const DRAWER_LAYOUT = "(max-width: 600px)"
75
+
76
+ /**
77
+ * The pointer travel, in px, that turns a press on the header into a drag instead of a tap.
78
+ */
79
+ const DRAG_TRAVEL_PX = 3
80
+
81
+ /**
82
+ * The downward travel, in px, that turns a pull at the top of the panel's scroll into a drawer drag.
83
+ */
84
+ const OVERSCROLL_PROMOTE_PX = 8
85
+
86
+ /**
87
+ * Renders the search and result panels, map control rail and side sheets over the map.
88
+ *
89
+ * On narrow screens, users can drag the search panel between bottom-drawer detents.
73
90
  */
74
91
  export function GeocoderControls({
75
92
  runtime,
@@ -79,115 +96,553 @@ export function GeocoderControls({
79
96
  panels,
80
97
  presets,
81
98
  placeholder,
99
+ map = null,
100
+ initialQuery = null,
101
+ onSubmitQuery,
82
102
  onSelectVersion,
83
103
  onForceWASMChange,
104
+ developer = false,
84
105
  }: GeocoderControlsProps): ReactNode {
85
106
  const versions = runtime.availableVersions ?? []
86
107
  const { busy, result, selectedCandidate } = geocode
87
108
  const loading = runtime.loading
88
109
  const errorMessage = geocode.parseError ?? runtime.errorMessage ?? null
89
110
 
111
+ // Only one side sheet is open at a time, because two would stack on the same edge.
112
+ const [openSheet, setOpenSheet] = useState<SheetName>(developer ? "developer" : null)
113
+
114
+ // This flag hides the result panel until the next query resets it.
115
+ const [resultDismissed, setResultDismissed] = useState(false)
116
+
117
+ // The drawer height is in px.
118
+ // A `null` height leaves the size to the stylesheet.
119
+ const sheetRef = useRef<HTMLElement>(null)
120
+ const [sheetHeight, setSheetHeight] = useState<number | null>(null)
121
+ const sheetDragRef = useRef<{ startY: number; startHeight: number; moved: boolean } | null>(null)
122
+
123
+ // The detents are computed on each call, so a rotated or resized viewport needs no resize listener.
124
+ const sheetDetents = useCallback(() => {
125
+ const viewport = window.innerHeight
126
+
127
+ return {
128
+ medium: viewport * 0.52,
129
+ large: viewport * 0.88,
130
+ floor: viewport * 0.15,
131
+ // A drag released below this height dismisses the result.
132
+ dismissBelow: viewport * 0.28,
133
+ // At or below this height, the drawer counts as collapsed.
134
+ // The control rail then returns.
135
+ collapsedBelow: viewport * 0.2,
136
+ }
137
+ }, [])
138
+
139
+ // This ref holds a pull that started at the top of the panel's scroll.
140
+ // It becomes a drag after `OVERSCROLL_PROMOTE_PX` of downward travel.
141
+ const overscrollRef = useRef<{ startY: number; pointerId: number } | null>(null)
142
+
143
+ // Drag gestures apply only in the drawer layout.
144
+ // The desktop column is sized by its content.
145
+ const isDrawerLayout = () => globalThis.window !== undefined && globalThis.matchMedia(DRAWER_LAYOUT).matches
146
+
147
+ const beginSheetDrag = useCallback((clientY: number, pointerId: number) => {
148
+ const sheet = sheetRef.current
149
+
150
+ if (!sheet) return
151
+
152
+ sheet.setPointerCapture(pointerId)
153
+ sheetDragRef.current = { startY: clientY, startHeight: sheet.getBoundingClientRect().height, moved: false }
154
+ }, [])
155
+
156
+ // The whole header is the grab target, except the search input and the close button.
157
+ const onHeaderPointerDown = useCallback(
158
+ (event: React.PointerEvent<HTMLDivElement>) => {
159
+ if (!isDrawerLayout()) return
160
+
161
+ if (event.target instanceof Element && event.target.closest("input, .mw-map-sheet__close")) return
162
+
163
+ beginSheetDrag(event.clientY, event.pointerId)
164
+ },
165
+ [beginSheetDrag]
166
+ )
167
+
168
+ const onGripPointerMove = useCallback(
169
+ (event: React.PointerEvent<HTMLElement>) => {
170
+ const drag = sheetDragRef.current
171
+
172
+ if (!drag) return
173
+
174
+ // An upward drag makes the drawer taller, so the delta is inverted against the pointer's y.
175
+ const delta = drag.startY - event.clientY
176
+
177
+ if (Math.abs(delta) > DRAG_TRAVEL_PX) {
178
+ drag.moved = true
179
+ }
180
+
181
+ const { floor, large } = sheetDetents()
182
+
183
+ setSheetHeight(Math.min(large, Math.max(floor, drag.startHeight + delta)))
184
+ },
185
+ [sheetDetents]
186
+ )
187
+
188
+ // A tap, keyboard activation of the handle and the handle's `aria-expanded` state
189
+ // all use this midpoint to select the drawer detent.
190
+ const detentMidpoint = useCallback(() => {
191
+ const { medium, large } = sheetDetents()
192
+
193
+ return (medium + large) / 2
194
+ }, [sheetDetents])
195
+
196
+ const atLargeDetent = () => sheetHeight !== null && sheetHeight > detentMidpoint()
197
+
198
+ const toggleDetent = useCallback(() => {
199
+ const { medium, large } = sheetDetents()
200
+ const current = sheetRef.current?.getBoundingClientRect().height ?? medium
201
+
202
+ setSheetHeight(current > detentMidpoint() ? medium : large)
203
+ }, [sheetDetents, detentMidpoint])
204
+
205
+ const onGripPointerUp = useCallback(() => {
206
+ const drag = sheetDragRef.current
207
+
208
+ if (!drag) return
209
+
210
+ sheetDragRef.current = null
211
+
212
+ const { medium, large, dismissBelow } = sheetDetents()
213
+ const midpoint = (medium + large) / 2
214
+ const current = sheetRef.current?.getBoundingClientRect().height ?? medium
215
+
216
+ // A press without travel counts as a tap.
217
+ // It toggles between the two detents.
218
+ if (!drag.moved) {
219
+ toggleDetent()
220
+
221
+ return
222
+ }
223
+
224
+ if (current < dismissBelow) {
225
+ setResultDismissed(true)
226
+ setSheetHeight(null)
227
+
228
+ return
229
+ }
230
+
231
+ setSheetHeight(current > midpoint ? large : medium)
232
+ }, [sheetDetents, toggleDetent])
233
+
234
+ // A downward pull on a panel scrolled to its top drags the drawer down.
235
+ // This handler only arms the pull; `onPanelPointerMove` promotes it, so taps
236
+ // and upward scrolls are unaffected.
237
+ const onPanelPointerDown = useCallback((event: React.PointerEvent<HTMLElement>) => {
238
+ if (!isDrawerLayout()) return
239
+
240
+ if (sheetDragRef.current) return
241
+
242
+ const sheet = sheetRef.current
243
+
244
+ if (!sheet || sheet.scrollTop > 0) return
245
+
246
+ if (event.target instanceof Element && event.target.closest(".mw-map-panel__header")) return
247
+
248
+ overscrollRef.current = { startY: event.clientY, pointerId: event.pointerId }
249
+ }, [])
250
+
251
+ const onPanelPointerMove = useCallback(
252
+ (event: React.PointerEvent<HTMLElement>) => {
253
+ if (sheetDragRef.current) {
254
+ onGripPointerMove(event)
255
+
256
+ return
257
+ }
258
+
259
+ const armed = overscrollRef.current
260
+
261
+ if (!armed || armed.pointerId !== event.pointerId) return
262
+
263
+ const sheet = sheetRef.current
264
+
265
+ if (!sheet) return
266
+
267
+ if (sheet.scrollTop > 0) {
268
+ overscrollRef.current = null
269
+
270
+ return
271
+ }
272
+
273
+ if (event.clientY - armed.startY < OVERSCROLL_PROMOTE_PX) return
274
+
275
+ overscrollRef.current = null
276
+ // The drag starts from the pull's origin, so the drawer does not jump by the threshold.
277
+ sheetDragRef.current = { startY: armed.startY, startHeight: sheet.getBoundingClientRect().height, moved: true }
278
+ sheet.setPointerCapture(event.pointerId)
279
+ onGripPointerMove(event)
280
+ },
281
+ [onGripPointerMove]
282
+ )
283
+
284
+ const onPanelPointerUp = useCallback(() => {
285
+ overscrollRef.current = null
286
+ onGripPointerUp()
287
+ }, [onGripPointerUp])
288
+
289
+ const { bearing, resetNorth } = useMapBearing(map)
290
+
291
+ const toggleSheet = (name: Exclude<SheetName, null>) => setOpenSheet((current) => (current === name ? null : name))
292
+ const closeSheet = useCallback(() => setOpenSheet(null), [])
293
+
294
+ // Every user-initiated query goes through this function, so `onSubmitQuery` fires from one place.
295
+ const runQuery = useCallback(
296
+ (query: string) => {
297
+ // A new query shows the result panel again at its default height.
298
+ setResultDismissed(false)
299
+ setSheetHeight(null)
300
+ onSubmitQuery?.(query)
301
+ void geocode.submit(query)
302
+ },
303
+ [onSubmitQuery, geocode]
304
+ )
305
+
306
+ // The initial query waits for `runtime.ready` because the parse pipeline drops
307
+ // a submit made before the model loads.
308
+ // The ref makes it run once, so a later render cannot overwrite the user's own query.
309
+ const autoRanInitialQuery = useRef(false)
310
+
311
+ useEffect(() => {
312
+ if (autoRanInitialQuery.current) return
313
+
314
+ if (!initialQuery || !runtime.ready) return
315
+
316
+ autoRanInitialQuery.current = true
317
+ // This calls `geocode.submit` directly because the query is already in the URL.
318
+ void geocode.submit(initialQuery)
319
+ }, [initialQuery, runtime.ready, geocode])
320
+
321
+ // A drawer-breakpoint crossing clears any dragged height.
322
+ // The height could clip the desktop column.
323
+ useEffect(() => {
324
+ const query = globalThis.matchMedia(DRAWER_LAYOUT)
325
+ const onChange = () => setSheetHeight(null)
326
+
327
+ query.addEventListener("change", onChange)
328
+
329
+ return () => query.removeEventListener("change", onChange)
330
+ }, [])
331
+
332
+ // A user pan, zoom, or rotate shrinks the drawer to its floor detent without dismissing the result.
333
+ // Programmatic camera moves, such as the fly-to after a query, have no `originalEvent` and are ignored.
334
+ useEffect(() => {
335
+ if (!map) return
336
+
337
+ const collapse = (event: { originalEvent?: unknown }) => {
338
+ if (!event.originalEvent) return
339
+
340
+ if (!isDrawerLayout()) return
341
+
342
+ const { floor } = sheetDetents()
343
+
344
+ setSheetHeight((current) => (current !== null && current <= floor ? current : floor))
345
+ }
346
+
347
+ map.on("dragstart", collapse)
348
+ map.on("zoomstart", collapse)
349
+ map.on("rotatestart", collapse)
350
+
351
+ return () => {
352
+ map.off("dragstart", collapse)
353
+ map.off("zoomstart", collapse)
354
+ map.off("rotatestart", collapse)
355
+ }
356
+ }, [map, sheetDetents])
357
+
358
+ // Escape dismisses the result only while no side sheet is open.
359
+ // `MapSheet` also listens for Escape on the document, so one keystroke would otherwise close both.
360
+ useEffect(() => {
361
+ if (openSheet) return
362
+
363
+ const onKeyDown = (event: KeyboardEvent) => {
364
+ if (event.key === "Escape") {
365
+ setResultDismissed(true)
366
+ }
367
+ }
368
+
369
+ document.addEventListener("keydown", onKeyDown)
370
+
371
+ return () => document.removeEventListener("keydown", onKeyDown)
372
+ }, [openSheet])
373
+
374
+ // A map-label click searches for its name.
375
+ const pickLabel = useCallback(
376
+ (name: string) => {
377
+ geocode.setText(name)
378
+ geocode.reset()
379
+ runQuery(name)
380
+ },
381
+ [geocode, runQuery]
382
+ )
383
+
384
+ useMapLabelPick(map, pickLabel)
385
+
386
+ const chips = presets.map((preset) => ({ label: preset.label, value: preset.value }))
387
+ // The model download does not open the result panel.
388
+ // The progress bar reports it instead.
389
+ const showSheet = Boolean(busy || result || errorMessage) && !resultDismissed
390
+
391
+ // The control rail hides while the drawer is raised above its collapsed height.
392
+ const drawerRaised =
393
+ showSheet &&
394
+ (sheetHeight === null || (globalThis.window !== undefined && sheetHeight > sheetDetents().collapsedBelow))
395
+
396
+ const bundleLoading = Boolean(loading && !runtime.ready)
397
+ const steps = loading?.stepLabels.length ?? 0
398
+
399
+ // The model downloads during the first step.
400
+ // While bytes arrive, the bar follows the byte fraction scaled into the first step's share,
401
+ // so it does not move backwards when step progress takes over.
402
+ const stepFraction = steps ? ((loading?.stepIndex ?? 0) + 1) / steps : null
403
+ const byteFraction = loading?.byteFraction
404
+
405
+ const fraction =
406
+ typeof byteFraction === "number" && steps ? (byteFraction * 1) / steps : (byteFraction ?? stepFraction)
407
+
90
408
  return (
91
- <section className="mw-demo-controls">
92
- {panels.header}
93
- {panels.releaseInfo}
94
-
95
- <VersionPicker
96
- versions={versions}
97
- selected={runtime.selectedVersion ?? null}
98
- onSelect={onSelectVersion}
99
- disabled={busy}
100
- />
101
-
102
- <BackendControl
103
- activeBackend={runtime.activeBackend}
104
- forceWASM={runtime.forceWASM ?? false}
105
- onForceWASMChange={onForceWASMChange}
106
- />
107
-
108
- <CompareToggle
109
- versions={versions}
110
- primaryVersion={runtime.selectedVersion ?? null}
111
- compareMode={compare.compareMode}
112
- onCompareModeChange={compare.setCompareMode}
113
- compareVersion={compare.compareVersion}
114
- onCompareVersionChange={compare.setCompareVersion}
115
- disabled={busy}
116
- />
117
-
118
- <QueryForm
119
- value={geocode.text}
120
- onChange={geocode.setText}
121
- onSubmit={geocode.submit}
122
- disabled={!runtime.ready}
123
- busy={busy}
124
- placeholder={placeholder}
125
- onKeyDown={autocomplete.onInputKeyDown}
126
- inputProps={autocomplete.inputProps}
127
- />
128
-
129
- {panels.bias}
130
-
131
- <PlaceAutocomplete
132
- suggestions={autocomplete.suggestions}
133
- activeIndex={autocomplete.activeIndex}
134
- onPick={autocomplete.pick}
135
- onHover={autocomplete.setActiveIndex}
136
- listboxID={autocomplete.listboxID}
137
- optionID={autocomplete.optionID}
138
- />
139
-
140
- <PresetChips
141
- presets={presets}
142
- disabled={!runtime.ready || busy}
143
- onPick={(value) => {
144
- geocode.setText(value)
145
- geocode.reset()
146
- }}
147
- trailing={panels.permalink ? panels.permalink(geocode.text) : null}
148
- />
149
-
150
- {loading && !runtime.ready ? (
151
- <LoadingIndicator
152
- mode="staged"
153
- steps={loading.stepLabels.length ? loading.stepLabels : undefined}
154
- activeStep={loading.stepIndex}
155
- label={loading.progress}
156
- />
157
- ) : null}
158
- {errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
409
+ <>
410
+ <MapProgressBar active={bundleLoading} fraction={fraction} label="Loading the geocoder" />
159
411
 
160
- {panels.aboveResult ? panels.aboveResult({ result }) : null}
412
+ {/* One panel holds the search, examples and result. On desktop it is a left column sized to its content.
413
+ On narrow screens it is a bottom drawer. */}
414
+ <section
415
+ className="mw-map-panel"
416
+ aria-label="Search and results"
417
+ ref={sheetRef}
418
+ onPointerDown={onPanelPointerDown}
419
+ onPointerMove={onPanelPointerMove}
420
+ onPointerUp={onPanelPointerUp}
421
+ onPointerCancel={onPanelPointerUp}
422
+ {...(sheetHeight === null ? {} : { style: { maxHeight: `${Math.round(sheetHeight)}px` } })}
423
+ >
424
+ {/*
425
+ * The header holds the grab bar and search field.
426
+ * It stays pinned while the result scrolls.
427
+ */}
428
+ <div className="mw-map-panel__header" onPointerDown={onHeaderPointerDown}>
429
+ <div className="mw-map-panel__grip">
430
+ {/* The handle appears only with a result, because there is no content to expand without one. */}
431
+ {showSheet ? (
432
+ <button
433
+ type="button"
434
+ className="mw-map-sheet__handle"
435
+ aria-label="Resize the panel"
436
+ aria-expanded={atLargeDetent()}
437
+ // The header handles pointer gestures.
438
+ // A click with `detail === 0` comes from the keyboard,
439
+ // so only that toggles the detent here.
440
+ onClick={(event) => {
441
+ if (event.detail !== 0) return
161
442
 
162
- {busy ? (
163
- <div className="mw-result">
164
- <LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
443
+ toggleDetent()
444
+ }}
445
+ />
446
+ ) : null}
447
+
448
+ {showSheet ? (
449
+ <SheetClose
450
+ label="Close the result"
451
+ className="mw-map-sheet__close--floating"
452
+ onClose={() => setResultDismissed(true)}
453
+ />
454
+ ) : null}
455
+ </div>
456
+
457
+ <form
458
+ className="mw-map-panel__search"
459
+ onSubmit={(event) => {
460
+ event.preventDefault()
461
+ runQuery(geocode.text)
462
+ }}
463
+ >
464
+ {/* The search bar has no trailing control because `type="search"` provides a native clear button. */}
465
+ <MapSearchBar label="Search addresses" leading={<SearchGlyph />} busy={busy}>
466
+ <input
467
+ id="mw-pipeline-input"
468
+ type="search"
469
+ aria-label="Address"
470
+ value={geocode.text}
471
+ onChange={(event) => geocode.setText(event.target.value)}
472
+ // Focus selects the value only while it equals the placeholder, so typing replaces it.
473
+ onFocus={(event) => {
474
+ if (placeholder && event.currentTarget.value === placeholder) {
475
+ event.currentTarget.select()
476
+ }
477
+ }}
478
+ disabled={!runtime.ready}
479
+ placeholder={placeholder}
480
+ onKeyDown={autocomplete.onInputKeyDown}
481
+ {...autocomplete.inputProps}
482
+ />
483
+ </MapSearchBar>
484
+ </form>
165
485
  </div>
166
- ) : result ? (
167
- panels.result ? (
168
- panels.result({
169
- result,
170
- selectedCandidate,
171
- selectedCandidateIndex: geocode.selectedCandidateIndex,
172
- onSelectCandidate: geocode.selectCandidate,
173
- })
174
- ) : (
175
- <ResultPanel
176
- result={result}
177
- selectedCandidate={selectedCandidate}
178
- selectedCandidateIndex={geocode.selectedCandidateIndex}
179
- onSelectCandidate={geocode.selectCandidate}
180
- extras={panels.extras}
181
- failure={panels.failure}
182
- />
183
- )
486
+
487
+ <PlaceAutocomplete
488
+ suggestions={autocomplete.suggestions}
489
+ activeIndex={autocomplete.activeIndex}
490
+ onPick={autocomplete.pick}
491
+ onHover={autocomplete.setActiveIndex}
492
+ listboxID={autocomplete.listboxID}
493
+ optionID={autocomplete.optionID}
494
+ />
495
+
496
+ <MapChipRow
497
+ chips={chips}
498
+ label="Example addresses"
499
+ activeValue={geocode.text}
500
+ disabled={!runtime.ready || busy}
501
+ onPick={(value) => {
502
+ geocode.setText(value)
503
+ geocode.reset()
504
+ runQuery(value)
505
+ }}
506
+ />
507
+
508
+ {panels.bias}
509
+
510
+ {showSheet ? (
511
+ <div className="mw-map-panel__result">
512
+ {errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
513
+
514
+ {panels.aboveResult ? panels.aboveResult({ result }) : null}
515
+
516
+ {busy ? (
517
+ <div className="mw-result">
518
+ <LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
519
+ </div>
520
+ ) : result ? (
521
+ panels.result ? (
522
+ panels.result({
523
+ result,
524
+ selectedCandidate,
525
+ selectedCandidateIndex: geocode.selectedCandidateIndex,
526
+ onSelectCandidate: geocode.selectCandidate,
527
+ })
528
+ ) : (
529
+ <ResultPanel
530
+ result={result}
531
+ selectedCandidate={selectedCandidate}
532
+ selectedCandidateIndex={geocode.selectedCandidateIndex}
533
+ onSelectCandidate={geocode.selectCandidate}
534
+ extras={panels.extras}
535
+ failure={panels.failure}
536
+ />
537
+ )
538
+ ) : null}
539
+
540
+ {panels.compare
541
+ ? panels.compare({ result, compareMode: compare.compareMode, compareVersion: compare.compareVersion })
542
+ : null}
543
+
544
+ {panels.permalink ? <div className="mw-map-sheet__actions">{panels.permalink(geocode.text)}</div> : null}
545
+ </div>
546
+ ) : null}
547
+ </section>
548
+
549
+ {/* Every floating control uses this rail to prevent overlap. The compass has its own group because it appears
550
+ and disappears. The rail hides while the drawer is raised. */}
551
+ <MapControlStack label="Map controls" className={drawerRaised ? "mw-map-control-stack--drawer-open" : undefined}>
552
+ <MapControlGroup>
553
+ <MapControlButton
554
+ label="About this geocoder"
555
+ active={openSheet === "about"}
556
+ onPress={() => toggleSheet("about")}
557
+ >
558
+ <span aria-hidden="true">i</span>
559
+ </MapControlButton>
560
+
561
+ {panels.layers ? (
562
+ <MapControlButton label="Map layers" active={openSheet === "layers"} onPress={() => toggleSheet("layers")}>
563
+ <span aria-hidden="true">≡</span>
564
+ </MapControlButton>
565
+ ) : null}
566
+
567
+ <MapControlButton
568
+ label="Developer controls"
569
+ active={openSheet === "developer"}
570
+ onPress={() => toggleSheet("developer")}
571
+ >
572
+ <span aria-hidden="true">⚙</span>
573
+ </MapControlButton>
574
+ </MapControlGroup>
575
+
576
+ <MapControlGroup className="mw-map-control-group--compass">
577
+ <MapCompass bearing={bearing} onResetNorth={resetNorth} />
578
+ </MapControlGroup>
579
+ </MapControlStack>
580
+
581
+ {openSheet === "about" ? (
582
+ <MapSheet title="About" onClose={closeSheet}>
583
+ {panels.header}
584
+ </MapSheet>
184
585
  ) : null}
185
586
 
186
- {panels.compare
187
- ? panels.compare({ result, compareMode: compare.compareMode, compareVersion: compare.compareVersion })
188
- : null}
587
+ {openSheet === "layers" ? (
588
+ <MapSheet title="Layers" onClose={closeSheet}>
589
+ {panels.layers?.({ map })}
590
+ </MapSheet>
591
+ ) : null}
592
+
593
+ {openSheet === "developer" ? (
594
+ <MapSheet title="Developer" onClose={closeSheet}>
595
+ {panels.releaseInfo ? (
596
+ <div className="mw-map-sheet__row">
597
+ <span className="mw-map-sheet__label">This release</span>
598
+ {panels.releaseInfo}
599
+ </div>
600
+ ) : null}
601
+
602
+ <div className="mw-map-sheet__row">
603
+ <VersionPicker
604
+ versions={versions}
605
+ selected={runtime.selectedVersion ?? null}
606
+ onSelect={onSelectVersion}
607
+ disabled={busy}
608
+ />
609
+ </div>
610
+
611
+ <div className="mw-map-sheet__row">
612
+ <BackendControl
613
+ activeBackend={runtime.activeBackend}
614
+ forceWASM={runtime.forceWASM ?? false}
615
+ onForceWASMChange={onForceWASMChange}
616
+ />
617
+ </div>
618
+
619
+ <div className="mw-map-sheet__row">
620
+ <span className="mw-map-sheet__label">Debug info</span>
621
+ <DebugInfo
622
+ activeBackend={runtime.activeBackend}
623
+ forceWASM={runtime.forceWASM}
624
+ selectedVersion={runtime.selectedVersion}
625
+ ready={runtime.ready}
626
+ />
627
+ </div>
628
+
629
+ <div className="mw-map-sheet__row">
630
+ <CompareToggle
631
+ versions={versions}
632
+ primaryVersion={runtime.selectedVersion ?? null}
633
+ compareMode={compare.compareMode}
634
+ onCompareModeChange={compare.setCompareMode}
635
+ compareVersion={compare.compareVersion}
636
+ onCompareVersionChange={compare.setCompareVersion}
637
+ disabled={busy}
638
+ />
639
+ </div>
640
+
641
+ {panels.developerExtras}
642
+ </MapSheet>
643
+ ) : null}
189
644
 
190
645
  {panels.footer}
191
- </section>
646
+ </>
192
647
  )
193
648
  }