@mailwoman/react 7.4.0 → 7.6.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 (156) hide show
  1. package/common/PresetChips.tsx +4 -1
  2. package/index.ts +31 -0
  3. package/map/BackendControl.tsx +39 -0
  4. package/map/CompareToggle.tsx +81 -0
  5. package/map/DemoControls.tsx +173 -0
  6. package/map/DemoMap.tsx +98 -0
  7. package/map/GeocoderDemo.tsx +178 -0
  8. package/map/OverlayLayers.tsx +46 -0
  9. package/map/PlaceAutocomplete.tsx +69 -0
  10. package/map/PlaceMarker.tsx +40 -0
  11. package/map/ResolvedPlaceLayers.tsx +63 -0
  12. package/map/ResultCamera.tsx +60 -0
  13. package/map/ResultOverlay.tsx +61 -0
  14. package/map/ResultPanel.tsx +84 -0
  15. package/map/VersionPicker.tsx +62 -0
  16. package/map/geometry.ts +135 -0
  17. package/map/index.ts +73 -0
  18. package/map/place-render.ts +157 -0
  19. package/map/types.ts +207 -0
  20. package/map/useCompareState.ts +45 -0
  21. package/map/useDemoGeocode.ts +67 -0
  22. package/map/useMapPlaceRender.ts +21 -0
  23. package/map/usePlaceAutocomplete.ts +195 -0
  24. package/out/common/PresetChips.d.ts +3 -1
  25. package/out/common/PresetChips.d.ts.map +1 -1
  26. package/out/common/PresetChips.js +2 -2
  27. package/out/common/PresetChips.js.map +1 -1
  28. package/out/index.d.ts +4 -1
  29. package/out/index.d.ts.map +1 -1
  30. package/out/index.js +2 -0
  31. package/out/index.js.map +1 -1
  32. package/out/map/BackendControl.d.ts +24 -0
  33. package/out/map/BackendControl.d.ts.map +1 -0
  34. package/out/map/BackendControl.js +6 -0
  35. package/out/map/BackendControl.js.map +1 -0
  36. package/out/map/CompareToggle.d.ts +35 -0
  37. package/out/map/CompareToggle.d.ts.map +1 -0
  38. package/out/map/CompareToggle.js +9 -0
  39. package/out/map/CompareToggle.js.map +1 -0
  40. package/out/map/DemoControls.d.ts +44 -0
  41. package/out/map/DemoControls.d.ts.map +1 -0
  42. package/out/map/DemoControls.js +28 -0
  43. package/out/map/DemoControls.js.map +1 -0
  44. package/out/map/DemoMap.d.ts +59 -0
  45. package/out/map/DemoMap.d.ts.map +1 -0
  46. package/out/map/DemoMap.js +12 -0
  47. package/out/map/DemoMap.js.map +1 -0
  48. package/out/map/GeocoderDemo.d.ts +47 -0
  49. package/out/map/GeocoderDemo.d.ts.map +1 -0
  50. package/out/map/GeocoderDemo.js +73 -0
  51. package/out/map/GeocoderDemo.js.map +1 -0
  52. package/out/map/OverlayLayers.d.ts +23 -0
  53. package/out/map/OverlayLayers.d.ts.map +1 -0
  54. package/out/map/OverlayLayers.js +27 -0
  55. package/out/map/OverlayLayers.js.map +1 -0
  56. package/out/map/PlaceAutocomplete.d.ts +34 -0
  57. package/out/map/PlaceAutocomplete.d.ts.map +1 -0
  58. package/out/map/PlaceAutocomplete.js +9 -0
  59. package/out/map/PlaceAutocomplete.js.map +1 -0
  60. package/out/map/PlaceMarker.d.ts +27 -0
  61. package/out/map/PlaceMarker.d.ts.map +1 -0
  62. package/out/map/PlaceMarker.js +9 -0
  63. package/out/map/PlaceMarker.js.map +1 -0
  64. package/out/map/ResolvedPlaceLayers.d.ts +34 -0
  65. package/out/map/ResolvedPlaceLayers.d.ts.map +1 -0
  66. package/out/map/ResolvedPlaceLayers.js +11 -0
  67. package/out/map/ResolvedPlaceLayers.js.map +1 -0
  68. package/out/map/ResultCamera.d.ts +34 -0
  69. package/out/map/ResultCamera.d.ts.map +1 -0
  70. package/out/map/ResultCamera.js +43 -0
  71. package/out/map/ResultCamera.js.map +1 -0
  72. package/out/map/ResultOverlay.d.ts +34 -0
  73. package/out/map/ResultOverlay.d.ts.map +1 -0
  74. package/out/map/ResultOverlay.js +18 -0
  75. package/out/map/ResultOverlay.js.map +1 -0
  76. package/out/map/ResultPanel.d.ts +34 -0
  77. package/out/map/ResultPanel.d.ts.map +1 -0
  78. package/out/map/ResultPanel.js +12 -0
  79. package/out/map/ResultPanel.js.map +1 -0
  80. package/out/map/VersionPicker.d.ts +30 -0
  81. package/out/map/VersionPicker.d.ts.map +1 -0
  82. package/out/map/VersionPicker.js +8 -0
  83. package/out/map/VersionPicker.js.map +1 -0
  84. package/out/map/geometry.d.ts +55 -0
  85. package/out/map/geometry.d.ts.map +1 -0
  86. package/out/map/geometry.js +99 -0
  87. package/out/map/geometry.js.map +1 -0
  88. package/out/map/index.d.ts +52 -0
  89. package/out/map/index.d.ts.map +1 -0
  90. package/out/map/index.js +36 -0
  91. package/out/map/index.js.map +1 -0
  92. package/out/map/place-render.d.ts +85 -0
  93. package/out/map/place-render.d.ts.map +1 -0
  94. package/out/map/place-render.js +98 -0
  95. package/out/map/place-render.js.map +1 -0
  96. package/out/map/types.d.ts +193 -0
  97. package/out/map/types.d.ts.map +1 -0
  98. package/out/map/types.js +18 -0
  99. package/out/map/types.js.map +1 -0
  100. package/out/map/useCompareState.d.ts +25 -0
  101. package/out/map/useCompareState.d.ts.map +1 -0
  102. package/out/map/useCompareState.js +27 -0
  103. package/out/map/useCompareState.js.map +1 -0
  104. package/out/map/useDemoGeocode.d.ts +36 -0
  105. package/out/map/useDemoGeocode.d.ts.map +1 -0
  106. package/out/map/useDemoGeocode.js +44 -0
  107. package/out/map/useDemoGeocode.js.map +1 -0
  108. package/out/map/useMapPlaceRender.d.ts +15 -0
  109. package/out/map/useMapPlaceRender.d.ts.map +1 -0
  110. package/out/map/useMapPlaceRender.js +18 -0
  111. package/out/map/useMapPlaceRender.js.map +1 -0
  112. package/out/map/usePlaceAutocomplete.d.ts +61 -0
  113. package/out/map/usePlaceAutocomplete.d.ts.map +1 -0
  114. package/out/map/usePlaceAutocomplete.js +122 -0
  115. package/out/map/usePlaceAutocomplete.js.map +1 -0
  116. package/out/pipeline/QueryForm.d.ts +6 -2
  117. package/out/pipeline/QueryForm.d.ts.map +1 -1
  118. package/out/pipeline/QueryForm.js +2 -2
  119. package/out/pipeline/QueryForm.js.map +1 -1
  120. package/out/poi/LiveResultsBlock.d.ts +3 -3
  121. package/out/poi/LiveResultsBlock.d.ts.map +1 -1
  122. package/out/poi/LiveResultsBlock.js +2 -2
  123. package/out/poi/LiveResultsBlock.js.map +1 -1
  124. package/out/poi/POIExplorer.d.ts +7 -1
  125. package/out/poi/POIExplorer.d.ts.map +1 -1
  126. package/out/poi/POIExplorer.js +17 -5
  127. package/out/poi/POIExplorer.js.map +1 -1
  128. package/out/poi/SubjectPanel.d.ts +3 -2
  129. package/out/poi/SubjectPanel.d.ts.map +1 -1
  130. package/out/poi/SubjectPanel.js +8 -1
  131. package/out/poi/SubjectPanel.js.map +1 -1
  132. package/out/poi/runtime.d.ts +13 -4
  133. package/out/poi/runtime.d.ts.map +1 -1
  134. package/out/poi/runtime.js +36 -12
  135. package/out/poi/runtime.js.map +1 -1
  136. package/out/poi/types.d.ts +31 -4
  137. package/out/poi/types.d.ts.map +1 -1
  138. package/out/poi/usePOISearch.d.ts +8 -1
  139. package/out/poi/usePOISearch.d.ts.map +1 -1
  140. package/out/poi/usePOISearch.js +47 -12
  141. package/out/poi/usePOISearch.js.map +1 -1
  142. package/out/runtime/useDemoRuntime.d.ts +113 -0
  143. package/out/runtime/useDemoRuntime.d.ts.map +1 -0
  144. package/out/runtime/useDemoRuntime.js +135 -0
  145. package/out/runtime/useDemoRuntime.js.map +1 -0
  146. package/package.json +31 -10
  147. package/pipeline/QueryForm.tsx +17 -2
  148. package/poi/LiveResultsBlock.tsx +4 -4
  149. package/poi/POIExplorer.tsx +39 -8
  150. package/poi/SubjectPanel.tsx +47 -12
  151. package/poi/runtime.ts +35 -10
  152. package/poi/types.ts +34 -4
  153. package/poi/usePOISearch.ts +62 -11
  154. package/runtime/useDemoRuntime.ts +245 -0
  155. package/styles.css +166 -0
  156. package/vitest.node.config.ts +20 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `computeMapPlaceRenderSpec` — the PURE core that replaces the docs demo's ~100-line imperative
