@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
@@ -20,9 +20,11 @@ export interface PresetChipsProps {
20
20
  disabled?: boolean
21
21
  /** Leading label. @default "Try:" */
22
22
  caption?: string
23
+ /** Optional trailing content rendered inside the chip row after the presets (e.g. an inline permalink button). */
24
+ trailing?: ReactNode
23
25
  }
24
26
 
25
- export function PresetChips({ presets, onPick, disabled, caption = "Try:" }: PresetChipsProps): ReactNode {
27
+ export function PresetChips({ presets, onPick, disabled, caption = "Try:", trailing }: PresetChipsProps): ReactNode {
26
28
  return (
27
29
  <div className="mw-presets">
28
30
  <span className="mw-presets__label">{caption}</span>
@@ -38,6 +40,7 @@ export function PresetChips({ presets, onPick, disabled, caption = "Try:" }: Pre
38
40
  {preset.label}
39
41
  </button>
40
42
  ))}
43
+ {trailing}
41
44
  </div>
42
45
  )
43
46
  }
package/index.ts CHANGED
@@ -51,12 +51,15 @@ export type {
51
51
  CategoryRecord,
52
52
  LiveSearchState,
53
53
  LoadPOIRuntime,
54
+ POIBrandSubject,
55
+ POICategorySubject,
54
56
  POIExplorerResult,
55
57
  POILiveSearch,
56
58
  POILiveSearchResult,
57
59
  POIRuntime,
58
60
  POISearchHit,
59
61
  POISubject,
62
+ POISubjectBase,
60
63
  TaxonomyLookup,
61
64
  } from "./poi/types.ts"
62
65
  export { usePOISearch } from "./poi/usePOISearch.ts"
@@ -90,3 +93,31 @@ export type {
90
93
  } from "./pipeline/types.ts"
91
94
  export { useParsePipeline } from "./pipeline/useParsePipeline.ts"
92
95
  export type { UseParsePipeline, UseParsePipelineOptions } from "./pipeline/useParsePipeline.ts"
