@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
@@ -0,0 +1,317 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The docs search modal. A native `<dialog>` opened with `showModal()` supplies the focus trap, the inert
7
+ * background, the backdrop, the close on Escape and the return of focus to the opener. The query field is an
8
+ * `<input type="search">` with the ARIA combobox role. DOM focus stays on the input, and the selected hit is
9
+ * named by `aria-activedescendant`. The component owns no data access: `search` is passed in.
10
+ */
11
+
12
+ import { type KeyboardEvent, type MouseEvent, type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react"
13
+
14
+ import { useDebouncedValue } from "#common/useDebouncedValue"
15
+
16
+ import type { SearchHit, SearchResponse } from "./types.ts"
17
+
18
+ export interface SearchModalProps {
19
+ open: boolean
20
+ onClose: () => void
21
+ /**
22
+ * Called with the hit's site-relative link on Enter or on an unmodified click.
23
+ * Absent, the link navigates as an ordinary anchor.
24
+ */
25
+ onNavigate?: (href: string) => void
26
+ /**
27
+ * Runs the query.
28
+ *
29
+ * The function must keep a stable identity across renders (for example through `useCallback`),
30
+ * because a new identity refires the request for the current query.
31
+ */
32
+ search: (q: string, signal: AbortSignal) => Promise<SearchResponse>
33
+ }
34
+
35
+ type Status =
36
+ | { kind: "idle" }
37
+ | { kind: "loading" }
38
+ | { kind: "results"; response: SearchResponse }
39
+ | { kind: "failed" }
40
+
41
+ const DEBOUNCE_MS = 150
42
+
43
+ /**
44
+ * How long a request runs before the live region announces that the index is loading.
45
+ */
46
+ const LOADING_ANNOUNCE_MS = 300
47
+ const MAX_QUERY_LENGTH = 200
48
+ const DEFAULT_CATEGORY = "Documentation"
49
+
50
+ /**
51
+ * The site-relative link for a hit.
52
+ */
53
+ export function hitHref(hit: Pick<SearchHit, "url" | "anchor">): string {
54
+ return hit.anchor === "" ? hit.url : `${hit.url}#${hit.anchor}`
55
+ }
56
+
57
+ function statusText(status: Status): string {
58
+ if (status.kind === "failed") return "Search is unavailable. Try again in a moment."
59
+
60
+ if (status.kind === "loading") return "Loading the search index…"
61
+
62
+ if (status.kind === "idle") return ""
63
+
64
+ const { response } = status
65
+
66
+ if (!response.hits.length) return `No results for “${response.query}”.`
67
+
68
+ return response.hits.length === 1 ? "1 result." : `${response.hits.length} results.`
69
+ }
70
+
71
+ function highlighted(hit: SearchHit): ReactNode[] {
72
+ const parts: ReactNode[] = []
73
+ let cursor = 0
74
+
75
+ for (const [start, end] of hit.highlights) {
76
+ if (start < cursor) continue
77
+
78
+ parts.push(hit.snippet.slice(cursor, start), <mark key={start}>{hit.snippet.slice(start, end)}</mark>)
79
+ cursor = end
80
+ }
81
+
82
+ parts.push(hit.snippet.slice(cursor))
83
+
84
+ return parts
85
+ }
86
+
87
+ function title(hit: SearchHit): string {
88
+ return hit.hierarchy
89
+ .slice(1)
90
+ .filter((entry): entry is string => entry !== null)
91
+ .join(" › ")
92
+ }
93
+
94
+ export function SearchModal({ open, onClose, onNavigate, search }: SearchModalProps) {
95
+ const dialogRef = useRef<HTMLDialogElement>(null)
96
+ const inputRef = useRef<HTMLInputElement>(null)
97
+ const baseID = useId()
98
+ const listboxID = `${baseID}-listbox`
99
+ const [query, setQuery] = useState("")
100
+ const [settled, setSettled] = useState<Status>({ kind: "idle" })
101
+ const [selected, setSelected] = useState(0)
102
+ const [loading, setLoading] = useState(false)
103
+ const trimmed = query.trim().slice(0, MAX_QUERY_LENGTH)
104
+ const debounced = useDebouncedValue(trimmed, DEBOUNCE_MS)
105
+
106
+ useEffect(() => {
107
+ const dialog = dialogRef.current
108
+
109
+ if (!dialog) return
110
+
111
+ if (open && !dialog.open) {
112
+ dialog.showModal()
113
+ inputRef.current?.focus()
114
+ } else if (!open && dialog.open) {
115
+ dialog.close()
116
+ }
117
+ }, [open])
118
+
119
+ useEffect(() => {
120
+ if (debounced === "") return
121
+
122
+ // Aborting the earlier request is what keeps a late response from replacing newer results.
123
+ const controller = new AbortController()
124
+ const loadingTimer = setTimeout(() => setLoading(true), LOADING_ANNOUNCE_MS)
125
+
126
+ search(debounced, controller.signal).then(
127
+ (response) => {
128
+ if (controller.signal.aborted) return
129
+
130
+ clearTimeout(loadingTimer)
131
+ setLoading(false)
132
+ setSettled({ kind: "results", response })
133
+ setSelected(0)
134
+ },
135
+ () => {
136
+ if (controller.signal.aborted) return
137
+
138
+ clearTimeout(loadingTimer)
139
+ setLoading(false)
140
+ setSettled({ kind: "failed" })
141
+ }
142
+ )
143
+
144
+ return () => {
145
+ clearTimeout(loadingTimer)
146
+ setLoading(false)
147
+ controller.abort()
148
+ }
149
+ }, [debounced, search])
150
+
151
+ // An empty query shows no results, whatever the last settled response was.
152
+ const status: Status = debounced === "" ? { kind: "idle" } : settled
153
+ const response = status.kind === "results" ? status.response : undefined
154
+ // The announcement reports a slow request while the last results stay on screen.
155
+ const announced: Status = loading && debounced !== "" ? { kind: "loading" } : status
156
+
157
+ const groups = useMemo(() => {
158
+ const hits = response?.hits ?? []
159
+ const byCategory = new Map<string, SearchHit[]>()
160
+
161
+ for (const hit of hits) {
162
+ const category = hit.hierarchy[0] ?? DEFAULT_CATEGORY
163
+
164
+ byCategory.set(category, [...(byCategory.get(category) ?? []), hit])
165
+ }
166
+
167
+ // Options are numbered in display order so the arrow keys follow what the reader sees.
168
+ let order = 0
169
+
170
+ return [...byCategory].map(([category, entries]) => ({
171
+ category,
172
+ entries: entries.map((hit) => ({ hit, order: order++ })),
173
+ }))
174
+ }, [response])
175
+
176
+ const ordered = groups.flatMap((group) => group.entries)
177
+ const optionID = (order: number) => `${baseID}-option-${order}`
178
+
179
+ function go(hit: SearchHit) {
180
+ const href = hitHref(hit)
181
+
182
+ onClose()
183
+
184
+ if (onNavigate) {
185
+ onNavigate(href)
186
+ } else {
187
+ globalThis.location.assign(href)
188
+ }
189
+ }
190
+
191
+ function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
192
+ // A key that commits or edits an IME composition is not a command.
193
+ if (event.nativeEvent.isComposing) return
194
+
195
+ if (!ordered.length) return
196
+
197
+ const last = ordered.length - 1
198
+
199
+ const next =
200
+ event.key === "ArrowDown"
201
+ ? selected === last
202
+ ? 0
203
+ : selected + 1
204
+ : event.key === "ArrowUp"
205
+ ? selected === 0
206
+ ? last
207
+ : selected - 1
208
+ : event.key === "Home"
209
+ ? 0
210
+ : event.key === "End"
211
+ ? last
212
+ : undefined
213
+
214
+ if (next !== undefined) {
215
+ event.preventDefault()
216
+ setSelected(next)
217
+ document.getElementById(optionID(next))?.scrollIntoView({ block: "nearest" })
218
+ } else if (event.key === "Enter") {
219
+ event.preventDefault()
220
+
221
+ // The hits on screen answer an earlier query until the debounce settles.
222
+ if (trimmed !== debounced) return
223
+
224
+ const entry = ordered[selected]
225
+
226
+ if (entry) {
227
+ go(entry.hit)
228
+ }
229
+ }
230
+ }
231
+
232
+ function onDialogClick(event: MouseEvent<HTMLDialogElement>) {
233
+ // A click whose target is the dialog element itself landed on the backdrop.
234
+ if (event.target === event.currentTarget) {
235
+ event.currentTarget.close()
236
+ }
237
+ }
238
+
239
+ function onLinkClick(event: MouseEvent<HTMLAnchorElement>, hit: SearchHit) {
240
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return
241
+
242
+ event.preventDefault()
243
+ go(hit)
244
+ }
245
+
246
+ return (
247
+ <dialog
248
+ ref={dialogRef}
249
+ className="mw-search"
250
+ aria-label="Search the documentation"
251
+ onClose={onClose}
252
+ onClick={onDialogClick}
253
+ >
254
+ <form role="search" className="mw-search__form" onSubmit={(event) => event.preventDefault()}>
255
+ <input
256
+ ref={inputRef}
257
+ type="search"
258
+ className="mw-search__input"
259
+ role="combobox"
260
+ aria-label="Search the documentation"
261
+ aria-autocomplete="list"
262
+ aria-expanded={ordered.length > 0}
263
+ aria-controls={listboxID}
264
+ aria-activedescendant={ordered.length ? optionID(selected) : undefined}
265
+ autoComplete="off"
266
+ spellCheck={false}
267
+ maxLength={MAX_QUERY_LENGTH}
268
+ placeholder="Search the documentation"
269
+ value={query}
270
+ onChange={(event) => setQuery(event.target.value)}
271
+ onKeyDown={onKeyDown}
272
+ />
273
+ </form>
274
+
275
+ {response?.corrected !== undefined && (
276
+ <p className="mw-search__corrected">Showing results for “{response.corrected}”</p>
277
+ )}
278
+
279
+ <div id={listboxID} role="listbox" aria-label="Search results" className="mw-search__results">
280
+ {groups.map((group, groupIndex) => (
281
+ <div
282
+ key={group.category}
283
+ role="group"
284
+ aria-labelledby={`${baseID}-group-${groupIndex}`}
285
+ className="mw-search__group"
286
+ >
287
+ <div id={`${baseID}-group-${groupIndex}`} className="mw-search__category">
288
+ {group.category}
289
+ </div>
290
+ {group.entries.map(({ hit, order }) => (
291
+ // The option is the link itself: an option may not contain an interactive element, and
292
+ // `option` is an allowed role on `a[href]`, which keeps the URL for open-in-new-tab.
293
+ <a
294
+ key={order}
295
+ id={optionID(order)}
296
+ role="option"
297
+ aria-selected={order === selected}
298
+ href={hitHref(hit)}
299
+ tabIndex={-1}
300
+ className="mw-search__option"
301
+ onPointerMove={() => setSelected(order)}
302
+ onClick={(event) => onLinkClick(event, hit)}
303
+ >
304
+ <span className="mw-search__title">{title(hit)}</span>
305
+ {hit.snippet !== "" && <span className="mw-search__snippet">{highlighted(hit)}</span>}
306
+ </a>
307
+ ))}
308
+ </div>
309
+ ))}
310
+ </div>
311
+
312
+ <p aria-live="polite" className="mw-search__status">
313
+ {statusText(announced)}
314
+ </p>
315
+ </dialog>
316
+ )
317
+ }
@@ -0,0 +1,144 @@
1
+ /* ---------------------------------------------------------------------------
2
+ * The docs search modal.
3
+ *
4
+ * The native <dialog> has zero padding, so the only click whose target is the dialog element itself is a click on
5
+ * the backdrop. The padding sits on the form, the results and the status line instead.
6
+ *
7
+ * The selected option is marked by an inset accent bar as well as a fill, so the selection holds 3:1 against the
8
+ * panel without depending on the fill alone. Colors, radii, spacing, type and motion read semantic tokens from
9
+ * tokens.css. The layout literals are the panel width and height caps, the 44px input height, the 3px selection
10
+ * bar and the backdrop scrim, for which no token exists.
11
+ * --------------------------------------------------------------------------- */
12
+
13
+ .mw-search {
14
+ width: min(40rem, 92vw);
15
+ max-height: 70vh;
16
+ margin: 12vh auto 0;
17
+ padding: 0;
18
+ overflow: hidden;
19
+ color: var(--color-text-primary);
20
+ font-family: var(--font-family-text);
21
+ font-size: var(--font-size-callout);
22
+ line-height: var(--line-height-body);
23
+ background: var(--color-background-raised);
24
+ border: 1px solid var(--control-border);
25
+ border-radius: var(--radius-panel);
26
+ box-shadow: var(--material-glass-shadow);
27
+ }
28
+
29
+ .mw-search[open] {
30
+ display: flex;
31
+ flex-direction: column;
32
+ }
33
+
34
+ .mw-search::backdrop {
35
+ background: oklch(0% 0 264 / 0.5);
36
+ }
37
+
38
+ .mw-search__form {
39
+ padding: var(--space-5);
40
+ border-bottom: 1px solid var(--color-separator);
41
+ }
42
+
43
+ .mw-search__input {
44
+ width: 100%;
45
+ min-height: 44px;
46
+ padding: var(--space-3) var(--space-4);
47
+ color: var(--control-foreground);
48
+ font: inherit;
49
+ font-size: var(--font-size-body);
50
+ background: var(--control-background);
51
+ border: 1px solid var(--control-border);
52
+ border-radius: var(--radius-control);
53
+ }
54
+
55
+ .mw-search__input:focus-visible {
56
+ outline: 2px solid var(--color-accent);
57
+ outline-offset: 1px;
58
+ }
59
+
60
+ .mw-search__corrected {
61
+ margin: 0;
62
+ padding: var(--space-3) var(--space-5) 0;
63
+ color: var(--color-text-secondary);
64
+ font-size: var(--font-size-footnote);
65
+ }
66
+
67
+ .mw-search__results {
68
+ flex: 1 1 auto;
69
+ min-height: 0;
70
+ padding: var(--space-3) var(--space-4);
71
+ overflow-y: auto;
72
+ }
73
+
74
+ .mw-search__category {
75
+ padding: var(--space-4) var(--space-4) var(--space-2);
76
+ color: var(--color-text-secondary);
77
+ font-size: var(--font-size-footnote);
78
+ font-weight: var(--font-weight-semibold);
79
+ }
80
+
81
+ .mw-search__option {
82
+ border-radius: var(--radius-control);
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: var(--space-1);
86
+ padding: var(--space-3) var(--space-4);
87
+ color: inherit;
88
+ text-decoration: none;
89
+ }
90
+
91
+ .mw-search__option[aria-selected="true"] {
92
+ background: var(--color-background-sunken);
93
+ box-shadow: inset 3px 0 0 var(--color-accent);
94
+ }
95
+
96
+ .mw-search__title {
97
+ font-size: var(--font-size-subheadline);
98
+ font-weight: var(--font-weight-semibold);
99
+ }
100
+
101
+ .mw-search__snippet {
102
+ color: var(--color-text-secondary);
103
+ font-size: var(--font-size-footnote);
104
+ }
105
+
106
+ .mw-search__snippet mark {
107
+ color: var(--color-text-primary);
108
+ font-weight: var(--font-weight-semibold);
109
+ background: var(--color-accent-background);
110
+ border-radius: var(--radius-tick);
111
+ }
112
+
113
+ .mw-search__status {
114
+ margin: 0;
115
+ padding: var(--space-3) var(--space-5);
116
+ color: var(--color-text-secondary);
117
+ font-size: var(--font-size-footnote);
118
+ border-top: 1px solid var(--color-separator);
119
+ }
120
+
121
+ /* An empty live region stays rendered, because a region that appears with its text is often not announced. */
122
+ .mw-search__status:empty {
123
+ position: absolute;
124
+ width: 1px;
125
+ height: 1px;
126
+ padding: 0;
127
+ overflow: hidden;
128
+ border: 0;
129
+ clip-path: inset(50%);
130
+ white-space: nowrap;
131
+ }
132
+
133
+ @media (prefers-reduced-motion: no-preference) {
134
+ .mw-search[open] {
135
+ animation: mw-search-open var(--duration-standard) var(--easing-decelerate);
136
+ }
137
+ }
138
+
139
+ @keyframes mw-search-open {
140
+ from {
141
+ opacity: 0;
142
+ transform: translateY(calc(var(--space-4) * -1));
143
+ }
144
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The record the docs build indexes and the response the search modal renders. The modal and the
7
+ * query code share these through `import type`.
8
+ */
9
+
10
+ /**
11
+ * The number of hierarchy entries: the sidebar category, then `h1` through `h6`.
12
+ */
13
+ export const HIERARCHY_DEPTH = 7
14
+
15
+ export interface SearchRecord {
16
+ /**
17
+ * The first 32 hex characters of `sha256(url + "#" + anchor + "\n" + position)`.
18
+ */
19
+ id: string
20
+ /**
21
+ * Site-relative path without the anchor, such as `/docs/developers/reference/cli`.
22
+ */
23
+ url: string
24
+ /**
25
+ * The heading id the record links to, or the empty string for the top of the page.
26
+ */
27
+ anchor: string
28
+ /**
29
+ * Seven entries, `lvl0` through `lvl6`, with `null` below the record's own level.
30
+ */
31
+ hierarchy: (string | null)[]
32
+ /**
33
+ * Aggregated text under the heading, or the empty string for a heading without body text.
34
+ */
35
+ content: string
36
+ /**
37
+ * Depth of the deepest non-null hierarchy entry, 0 through 6.
38
+ */
39
+ level: number
40
+ /**
41
+ * Zero-based order of the record on its page.
42
+ */
43
+ position: number
44
+ }
45
+
46
+ export interface SearchHit {
47
+ url: string
48
+ anchor: string
49
+ hierarchy: (string | null)[]
50
+ /**
51
+ * At most 160 characters of content around the first match.
52
+ */
53
+ snippet: string
54
+ /**
55
+ * Character ranges within `snippet` that matched a query token.
56
+ */
57
+ highlights: [start: number, end: number][]
58
+ }
59
+
60
+ export interface SearchResponse {
61
+ query: string
62
+ /**
63
+ * The text actually searched when a token was corrected against the vocabulary.
64
+ */
65
+ corrected?: string
66
+ hits: SearchHit[]
67
+ }
package/lib/search.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file The docs search modal and its wire types.
6
+ */
7
+
8
+ export { hitHref, SearchModal } from "./search/SearchModal.tsx"
9
+ export type { SearchModalProps } from "./search/SearchModal.tsx"
10
+ export { HIERARCHY_DEPTH } from "./search/types.ts"
11
+ export type { SearchHit, SearchRecord, SearchResponse } from "./search/types.ts"
@@ -3,10 +3,19 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `About` — the collapsible explainer for the browser geocoder: the neural model, the confidence colours, the
7
- * gazetteer resolver, the byte-range database and the FST prior. Prose about the product a host places above or
8
- * beside the geocoder; it renders nothing live.
6
+ * `About` — the explainer for the browser geocoder: the neural model, the confidence colors, the gazetteer resolver, the byte-range database and the FST prior. It renders no live content.
9
7
  */
10
8
  import type { ReactNode } from "react";
11
- export declare function About(): ReactNode;
9
+ export interface AboutProps {
10
+ /**
11
+ * Wraps the prose in its own `<details>` disclosure.
12
+ *
13
+ * Set false where the host is already the disclosure, because a summary repeating
14
+ * an About button's words is one control too many.
15
+ *
16
+ * @default true
17
+ */
18
+ collapsible?: boolean;
19
+ }
20
+ export declare function About({ collapsible }?: AboutProps): ReactNode;
12
21
  //# sourceMappingURL=About.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"About.d.ts","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,wBAAgB,KAAK,IAAI,SAAS,CA4EjC"}
1
+ {"version":3,"file":"About.d.ts","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,UAAU;IAC1B;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,KAAK,CAAC,EAAE,WAAkB,EAAE,GAAE,UAAe,GAAG,SAAS,CAkFxE"}
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function About() {
3
- return (_jsxs("details", { className: "mw-about", children: [_jsxs("summary", { className: "mw-about__summary", children: [_jsx("span", { className: "mw-about__icon", children: "\u2139\uFE0F" }), "About this geocoder"] }), _jsxs("div", { className: "mw-about__body", children: [_jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Neural model" }), _jsxs("p", { children: ["A ~29M-parameter BERT-style encoder classifies each token into one of 33 BIO labels covering 16 address components (street, house number, unit, locality, region, postcode, venue, country, intersection, etc.). It runs entirely in your browser via ", _jsx("strong", { children: "onnxruntime-web" }), " (WebGPU with WASM SIMD fallback). The model ships as a 37.6 MB int8-quantized ONNX bundle; fp32 weights are also available for higher accuracy."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Confidence colors" }), _jsx("p", { children: "Every classified span carries a confidence score between 0 and 1. The colors in the results table give you a quick visual read:" }), _jsxs("ul", { className: "mw-about__legend", children: [_jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--high" }), _jsx("strong", { children: "Green" }), " (\u2265 0.8) \u2014 high confidence. The model is nearly certain."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--mid" }), _jsx("strong", { children: "Amber" }), " (0.5\u20130.8) \u2014 moderate confidence. Worth verifying."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--low" }), _jsx("strong", { children: "Red" }), " (< 0.5) \u2014 low confidence. The model is unsure \u2014 likely wrong."] })] }), _jsxs("p", { children: ["These are raw model scores. For calibrated probabilities, the library ships a per-locale isotonic calibrator (", _jsx("code", { children: "calibration.json" }), ") that maps the raw scores to well-calibrated probability estimates."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "WOF resolver" }), _jsxs("p", { children: ["After classification, the resolver queries a ", _jsx("strong", { children: "Who's On First" }), " (WOF) SQLite gazetteer to match the parsed components \u2014 locality, region, and postcode \u2014 against real-world places with coordinates. The cascade tries postcode first (most precise), then locality, then the raw input text. The result is a sorted list of candidate places with WOF IDs, placetypes, lat/lon, and bounding boxes."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Byte-range gazetteer" }), _jsxs("p", { children: ["The gazetteer is a multi-gigabyte SQLite file served from a public bucket. It loads via", " ", _jsx("strong", { children: "sql.js-httpvfs" }), ", which range-requests only the pages a lookup touches, so your browser fetches a few hundred kilobytes per session instead of the whole file."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "FST prior" }), _jsxs("p", { children: ["A ", _jsx("strong", { children: "finite-state transducer" }), " (FST) built from ~94K US place names runs before the neural classifier. It pre-annotates known locality, region, and postcode spans in the input \u2014 places like \"Springfield\" or \"Cook County\" that the FST can match exactly. These \"FST prior\" hits feed the classifier as strong hints, improving locality and region recall on addresses where the neural model alone might miss them. The FST is active whenever the selected release includes it."] })] })] })] }));
2
+ export function About({ collapsible = true } = {}) {
3
+ const body = (_jsxs("div", { className: "mw-about__body", children: [_jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Neural model" }), _jsxs("p", { children: ["A ~29M-parameter BERT-style encoder classifies each token into one of 33 BIO labels covering 16 address components (street, house number, unit, locality, region, postcode, venue, country, intersection, etc.). It runs entirely in your browser via ", _jsx("strong", { children: "onnxruntime-web" }), " (WebGPU with WASM SIMD fallback). The model ships as a 37.6 MB int8-quantized ONNX bundle; fp32 weights are also available for higher accuracy."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Confidence colors" }), _jsx("p", { children: "Every classified span carries a confidence score between 0 and 1. The colors in the results table give you a quick visual read:" }), _jsxs("ul", { className: "mw-about__legend", children: [_jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--high" }), _jsx("strong", { children: "Green" }), " (\u2265 0.8) \u2014 high confidence. The model is nearly certain."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--mid" }), _jsx("strong", { children: "Amber" }), " (0.5\u20130.8) \u2014 moderate confidence. Worth verifying."] }), _jsxs("li", { className: "mw-about__item", children: [_jsx("span", { className: "mw-about__swatch mw-about__swatch--low" }), _jsx("strong", { children: "Red" }), " (< 0.5) \u2014 low confidence. The model is unsure \u2014 likely wrong."] })] }), _jsxs("p", { children: ["These are raw model scores. For calibrated probabilities, the library ships a per-locale isotonic calibrator (", _jsx("code", { children: "calibration.json" }), ") that maps the raw scores to well-calibrated probability estimates."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "WOF resolver" }), _jsxs("p", { children: ["After classification, the resolver queries a ", _jsx("strong", { children: "Who's On First" }), " (WOF) SQLite gazetteer to match the parsed components \u2014 locality, region, and postcode \u2014 against real-world places with coordinates. The cascade tries postcode first (most precise), then locality, then the raw input text. The result is a sorted list of candidate places with WOF IDs, placetypes, lat/lon, and bounding boxes."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "Byte-range gazetteer" }), _jsxs("p", { children: ["The gazetteer is a multi-gigabyte SQLite file served from a public bucket. It loads via", " ", _jsx("strong", { children: "sqlite-wasm" }), " over a read-only HTTP file system. It range-requests only the pages a lookup touches. Therefore, your browser fetches a few hundred kilobytes per session instead of the whole file."] })] }), _jsxs("section", { className: "mw-about__section", children: [_jsx("h3", { className: "mw-about__heading", children: "FST prior" }), _jsxs("p", { children: ["A ", _jsx("strong", { children: "finite-state transducer" }), " (FST) built from ~94K US place names runs before the neural classifier. It pre-annotates known locality, region, and postcode spans in the input \u2014 places like \"Springfield\" or \"Cook County\" that the FST can match exactly. These \"FST prior\" hits feed the classifier as strong hints, improving locality and region recall on addresses where the neural model alone might miss them. The FST is active whenever the selected release includes it."] })] })] }));
4
+ if (!collapsible)
5
+ return body;
6
+ return (_jsxs("details", { className: "mw-about", children: [_jsxs("summary", { className: "mw-about__summary", children: [_jsx("span", { className: "mw-about__icon", children: "\u2139\uFE0F" }), "About this geocoder"] }), body] }));
4
7
  }