7
+ * marker/bbox/camera redraw effect (`_app.tsx:601-705`). It takes an ALREADY-RESOLVED place (the async
8
+ * polygon-DB fetch is a host/runtime concern — a later phase — so a crisp polygon arrives pre-fetched
9
+ * as `place.geometry`) and returns a declarative render spec: the marker position(s), the outline
10
+ * geometry to draw, and the camera target. No map instance, no DOM, no `react-map-gl` — so it is
11
+ * node-testable (see `place-render.node.test.ts`). The declarative components in this folder render
12
+ * the spec; a controlled-viewport consumer applies the camera.
13
+ *
14
+ * The decision cascade is a faithful, side-effect-free transcription of the imperative effect:
15
+ * 1. street tier (situs / interp) → exact-radius circle, fly to a tight zoom
16
+ * 2. crisp admin polygon (pre-fetched) → draw the polygon, fit its bounds
17
+ * 3. anchor-centroid postcode (no bbox) → ~3 km "around here" circle, fly to zoom 11
18
+ * 4. bbox with real extent → bbox-sized approximate circle, fit the bbox
19
+ * 5. bare point → no outline, fly to zoom 12
20
+ */
21
+
22
+ import type { ResolvedPlaceView } from "../pipeline/types.ts"
23
+ import { approxCircleGeometry, bboxToBounds, geomBounds, radiusCircleGeometry } from "./geometry.ts"
24
+ import type { BoundsTuple, PlaceBBox, PlaceGeometry } from "./geometry.ts"
25
+
26
+ /** `[longitude, latitude]`. */
27
+ export type LngLat = [number, number]
28
+
29
+ /** The street-level resolution tier (#377): `address_point` = exact building; `interpolated` = TIGER estimate. */
30
+ export type PlaceTier = "address_point" | "interpolated"
31
+
32
+ /**
33
+ * The resolved-place shape the map render consumes — the pipeline {@link ResolvedPlaceView} plus the map-only extras the
34
+ * demo's `ResolvedHit` carries (bbox, street tier + uncertainty), and an optional PRE-FETCHED crisp polygon. Extending
35
+ * `ResolvedPlaceView` keeps the map render aligned with the shared parse result; the extras are additive.
36
+ */
37
+ export interface ResolvedMapPlace extends ResolvedPlaceView {
38
+ /** The place's bounding box, when the gazetteer carries one (admin places). Absent for anchor-centroid postcodes. */
39
+ bbox?: PlaceBBox
40
+ /** Street-level tier, when this hit came from the situs/interp tier rather than the WOF admin cascade. */
41
+ tier?: PlaceTier
42
+ /** Honest uncertainty radius in meters for a street-level tier (10 m situs floor; calibrated interp). */
43
+ uncertaintyM?: number
44
+ /**
45
+ * The crisp admin polygon, when the host has ALREADY fetched it from the sibling polygon DB. Its presence drives the
46
+ * polygon path; the async fetch itself stays out of this pure function (a runtime concern in a later phase).
47
+ */
48
+ geometry?: PlaceGeometry
49
+ }
50
+
51
+ /**
52
+ * The camera target the render computes. `center` (fly to a point at a zoom) HAS a declarative equivalent — a consumer
53
+ * can feed it to a controlled `viewState` (see {@link cameraToViewState}). `bounds` (fit a box with pixel padding) does
54
+ * NOT — `fitBounds` needs the map's pixel dimensions, so it is applied imperatively by `<ResultCamera>`.
55
+ */
56
+ export type MapCameraTarget =
57
+ | { kind: "center"; center: LngLat; zoom: number }
58
+ | { kind: "bounds"; bounds: BoundsTuple; padding: number }
59
+
60
+ /** The declarative render spec for one resolved place — the pure output of {@link computeMapPlaceRenderSpec}. */
61
+ export interface MapPlaceRenderSpec {
62
+ /** Marker position(s) as `[lon, lat]`. Single-element today; an array leaves room for multi-marker later. */
63
+ markers: LngLat[]
64
+ /** The outline geometry (polygon / circle) to draw, or `null` when the place renders as a bare point. */
65
+ outline: PlaceGeometry | null
66
+ /** The camera target — animate or fit to this. */
67
+ camera: MapCameraTarget
68
+ }
69
+
70
+ /** Zoom levels the imperative effect flew to, kept named so the cascade reads as intent, not magic numbers. */
71
+ const ZOOM = {
72
+ addressPoint: 17,
73
+ interpolated: 15,
74
+ postcode: 11,
75
+ point: 12,
76
+ } as const
77
+
78
+ /** Padding (px) `fitBounds` insets a fitted box by, matching the ported effect. */
79
+ const FIT_PADDING = 40
80
+
81
+ /** The minimum lat/lon span (degrees) a bbox must exceed to be treated as a real extent rather than a point. */
82
+ const MIN_EXTENT_DEG = 0.001
83
+
84
+ /**
85
+ * Map a resolved place to its declarative render spec. Pure — same input, same output, no side effects. The `place`
86
+ * arrives fully resolved (crisp polygon pre-fetched into `place.geometry` when available), so this is the honest
87
+ * inverse of the old redraw effect with the imperative map mutation and the async DB load removed.
88
+ */
89
+ export function computeMapPlaceRenderSpec(place: ResolvedMapPlace): MapPlaceRenderSpec {
90
+ const markers: LngLat[] = [[place.lon, place.lat]]
91
+
92
+ // 1. Street tier (#377): the honest uncertainty circle (exact meter radius) + a tight zoom. Takes precedence over
93
+ // the admin polygon/bbox paths — a precise point gets no admin boundary.
94
+ if (place.tier && place.uncertaintyM != null) {
95
+ return {
96
+ markers,
97
+ outline: radiusCircleGeometry(place.lat, place.lon, place.uncertaintyM),
98
+ camera: {
99
+ kind: "center",
100
+ center: [place.lon, place.lat],
101
+ zoom: place.tier === "address_point" ? ZOOM.addressPoint : ZOOM.interpolated,
102
+ },
103
+ }
104
+ }
105
+
106
+ // 2. Crisp admin polygon (host pre-fetched it from the polygon DB) — draw the real boundary and fit it.
107
+ if (place.geometry) {
108
+ return {
109
+ markers,
110
+ outline: place.geometry,
111
+ camera: { kind: "bounds", bounds: bboxToBounds(geomBounds(place.geometry)), padding: FIT_PADDING },
112
+ }
113
+ }
114
+
115
+ // 3. Anchor-centroid postcode: no bbox, no polygon — a default ~3 km circle says "approximately here" without
116
+ // inventing a boundary.
117
+ if (!place.bbox && place.placetype === "postcode") {
118
+ return {
119
+ markers,
120
+ outline: approxCircleGeometry(place.lat, place.lon),
121
+ camera: { kind: "center", center: [place.lon, place.lat], zoom: ZOOM.postcode },
122
+ }
123
+ }
124
+
125
+ // 4. A bbox with real extent — draw an approximate CIRCLE sized from the bbox (a rectangle would read as a wrong,
126
+ // real boundary) and fit the bbox.
127
+ const bbox = place.bbox
128
+
129
+ if (bbox && Math.max(bbox.maxLat - bbox.minLat, bbox.maxLon - bbox.minLon) > MIN_EXTENT_DEG) {
130
+ return {
131
+ markers,
132
+ outline: approxCircleGeometry(place.lat, place.lon, bbox),
133
+ camera: { kind: "bounds", bounds: bboxToBounds(bbox), padding: FIT_PADDING },
134
+ }
135
+ }
136
+
137
+ // 5. A bare point (no tier, no polygon, no meaningful bbox) — just a marker and a mid zoom.
138
+ return {
139
+ markers,
140
+ outline: null,
141
+ camera: { kind: "center", center: [place.lon, place.lat], zoom: ZOOM.point },
142
+ }
143
+ }
144
+
145
+ /**
146
+ * The DECLARATIVE camera path: reshape a `center` target into a `viewState` patch a controlled `<DemoMap viewState>`
147
+ * can apply directly (a hard jump, no animation). Returns `null` for a `bounds` target — fitting a box to the viewport
148
+ * needs the map's pixel dimensions, which only the live map has, so that case is applied imperatively by
149
+ * `<ResultCamera>`. Pure + node-testable.
150
+ */
151
+ export function cameraToViewState(
152
+ camera: MapCameraTarget
153
+ ): { longitude: number; latitude: number; zoom: number } | null {
154
+ if (camera.kind !== "center") return null
155
+
156
+ return { longitude: camera.center[0], latitude: camera.center[1], zoom: camera.zoom }
157
+ }
package/map/types.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Types for the geocoder-demo map surface. Mirrors the pipeline seam: the package owns the UI state
7
+ * machine + the declarative map, while the host injects a {@link DemoRuntime} that owns ONNX / httpvfs
8
+ * / R2 and the composed map style. {@link DemoRuntime} EXTENDS {@link PipelineRuntime} so the shared
9
+ * `runParse` / `parseStageLabels` / `loading` contract is reused, and adds the map-specific surface
10
+ * (style, overlays, initial center, viewport bias, backend/version selection). Phase 4 adds the
11
+ * `resolveMapPlace` enricher, the {@link DemoPanels} injection bag, and the {@link DemoCompareContext}.
12
+ *
13
+ * The map-spec types are imported type-only from `react-map-gl/maplibre`; nothing here loads maplibre at
14
+ * runtime, so this module stays node-safe (its concrete-value CONSUMERS — `DemoMap`, `GeocoderDemo` —
15
+ * are the ones gated behind the `@mailwoman/react/map` subpath).
16
+ */
17
+
18
+ import type { ReactNode } from "react"
19
+ import type { LayerSpecification, SourceSpecification } from "react-map-gl/maplibre"
20
+
21
+ import type { ParseResult, PipelineRuntime, ResolvedPlaceView } from "../pipeline/types.ts"
22
+ import type { DemoMapStyle } from "./DemoMap.tsx"
23
+ import type { ResolvedMapPlace } from "./place-render.ts"
24
+
25
+ /** `[longitude, latitude]`. */
26
+ export type LngLatTuple = [number, number]
27
+
28
+ /** A viewport bias handed to `runParse` — the map's current center (and optionally zoom) as a soft prior. */
29
+ export interface MapBias {
30
+ /** Map center as `[lon, lat]`, typically read from `map.getCenter()`. */
31
+ center: LngLatTuple
32
+ /** Current zoom, if the host wants distance-aware biasing. */
33
+ zoom?: number
34
+ }
35
+
36
+ /**
37
+ * A host-supplied overlay: one map `<Source>` plus one or more `<Layer>`s laid over the basemap (coverage "fog of war",
38
+ * race-dots, …). The host composes the specs; the package renders them declaratively in a later phase.
39
+ */
40
+ export interface OverlaySpec {
41
+ /** Stable id — used as the `<Source>` id and the layer-id prefix. */
42
+ id: string
43
+ /** The map source spec (vector/geojson/raster). */
44
+ source: SourceSpecification
45
+ /** The layers drawn from that source. */
46
+ layers: LayerSpecification[]
47
+ /** Whether the overlay is visible initially. @default true */
48
+ visible?: boolean
49
+ /** Human label for a layer-toggle control. */
50
+ label?: string
51
+ }
52
+
53
+ /** One autocomplete suggestion produced by the host's FST prefix-walk. */
54
+ export interface Suggestion {
55
+ /** The text inserted when the suggestion is picked. */
56
+ value: string
57
+ /** Optional display label if it differs from `value`. */
58
+ label?: string
59
+ /** Optional place kind for badge/icon rendering. */
60
+ placetype?: string
61
+ }
62
+
63
+ /** A selectable model bundle (version tag + a display label the picker shows). */
64
+ export interface DemoVersionOption {
65
+ /** The version tag (e.g. a git tag or model-card version). */
66
+ version: string
67
+ /** Display label; falls back to `version`. */
68
+ label?: string
69
+ }
70
+
71
+ /** Which neural backend the demo is currently running on. */
72
+ export type DemoBackend = "webgpu" | "wasm"
73
+
74
+ /**
75
+ * The injected demo runtime. Extends {@link PipelineRuntime} (shared `runParse` / `parseStageLabels` / `loading` /
76
+ * `ready`) with the map + version/backend surface the demo needs. The host composes `mapStyle` (via cartographer's
77
+ * `StyleSpecificationComposer` + the tile-worker TileJSON), supplies the overlay specs, the initial center (from
78
+ * geolocation), the FST autocomplete, and the calibrator — nothing in the package imports `@mailwoman/cartographer`,
79
+ * `@mailwoman/neural-web`, httpvfs, or Docusaurus.
80
+ */
81
+ export interface DemoRuntime extends PipelineRuntime {
82
+ // ── Map ────────────────────────────────────────────────────────────────
83
+ /** The composed basemap style (URL or `StyleSpecification`). */
84
+ mapStyle: DemoMapStyle
85
+ /** Host-supplied overlays (coverage, race-dots, …). */
86
+ overlays?: OverlaySpec[]
87
+ /** Initial map center as `[lon, lat]` (the host's browser-geolocation result). */
88
+ initialCenter: LngLatTuple
89
+ /** Initial zoom for the first camera. */
90
+ initialZoom?: number
91
+
92
+ // ── Parse extras layered over PipelineRuntime.runParse ──────────────────
93
+ /**
94
+ * A bias-aware parse. The map demo feeds the current viewport center as a soft prior; when absent the host falls back
95
+ * to the base {@link PipelineRuntime.runParse}. Kept separate so the shared `runParse` contract is unchanged.
96
+ */
97
+ runParseWithBias?: (
98
+ input: string,
99
+ bias: MapBias | null,
100
+ hooks: { onStage: (stage: number) => void }
101
+ ) => ReturnType<PipelineRuntime["runParse"]>
102
+ /** FST prefix-walk autocomplete, wrapped by the host. */
103
+ autocomplete?: (query: string) => Promise<Suggestion[]>
104
+ /** Maps a raw model score to a calibrated one; `null` when no calibration table is loaded. */
105
+ calibrator?: (raw: number) => number | null
106
+ /**
107
+ * Enrich the selected candidate into the richer {@link ResolvedMapPlace} the declarative map render consumes (bbox,
108
+ * street tier + uncertainty, a pre-fetched crisp polygon) — the fields that live on the demo's `ResolvedHit` but not
109
+ * on the shared {@link ResolvedPlaceView}. The host owns this because those extras (and the async polygon fetch in the
110
+ * real demo) are host/gazetteer concerns; the package keeps {@link ParseResult} unpolluted. Absent → the candidate
111
+ * renders as a bare point (marker + a mid-zoom fly-to). Returning `null` also renders nothing.
112
+ */
113
+ resolveMapPlace?: (candidate: ResolvedPlaceView, result: ParseResult) => ResolvedMapPlace | null
114
+
115
+ // ── Version + backend selection ─────────────────────────────────────────
116
+ /** The selectable model bundles the version picker offers. */
117
+ availableVersions?: DemoVersionOption[]
118
+ /** The currently-selected model version. */
119
+ selectedVersion?: string
120
+ /** Switch the active model bundle (re-loads weights/tokenizer/gazetteer). */
121
+ selectVersion?: (version: string) => void
122
+ /** The backend the neural runtime resolved to (e.g. `webgpu (28 MB int8)`); free-form for the label. */
123
+ activeBackend?: string
124
+ /** Whether the CPU/WASM backend is currently forced (the controlled value for the backend toggle). */
125
+ forceWASM?: boolean
126
+ /** Force the WASM backend (opt out of WebGPU), for the backend toggle. */
127
+ setForceWASM?: (forceWASM: boolean) => void
128
+ }
129
+
130
+ /** The compare-mode state a {@link DemoPanels.compare} render-prop receives (the second parse itself stays host-side). */
131
+ export interface DemoCompareContext {
132
+ /** The current primary parse result, or `null` before the first submit. */
133
+ result: ParseResult | null
134
+ /** Whether the compare toggle is on. */
135
+ compareMode: boolean
136
+ /** The version selected to compare against, or `null` when none is chosen. */
137
+ compareVersion: string | null
138
+ }
139
+
140
+ /**
141
+ * The state a {@link DemoPanels.result} render-prop receives, so a host can render its OWN result block (the docs
142
+ * `<ResultPanel>` with its span-highlight / timing / hierarchy / precision detail) in place of the package's default
143
+ * {@link ResultPanel}. Everything the default panel needs is passed through; the candidate-selection state stays owned
144
+ * by the package (`useDemoGeocode`).
145
+ */
146
+ export interface DemoResultContext {
147
+ /** The current parse+resolve result. */
148
+ result: ParseResult
149
+ /** The selected candidate (falls back to the first), enriched for the resolved-place detail. */
150
+ selectedCandidate: ResolvedPlaceView | null
151
+ /** The selected candidate index, for the picker's active state. */
152
+ selectedCandidateIndex: number
153
+ /** Fired when a candidate in the picker is chosen. */
154
+ onSelectCandidate: (index: number) => void
155
+ }
156
+
157
+ /**
158
+ * Host-injected panels for {@link GeocoderDemo}, the map analogue of `PipelinePanels`. Each is an already-rendered
159
+ * `ReactNode` (or a thunk of the parse result / compare state) so the package needs neither the heavy docs visualizers
160
+ * (ModelVisualizer, VersionCompare, AboutDemo, PermalinkButton) nor their data types. Every field is optional — the
161
+ * fake-runtime Storybook stories pass none and still render the whole demo.
162
+ */
163
+ export interface DemoPanels {
164
+ /** Rendered at the top of the control panel (e.g. the docs "About this demo"). */
165
+ header?: ReactNode
166
+ /** One-line release blurb for the selected version. */
167
+ releaseInfo?: ReactNode
168
+ /** Rendered at the bottom of the control panel (e.g. a guided tour). */
169
+ footer?: ReactNode
170
+ /**
171
+ * A device-location / proximity-bias control, rendered between the query form and the autocomplete list (the demo's
172
+ * "📍 Use my location" row). Host-owned so the geolocation permission + the bias it feeds into the host's
173
+ * {@link DemoRuntime.runParseWithBias} stay a host concern.
174
+ */
175
+ bias?: ReactNode
176
+ /** Heavy visualizers (span highlight, tree, timing, BIO, …), rendered from the result. */
177
+ extras?: (result: ParseResult) => ReactNode
178
+ /**
179
+ * Rendered just above the result block (present or empty). The demo's opt-in display toggles live here — calibrated
180
+ * confidence + dev-mode — because the host owns both the toggle state AND the {@link result} / {@link debugDrawer}
181
+ * renderers those toggles drive.
182
+ */
183
+ aboveResult?: (context: { result: ParseResult | null }) => ReactNode
184
+ /**
185
+ * Replace the package's default {@link ResultPanel} entirely. When provided, the host renders its own result block
186
+ * (the docs `<ResultPanel>` — span highlight, timing, hierarchy, precision detail, calibrated confidences) from the
187
+ * {@link DemoResultContext}. Absent → the built-in panel renders.
188
+ */
189
+ result?: (context: DemoResultContext) => ReactNode
190
+ /**
191
+ * Rendered in place of the resolved-place panel when nothing resolved (host's FailureDiagnostic). Ignored when
192
+ * {@link result} is set.
193
+ */
194
+ failure?: (result: ParseResult) => ReactNode
195
+ /** The version-compare view — the host renders its own diff from the compare state it owns. */
196
+ compare?: (context: DemoCompareContext) => ReactNode
197
+ /**
198
+ * The model-visualizer / debug drawer, mounted beside the map (host's ModelVisualizer). A render-prop so the host can
199
+ * trace the CURRENT result (its input) — the package passes the live parse result; the host gates on its own dev-mode
200
+ * state and returns `null` when the drawer is closed.
201
+ */
202
+ debugDrawer?: (context: { result: ParseResult | null }) => ReactNode
203
+ /** Extra map controls mounted as `<DemoMap>` children (host's DebugControl / LayerToggle via `useControl`). */
204
+ mapControls?: ReactNode
205
+ /** A permalink control for the current address (host's PermalinkButton). */
206
+ permalink?: (text: string) => ReactNode
207
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useCompareState` — the small headless state machine behind the demo's compare toggle
7
+ * (`_app.tsx:142-143, 1238-1305`): whether compare mode is on, and which version to compare against.
8
+ * Turning compare off clears the chosen version; selecting a compare version equal to the primary is
9
+ * rejected (the picker filters it out, this guards the programmatic path). The SECOND parse itself is a
10
+ * host concern surfaced through `panels.compare` — this hook owns only the toggle + selection.
11
+ */
12
+
13
+ import { useCallback, useState } from "react"
14
+
15
+ export interface UseCompareState {
16
+ /** Whether compare mode is on. */
17
+ compareMode: boolean
18
+ /** The version chosen to compare against, or `null`. */
19
+ compareVersion: string | null
20
+ /** Flip compare mode; turning it off clears the chosen version. */
21
+ setCompareMode: (compareMode: boolean) => void
22
+ /** Choose a compare version (`null` clears it). */
23
+ setCompareVersion: (version: string | null) => void
24
+ /** Keep the compare selection distinct from the primary — call when the primary version changes. */
25
+ clearIfPrimary: (primaryVersion: string) => void
26
+ }
27
+
28
+ export function useCompareState(): UseCompareState {
29
+ const [compareMode, setCompareModeState] = useState(false)
30
+ const [compareVersion, setCompareVersion] = useState<string | null>(null)
31
+
32
+ const setCompareMode = useCallback((next: boolean) => {
33
+ setCompareModeState(next)
34
+
35
+ if (!next) {
36
+ setCompareVersion(null)
37
+ }
38
+ }, [])
39
+
40
+ const clearIfPrimary = useCallback((primaryVersion: string) => {
41
+ setCompareVersion((prev) => (prev === primaryVersion ? null : prev))
42
+ }, [])
43
+
44
+ return { compareMode, compareVersion, setCompareMode, setCompareVersion, clearIfPrimary }
45
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useDemoGeocode` — the parse+resolve state machine for the geocoder map demo. It REUSES the pipeline's
7
+ * `useParsePipeline` (text / busy / stage / result / candidate selection) and layers on the two map-only
8
+ * concerns the demo adds over the base explorer:
9
+ *
10
+ * 1. Viewport bias — when the runtime exposes `runParseWithBias`, the current map center (read through
11
+ * an injected `getBias`, itself reading the `MapRef`) rides along as a soft proximity prior. The
12
+ * bias is injected by deriving a `runParse` that binds it, so `useParsePipeline` is reused verbatim.
13
+ * 2. The map render place — the selected candidate, enriched by the host's `resolveMapPlace` into the
14
+ * richer {@link ResolvedMapPlace} the declarative overlays consume (bbox / tier / polygon). Absent
15
+ * an enricher, the candidate renders as a bare point.
16
+ *
17
+ * No maplibre import at module scope — the map handle is reached only through the injected `getBias`
18
+ * callback, so this hook stays independent of the map binding.
19
+ */
20
+
21
+ import { useMemo } from "react"
22
+
23
+ import type { PipelineRuntime } from "../pipeline/types.ts"
24
+ import { useParsePipeline, type UseParsePipeline } from "../pipeline/useParsePipeline.ts"
25
+ import type { ResolvedMapPlace } from "./place-render.ts"
26
+ import type { DemoRuntime, MapBias } from "./types.ts"
27
+
28
+ export interface UseDemoGeocodeOptions {
29
+ /** The injected demo runtime (extends `PipelineRuntime` with the map + bias surface). */
30
+ runtime: DemoRuntime
31
+ /** Address to pre-fill. */
32
+ defaultText: string
33
+ /** Read the current viewport bias (the map center) at submit time. Absent → no bias. */
34
+ getBias?: () => MapBias | null
35
+ }
36
+
37
+ export interface UseDemoGeocode extends UseParsePipeline {
38
+ /** The selected candidate enriched into the map-render shape (bbox / tier / polygon), or `null`. */
39
+ mapPlace: ResolvedMapPlace | null
40
+ }
41
+
42
+ export function useDemoGeocode({ runtime, defaultText, getBias }: UseDemoGeocodeOptions): UseDemoGeocode {
43
+ // Bind the viewport bias into a derived `runParse` so `useParsePipeline` is reused unchanged. When the runtime has no
44
+ // bias-aware parse, pass it straight through.
45
+ const geoRuntime = useMemo<PipelineRuntime>(() => {
46
+ const withBias = runtime.runParseWithBias
47
+
48
+ if (!withBias) return runtime
49
+
50
+ return {
51
+ ...runtime,
52
+ runParse: (input, hooks) => withBias(input, getBias?.() ?? null, hooks),
53
+ }
54
+ }, [runtime, getBias])
55
+
56
+ const pipeline = useParsePipeline({ runtime: geoRuntime, defaultText })
57
+
58
+ const mapPlace = useMemo<ResolvedMapPlace | null>(() => {
59
+ if (!pipeline.selectedCandidate || !pipeline.result) return null
60
+
61
+ return runtime.resolveMapPlace
62
+ ? runtime.resolveMapPlace(pipeline.selectedCandidate, pipeline.result)
63
+ : pipeline.selectedCandidate
64
+ }, [pipeline.selectedCandidate, pipeline.result, runtime])
65
+
66
+ return { ...pipeline, mapPlace }
67
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `useMapPlaceRender` — the thin React memo wrapper over the pure {@link computeMapPlaceRenderSpec}. It
7
+ * recomputes the render spec only when the resolved place changes, so the declarative overlays render a
8
+ * stable spec object. The MATH lives in `place-render.ts` (pure, node-tested); this file adds nothing
9
+ * but memoization, so it stays trivially correct. A `null` place (no result yet, or a result with no
10
+ * candidate) yields `null` — the overlays render nothing.
11
+ */
12
+
13
+ import { useMemo } from "react"
14
+
15
+ import { computeMapPlaceRenderSpec } from "./place-render.ts"
16
+ import type { MapPlaceRenderSpec, ResolvedMapPlace } from "./place-render.ts"
17
+
18
+ /** Memoize the render spec for a resolved place; `null` in → `null` out (nothing to draw). */
19
+ export function useMapPlaceRender(place: ResolvedMapPlace | null | undefined): MapPlaceRenderSpec | null {
20
+ return useMemo(() => (place ? computeMapPlaceRenderSpec(place) : null), [place])
21
+ }