@mailwoman/react 7.5.0 → 7.7.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.
- package/common/PresetChips.tsx +4 -1
- package/index.ts +28 -0
- package/map/BackendControl.tsx +39 -0
- package/map/CompareToggle.tsx +81 -0
- package/map/DemoControls.tsx +173 -0
- package/map/DemoMap.tsx +98 -0
- package/map/GeocoderDemo.tsx +178 -0
- package/map/OverlayLayers.tsx +46 -0
- package/map/PlaceAutocomplete.tsx +69 -0
- package/map/PlaceMarker.tsx +40 -0
- package/map/ResolvedPlaceLayers.tsx +63 -0
- package/map/ResultCamera.tsx +60 -0
- package/map/ResultOverlay.tsx +61 -0
- package/map/ResultPanel.tsx +84 -0
- package/map/VersionPicker.tsx +62 -0
- package/map/geometry.ts +135 -0
- package/map/index.ts +73 -0
- package/map/place-render.ts +157 -0
- package/map/types.ts +207 -0
- package/map/useCompareState.ts +45 -0
- package/map/useDemoGeocode.ts +67 -0
- package/map/useMapPlaceRender.ts +21 -0
- package/map/usePlaceAutocomplete.ts +195 -0
- package/out/common/PresetChips.d.ts +3 -1
- package/out/common/PresetChips.d.ts.map +1 -1
- package/out/common/PresetChips.js +2 -2
- package/out/common/PresetChips.js.map +1 -1
- package/out/index.d.ts +3 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +2 -0
- package/out/index.js.map +1 -1
- package/out/map/BackendControl.d.ts +24 -0
- package/out/map/BackendControl.d.ts.map +1 -0
- package/out/map/BackendControl.js +6 -0
- package/out/map/BackendControl.js.map +1 -0
- package/out/map/CompareToggle.d.ts +35 -0
- package/out/map/CompareToggle.d.ts.map +1 -0
- package/out/map/CompareToggle.js +9 -0
- package/out/map/CompareToggle.js.map +1 -0
- package/out/map/DemoControls.d.ts +44 -0
- package/out/map/DemoControls.d.ts.map +1 -0
- package/out/map/DemoControls.js +28 -0
- package/out/map/DemoControls.js.map +1 -0
- package/out/map/DemoMap.d.ts +59 -0
- package/out/map/DemoMap.d.ts.map +1 -0
- package/out/map/DemoMap.js +12 -0
- package/out/map/DemoMap.js.map +1 -0
- package/out/map/GeocoderDemo.d.ts +47 -0
- package/out/map/GeocoderDemo.d.ts.map +1 -0
- package/out/map/GeocoderDemo.js +73 -0
- package/out/map/GeocoderDemo.js.map +1 -0
- package/out/map/OverlayLayers.d.ts +23 -0
- package/out/map/OverlayLayers.d.ts.map +1 -0
- package/out/map/OverlayLayers.js +27 -0
- package/out/map/OverlayLayers.js.map +1 -0
- package/out/map/PlaceAutocomplete.d.ts +34 -0
- package/out/map/PlaceAutocomplete.d.ts.map +1 -0
- package/out/map/PlaceAutocomplete.js +9 -0
- package/out/map/PlaceAutocomplete.js.map +1 -0
- package/out/map/PlaceMarker.d.ts +27 -0
- package/out/map/PlaceMarker.d.ts.map +1 -0
- package/out/map/PlaceMarker.js +9 -0
- package/out/map/PlaceMarker.js.map +1 -0
- package/out/map/ResolvedPlaceLayers.d.ts +34 -0
- package/out/map/ResolvedPlaceLayers.d.ts.map +1 -0
- package/out/map/ResolvedPlaceLayers.js +11 -0
- package/out/map/ResolvedPlaceLayers.js.map +1 -0
- package/out/map/ResultCamera.d.ts +34 -0
- package/out/map/ResultCamera.d.ts.map +1 -0
- package/out/map/ResultCamera.js +43 -0
- package/out/map/ResultCamera.js.map +1 -0
- package/out/map/ResultOverlay.d.ts +34 -0
- package/out/map/ResultOverlay.d.ts.map +1 -0
- package/out/map/ResultOverlay.js +18 -0
- package/out/map/ResultOverlay.js.map +1 -0
- package/out/map/ResultPanel.d.ts +34 -0
- package/out/map/ResultPanel.d.ts.map +1 -0
- package/out/map/ResultPanel.js +12 -0
- package/out/map/ResultPanel.js.map +1 -0
- package/out/map/VersionPicker.d.ts +30 -0
- package/out/map/VersionPicker.d.ts.map +1 -0
- package/out/map/VersionPicker.js +8 -0
- package/out/map/VersionPicker.js.map +1 -0
- package/out/map/geometry.d.ts +55 -0
- package/out/map/geometry.d.ts.map +1 -0
- package/out/map/geometry.js +99 -0
- package/out/map/geometry.js.map +1 -0
- package/out/map/index.d.ts +52 -0
- package/out/map/index.d.ts.map +1 -0
- package/out/map/index.js +36 -0
- package/out/map/index.js.map +1 -0
- package/out/map/place-render.d.ts +85 -0
- package/out/map/place-render.d.ts.map +1 -0
- package/out/map/place-render.js +98 -0
- package/out/map/place-render.js.map +1 -0
- package/out/map/types.d.ts +193 -0
- package/out/map/types.d.ts.map +1 -0
- package/out/map/types.js +18 -0
- package/out/map/types.js.map +1 -0
- package/out/map/useCompareState.d.ts +25 -0
- package/out/map/useCompareState.d.ts.map +1 -0
- package/out/map/useCompareState.js +27 -0
- package/out/map/useCompareState.js.map +1 -0
- package/out/map/useDemoGeocode.d.ts +36 -0
- package/out/map/useDemoGeocode.d.ts.map +1 -0
- package/out/map/useDemoGeocode.js +44 -0
- package/out/map/useDemoGeocode.js.map +1 -0
- package/out/map/useMapPlaceRender.d.ts +15 -0
- package/out/map/useMapPlaceRender.d.ts.map +1 -0
- package/out/map/useMapPlaceRender.js +18 -0
- package/out/map/useMapPlaceRender.js.map +1 -0
- package/out/map/usePlaceAutocomplete.d.ts +61 -0
- package/out/map/usePlaceAutocomplete.d.ts.map +1 -0
- package/out/map/usePlaceAutocomplete.js +122 -0
- package/out/map/usePlaceAutocomplete.js.map +1 -0
- package/out/pipeline/QueryForm.d.ts +6 -2
- package/out/pipeline/QueryForm.d.ts.map +1 -1
- package/out/pipeline/QueryForm.js +2 -2
- package/out/pipeline/QueryForm.js.map +1 -1
- package/out/runtime/useDemoRuntime.d.ts +113 -0
- package/out/runtime/useDemoRuntime.d.ts.map +1 -0
- package/out/runtime/useDemoRuntime.js +135 -0
- package/out/runtime/useDemoRuntime.js.map +1 -0
- package/package.json +27 -6
- package/pipeline/QueryForm.tsx +17 -2
- package/runtime/useDemoRuntime.ts +245 -0
- package/styles.css +145 -0
- package/vitest.node.config.ts +20 -0
package/common/PresetChips.tsx
CHANGED
|
@@ -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
|
@@ -93,3 +93,31 @@ export type {
|
|
|
93
93
|
} from "./pipeline/types.ts"
|
|
94
94
|
export { useParsePipeline } from "./pipeline/useParsePipeline.ts"
|
|
95
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
|
+
}
|
package/map/DemoMap.tsx
ADDED
|
@@ -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
|
+
}
|