5
8
  //# sourceMappingURL=About.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"About.js","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":";AAYA,MAAM,UAAU,KAAK;IACpB,OAAO,CACN,mBAAS,SAAS,EAAC,UAAU,aAC5B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,eAAM,SAAS,EAAC,gBAAgB,6BAAU,2BAEjC,EACV,eAAK,SAAS,EAAC,gBAAgB,aAC9B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,kRAGmC,+CAAgC,wJAE/D,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,kCAAuB,EACxD,0JAGI,EACJ,cAAI,SAAS,EAAC,kBAAkB,aAC/B,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,yCAAyC,GAAG,EAC5D,qCAAsB,0EAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,qCAAsB,oEAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,mCAAoB,gFAChB,IACD,EACL,0IAEE,8CAA6B,4EAC3B,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,yEAC8C,8CAAoC,uVAI9E,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,qCAA0B,EAC3D,mHACyF,GAAG,EAC3F,8CAA+B,sJAE5B,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,0BAAe,EAChD,8BACG,uDAAwC,0cAKvC,IACK,IACL,IACG,CACV,CAAA;AACF,CAAC"}
1
+ {"version":3,"file":"About.js","sourceRoot":"","sources":["../../lib/common/About.tsx"],"names":[],"mappings":";AAsBA,MAAM,UAAU,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,EAAE,GAAe,EAAE;IAC5D,MAAM,IAAI,GAAG,CACZ,eAAK,SAAS,EAAC,gBAAgB,aAC9B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,kRAGmC,+CAAgC,wJAE/D,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,kCAAuB,EACxD,0JAGI,EACJ,cAAI,SAAS,EAAC,kBAAkB,aAC/B,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,yCAAyC,GAAG,EAC5D,qCAAsB,0EAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,qCAAsB,oEAClB,EACL,cAAI,SAAS,EAAC,gBAAgB,aAC7B,eAAM,SAAS,EAAC,wCAAwC,GAAG,EAC3D,mCAAoB,gFAChB,IACD,EACL,0IAEC,8CAA6B,4EAC1B,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,6BAAkB,EACnD,yEAC8C,8CAAoC,uVAI9E,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,qCAA0B,EAC3D,mHACyF,GAAG,EAC3F,2CAA4B,6LAEzB,IACK,EAEV,mBAAS,SAAS,EAAC,mBAAmB,aACrC,aAAI,SAAS,EAAC,mBAAmB,0BAAe,EAChD,8BACG,uDAAwC,0cAKvC,IACK,IACL,CACN,CAAA;IAED,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IAE7B,OAAO,CACN,mBAAS,SAAS,EAAC,UAAU,aAC5B,mBAAS,SAAS,EAAC,mBAAmB,aACrC,eAAM,SAAS,EAAC,gBAAgB,6BAAU,2BAEjC,EACT,IAAI,IACI,CACV,CAAA;AACF,CAAC"}
@@ -3,21 +3,12 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `ClientOnly` — a portable SSR boundary. The docs explorers wrapped Docusaurus's `<BrowserOnly>`;
7
- * this is the framework-agnostic equivalent so the components stay usable in any React app (Next,
8
- * Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
9
- * `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
10
- *
11
- * The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
12
- * server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
13
- * earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
14
- * render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
15
- * mechanism, not an accident, and the store form gets the same second paint without a set-state.
6
+ * `ClientOnly` — a portable SSR boundary that renders `fallback` on the server and first client paint. It swaps to `children()` after mounting, keeping timers, clipboard and dynamic imports off the server render.
16
7
  */
17
8
  import { type ReactNode } from "react";
18
9
  export interface ClientOnlyProps {
19
10
  /**
20
- * Rendered once mounted in the browser. A thunk so its (browser-only) imports never run on the server.
11
+ * Rendered once mounted in the browser, as a thunk so its browser-only imports never run on the server.
21
12
  */
22
13
  children: () => ReactNode;
23
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAA;AAS5D,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAe,EAAE,EAAE,eAAe,GAAG,SAAS,CAQpF"}
1
+ {"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAA;AAM5D,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAe,EAAE,EAAE,eAAe,GAAG,SAAS,CAQpF"}
@@ -3,21 +3,9 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `ClientOnly` — a portable SSR boundary. The docs explorers wrapped Docusaurus's `<BrowserOnly>`;
7
- * this is the framework-agnostic equivalent so the components stay usable in any React app (Next,
8
- * Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
9
- * `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
10
- *
11
- * The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
12
- * server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
13
- * earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
14
- * render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
15
- * mechanism, not an accident, and the store form gets the same second paint without a set-state.
6
+ * `ClientOnly` — a portable SSR boundary that renders `fallback` on the server and first client paint. It swaps to `children()` after mounting, keeping timers, clipboard and dynamic imports off the server render.
16
7
  */
17
8
  import { useSyncExternalStore } from "react";
18
- /**
19
- * The boundary never changes after mount, so nothing ever notifies.
20
- */
21
9
  function subscribeNever() {
22
10
  return () => { };
23
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAkB,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5D;;GAEG;AACH,SAAS,cAAc;IACtB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;AAChB,CAAC;AAaD,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,EAAmB;IACxE,MAAM,OAAO,GAAG,oBAAoB,CACnC,cAAc,EACd,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACX,CAAA;IAED,OAAO,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvC,CAAC"}
1
+ {"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAkB,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5D,SAAS,cAAc;IACtB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;AAChB,CAAC;AAaD,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,EAAmB;IACxE,MAAM,OAAO,GAAG,oBAAoB,CACnC,cAAc,EACd,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACX,CAAA;IAED,OAAO,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvC,CAAC"}
@@ -9,7 +9,9 @@
9
9
  import type { ReactNode } from "react";
10
10
  export interface CopyButtonProps {
11
11
  /**
12
- * The text to copy. If a thunk, it's evaluated at click time (so callers can copy the latest value).
12
+ * The text to copy.
13
+ *
14
+ * If a thunk, it's evaluated at click time (so callers can copy the latest value).
13
15
  */
14
16
  value: string | (() => string);
15
17
  /**