@mailwoman/react 9.3.0 → 10.0.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 (246) hide show
  1. package/fonts.css +59 -0
  2. package/lib/common/About.tsx +105 -0
  3. package/lib/common/KindBadge.tsx +2 -7
  4. package/lib/common/confidence-tiers.ts +50 -0
  5. package/lib/common/index.ts +22 -0
  6. package/lib/common/text-tokens.ts +88 -0
  7. package/lib/index.ts +10 -103
  8. package/lib/map/BackendControl.tsx +7 -3
  9. package/lib/map/CompareToggle.tsx +5 -4
  10. package/lib/map/{GeocoderDemo.tsx → Geocoder.tsx} +81 -61
  11. package/lib/map/GeocoderControls.tsx +352 -0
  12. package/lib/map/{DemoMap.tsx → MapCanvas.tsx} +15 -15
  13. package/lib/map/MapChipRow.tsx +77 -0
  14. package/lib/map/MapCompass.tsx +77 -0
  15. package/lib/map/MapControlStack.tsx +98 -0
  16. package/lib/map/MapFooter.tsx +115 -0
  17. package/lib/map/MapProgressBar.tsx +58 -0
  18. package/lib/map/MapSearchBar.tsx +68 -0
  19. package/lib/map/MapSheet.tsx +62 -0
  20. package/lib/map/PlaceMarker.tsx +1 -1
  21. package/lib/map/ResolvedPlaceLayers.tsx +3 -3
  22. package/lib/map/ResultCamera.tsx +1 -1
  23. package/lib/map/ResultPanel.tsx +2 -2
  24. package/lib/map/VersionPicker.tsx +3 -3
  25. package/lib/map/fake-runtime.ts +122 -0
  26. package/lib/map/index.ts +35 -16
  27. package/lib/map/map-debug.ts +51 -0
  28. package/lib/map/place-render.ts +3 -2
  29. package/lib/map/types.ts +58 -42
  30. package/lib/map/{useDemoGeocode.ts → useGeocode.ts} +8 -8
  31. package/lib/map/useMapBearing.ts +56 -0
  32. package/lib/map/useMapLabelPick.ts +123 -0
  33. package/lib/pipeline/CandidatePicker.tsx +1 -1
  34. package/lib/pipeline/ComponentTable.tsx +1 -2
  35. package/lib/pipeline/ConfidenceCell.tsx +7 -19
  36. package/lib/pipeline/FailureDiagnostic.tsx +56 -0
  37. package/lib/pipeline/ResolvedPlace.tsx +17 -4
  38. package/lib/pipeline/SpanHighlight.tsx +97 -0
  39. package/lib/pipeline/TimingPanel.tsx +65 -0
  40. package/lib/pipeline/TreeView.tsx +55 -0
  41. package/lib/pipeline/copy.ts +1 -1
  42. package/lib/pipeline/index.ts +36 -0
  43. package/lib/pipeline/types.ts +2 -75
  44. package/lib/pipeline/useParsePipeline.ts +31 -19
  45. package/lib/poi/index.ts +39 -0
  46. package/lib/poi/types.ts +2 -1
  47. package/lib/runtime/index.ts +16 -0
  48. package/lib/runtime/{useDemoRuntime.ts → useReleaseRuntime.ts} +22 -24
  49. package/out/common/About.d.ts +21 -0
  50. package/out/common/About.d.ts.map +1 -0
  51. package/out/common/About.js +8 -0
  52. package/out/common/About.js.map +1 -0
  53. package/out/common/KindBadge.d.ts +2 -9
  54. package/out/common/KindBadge.d.ts.map +1 -1
  55. package/out/common/KindBadge.js.map +1 -1
  56. package/out/common/confidence-tiers.d.ts +36 -0
  57. package/out/common/confidence-tiers.d.ts.map +1 -0
  58. package/out/common/confidence-tiers.js +42 -0
  59. package/out/common/confidence-tiers.js.map +1 -0
  60. package/out/common/index.d.ts +22 -0
  61. package/out/common/index.d.ts.map +1 -0
  62. package/out/common/index.js +15 -0
  63. package/out/common/index.js.map +1 -0
  64. package/out/common/text-tokens.d.ts +46 -0
  65. package/out/common/text-tokens.d.ts.map +1 -0
  66. package/out/common/text-tokens.js +52 -0
  67. package/out/common/text-tokens.js.map +1 -0
  68. package/out/index.d.ts +5 -51
  69. package/out/index.d.ts.map +1 -1
  70. package/out/index.js +4 -30
  71. package/out/index.js.map +1 -1
  72. package/out/map/BackendControl.d.ts.map +1 -1
  73. package/out/map/BackendControl.js +1 -1
  74. package/out/map/BackendControl.js.map +1 -1
  75. package/out/map/CompareToggle.d.ts +2 -2
  76. package/out/map/CompareToggle.d.ts.map +1 -1
  77. package/out/map/CompareToggle.js +1 -1
  78. package/out/map/CompareToggle.js.map +1 -1
  79. package/out/map/Geocoder.d.ts +67 -0
  80. package/out/map/Geocoder.d.ts.map +1 -0
  81. package/out/map/Geocoder.js +98 -0
  82. package/out/map/Geocoder.js.map +1 -0
  83. package/out/map/GeocoderControls.d.ts +81 -0
  84. package/out/map/GeocoderControls.d.ts.map +1 -0
  85. package/out/map/GeocoderControls.js +79 -0
  86. package/out/map/GeocoderControls.js.map +1 -0
  87. package/out/map/{DemoMap.d.ts → MapCanvas.d.ts} +15 -15
  88. package/out/map/MapCanvas.d.ts.map +1 -0
  89. package/out/map/{DemoMap.js → MapCanvas.js} +2 -2
  90. package/out/map/MapCanvas.js.map +1 -0
  91. package/out/map/MapChipRow.d.ts +47 -0
  92. package/out/map/MapChipRow.d.ts.map +1 -0
  93. package/out/map/MapChipRow.js +12 -0
  94. package/out/map/MapChipRow.js.map +1 -0
  95. package/out/map/MapCompass.d.ts +42 -0
  96. package/out/map/MapCompass.d.ts.map +1 -0
  97. package/out/map/MapCompass.js +11 -0
  98. package/out/map/MapCompass.js.map +1 -0
  99. package/out/map/MapControlStack.d.ts +57 -0
  100. package/out/map/MapControlStack.d.ts.map +1 -0
  101. package/out/map/MapControlStack.js +15 -0
  102. package/out/map/MapControlStack.js.map +1 -0
  103. package/out/map/MapFooter.d.ts +40 -0
  104. package/out/map/MapFooter.d.ts.map +1 -0
  105. package/out/map/MapFooter.js +51 -0
  106. package/out/map/MapFooter.js.map +1 -0
  107. package/out/map/MapProgressBar.d.ts +34 -0
  108. package/out/map/MapProgressBar.d.ts.map +1 -0
  109. package/out/map/MapProgressBar.js +8 -0
  110. package/out/map/MapProgressBar.js.map +1 -0
  111. package/out/map/MapSearchBar.d.ts +49 -0
  112. package/out/map/MapSearchBar.d.ts.map +1 -0
  113. package/out/map/MapSearchBar.js +6 -0
  114. package/out/map/MapSearchBar.js.map +1 -0
  115. package/out/map/MapSheet.d.ts +32 -0
  116. package/out/map/MapSheet.d.ts.map +1 -0
  117. package/out/map/MapSheet.js +33 -0
  118. package/out/map/MapSheet.js.map +1 -0
  119. package/out/map/OverlayLayers.js +1 -1
  120. package/out/map/PlaceMarker.d.ts +1 -1
  121. package/out/map/ResolvedPlaceLayers.d.ts +3 -3
  122. package/out/map/ResolvedPlaceLayers.js +1 -1
  123. package/out/map/ResultCamera.d.ts +1 -1
  124. package/out/map/ResultCamera.js +1 -1
  125. package/out/map/ResultPanel.d.ts +2 -2
  126. package/out/map/ResultPanel.d.ts.map +1 -1
  127. package/out/map/ResultPanel.js +1 -1
  128. package/out/map/ResultPanel.js.map +1 -1
  129. package/out/map/VersionPicker.d.ts +3 -3
  130. package/out/map/VersionPicker.d.ts.map +1 -1
  131. package/out/map/fake-runtime.d.ts +38 -0
  132. package/out/map/fake-runtime.d.ts.map +1 -0
  133. package/out/map/fake-runtime.js +107 -0
  134. package/out/map/fake-runtime.js.map +1 -0
  135. package/out/map/index.d.ts +27 -10
  136. package/out/map/index.d.ts.map +1 -1
  137. package/out/map/index.js +16 -6
  138. package/out/map/index.js.map +1 -1
  139. package/out/map/map-debug.d.ts +31 -0
  140. package/out/map/map-debug.d.ts.map +1 -0
  141. package/out/map/map-debug.js +31 -0
  142. package/out/map/map-debug.js.map +1 -0
  143. package/out/map/place-render.d.ts +2 -2
  144. package/out/map/place-render.d.ts.map +1 -1
  145. package/out/map/place-render.js +1 -1
  146. package/out/map/place-render.js.map +1 -1
  147. package/out/map/types.d.ts +60 -42
  148. package/out/map/types.d.ts.map +1 -1
  149. package/out/map/types.js +5 -5
  150. package/out/map/{useDemoGeocode.d.ts → useGeocode.d.ts} +9 -9
  151. package/out/map/useGeocode.d.ts.map +1 -0
  152. package/out/map/{useDemoGeocode.js → useGeocode.js} +4 -4
  153. package/out/map/useGeocode.js.map +1 -0
  154. package/out/map/useMapBearing.d.ts +28 -0
  155. package/out/map/useMapBearing.d.ts.map +1 -0
  156. package/out/map/useMapBearing.js +35 -0
  157. package/out/map/useMapBearing.js.map +1 -0
  158. package/out/map/useMapLabelPick.d.ts +23 -0
  159. package/out/map/useMapLabelPick.d.ts.map +1 -0
  160. package/out/map/useMapLabelPick.js +104 -0
  161. package/out/map/useMapLabelPick.js.map +1 -0
  162. package/out/pipeline/CandidatePicker.d.ts +1 -1
  163. package/out/pipeline/CandidatePicker.d.ts.map +1 -1
  164. package/out/pipeline/CandidatePicker.js +1 -1
  165. package/out/pipeline/CandidatePicker.js.map +1 -1
  166. package/out/pipeline/ComponentTable.d.ts +1 -1
  167. package/out/pipeline/ComponentTable.d.ts.map +1 -1
  168. package/out/pipeline/ComponentTable.js.map +1 -1
  169. package/out/pipeline/ConfidenceCell.d.ts +2 -2
  170. package/out/pipeline/ConfidenceCell.d.ts.map +1 -1
  171. package/out/pipeline/ConfidenceCell.js +4 -16
  172. package/out/pipeline/ConfidenceCell.js.map +1 -1
  173. package/out/pipeline/FailureDiagnostic.d.ts +15 -0
  174. package/out/pipeline/FailureDiagnostic.d.ts.map +1 -0
  175. package/out/pipeline/FailureDiagnostic.js +21 -0
  176. package/out/pipeline/FailureDiagnostic.js.map +1 -0
  177. package/out/pipeline/PipelineExplorer.js +1 -1
  178. package/out/pipeline/QueryForm.js +1 -1
  179. package/out/pipeline/ResolvedPlace.d.ts +1 -1
  180. package/out/pipeline/ResolvedPlace.d.ts.map +1 -1
  181. package/out/pipeline/ResolvedPlace.js +4 -10
  182. package/out/pipeline/ResolvedPlace.js.map +1 -1
  183. package/out/pipeline/SpanHighlight.d.ts +23 -0
  184. package/out/pipeline/SpanHighlight.d.ts.map +1 -0
  185. package/out/pipeline/SpanHighlight.js +31 -0
  186. package/out/pipeline/SpanHighlight.js.map +1 -0
  187. package/out/pipeline/TimingPanel.d.ts +16 -0
  188. package/out/pipeline/TimingPanel.d.ts.map +1 -0
  189. package/out/pipeline/TimingPanel.js +20 -0
  190. package/out/pipeline/TimingPanel.js.map +1 -0
  191. package/out/pipeline/TreeView.d.ts +17 -0
  192. package/out/pipeline/TreeView.d.ts.map +1 -0
  193. package/out/pipeline/TreeView.js +15 -0
  194. package/out/pipeline/TreeView.js.map +1 -0
  195. package/out/pipeline/copy.d.ts +1 -1
  196. package/out/pipeline/copy.d.ts.map +1 -1
  197. package/out/pipeline/index.d.ts +33 -0
  198. package/out/pipeline/index.d.ts.map +1 -0
  199. package/out/pipeline/index.js +21 -0
  200. package/out/pipeline/index.js.map +1 -0
  201. package/out/pipeline/types.d.ts +2 -68
  202. package/out/pipeline/types.d.ts.map +1 -1
  203. package/out/pipeline/types.js +1 -1
  204. package/out/pipeline/useParsePipeline.d.ts +7 -3
  205. package/out/pipeline/useParsePipeline.d.ts.map +1 -1
  206. package/out/pipeline/useParsePipeline.js +6 -2
  207. package/out/pipeline/useParsePipeline.js.map +1 -1
  208. package/out/poi/LiveResultsBlock.js +1 -1
  209. package/out/poi/POIExplorer.js +1 -1
  210. package/out/poi/SubjectPanel.js +1 -1
  211. package/out/poi/index.d.ts +23 -0
  212. package/out/poi/index.d.ts.map +1 -0
  213. package/out/poi/index.js +15 -0
  214. package/out/poi/index.js.map +1 -0
  215. package/out/poi/runtime.d.ts.map +1 -1
  216. package/out/poi/types.d.ts +2 -1
  217. package/out/poi/types.d.ts.map +1 -1
  218. package/out/runtime/index.d.ts +9 -0
  219. package/out/runtime/index.d.ts.map +1 -0
  220. package/out/runtime/index.js +8 -0
  221. package/out/runtime/index.js.map +1 -0
  222. package/out/runtime/{useDemoRuntime.d.ts → useReleaseRuntime.d.ts} +18 -20
  223. package/out/runtime/useReleaseRuntime.d.ts.map +1 -0
  224. package/out/runtime/{useDemoRuntime.js → useReleaseRuntime.js} +4 -4
  225. package/out/runtime/useReleaseRuntime.js.map +1 -0
  226. package/package.json +222 -31
  227. package/styleframe.config.ts +329 -0
  228. package/styles.css +1995 -663
  229. package/tokens/index.css +116 -0
  230. package/tokens/tokens.json +528 -0
  231. package/tokens/tokens.resolver.json +185 -0
  232. package/lib/map/DemoControls.tsx +0 -194
  233. package/out/map/DemoControls.d.ts +0 -64
  234. package/out/map/DemoControls.d.ts.map +0 -1
  235. package/out/map/DemoControls.js +0 -30
  236. package/out/map/DemoControls.js.map +0 -1
  237. package/out/map/DemoMap.d.ts.map +0 -1
  238. package/out/map/DemoMap.js.map +0 -1
  239. package/out/map/GeocoderDemo.d.ts +0 -57
  240. package/out/map/GeocoderDemo.d.ts.map +0 -1
  241. package/out/map/GeocoderDemo.js +0 -98
  242. package/out/map/GeocoderDemo.js.map +0 -1
  243. package/out/map/useDemoGeocode.d.ts.map +0 -1
  244. package/out/map/useDemoGeocode.js.map +0 -1
  245. package/out/runtime/useDemoRuntime.d.ts.map +0 -1
  246. package/out/runtime/useDemoRuntime.js.map +0 -1