96
+
97
+ // ── Demo runtime (shared load orchestration; node-safe — no onnx/httpvfs/maplibre) ──
98
+ export { useDemoRuntime } from "./runtime/useDemoRuntime.ts"
99
+ export type {
100
+ DemoAssetsLoadContext,
101
+ DemoLoaderState,
102
+ DemoManifest,
103
+ DemoReleaseBase,
104
+ DemoRuntimeConfig,
105
+ } from "./runtime/useDemoRuntime.ts"
106
+
107
+ // ── Map (types only from root) ──────────────────────────────────────────────
108
+ // The demo-map SURFACE lives behind the `@mailwoman/react/map` subpath so `maplibre-gl` / `react-map-gl`
109
+ // (WebGL + DOM at import) never enter the package-root graph — a bare `import("@mailwoman/react")` in node
110
+ // must not pull them. Only the CONTRACT types are re-exported here (fully erased at compile time, so no
111
+ // runtime linkage): the `DemoMap` component + hooks are import-able solely via `@mailwoman/react/map`.
112
+ export type {
113
+ DemoBackend,
114
+ DemoMapExtraProps,
115
+ DemoMapProps,
116
+ DemoMapStyle,
117
+ DemoRuntime,
118
+ DemoVersionOption,
119
+ LngLatTuple,
120
+ MapBias,
121
+ OverlaySpec,
122
+ Suggestion,
123
+ } from "./map/index.ts"
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<BackendControl>` — the neural-backend indicator + "Force WASM" checkbox from the demo's control
7
+ * panel (`_app.tsx:1255-1280`), as a dumb presentational unit. It shows which backend the runtime
8
+ * resolved to (WebGPU / WASM, with the int8 size the host formats into the label) and lets the visitor
9
+ * opt out of WebGPU. Props in, one boolean event out — the host re-loads the bundle on the toggle.
10
+ *
11
+ * NODE-SAFE: pure React, no maplibre.
12
+ */
13
+
14
+ import type { ReactNode } from "react"
15
+
16
+ export interface BackendControlProps {
17
+ /** The backend the runtime resolved to (e.g. `webgpu (28 MB int8)`); empty before it is known. */
18
+ activeBackend?: string
19
+ /** Whether the CPU/WASM backend is currently forced. */
20
+ forceWASM: boolean
21
+ /** Fired when the visitor toggles the "Force WASM" checkbox. */
22
+ onForceWASMChange: (forceWASM: boolean) => void
23
+ }
24
+
25
+ /** The backend indicator + WASM opt-out. */
26
+ export function BackendControl({ activeBackend, forceWASM, onForceWASMChange }: BackendControlProps): ReactNode {
27
+ return (
28
+ <div className="mw-demo-backend">
29
+ {activeBackend ? (
30
+ <span className="mw-demo-backend__active">
31
+ Backend: <code>{activeBackend}</code>
32
+ </span>
33
+ ) : null}
34
+ <label className="mw-demo-backend__toggle">
35
+ <input type="checkbox" checked={forceWASM} onChange={(e) => onForceWASMChange(e.target.checked)} /> Force WASM
36
+ </label>
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<CompareToggle>` — the "Compare" checkbox plus the compare-version `<select>` from the demo's
7
+ * control panel (`_app.tsx:1281-1339`), as a dumb presentational unit. Turning it on reveals a second
8
+ * version picker (the primary version filtered out); the actual second parse is a host concern the
9
+ * composed demo surfaces through `panels.compare`. Props in, two events out.
10
+ *
11
+ * NODE-SAFE: pure React, no maplibre.
12
+ */
13
+
14
+ import type { ReactNode } from "react"
15
+
16
+ import type { DemoVersionOption } from "./types.ts"
17
+
18
+ export interface CompareToggleProps {
19
+ /** The selectable model bundles (the primary version is filtered out of the compare list). */
20
+ versions: ReadonlyArray<DemoVersionOption>
21
+ /** The primary version, excluded from the compare options. */
22
+ primaryVersion: string | null
23
+ /** Whether compare mode is on. */
24
+ compareMode: boolean
25
+ /** Fired when the visitor flips compare mode. */
26
+ onCompareModeChange: (compareMode: boolean) => void
27
+ /** The version chosen to compare against, or `null` when none is chosen. */
28
+ compareVersion: string | null
29
+ /** Fired with the chosen compare version (or `null` when the empty option is picked). */
30
+ onCompareVersionChange: (version: string | null) => void
31
+ /** Disable the compare-version select (e.g. while a parse or compare load runs). */
32
+ disabled?: boolean
33
+ /** A status line rendered under the select (e.g. the compare backend, or a "Loading…" line). */
34
+ status?: ReactNode
35
+ }
36
+
37
+ /** The compare toggle + (when on) the compare-version selector. Renders `null` with fewer than two versions. */
38
+ export function CompareToggle({
39
+ versions,
40
+ primaryVersion,
41
+ compareMode,
42
+ onCompareModeChange,
43
+ compareVersion,
44
+ onCompareVersionChange,
45
+ disabled,
46
+ status,
47
+ }: CompareToggleProps): ReactNode {
48
+ if (versions.length < 2) return null
49
+
50
+ const options = versions.filter((v) => v.version !== primaryVersion)
51
+
52
+ return (
53
+ <div className="mw-demo-compare">
54
+ <label className="mw-demo-compare__toggle">
55
+ <input type="checkbox" checked={compareMode} onChange={(e) => onCompareModeChange(e.target.checked)} /> Compare
56
+ </label>
57
+ {compareMode ? (
58
+ <div className="mw-demo-control">
59
+ <label className="mw-demo-control__label" htmlFor="mw-demo-compare-version">
60
+ Compare with
61
+ </label>
62
+ <select
63
+ id="mw-demo-compare-version"
64
+ className="mw-demo-control__select"
65
+ value={compareVersion ?? ""}
66
+ onChange={(e) => onCompareVersionChange(e.target.value || null)}
67
+ disabled={disabled}
68
+ >
69
+ <option value="">Select version…</option>
70
+ {options.map((v) => (
71
+ <option key={v.version} value={v.version}>
72
+ {v.label ?? v.version}
73
+ </option>
74
+ ))}
75
+ </select>
76
+ {status ? <p className="mw-demo-compare__status">{status}</p> : null}
77
+ </div>
78
+ ) : null}
79
+ </div>
80
+ )
81
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<DemoControls>` — the geocoder demo's floating control panel, assembled from the demo-specific units
7
+ * (VersionPicker / CompareToggle / BackendControl / PlaceAutocomplete / ResultPanel) and the REUSED
8
+ * pipeline units (QueryForm / PresetChips / LoadingIndicator). It is the presentational composition the
9
+ * `_app.tsx` control `<section>` (`:1222-1514`) becomes — it reads the injected {@link DemoRuntime} for
10
+ * ready/version/backend/loading state and threads the {@link useDemoGeocode} + {@link usePlaceAutocomplete}
11
+ * + {@link useCompareState} hooks into the controls. Host-specific visualizers arrive via {@link DemoPanels}.
12
+ *
13
+ * NODE-SAFE: pure React + the shared/demo units, no maplibre. It rides `@mailwoman/react/map` as a
14
+ * demo-specific control, not for WebGL.
15
+ */
16
+
17
+ import type { ReactNode } from "react"
18
+
19
+ import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
20
+ import { PresetChips, type Preset } from "../common/PresetChips.tsx"
21
+ import { QueryForm } from "../pipeline/QueryForm.tsx"
22
+ import { BackendControl } from "./BackendControl.tsx"
23
+ import { CompareToggle } from "./CompareToggle.tsx"
24
+ import { PlaceAutocomplete } from "./PlaceAutocomplete.tsx"
25
+ import { ResultPanel } from "./ResultPanel.tsx"
26
+ import type { DemoPanels, DemoRuntime } from "./types.ts"
27
+ import type { UseCompareState } from "./useCompareState.ts"
28
+ import type { UseDemoGeocode } from "./useDemoGeocode.ts"
29
+ import type { UsePlaceAutocomplete } from "./usePlaceAutocomplete.ts"
30
+ import { VersionPicker } from "./VersionPicker.tsx"
31
+
32
+ export interface DemoControlsProps {
33
+ /** The injected demo runtime. */
34
+ runtime: DemoRuntime
35
+ /** The parse+resolve state machine. */
36
+ geocode: UseDemoGeocode
37
+ /** The place-autocomplete combobox state. */
38
+ autocomplete: UsePlaceAutocomplete
39
+ /** The compare-mode state. */
40
+ compare: UseCompareState
41
+ /** Host-injected panels (about, release blurb, compare, permalink, extras, failure). */
42
+ panels: DemoPanels
43
+ /** Example chips. */
44
+ presets: ReadonlyArray<Preset>
45
+ /** Input placeholder. */
46
+ placeholder: string
47
+ /** Select a model version (the composed demo also clears a now-colliding compare selection). */
48
+ onSelectVersion: (version: string) => void
49
+ /** Toggle the forced WASM backend. */
50
+ onForceWASMChange: (forceWASM: boolean) => void
51
+ }
52
+
53
+ /** The floating control panel. */
54
+ export function DemoControls({
55
+ runtime,
56
+ geocode,
57
+ autocomplete,
58
+ compare,
59
+ panels,
60
+ presets,
61
+ placeholder,
62
+ onSelectVersion,
63
+ onForceWASMChange,
64
+ }: DemoControlsProps): ReactNode {
65
+ const versions = runtime.availableVersions ?? []
66
+ const { busy, result, selectedCandidate } = geocode
67
+ const loading = runtime.loading
68
+ const errorMessage = geocode.parseError ?? runtime.errorMessage ?? null
69
+
70
+ return (
71
+ <section className="mw-demo-controls">
72
+ {panels.header}
73
+ {panels.releaseInfo}
74
+
75
+ <VersionPicker
76
+ versions={versions}
77
+ selected={runtime.selectedVersion ?? null}
78
+ onSelect={onSelectVersion}
79
+ disabled={busy}
80
+ />
81
+
82
+ <BackendControl
83
+ activeBackend={runtime.activeBackend}
84
+ forceWASM={runtime.forceWASM ?? false}
85
+ onForceWASMChange={onForceWASMChange}
86
+ />
87
+
88
+ <CompareToggle
89
+ versions={versions}
90
+ primaryVersion={runtime.selectedVersion ?? null}
91
+ compareMode={compare.compareMode}
92
+ onCompareModeChange={compare.setCompareMode}
93
+ compareVersion={compare.compareVersion}
94
+ onCompareVersionChange={compare.setCompareVersion}
95
+ disabled={busy}
96
+ />
97
+
98
+ <QueryForm
99
+ value={geocode.text}
100
+ onChange={geocode.setText}
101
+ onSubmit={geocode.submit}
102
+ disabled={!runtime.ready}
103
+ busy={busy}
104
+ placeholder={placeholder}
105
+ onKeyDown={autocomplete.onInputKeyDown}
106
+ inputProps={autocomplete.inputProps}
107
+ />
108
+
109
+ {panels.bias}
110
+
111
+ <PlaceAutocomplete
112
+ suggestions={autocomplete.suggestions}
113
+ activeIndex={autocomplete.activeIndex}
114
+ onPick={autocomplete.pick}
115
+ onHover={autocomplete.setActiveIndex}
116
+ listboxId={autocomplete.listboxId}
117
+ optionId={autocomplete.optionId}
118
+ />
119
+
120
+ <PresetChips
121
+ presets={presets}
122
+ disabled={!runtime.ready || busy}
123
+ onPick={(value) => {
124
+ geocode.setText(value)
125
+ geocode.reset()
126
+ }}
127
+ trailing={panels.permalink ? panels.permalink(geocode.text) : null}
128
+ />
129
+
130
+ {loading && !runtime.ready ? (
131
+ <LoadingIndicator
132
+ mode="staged"
133
+ steps={loading.stepLabels.length > 0 ? loading.stepLabels : undefined}
134
+ activeStep={loading.stepIndex}
135
+ label={loading.progress}
136
+ />
137
+ ) : null}
138
+ {errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
139
+
140
+ {panels.aboveResult ? panels.aboveResult({ result }) : null}
141
+
142
+ {busy ? (
143
+ <div className="mw-result">
144
+ <LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
145
+ </div>
146
+ ) : result ? (
147
+ panels.result ? (
148
+ panels.result({
149
+ result,
150
+ selectedCandidate,
151
+ selectedCandidateIndex: geocode.selectedCandidateIndex,
152
+ onSelectCandidate: geocode.selectCandidate,
153
+ })
154
+ ) : (
155
+ <ResultPanel
156
+ result={result}
157
+ selectedCandidate={selectedCandidate}
158
+ selectedCandidateIndex={geocode.selectedCandidateIndex}
159
+ onSelectCandidate={geocode.selectCandidate}
160
+ extras={panels.extras}
161
+ failure={panels.failure}
162
+ />
163
+ )
164
+ ) : null}
165
+
166
+ {panels.compare
167
+ ? panels.compare({ result, compareMode: compare.compareMode, compareVersion: compare.compareVersion })
168
+ : null}
169
+
170
+ {panels.footer}
171
+ </section>
172
+ )
173
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<DemoMap>` — the declarative map shell for the geocoder demo, over `react-map-gl/maplibre` (v8).
7
+ * Phase 1 is the shell only: a `<Map>` that takes a host-composed `mapStyle`, an initial (or
8
+ * controlled) view state, and a `children` slot for the overlays/markers that land in later phases.
9
+ * No overlay/marker/camera logic lives here yet.
10
+ *
11
+ * NODE-IMPORT SAFETY: this module imports `react-map-gl/maplibre` (which pulls `maplibre-gl` — WebGL
12
+ * + DOM at import) at module scope, so it is reachable ONLY through the `@mailwoman/react/map`
13
+ * subpath, never from the package root (`index.ts`). The bare `import("@mailwoman/react")` a node
14
+ * consumer (or the CI smoke IMPORT_CHECK) runs must never transitively load this file. Keep it out of
15
+ * the root barrel.
16
+ *
17
+ * CSS: the package imports no CSS from its modules (the node-safe invariant), so a host that renders
18
+ * `<DemoMap>` must import `maplibre-gl/dist/maplibre-gl.css` itself (plus `@mailwoman/react/styles.css`
19
+ * for the `.mw-demo-map` container). `<DemoMap>` is intrinsically a client component — a host that
20
+ * server-renders should wrap it in a client boundary (the package's `ClientOnly`), exactly as the
21
+ * composed `GeocoderDemo` will in a later phase.
22
+ */
23
+
24
+ import type { CSSProperties, ReactNode, Ref } from "react"
25
+ import { Map } from "react-map-gl/maplibre"
26
+ import type { MapProps, MapRef, ViewStateChangeEvent } from "react-map-gl/maplibre"
27
+
28
+ /** The style a `<DemoMap>` renders — a style URL or an inline/composed `StyleSpecification`. */
29
+ export type DemoMapStyle = NonNullable<MapProps["mapStyle"]>
30
+
31
+ /**
32
+ * Escape hatch for the `<Map>` props `<DemoMap>` does not surface explicitly (e.g. `minZoom`, `attributionControl`,
33
+ * `maplibreLogo`). The controlled fields DemoMap owns are omitted so they can't be set twice.
34
+ */
35
+ export type DemoMapExtraProps = Partial<
36
+ Omit<MapProps, "mapStyle" | "initialViewState" | "viewState" | "onMove" | "children" | "style" | "ref">
37
+ >
38
+
39
+ export interface DemoMapProps {
40
+ /** The map style — host-composed `StyleSpecification` or a style URL. */
41
+ mapStyle: DemoMapStyle
42
+ /** Uncontrolled initial camera. Use this OR `viewState`, not both. */
43
+ initialViewState?: MapProps["initialViewState"]
44
+ /** Controlled camera. Pair with `onMove` to persist it. */
45
+ viewState?: MapProps["viewState"]
46
+ /** Fired on every camera change (drag/zoom/rotate) — the seam for viewport-bias persistence. */
47
+ onMove?: (event: ViewStateChangeEvent) => void
48
+ /** Map projection. @default "globe" (matches the docs `DashboardMap`). */
49
+ projection?: MapProps["projection"]
50
+ /** Forwarded to the underlying `<Map>` for imperative access (`useMap`/`flyTo` in later phases). */
51
+ mapRef?: Ref<MapRef>
52
+ /** Overlays, markers, and controls — rendered as `<Map>` children. Empty in phase 1. */
53
+ children?: ReactNode
54
+ /** Class on the wrapper element. */
55
+ className?: string
56
+ /** Inline style on the wrapper element (sizing lives here or on `.mw-demo-map`). */
57
+ style?: CSSProperties
58
+ /** Remaining `<Map>` props DemoMap does not surface explicitly. */
59
+ mapProps?: DemoMapExtraProps
60
+ }
61
+
62
+ const FILL: CSSProperties = { width: "100%", height: "100%" }
63
+
64
+ /**
65
+ * The controlled-viewport map shell. Renders a sized wrapper around a `react-map-gl/maplibre` `<Map>`; everything
66
+ * host-specific (the composed `mapStyle`, the initial center) is injected, and overlays ride in as `children`.
67
+ */
68
+ export function DemoMap({
69
+ mapStyle,
70
+ initialViewState,
71
+ viewState,
72
+ onMove,
73
+ projection = "globe",
74
+ mapRef,
75
+ children,
76
+ className,
77
+ style,
78
+ mapProps,
79
+ }: DemoMapProps): ReactNode {
80
+ const wrapperClassName = className ? `mw-demo-map ${className}` : "mw-demo-map"
81
+
82
+ return (
83
+ <div className={wrapperClassName} style={style}>
84
+ <Map
85
+ {...mapProps}
86
+ ref={mapRef}
87
+ mapStyle={mapStyle}
88
+ initialViewState={initialViewState}
89
+ viewState={viewState}
90
+ onMove={onMove}
91
+ projection={projection}
92
+ style={FILL}
93
+ >
94
+ {children}
95
+ </Map>
96
+ </div>
97
+ )
98
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<GeocoderDemo>` — the WHOLE geocoder demo, composed. It is the map analogue of `PipelineExplorer`
7
+ * and takes the SAME DI seam 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 /
9
+ * About / Permalink). Everything here is composition + a `ClientOnly` boundary:
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
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),
15
+ * {@link usePlaceAutocomplete}, {@link useCompareState}.
16
+ *
17
+ * Because it pulls {@link DemoMap} (→ `react-map-gl` → `maplibre-gl`, WebGL + DOM at import), it lives on
18
+ * the `@mailwoman/react/map` subpath ONLY — never the package root. The whole thing renders in Storybook
19
+ * over a fake runtime (offline stub style + canned geocode) with no network, no ONNX, no gazetteer.
20
+ */
21
+
22
+ import { type ReactNode, useCallback, useRef } from "react"
23
+ import type { MapRef } from "react-map-gl/maplibre"
24
+
25
+ import { ClientOnly } from "../common/ClientOnly.tsx"
26
+ import { type Preset } from "../common/PresetChips.tsx"
27
+ import { DemoControls } from "./DemoControls.tsx"
28
+ import { DemoMap } from "./DemoMap.tsx"
29
+ import { OverlayLayers } from "./OverlayLayers.tsx"
30
+ import { ResolvedPlaceLayers } from "./ResolvedPlaceLayers.tsx"
31
+ import type { DemoPanels, DemoRuntime, MapBias } from "./types.ts"
32
+ import { useCompareState } from "./useCompareState.ts"
33
+ import { useDemoGeocode } from "./useDemoGeocode.ts"
34
+ import { useMapPlaceRender } from "./useMapPlaceRender.ts"
35
+ import { usePlaceAutocomplete } from "./usePlaceAutocomplete.ts"
36
+
37
+ export interface GeocoderDemoProps {
38
+ /** The injected demo runtime (map style + overlays + parse + version/backend). */
39
+ runtime: DemoRuntime
40
+ /** Host-injected panels (about, release blurb, compare, permalink, debug drawer, map controls, …). */
41
+ panels?: DemoPanels
42
+ /** Address to pre-fill. */
43
+ defaultAddress?: string
44
+ /** Example chips. @default the empty set (host supplies its own). */
45
+ presets?: ReadonlyArray<Preset>
46
+ /**
47
+ * Only hint the viewport bias once the visitor has zoomed past the global view — a whole-globe center is noise.
48
+ * Matches the demo's `map.getZoom() >= 4` gate. @default 4
49
+ */
50
+ minBiasZoom?: number
51
+ /**
52
+ * Fly/fit the map to the resolved place on each result (via {@link ResolvedPlaceLayers}). @default true. Set false
53
+ * for a host that drives the camera itself (a controlled `<DemoMap viewState>`), or to keep a headless test
54
+ * deterministic — the marker + outline still render, only the animated camera move is skipped.
55
+ */
56
+ applyResultCamera?: boolean
57
+ }
58
+
59
+ interface GeocoderDemoInnerProps extends Required<
60
+ Pick<GeocoderDemoProps, "runtime" | "defaultAddress" | "minBiasZoom" | "applyResultCamera">
61
+ > {
62
+ panels: DemoPanels
63
+ presets: ReadonlyArray<Preset>
64
+ }
65
+
66
+ function GeocoderDemoInner({
67
+ runtime,
68
+ panels,
69
+ defaultAddress,
70
+ presets,
71
+ minBiasZoom,
72
+ applyResultCamera,
73
+ }: GeocoderDemoInnerProps): ReactNode {
74
+ const mapRef = useRef<MapRef>(null)
75
+
76
+ // Read the viewport bias at submit time — through the map handle, never a threaded state value, so granting/zooming
77
+ // mid-session doesn't re-create the parse callback. Below the min-bias zoom, a whole-globe center is noise → null.
78
+ const getBias = useCallback((): MapBias | null => {
79
+ const map = mapRef.current?.getMap()
80
+
81
+ if (!map) return null
82
+ const zoom = map.getZoom()
83
+
84
+ if (zoom < minBiasZoom) return null
85
+ const center = map.getCenter()
86
+
87
+ return { center: [center.lng, center.lat], zoom }
88
+ }, [minBiasZoom])
89
+
90
+ const geocode = useDemoGeocode({ runtime, defaultText: defaultAddress, getBias })
91
+ const compare = useCompareState()
92
+ const autocomplete = usePlaceAutocomplete({
93
+ text: geocode.text,
94
+ setText: geocode.setText,
95
+ autocomplete: runtime.autocomplete,
96
+ })
97
+
98
+ const spec = useMapPlaceRender(geocode.mapPlace)
99
+
100
+ const onSelectVersion = useCallback(
101
+ (version: string) => {
102
+ runtime.selectVersion?.(version)
103
+ compare.clearIfPrimary(version)
104
+ },
105
+ [runtime, compare]
106
+ )
107
+
108
+ const onForceWASMChange = useCallback((forceWASM: boolean) => runtime.setForceWASM?.(forceWASM), [runtime])
109
+
110
+ return (
111
+ <div className="mw-geocoder-demo">
112
+ <div className="mw-geocoder-demo__map">
113
+ <DemoMap
114
+ mapStyle={runtime.mapStyle}
115
+ mapRef={mapRef}
116
+ initialViewState={{
117
+ longitude: runtime.initialCenter[0],
118
+ latitude: runtime.initialCenter[1],
119
+ zoom: runtime.initialZoom ?? 3,
120
+ }}
121
+ style={{ width: "100%", height: "100%" }}
122
+ // Match the live demo's chrome: one compact attribution pill (the map's own default is a wide, always-open
123
+ // "MapLibre | © …" bar), no maplibre wordmark logo.
124
+ mapProps={{ attributionControl: { compact: true }, maplibreLogo: false }}
125
+ >
126
+ <OverlayLayers overlays={runtime.overlays} />
127
+ <ResolvedPlaceLayers spec={spec} applyCamera={applyResultCamera} />
128
+ {panels.mapControls}
129
+ </DemoMap>
130
+ </div>
131
+
132
+ <DemoControls
133
+ runtime={runtime}
134
+ geocode={geocode}
135
+ autocomplete={autocomplete}
136
+ compare={compare}
137
+ panels={panels}
138
+ presets={presets}
139
+ placeholder={defaultAddress}
140
+ onSelectVersion={onSelectVersion}
141
+ onForceWASMChange={onForceWASMChange}
142
+ />
143
+
144
+ {panels.debugDrawer ? panels.debugDrawer({ result: geocode.result }) : null}
145
+ </div>
146
+ )
147
+ }
148
+
149
+ /** The composed geocoder demo, behind a `ClientOnly` SSR boundary (the map is intrinsically a client component). */
150
+ export function GeocoderDemo({
151
+ runtime,
152
+ panels = {},
153
+ defaultAddress = "",
154
+ presets = [],
155
+ minBiasZoom = 4,
156
+ applyResultCamera = true,
157
+ }: GeocoderDemoProps): ReactNode {
158
+ return (
159
+ <ClientOnly
160
+ fallback={
161
+ <div className="mw-geocoder-demo">
162
+ <p>Loading demo…</p>
163
+ </div>
164
+ }
165
+ >
166
+ {() => (
167
+ <GeocoderDemoInner
168
+ runtime={runtime}
169
+ panels={panels}
170
+ defaultAddress={defaultAddress}
171
+ presets={presets}
172
+ minBiasZoom={minBiasZoom}
173
+ applyResultCamera={applyResultCamera}
174
+ />
175
+ )}
176
+ </ClientOnly>
177
+ )
178
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `<OverlayLayers>` — renders the host-supplied {@link OverlaySpec}s (coverage "fog of war",
7
+ * race-dots, …) as declarative `<Source>` + `<Layer>`s. This replaces the demo's imperative
8
+ * `map.addSource` / `map.addLayer` overlay loops (and their `isStyleLoaded()` / `styledata` gates):
9
+ * the host composes the specs, the package renders them, react-map-gl owns the add/remove lifecycle.
10
+ *
11
+ * Each overlay's `visible` flag flips the layers' `visibility` layout property — the declarative seam a
12
+ * layer-toggle control drives in a later phase. NODE-IMPORT SAFETY: imports `react-map-gl/maplibre` —
13
+ * reachable only via `@mailwoman/react/map`.
14
+ */
15
+
16
+ import { Fragment, type ReactNode } from "react"
17
+ import { Layer, Source } from "react-map-gl/maplibre"
18
+
19
+ import type { OverlaySpec } from "./types.ts"
20
+
21
+ export interface OverlayLayersProps {
22
+ /** The overlays to render, in draw order (first is drawn first / lowest). */
23
+ overlays?: OverlaySpec[]
24
+ }
25
+
26
+ /** Render each overlay as one `<Source>` and its `<Layer>`s, honoring the `visible` flag via `visibility`. */
27
+ export function OverlayLayers({ overlays }: OverlayLayersProps): ReactNode {
28
+ if (!overlays || overlays.length === 0) return null
29
+
30
+ return (
31
+ <>
32
+ {overlays.map((overlay) => {
33
+ const visibility = overlay.visible === false ? "none" : "visible"
34
+
35
+ return (
36
+ <Fragment key={overlay.id}>
37
+ <Source id={overlay.id} {...overlay.source} />
38
+ {overlay.layers.map((layer) => (
39
+ <Layer key={layer.id} {...layer} layout={{ ...layer.layout, visibility }} />
40
+ ))}
41
+ </Fragment>
42
+ )
43
+ })}
44
+ </>
45
+ )
46
+ }