@@ -3,47 +3,47 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `<GeocoderDemo>` — the WHOLE geocoder demo, composed. It is the map analogue of `PipelineExplorer`
7
- * and takes the SAME DI shape: an injected {@link DemoRuntime} (the host owns ONNX / httpvfs / R2 /
8
- * the composed map style) plus a {@link DemoPanels} bag (the host's ModelVisualizer / VersionCompare /
6
+ * `<Geocoder>` — the WHOLE geocoder, composed. It is the map analogue of `PipelineExplorer`
7
+ * and takes the SAME DI shape: an injected {@link GeocoderRuntime} (the host owns ONNX / httpvfs / R2 /
8
+ * the composed map style) plus a {@link GeocoderPanels} bag (the host's ModelVisualizer / VersionCompare /
9
9
  * About / Permalink). Everything here is composition + a `ClientOnly` boundary:
10
10
  *
11
- * - the floating {@link DemoControls} panel (version / compare / backend / query+autocomplete / result),
12
- * - the declarative {@link DemoMap} with the phase-2 overlays ({@link OverlayLayers}) and the
11
+ * - the floating {@link GeocoderControls} panel (version / compare / backend / query+autocomplete / result),
12
+ * - the declarative {@link MapCanvas} with the phase-2 overlays ({@link OverlayLayers}) and the
13
13
  * resolved-place marker/outline/camera ({@link ResolvedPlaceLayers}) driven by the parse state,
14
- * - the hooks that wire them: {@link useDemoGeocode} (parse + viewport bias + map place),
14
+ * - the hooks that wire them: {@link useGeocode} (parse + viewport bias + map place),
15
15
  * {@link usePlaceAutocomplete}, {@link useCompareState}.
16
16
  *
17
- * Because it pulls {@link DemoMap} (→ `react-map-gl` → `maplibre-gl`, WebGL + DOM at import), it lives on
17
+ * Because it pulls {@link MapCanvas} (→ `react-map-gl` → `maplibre-gl`, WebGL + DOM at import), it lives on
18
18
  * the `@mailwoman/react/map` subpath ONLY — never the package root. The whole thing renders in Storybook
19
19
  * over a fake runtime (offline stub style + canned geocode) with no network, no ONNX, no gazetteer.
20
20
  */
21
21
 
22
- import { type ReactNode, useCallback, useEffect, useRef } from "react"
22
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"
23
23
  import type { MapRef } from "react-map-gl/maplibre"
24
24
 
25
- import type { DemoPanels, DemoRuntime, MapBias } from "#map/types"
25
+ import type { GeocoderPanels, GeocoderRuntime, MapBias } from "#map/types"
26
26
  import { useCompareState } from "#map/useCompareState"
27
- import { useDemoGeocode } from "#map/useDemoGeocode"
27
+ import { useGeocode } from "#map/useGeocode"
28
28
  import { useMapPlaceRender } from "#map/useMapPlaceRender"
29
29
  import { usePlaceAutocomplete } from "#map/usePlaceAutocomplete"
30
30
 
31
31
  import { ClientOnly } from "../common/ClientOnly.tsx"
32
32
  import type { Preset } from "../common/PresetChips.tsx"
33
- import { DemoControls } from "./DemoControls.tsx"
34
- import { DemoMap } from "./DemoMap.tsx"
33
+ import { GeocoderControls } from "./GeocoderControls.tsx"
34
+ import { MapCanvas } from "./MapCanvas.tsx"
35
35
  import { OverlayLayers } from "./OverlayLayers.tsx"
36
36
  import { ResolvedPlaceLayers } from "./ResolvedPlaceLayers.tsx"
37
37
 
38
- export interface GeocoderDemoProps {
38
+ export interface GeocoderProps {
39
39
  /**
40
- * The injected demo runtime (map style + overlays + parse + version/backend).
40
+ * The injected geocoder runtime (map style + overlays + parse + version/backend).
41
41
  */
42
- runtime: DemoRuntime
42
+ runtime: GeocoderRuntime
43
43
  /**
44
44
  * Host-injected panels (about, release blurb, compare, permalink, debug drawer, map controls, …).
45
45
  */
46
- panels?: DemoPanels
46
+ panels?: GeocoderPanels
47
47
  /**
48
48
  * Address to pre-fill.
49
49
  */
@@ -52,74 +52,87 @@ export interface GeocoderDemoProps {
52
52
  * Example chips. @default the empty set (host supplies its own).
53
53
  */
54
54
  presets?: ReadonlyArray<Preset>
55
+ /**
56
+ * Open the developer disclosure on mount — the model version, the backend readout and compare. Collapsed by default
57
+ * so the address field is the first thing a visitor meets. @default false
58
+ */
59
+ developer?: boolean
60
+ /**
61
+ * Fired with the query each time one is submitted. The host writes it into its own URL — this package never touches
62
+ * `location`, because which parameter carries a query is the app's decision.
63
+ */
64
+ onSubmitQuery?: (query: string) => void
55
65
  /**
56
66
  * Only hint the viewport bias once the visitor has zoomed past the global view — a whole-globe center is noise.
57
- * Matches the demo's `map.getZoom() >= 4` threshold. @default 4
67
+ * Matches the `map.getZoom() >= 4` threshold. @default 4
58
68
  */
59
69
  minBiasZoom?: number
60
70
  /**
61
71
  * Fly/fit the map to the resolved place on each result (via {@link ResolvedPlaceLayers}). @default true. Set false
62
- * for a host that drives the camera itself (a controlled `<DemoMap viewState>`), or to keep a headless test
72
+ * for a host that drives the camera itself (a controlled `<MapCanvas viewState>`), or to keep a headless test
63
73
  * deterministic — the marker + outline still render, only the animated camera move is skipped.
64
74
  */
65
75
  applyResultCamera?: boolean
66
76
  }
67
77
 
68
- interface GeocoderDemoInnerProps extends Required<
69
- Pick<GeocoderDemoProps, "runtime" | "defaultAddress" | "minBiasZoom" | "applyResultCamera">
78
+ interface GeocoderInnerProps extends Required<
79
+ Pick<GeocoderProps, "runtime" | "defaultAddress" | "minBiasZoom" | "applyResultCamera" | "developer">
70
80
  > {
71
- panels: DemoPanels
81
+ panels: GeocoderPanels
72
82
  presets: ReadonlyArray<Preset>
83
+ onSubmitQuery?: (query: string) => void
73
84
  }
74
85
 
75
- function GeocoderDemoInner({
86
+ function GeocoderInner({
76
87
  runtime,
77
88
  panels,
78
89
  defaultAddress,
79
90
  presets,
80
91
  minBiasZoom,
81
92
  applyResultCamera,
82
- }: GeocoderDemoInnerProps): ReactNode {
93
+ developer,
94
+ onSubmitQuery,
95
+ }: GeocoderInnerProps): ReactNode {
83
96
  const mapRef = useRef<MapRef>(null)
84
-
97
+ // The chrome sits OUTSIDE `<MapCanvas>`, so it cannot take the handle from `useMap()`. A ref alone does not
98
+ // re-render the compass or the layer control when the map arrives, so the same poll that publishes the test
99
+ // handle also puts it in state — one poll, two consumers.
100
+ const [map, setMap] = useState<ReturnType<MapRef["getMap"]> | null>(null)
101
+
102
+ // The map announces itself; nothing polls for it. `onLoad` carries the instance, so the compass and the layer
103
+ // control render on the frame the map is ready rather than up to an interval later.
104
+ //
85
105
  // TEST INJECTION POINT: the e2e viewport-bias suite drives the REAL map (pan + zoom past the bias threshold)
86
- // before submitting, and a browser test cannot reach a React ref — so the live map handle is
87
- // republished on `globalThis.__mailwomanDemoMap`, the same global the pre-port demo carried. The
88
- // ref fills only after react-map-gl instantiates the map, hence the short poll; cleared on
89
- // unmount so a torn-down demo never leaves a stale handle behind.
90
- useEffect(() => {
91
- const host = globalThis as { __mailwomanDemoMap?: ReturnType<MapRef["getMap"]> }
92
-
93
- const timer = setInterval(() => {
94
- const map = mapRef.current?.getMap()
95
-
96
- if (map) {
97
- host.__mailwomanDemoMap = map
98
- clearInterval(timer)
99
- }
100
- }, 250)
101
-
102
- return () => {
103
- clearInterval(timer)
104
- delete host.__mailwomanDemoMap
105
- }
106
+ // before submitting, and a browser test cannot reach a React ref — so the same handle is republished on
107
+ // `globalThis.__mailwomanMapCanvas`, and cleared on unmount so a torn-down geocoder leaves no stale handle.
108
+ const onMapLoad = useCallback((event: { target: ReturnType<MapRef["getMap"]> }) => {
109
+ ;(globalThis as { __mailwomanMapCanvas?: ReturnType<MapRef["getMap"]> }).__mailwomanMapCanvas = event.target
110
+ setMap(event.target)
106
111
  }, [])
107
112
 
113
+ useEffect(
114
+ () => () => {
115
+ setMap(null)
116
+ delete (globalThis as { __mailwomanMapCanvas?: ReturnType<MapRef["getMap"]> }).__mailwomanMapCanvas
117
+ },
118
+ []
119
+ )
120
+
108
121
  // Read the viewport bias at submit time — through the map handle, never a threaded state value, so granting/zooming
109
122
  // mid-session doesn't re-create the parse callback. Below the min-bias zoom, a whole-globe center is noise → null.
110
123
  const getBias = useCallback((): MapBias | null => {
111
- const map = mapRef.current?.getMap()
124
+ const live = mapRef.current?.getMap()
112
125
 
113
- if (!map) return null
114
- const zoom = map.getZoom()
126
+ if (!live) return null
127
+ const zoom = live.getZoom()
115
128
 
116
129
  if (zoom < minBiasZoom) return null
117
- const center = map.getCenter()
130
+ const center = live.getCenter()
118
131
 
119
132
  return { center: [center.lng, center.lat], zoom }
120
133
  }, [minBiasZoom])
121
134
 
122
- const geocode = useDemoGeocode({ runtime, defaultText: defaultAddress, getBias })
135
+ const geocode = useGeocode({ runtime, defaultText: defaultAddress, getBias })
123
136
  const compare = useCompareState()
124
137
 
125
138
  const autocomplete = usePlaceAutocomplete({
@@ -143,7 +156,7 @@ function GeocoderDemoInner({
143
156
  return (
144
157
  <div className="mw-geocoder-demo">
145
158
  <div className="mw-geocoder-demo__map">
146
- <DemoMap
159
+ <MapCanvas
147
160
  mapStyle={runtime.mapStyle}
148
161
  mapRef={mapRef}
149
162
  initialViewState={{
@@ -152,17 +165,17 @@ function GeocoderDemoInner({
152
165
  zoom: runtime.initialZoom ?? 3,
153
166
  }}
154
167
  style={{ width: "100%", height: "100%" }}
155
- // Match the live demo's chrome: one compact attribution pill (the map's own default is a wide, always-open
156
- // "MapLibre | © …" bar), no maplibre wordmark logo.
157
- mapProps={{ attributionControl: { compact: true }, maplibreLogo: false }}
168
+ // One compact attribution pill (the map's own default is a wide, always-open "MapLibre | © …" bar), no
169
+ // maplibre wordmark logo.
170
+ mapProps={{ attributionControl: { compact: true }, maplibreLogo: false, onLoad: onMapLoad }}
158
171
  >
159
172
  <OverlayLayers overlays={runtime.overlays} />
160
173
  <ResolvedPlaceLayers spec={spec} applyCamera={applyResultCamera} />
161
174
  {panels.mapControls}
162
- </DemoMap>
175
+ </MapCanvas>
163
176
  </div>
164
177
 
165
- <DemoControls
178
+ <GeocoderControls
166
179
  runtime={runtime}
167
180
  geocode={geocode}
168
181
  autocomplete={autocomplete}
@@ -170,8 +183,11 @@ function GeocoderDemoInner({
170
183
  panels={panels}
171
184
  presets={presets}
172
185
  placeholder={defaultAddress}
186
+ map={map}
187
+ onSubmitQuery={onSubmitQuery}
173
188
  onSelectVersion={onSelectVersion}
174
189
  onForceWASMChange={onForceWASMChange}
190
+ developer={developer}
175
191
  />
176
192
 
177
193
  {panels.debugDrawer ? panels.debugDrawer({ result: geocode.result }) : null}
@@ -182,36 +198,40 @@ function GeocoderDemoInner({
182
198
  /**
183
199
  * Stable empty defaults — a fresh `{}`/`[]` per render would churn every downstream memo dep.
184
200
  */
185
- const NO_PANELS: DemoPanels = {}
201
+ const NO_PANELS: GeocoderPanels = {}
186
202
  const NO_PRESETS: ReadonlyArray<Preset> = []
187
203
 
188
204
  /**
189
- * The composed geocoder demo, behind a `ClientOnly` SSR boundary (the map is intrinsically a client component).
205
+ * The composed geocoder, behind a `ClientOnly` SSR boundary (the map is intrinsically a client component).
190
206
  */
191
- export function GeocoderDemo({
207
+ export function Geocoder({
192
208
  runtime,
193
209
  panels = NO_PANELS,
194
210
  defaultAddress = "",
195
211
  presets = NO_PRESETS,
196
212
  minBiasZoom = 4,
197
213
  applyResultCamera = true,
198
- }: GeocoderDemoProps): ReactNode {
214
+ developer = false,
215
+ onSubmitQuery,
216
+ }: GeocoderProps): ReactNode {
199
217
  return (
200
218
  <ClientOnly
201
219
  fallback={
202
220
  <div className="mw-geocoder-demo">
203
- <p>Loading demo…</p>
221
+ <p>Loading the geocoder…</p>
204
222
  </div>
205
223
  }
206
224
  >
207
225
  {() => (
208
- <GeocoderDemoInner
226
+ <GeocoderInner
209
227
  runtime={runtime}
210
228
  panels={panels}
211
229
  defaultAddress={defaultAddress}
212
230
  presets={presets}
213
231
  minBiasZoom={minBiasZoom}
214
232
  applyResultCamera={applyResultCamera}
233
+ developer={developer}
234
+ onSubmitQuery={onSubmitQuery}
215
235
  />
216
236
  )}
217
237
  </ClientOnly>
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The geocoder's chrome over the map: a search pill on top, example chips on one line beneath it, a control column
7
+ * down the right edge, and a result sheet that rises from the bottom when there is something to show.
8
+ *
9
+ * It replaces a 400px full-height panel that carried every control at once. The layout is the one the reference
10
+ * map apps use, and the reason is the map: chrome that floats over it leaves the map the page, where a slab makes
11
+ * the map the leftover third.
12
+ *
13
+ * NODE-SAFE: pure React + the shared units, no maplibre.
14
+ */
15
+
16
+ import { type ReactNode, useCallback, useState } from "react"
17
+ import type { MapInstance } from "react-map-gl/maplibre"
18
+
19
+ import type { GeocoderPanels, GeocoderRuntime } from "#map/types"
20
+ import type { UseCompareState } from "#map/useCompareState"
21
+ import type { UseGeocode } from "#map/useGeocode"
22
+ import { useMapBearing } from "#map/useMapBearing"
23
+ import { useMapLabelPick } from "#map/useMapLabelPick"
24
+ import type { UsePlaceAutocomplete } from "#map/usePlaceAutocomplete"
25
+
26
+ import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
27
+ import type { Preset } from "../common/PresetChips.tsx"
28
+ import { BackendControl } from "./BackendControl.tsx"
29
+ import { CompareToggle } from "./CompareToggle.tsx"
30
+ import { MapChipRow } from "./MapChipRow.tsx"
31
+ import { MapCompass } from "./MapCompass.tsx"
32
+ import { MapControlButton, MapControlGroup, MapControlStack } from "./MapControlStack.tsx"
33
+ import { MapProgressBar } from "./MapProgressBar.tsx"
34
+ import { MapSearchBar } from "./MapSearchBar.tsx"
35
+ import { MapSheet } from "./MapSheet.tsx"
36
+ import { PlaceAutocomplete } from "./PlaceAutocomplete.tsx"
37
+ import { ResultPanel } from "./ResultPanel.tsx"
38
+ import { VersionPicker } from "./VersionPicker.tsx"
39
+
40
+ export interface GeocoderControlsProps {
41
+ /**
42
+ * The injected geocoder runtime.
43
+ */
44
+ runtime: GeocoderRuntime
45
+ /**
46
+ * The parse+resolve state machine.
47
+ */
48
+ geocode: UseGeocode
49
+ /**
50
+ * The place-autocomplete combobox state.
51
+ */
52
+ autocomplete: UsePlaceAutocomplete
53
+ /**
54
+ * The compare-mode state.
55
+ */
56
+ compare: UseCompareState
57
+ /**
58
+ * Host-injected panels (about, release blurb, compare, permalink, extras, failure).
59
+ */
60
+ panels: GeocoderPanels
61
+ /**
62
+ * Example chips.
63
+ */
64
+ presets: ReadonlyArray<Preset>
65
+ /**
66
+ * Input placeholder.
67
+ */
68
+ placeholder: string
69
+ /**
70
+ * The live map, for the controls that read it: the compass takes its bearing, the host's layer control reads its
71
+ * style. `null` until react-map-gl instantiates the map.
72
+ */
73
+ map?: MapInstance | null
74
+ /**
75
+ * Fired with the query whenever one is submitted, before the parse starts. The host writes it into the URL; this
76
+ * package never touches `location`, because the address bar is the app's state, not a control's.
77
+ */
78
+ onSubmitQuery?: (query: string) => void
79
+ /**
80
+ * Select a model version (the composed geocoder also clears a now-colliding compare selection).
81
+ */
82
+ onSelectVersion: (version: string) => void
83
+ /**
84
+ * Toggle the forced WASM backend.
85
+ */
86
+ onForceWASMChange: (forceWASM: boolean) => void
87
+ /**
88
+ * Open the developer panel on mount. The model version, the backend readout and compare live inside it: they read on
89
+ * the model rather than on an address, and above the query field they were the first thing every visitor met.
90
+ *
91
+ * @default false
92
+ */
93
+ developer?: boolean
94
+ }
95
+
96
+ /**
97
+ * Which side sheet is open. At most one, because they share an edge and a phone gives each the whole panel.
98
+ */
99
+ type SheetName = "about" | "layers" | "developer" | null
100
+
101
+ /**
102
+ * The chrome. Everything positioned here floats over the map; nothing occupies a column of the page.
103
+ */
104
+ export function GeocoderControls({
105
+ runtime,
106
+ geocode,
107
+ autocomplete,
108
+ compare,
109
+ panels,
110
+ presets,
111
+ placeholder,
112
+ map = null,
113
+ onSubmitQuery,
114
+ onSelectVersion,
115
+ onForceWASMChange,
116
+ developer = false,
117
+ }: GeocoderControlsProps): ReactNode {
118
+ const versions = runtime.availableVersions ?? []
119
+ const { busy, result, selectedCandidate } = geocode
120
+ const loading = runtime.loading
121
+ const errorMessage = geocode.parseError ?? runtime.errorMessage ?? null
122
+
123
+ // One sheet at a time. Two open at once stack on the same edge, and on a phone each is the full panel.
124
+ const [openSheet, setOpenSheet] = useState<SheetName>(developer ? "developer" : null)
125
+ const { bearing, resetNorth } = useMapBearing(map)
126
+
127
+ const toggleSheet = (name: Exclude<SheetName, null>) => setOpenSheet((current) => (current === name ? null : name))
128
+ const closeSheet = useCallback(() => setOpenSheet(null), [])
129
+
130
+ // Every path that starts a query goes through here, so the URL is written in exactly one place — the event that
131
+ // caused it, rather than an effect watching the result after the fact.
132
+ const runQuery = useCallback(
133
+ (query: string) => {
134
+ onSubmitQuery?.(query)
135
+ void geocode.submit(query)
136
+ },
137
+ [onSubmitQuery, geocode]
138
+ )
139
+
140
+ // A label on the map is a search a visitor already typed by pointing at it.
141
+ const pickLabel = useCallback(
142
+ (name: string) => {
143
+ geocode.setText(name)
144
+ geocode.reset()
145
+ runQuery(name)
146
+ },
147
+ [geocode, runQuery]
148
+ )
149
+
150
+ useMapLabelPick(map, pickLabel)
151
+
152
+ const chips = presets.map((preset) => ({ label: preset.label, value: preset.value }))
153
+ // The bundle load no longer opens the result sheet: it reports on the bar at the top of the viewport and in the
154
+ // footer, so an empty sheet does not sit over the map for the length of a 38 MB download.
155
+ const showSheet = Boolean(busy || result || errorMessage)
156
+
157
+ const bundleLoading = Boolean(loading && !runtime.ready)
158
+ const steps = loading?.stepLabels.length ?? 0
159
+
160
+ return (
161
+ <>
162
+ <MapProgressBar
163
+ active={bundleLoading}
164
+ fraction={steps ? ((loading?.stepIndex ?? 0) + 1) / steps : null}
165
+ label="Loading the geocoder"
166
+ />
167
+
168
+ <div className="mw-map-chrome mw-map-chrome--top">
169
+ <form
170
+ className="mw-map-chrome__search"
171
+ onSubmit={(event) => {
172
+ event.preventDefault()
173
+ runQuery(geocode.text)
174
+ }}
175
+ >
176
+ {/*
177
+ * `type="search"` brings its own clear button, so the pill carries no trailing slot: a second cross
178
+ * beside the native one is two controls for one job, and the spinner that used to live there changed
179
+ * the field's height on every submit.
180
+ */}
181
+ <MapSearchBar label="Search addresses" leading={<span aria-hidden="true">⌕</span>} busy={busy}>
182
+ <input
183
+ id="mw-pipeline-input"
184
+ type="search"
185
+ aria-label="Address"
186
+ value={geocode.text}
187
+ onChange={(event) => geocode.setText(event.target.value)}
188
+ disabled={!runtime.ready}
189
+ placeholder={placeholder}
190
+ onKeyDown={autocomplete.onInputKeyDown}
191
+ {...autocomplete.inputProps}
192
+ />
193
+ </MapSearchBar>
194
+ </form>
195
+
196
+ <PlaceAutocomplete
197
+ suggestions={autocomplete.suggestions}
198
+ activeIndex={autocomplete.activeIndex}
199
+ onPick={autocomplete.pick}
200
+ onHover={autocomplete.setActiveIndex}
201
+ listboxID={autocomplete.listboxID}
202
+ optionID={autocomplete.optionID}
203
+ />
204
+
205
+ <MapChipRow
206
+ chips={chips}
207
+ label="Example addresses"
208
+ activeValue={geocode.text}
209
+ disabled={!runtime.ready || busy}
210
+ onPick={(value) => {
211
+ geocode.setText(value)
212
+ geocode.reset()
213
+ runQuery(value)
214
+ }}
215
+ />
216
+
217
+ {panels.bias}
218
+ </div>
219
+
220
+ {/*
221
+ * Every floating control lives in this one column, so nothing can land on top of anything else: the layer
222
+ * control joins the capsule rather than sitting in the left column, and the compass takes its own capsule
223
+ * below because it comes and goes and would otherwise resize the one above it.
224
+ */}
225
+ <MapControlStack label="Map controls">
226
+ <MapControlGroup>
227
+ <MapControlButton
228
+ label="About this geocoder"
229
+ active={openSheet === "about"}
230
+ onPress={() => toggleSheet("about")}
231
+ >
232
+ <span aria-hidden="true">i</span>
233
+ </MapControlButton>
234
+
235
+ {panels.layers ? (
236
+ <MapControlButton label="Map layers" active={openSheet === "layers"} onPress={() => toggleSheet("layers")}>
237
+ <span aria-hidden="true">≡</span>
238
+ </MapControlButton>
239
+ ) : null}
240
+
241
+ <MapControlButton
242
+ label="Developer controls"
243
+ active={openSheet === "developer"}
244
+ onPress={() => toggleSheet("developer")}
245
+ >
246
+ <span aria-hidden="true">⚙</span>
247
+ </MapControlButton>
248
+ </MapControlGroup>
249
+
250
+ <MapControlGroup className="mw-map-control-group--compass">
251
+ <MapCompass bearing={bearing} onResetNorth={resetNorth} />
252
+ </MapControlGroup>
253
+ </MapControlStack>
254
+
255
+ {openSheet === "about" ? (
256
+ <MapSheet title="About" onClose={closeSheet}>
257
+ {panels.header}
258
+ </MapSheet>
259
+ ) : null}
260
+
261
+ {openSheet === "layers" ? (
262
+ <MapSheet title="Layers" onClose={closeSheet}>
263
+ {panels.layers?.({ map })}
264
+ </MapSheet>
265
+ ) : null}
266
+
267
+ {openSheet === "developer" ? (
268
+ <MapSheet title="Developer" onClose={closeSheet}>
269
+ {panels.releaseInfo ? (
270
+ <div className="mw-map-sheet__row">
271
+ <span className="mw-map-sheet__label">This release</span>
272
+ {panels.releaseInfo}
273
+ </div>
274
+ ) : null}
275
+
276
+ <div className="mw-map-sheet__row">
277
+ <VersionPicker
278
+ versions={versions}
279
+ selected={runtime.selectedVersion ?? null}
280
+ onSelect={onSelectVersion}
281
+ disabled={busy}
282
+ />
283
+ </div>
284
+
285
+ <div className="mw-map-sheet__row">
286
+ <BackendControl
287
+ activeBackend={runtime.activeBackend}
288
+ forceWASM={runtime.forceWASM ?? false}
289
+ onForceWASMChange={onForceWASMChange}
290
+ />
291
+ </div>
292
+
293
+ <div className="mw-map-sheet__row">
294
+ <CompareToggle
295
+ versions={versions}
296
+ primaryVersion={runtime.selectedVersion ?? null}
297
+ compareMode={compare.compareMode}
298
+ onCompareModeChange={compare.setCompareMode}
299
+ compareVersion={compare.compareVersion}
300
+ onCompareVersionChange={compare.setCompareVersion}
301
+ disabled={busy}
302
+ />
303
+ </div>
304
+
305
+ {panels.developerExtras}
306
+ </MapSheet>
307
+ ) : null}
308
+
309
+ {showSheet ? (
310
+ <section className="mw-map-sheet mw-map-sheet--bottom" aria-label="Result">
311
+ <div className="mw-map-sheet__handle" aria-hidden="true" />
312
+
313
+ {errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
314
+
315
+ {panels.aboveResult ? panels.aboveResult({ result }) : null}
316
+
317
+ {busy ? (
318
+ <div className="mw-result">
319
+ <LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
320
+ </div>
321
+ ) : result ? (
322
+ panels.result ? (
323
+ panels.result({
324
+ result,
325
+ selectedCandidate,
326
+ selectedCandidateIndex: geocode.selectedCandidateIndex,
327
+ onSelectCandidate: geocode.selectCandidate,
328
+ })
329
+ ) : (
330
+ <ResultPanel
331
+ result={result}
332
+ selectedCandidate={selectedCandidate}
333
+ selectedCandidateIndex={geocode.selectedCandidateIndex}
334
+ onSelectCandidate={geocode.selectCandidate}
335
+ extras={panels.extras}
336
+ failure={panels.failure}
337
+ />
338
+ )
339
+ ) : null}
340
+
341
+ {panels.compare
342
+ ? panels.compare({ result, compareMode: compare.compareMode, compareVersion: compare.compareVersion })
343
+ : null}
344
+
345
+ {panels.permalink ? <div className="mw-map-sheet__actions">{panels.permalink(geocode.text)}</div> : null}
346
+ </section>
347
+ ) : null}
348
+
349
+ {panels.footer}
350
+ </>
351
+ )
352
+ }