@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.
- package/common/PresetChips.tsx +4 -1
- package/index.ts +31 -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 +4 -1
- 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/poi/LiveResultsBlock.d.ts +3 -3
- package/out/poi/LiveResultsBlock.d.ts.map +1 -1
- package/out/poi/LiveResultsBlock.js +2 -2
- package/out/poi/LiveResultsBlock.js.map +1 -1
- package/out/poi/POIExplorer.d.ts +7 -1
- package/out/poi/POIExplorer.d.ts.map +1 -1
- package/out/poi/POIExplorer.js +17 -5
- package/out/poi/POIExplorer.js.map +1 -1
- package/out/poi/SubjectPanel.d.ts +3 -2
- package/out/poi/SubjectPanel.d.ts.map +1 -1
- package/out/poi/SubjectPanel.js +8 -1
- package/out/poi/SubjectPanel.js.map +1 -1
- package/out/poi/runtime.d.ts +13 -4
- package/out/poi/runtime.d.ts.map +1 -1
- package/out/poi/runtime.js +36 -12
- package/out/poi/runtime.js.map +1 -1
- package/out/poi/types.d.ts +31 -4
- package/out/poi/types.d.ts.map +1 -1
- package/out/poi/usePOISearch.d.ts +8 -1
- package/out/poi/usePOISearch.d.ts.map +1 -1
- package/out/poi/usePOISearch.js +47 -12
- package/out/poi/usePOISearch.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 +31 -10
- package/pipeline/QueryForm.tsx +17 -2
- package/poi/LiveResultsBlock.tsx +4 -4
- package/poi/POIExplorer.tsx +39 -8
- package/poi/SubjectPanel.tsx +47 -12
- package/poi/runtime.ts +35 -10
- package/poi/types.ts +34 -4
- package/poi/usePOISearch.ts +62 -11
- package/runtime/useDemoRuntime.ts +245 -0
- package/styles.css +166 -0
- package/vitest.node.config.ts +20 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `useDemoRuntime` — the headless load-orchestration hook shared by every mailwoman demo surface (the
|
|
7
|
+
* inline doc-embeds via `DemoEmbed`, and the geocoder map page). It owns the version-selection state
|
|
8
|
+
* machine, the per-version load SEQUENCING, cancellation, and the ready / loading / error state — but
|
|
9
|
+
* NOTHING model- or map-specific. The actual asset fetchers (the ONNX classifier factory, the httpvfs
|
|
10
|
+
* WOF opener, the FST fetch, the releases.json fetch) are INJECTED by the host as async functions, so
|
|
11
|
+
* this module imports only React: no `onnxruntime-web`, no `sql.js-httpvfs`, no `maplibre-gl`, no
|
|
12
|
+
* `fetch`-specific plumbing. That keeps it node-import-safe and root-exportable from
|
|
13
|
+
* `@mailwoman/react` — the exact seam `PipelineRuntime` established, generalized to the loader itself.
|
|
14
|
+
*
|
|
15
|
+
* The hook is generic over `TAssets` (the opaque bundle the host's `loadAssets` returns — classifier,
|
|
16
|
+
* FST, WOF lookup, calibrator, …) and `TRelease` (the host's release-manifest entry). The package
|
|
17
|
+
* never inspects either; it just holds, reveals, and re-loads them across version/backend switches.
|
|
18
|
+
*/
|
|
19
|
+
/** The minimal contract a release-manifest entry must satisfy. Hosts extend this with their own fields. */
|
|
20
|
+
export interface DemoReleaseBase {
|
|
21
|
+
/** The version tag this entry describes (matched against `selectedVersion`). */
|
|
22
|
+
version: string;
|
|
23
|
+
/** Optional display label a version picker shows; falls back to `version`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The releases manifest the host fetches — the default version plus the selectable release entries. */
|
|
27
|
+
export interface DemoManifest<TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
28
|
+
/** The version selected on first load (before the user picks another). */
|
|
29
|
+
defaultVersion: string;
|
|
30
|
+
/** Every selectable release. */
|
|
31
|
+
releases: TRelease[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The progress channel handed to the host's `loadAssets`. The host reports load progress + the resolved backend + the
|
|
35
|
+
* staged step labels/index THROUGH these setters (all no-op once the load is superseded/aborted), while the hook owns
|
|
36
|
+
* the terminal state (revealing the assets + clearing progress on success, surfacing the error on failure).
|
|
37
|
+
*/
|
|
38
|
+
export interface DemoAssetsLoadContext {
|
|
39
|
+
/** Aborts when this load is superseded (version/backend switch) or the provider unmounts. */
|
|
40
|
+
signal: AbortSignal;
|
|
41
|
+
/** Whether the host should force the CPU/WASM backend (opt out of WebGPU) for this load. */
|
|
42
|
+
forceWASM: boolean;
|
|
43
|
+
/** Set the human-readable progress line (e.g. `Loading v7 model (~28 MB)…`). */
|
|
44
|
+
setProgress: (progress: string) => void;
|
|
45
|
+
/** Set the staged-loader step labels (e.g. `["Loading classifier", "Loading FST gazetteer"]`). */
|
|
46
|
+
setStepLabels: (labels: string[]) => void;
|
|
47
|
+
/** Advance the staged-loader step index (0-based). */
|
|
48
|
+
setStepIndex: (index: number) => void;
|
|
49
|
+
/** Report the backend the neural runtime resolved to (e.g. `webgpu (27 MB int8)`). */
|
|
50
|
+
setBackend: (backend: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/** The injected loaders the hook orchestrates. Nothing here is model- or map-aware — the host owns all that. */
|
|
53
|
+
export interface DemoRuntimeConfig<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
54
|
+
/**
|
|
55
|
+
* Fetch + normalize the releases manifest. Returns `null` when no manifest is available (the demo then shows nothing
|
|
56
|
+
* selectable). Rejecting surfaces `errorMessage`. Runs once on mount.
|
|
57
|
+
*/
|
|
58
|
+
loadManifest: (signal: AbortSignal) => Promise<DemoManifest<TRelease> | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Load the full asset bundle for one release — the classifier, FST, WOF lookup, calibrator, whatever the host needs.
|
|
61
|
+
* Runs on every version or `forceWASM` change. Report progress via `ctx`; return the bundle. Rejecting surfaces
|
|
62
|
+
* `errorMessage`. Bail early when `ctx.signal.aborted` — the hook discards a superseded result regardless.
|
|
63
|
+
*/
|
|
64
|
+
loadAssets: (release: TRelease, ctx: DemoAssetsLoadContext) => Promise<TAssets>;
|
|
65
|
+
/** The progress line shown before the manifest arrives. @default "Loading releases…" */
|
|
66
|
+
initialProgress?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The state `useDemoRuntime` produces — the load-orchestration state a demo surface renders + re-projects.
|
|
70
|
+
*
|
|
71
|
+
* NAMING (phase 4): this is the LOADER STATE, deliberately distinct from {@link DemoRuntime} (the injected runtime
|
|
72
|
+
* contract `<GeocoderDemo>` consumes). The two were same-stem before (`UseDemoRuntime` vs `DemoRuntime`); phase 4
|
|
73
|
+
* disambiguates by renaming the loader-state type to `DemoLoaderState`. A host builds a {@link DemoRuntime} by pairing
|
|
74
|
+
* this loader state (assets + backend + version) with the map surface (style, overlays, bias, parse) — see the map
|
|
75
|
+
* subpath's `DemoRuntime`.
|
|
76
|
+
*/
|
|
77
|
+
export interface DemoLoaderState<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
78
|
+
/** The releases manifest, once fetched. */
|
|
79
|
+
manifest: DemoManifest<TRelease> | null;
|
|
80
|
+
/** The currently-selected version, or `null` before the manifest resolves. */
|
|
81
|
+
selectedVersion: string | null;
|
|
82
|
+
/** The release entry matching `selectedVersion` (convenience over `manifest.releases.find`). */
|
|
83
|
+
selectedRelease: TRelease | null;
|
|
84
|
+
/** The loaded asset bundle for the selected version, or `null` while (re)loading. */
|
|
85
|
+
assets: TAssets | null;
|
|
86
|
+
/** Whether the asset bundle is loaded and ready to use. */
|
|
87
|
+
ready: boolean;
|
|
88
|
+
/** Human-readable load progress (`""` when idle/ready). */
|
|
89
|
+
loadingProgress: string;
|
|
90
|
+
/** Staged-loader step index (0-based; `-1` before the first step). */
|
|
91
|
+
loadingStepIndex: number;
|
|
92
|
+
/** Staged-loader step labels. */
|
|
93
|
+
loadingStepLabels: string[];
|
|
94
|
+
/** A load error (manifest or asset), distinct from any per-parse error a consumer tracks separately. */
|
|
95
|
+
errorMessage: string | null;
|
|
96
|
+
/** The backend the neural runtime resolved to (e.g. `webgpu (27 MB int8)`), or `""` before it's known. */
|
|
97
|
+
activeBackend: string;
|
|
98
|
+
/** Whether the CPU/WASM backend is currently forced. */
|
|
99
|
+
forceWASM: boolean;
|
|
100
|
+
/** Switch to a different version (clears any error, then reloads the asset bundle). */
|
|
101
|
+
selectVersion: (version: string) => void;
|
|
102
|
+
/** Force (or unforce) the CPU/WASM backend — reloads the asset bundle. */
|
|
103
|
+
setForceWASM: (forceWASM: boolean) => void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Drive the shared version → asset-bundle load state machine over a host-injected loader.
|
|
107
|
+
*
|
|
108
|
+
* Sequence: on mount `loadManifest` runs and its `defaultVersion` becomes the selection; each version (or `forceWASM`)
|
|
109
|
+
* change reloads the bundle via `loadAssets`, the previous load aborted first. The assets are revealed ATOMICALLY when
|
|
110
|
+
* `loadAssets` resolves (so `ready` flips exactly once per load), and consumers gate on `ready`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function useDemoRuntime<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase>(config: DemoRuntimeConfig<TAssets, TRelease>): DemoLoaderState<TAssets, TRelease>;
|
|
113
|
+
//# sourceMappingURL=useDemoRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDemoRuntime.d.ts","sourceRoot":"","sources":["../../runtime/useDemoRuntime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,2GAA2G;AAC3G,MAAM,WAAW,eAAe;IAC/B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,wGAAwG;AACxG,MAAM,WAAW,YAAY,CAAC,QAAQ,SAAS,eAAe,GAAG,eAAe;IAC/E,0EAA0E;IAC1E,cAAc,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,QAAQ,EAAE,QAAQ,EAAE,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,6FAA6F;IAC7F,MAAM,EAAE,WAAW,CAAA;IACnB,4FAA4F;IAC5F,SAAS,EAAE,OAAO,CAAA;IAClB,gFAAgF;IAChF,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,kGAAkG;IAClG,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IACzC,sDAAsD;IACtD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,sFAAsF;IACtF,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC;AAED,gHAAgH;AAChH,MAAM,WAAW,iBAAiB,CAAC,OAAO,EAAE,QAAQ,SAAS,eAAe,GAAG,eAAe;IAC7F;;;OAGG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAA;IAC7E;;;;OAIG;IACH,UAAU,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/E,wFAAwF;IACxF,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe,CAAC,OAAO,EAAE,QAAQ,SAAS,eAAe,GAAG,eAAe;IAC3F,2CAA2C;IAC3C,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAA;IACvC,8EAA8E;IAC9E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gGAAgG;IAChG,eAAe,EAAE,QAAQ,GAAG,IAAI,CAAA;IAChC,qFAAqF;IACrF,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,2DAA2D;IAC3D,KAAK,EAAE,OAAO,CAAA;IACd,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,CAAA;IACxB,iCAAiC;IACjC,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,wGAAwG;IACxG,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0GAA0G;IAC1G,aAAa,EAAE,MAAM,CAAA;IACrB,wDAAwD;IACxD,SAAS,EAAE,OAAO,CAAA;IAClB,uFAAuF;IACvF,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,0EAA0E;IAC1E,YAAY,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,SAAS,eAAe,GAAG,eAAe,EACzF,MAAM,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,GAC1C,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,CA2HpC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `useDemoRuntime` — the headless load-orchestration hook shared by every mailwoman demo surface (the
|
|
7
|
+
* inline doc-embeds via `DemoEmbed`, and the geocoder map page). It owns the version-selection state
|
|
8
|
+
* machine, the per-version load SEQUENCING, cancellation, and the ready / loading / error state — but
|
|
9
|
+
* NOTHING model- or map-specific. The actual asset fetchers (the ONNX classifier factory, the httpvfs
|
|
10
|
+
* WOF opener, the FST fetch, the releases.json fetch) are INJECTED by the host as async functions, so
|
|
11
|
+
* this module imports only React: no `onnxruntime-web`, no `sql.js-httpvfs`, no `maplibre-gl`, no
|
|
12
|
+
* `fetch`-specific plumbing. That keeps it node-import-safe and root-exportable from
|
|
13
|
+
* `@mailwoman/react` — the exact seam `PipelineRuntime` established, generalized to the loader itself.
|
|
14
|
+
*
|
|
15
|
+
* The hook is generic over `TAssets` (the opaque bundle the host's `loadAssets` returns — classifier,
|
|
16
|
+
* FST, WOF lookup, calibrator, …) and `TRelease` (the host's release-manifest entry). The package
|
|
17
|
+
* never inspects either; it just holds, reveals, and re-loads them across version/backend switches.
|
|
18
|
+
*/
|
|
19
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
20
|
+
/**
|
|
21
|
+
* Drive the shared version → asset-bundle load state machine over a host-injected loader.
|
|
22
|
+
*
|
|
23
|
+
* Sequence: on mount `loadManifest` runs and its `defaultVersion` becomes the selection; each version (or `forceWASM`)
|
|
24
|
+
* change reloads the bundle via `loadAssets`, the previous load aborted first. The assets are revealed ATOMICALLY when
|
|
25
|
+
* `loadAssets` resolves (so `ready` flips exactly once per load), and consumers gate on `ready`.
|
|
26
|
+
*/
|
|
27
|
+
export function useDemoRuntime(config) {
|
|
28
|
+
const { initialProgress = "Loading releases…" } = config;
|
|
29
|
+
const [manifest, setManifest] = useState(null);
|
|
30
|
+
const [selectedVersion, setSelectedVersion] = useState(null);
|
|
31
|
+
const [assets, setAssets] = useState(null);
|
|
32
|
+
const [loadingProgress, setLoadingProgress] = useState(initialProgress);
|
|
33
|
+
const [loadingStepIndex, setLoadingStepIndex] = useState(-1);
|
|
34
|
+
const [loadingStepLabels, setLoadingStepLabels] = useState([]);
|
|
35
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
36
|
+
const [activeBackend, setActiveBackend] = useState("");
|
|
37
|
+
const [forceWASM, setForceWASMState] = useState(false);
|
|
38
|
+
// Latest-ref the injected loaders: a host that re-creates them each render (an inline arrow) must NOT retrigger the
|
|
39
|
+
// load effects, which key ONLY on version/backend. The effects read `.current` at run time.
|
|
40
|
+
const loadManifestRef = useRef(config.loadManifest);
|
|
41
|
+
loadManifestRef.current = config.loadManifest;
|
|
42
|
+
const loadAssetsRef = useRef(config.loadAssets);
|
|
43
|
+
loadAssetsRef.current = config.loadAssets;
|
|
44
|
+
// Latest manifest for the version-load effect, so it can resolve the release WITHOUT depending on `manifest`
|
|
45
|
+
// identity — which would double-fire the load the instant the manifest first arrives (the selection transition
|
|
46
|
+
// null → defaultVersion already fires it once).
|
|
47
|
+
const manifestRef = useRef(null);
|
|
48
|
+
manifestRef.current = manifest;
|
|
49
|
+
// Mount: fetch the manifest, then select the default version.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
void (async () => {
|
|
53
|
+
try {
|
|
54
|
+
const data = await loadManifestRef.current(controller.signal);
|
|
55
|
+
if (controller.signal.aborted)
|
|
56
|
+
return;
|
|
57
|
+
if (data) {
|
|
58
|
+
setManifest(data);
|
|
59
|
+
setSelectedVersion(data.defaultVersion);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (controller.signal.aborted)
|
|
64
|
+
return;
|
|
65
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
return () => controller.abort();
|
|
69
|
+
}, []);
|
|
70
|
+
// Load the per-version asset bundle when the version (or the backend force) changes.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!selectedVersion)
|
|
73
|
+
return;
|
|
74
|
+
const release = manifestRef.current?.releases.find((r) => r.version === selectedVersion) ?? null;
|
|
75
|
+
if (!release)
|
|
76
|
+
return;
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const { signal } = controller;
|
|
79
|
+
const guard = (fn) => {
|
|
80
|
+
if (!signal.aborted) {
|
|
81
|
+
fn();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
void (async () => {
|
|
85
|
+
try {
|
|
86
|
+
setAssets(null);
|
|
87
|
+
setLoadingStepIndex(-1);
|
|
88
|
+
setLoadingStepLabels([]);
|
|
89
|
+
setActiveBackend("");
|
|
90
|
+
const ctx = {
|
|
91
|
+
signal,
|
|
92
|
+
forceWASM,
|
|
93
|
+
setProgress: (progress) => guard(() => setLoadingProgress(progress)),
|
|
94
|
+
setStepLabels: (labels) => guard(() => setLoadingStepLabels(labels)),
|
|
95
|
+
setStepIndex: (index) => guard(() => setLoadingStepIndex(index)),
|
|
96
|
+
setBackend: (backend) => guard(() => setActiveBackend(backend)),
|
|
97
|
+
};
|
|
98
|
+
const loaded = await loadAssetsRef.current(release, ctx);
|
|
99
|
+
if (signal.aborted)
|
|
100
|
+
return;
|
|
101
|
+
setAssets(loaded);
|
|
102
|
+
setLoadingProgress("");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (signal.aborted)
|
|
106
|
+
return;
|
|
107
|
+
setErrorMessage(error instanceof Error ? error.message : String(error));
|
|
108
|
+
setLoadingProgress("");
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return () => controller.abort();
|
|
112
|
+
}, [selectedVersion, forceWASM]);
|
|
113
|
+
const selectVersion = useCallback((version) => {
|
|
114
|
+
setSelectedVersion(version);
|
|
115
|
+
setErrorMessage(null);
|
|
116
|
+
}, []);
|
|
117
|
+
const setForceWASM = useCallback((next) => setForceWASMState(next), []);
|
|
118
|
+
const selectedRelease = useMemo(() => manifest?.releases.find((r) => r.version === selectedVersion) ?? null, [manifest, selectedVersion]);
|
|
119
|
+
return {
|
|
120
|
+
manifest,
|
|
121
|
+
selectedVersion,
|
|
122
|
+
selectedRelease,
|
|
123
|
+
assets,
|
|
124
|
+
ready: assets !== null,
|
|
125
|
+
loadingProgress,
|
|
126
|
+
loadingStepIndex,
|
|
127
|
+
loadingStepLabels,
|
|
128
|
+
errorMessage,
|
|
129
|
+
activeBackend,
|
|
130
|
+
forceWASM,
|
|
131
|
+
selectVersion,
|
|
132
|
+
setForceWASM,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=useDemoRuntime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDemoRuntime.js","sourceRoot":"","sources":["../../runtime/useDemoRuntime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AA6FzE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC7B,MAA4C;IAE5C,MAAM,EAAE,eAAe,GAAG,mBAAmB,EAAE,GAAG,MAAM,CAAA;IAExD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAgC,IAAI,CAAC,CAAA;IAC7E,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAC3E,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAA;IAC1D,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAS,eAAe,CAAC,CAAA;IAC/E,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5D,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAW,EAAE,CAAC,CAAA;IACxE,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACrE,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAS,EAAE,CAAC,CAAA;IAC9D,MAAM,CAAC,SAAS,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEtD,oHAAoH;IACpH,4FAA4F;IAC5F,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACnD,eAAe,CAAC,OAAO,GAAG,MAAM,CAAC,YAAY,CAAA;IAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IAC/C,aAAa,CAAC,OAAO,GAAG,MAAM,CAAC,UAAU,CAAA;IAEzC,6GAA6G;IAC7G,+GAA+G;IAC/G,gDAAgD;IAChD,MAAM,WAAW,GAAG,MAAM,CAAgC,IAAI,CAAC,CAAA;IAC/D,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAA;IAE9B,8DAA8D;IAC9D,SAAS,CAAC,GAAG,EAAE;QACd,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QAExC,KAAK,CAAC,KAAK,IAAI,EAAE;YAChB,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;gBAE7D,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAM;gBAErC,IAAI,IAAI,EAAE,CAAC;oBACV,WAAW,CAAC,IAAI,CAAC,CAAA;oBACjB,kBAAkB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;gBACxC,CAAC;YACF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAM;gBACrC,eAAe,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACxE,CAAC;QACF,CAAC,CAAC,EAAE,CAAA;QAEJ,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAChC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,qFAAqF;IACrF,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,eAAe;YAAE,OAAM;QAE5B,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,eAAe,CAAC,IAAI,IAAI,CAAA;QAEhG,IAAI,CAAC,OAAO;YAAE,OAAM;QAEpB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAA;QAC7B,MAAM,KAAK,GAAG,CAAC,EAAc,EAAE,EAAE;YAChC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACrB,EAAE,EAAE,CAAA;YACL,CAAC;QACF,CAAC,CAAA;QAED,KAAK,CAAC,KAAK,IAAI,EAAE;YAChB,IAAI,CAAC;gBACJ,SAAS,CAAC,IAAI,CAAC,CAAA;gBACf,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;gBACvB,oBAAoB,CAAC,EAAE,CAAC,CAAA;gBACxB,gBAAgB,CAAC,EAAE,CAAC,CAAA;gBAEpB,MAAM,GAAG,GAA0B;oBAClC,MAAM;oBACN,SAAS;oBACT,WAAW,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;oBACpE,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;oBACpE,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;oBAChE,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;iBAC/D,CAAA;gBAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;gBAExD,IAAI,MAAM,CAAC,OAAO;oBAAE,OAAM;gBAC1B,SAAS,CAAC,MAAM,CAAC,CAAA;gBACjB,kBAAkB,CAAC,EAAE,CAAC,CAAA;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,MAAM,CAAC,OAAO;oBAAE,OAAM;gBAC1B,eAAe,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;gBACvE,kBAAkB,CAAC,EAAE,CAAC,CAAA;YACvB,CAAC;QACF,CAAC,CAAC,EAAE,CAAA;QAEJ,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAChC,CAAC,EAAE,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC,CAAA;IAEhC,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,OAAe,EAAE,EAAE;QACrD,kBAAkB,CAAC,OAAO,CAAC,CAAA;QAC3B,eAAe,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhF,MAAM,eAAe,GAAG,OAAO,CAC9B,GAAG,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,eAAe,CAAC,IAAI,IAAI,EAC3E,CAAC,QAAQ,EAAE,eAAe,CAAC,CAC3B,CAAA;IAED,OAAO;QACN,QAAQ;QACR,eAAe;QACf,eAAe;QACf,MAAM;QACN,KAAK,EAAE,MAAM,KAAK,IAAI;QACtB,eAAe;QACf,gBAAgB;QAChB,iBAAiB;QACjB,YAAY;QACZ,aAAa;QACb,SAAS;QACT,aAAa;QACb,YAAY;KACZ,CAAA;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/react",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
4
4
|
"description": "React components + headless hooks for mailwoman — parse/geocode/POI explorers, Storybook-tested, Docusaurus-independent.",
|
|
5
5
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
6
6
|
"repository": {
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
".": {
|
|
40
40
|
"types": "./out/index.d.ts",
|
|
41
41
|
"default": "./out/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./map": {
|
|
44
|
+
"types": "./out/map/index.d.ts",
|
|
45
|
+
"default": "./out/map/index.js"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"publishConfig": {
|
|
@@ -49,36 +53,53 @@
|
|
|
49
53
|
".": {
|
|
50
54
|
"types": "./out/index.d.ts",
|
|
51
55
|
"default": "./out/index.js"
|
|
56
|
+
},
|
|
57
|
+
"./map": {
|
|
58
|
+
"types": "./out/map/index.d.ts",
|
|
59
|
+
"default": "./out/map/index.js"
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
},
|
|
55
63
|
"scripts": {
|
|
56
64
|
"storybook": "storybook dev -p 6007 --no-open",
|
|
57
65
|
"build-storybook": "storybook build",
|
|
58
|
-
"test:browser": "vitest run --config vitest.config.ts"
|
|
66
|
+
"test:browser": "vitest run --config vitest.config.ts",
|
|
67
|
+
"test:node": "vitest run --config vitest.node.config.ts"
|
|
59
68
|
},
|
|
60
69
|
"dependencies": {
|
|
61
|
-
"@mailwoman/kind-classifier": "7.
|
|
62
|
-
"@mailwoman/poi-taxonomy": "7.
|
|
63
|
-
"@mailwoman/query-shape": "7.
|
|
70
|
+
"@mailwoman/kind-classifier": "7.6.0",
|
|
71
|
+
"@mailwoman/poi-taxonomy": "7.6.0",
|
|
72
|
+
"@mailwoman/query-shape": "7.6.0"
|
|
64
73
|
},
|
|
65
74
|
"devDependencies": {
|
|
66
|
-
"@storybook/addon-docs": "^10.5.
|
|
67
|
-
"@storybook/react-vite": "^10.5.
|
|
75
|
+
"@storybook/addon-docs": "^10.5.2",
|
|
76
|
+
"@storybook/react-vite": "^10.5.2",
|
|
68
77
|
"@types/react": "^19.2.17",
|
|
69
78
|
"@types/react-dom": "^19.2.3",
|
|
70
79
|
"@vitejs/plugin-react": "^6.0.3",
|
|
71
80
|
"@vitest/browser": "4.1.10",
|
|
72
81
|
"@vitest/browser-playwright": "4.1.10",
|
|
82
|
+
"maplibre-gl": "^5.24.0",
|
|
73
83
|
"playwright": "^1.61.1",
|
|
74
84
|
"react": "^19.2.7",
|
|
75
85
|
"react-dom": "^19.2.7",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
86
|
+
"react-map-gl": "^8.1.1",
|
|
87
|
+
"storybook": "^10.5.2",
|
|
88
|
+
"vite": "^8.1.5",
|
|
78
89
|
"vitest": "4.1.10"
|
|
79
90
|
},
|
|
80
91
|
"peerDependencies": {
|
|
92
|
+
"maplibre-gl": ">=5",
|
|
81
93
|
"react": ">=18",
|
|
82
|
-
"react-dom": ">=18"
|
|
94
|
+
"react-dom": ">=18",
|
|
95
|
+
"react-map-gl": ">=8"
|
|
96
|
+
},
|
|
97
|
+
"peerDependenciesMeta": {
|
|
98
|
+
"maplibre-gl": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"react-map-gl": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
83
104
|
}
|
|
84
105
|
}
|
package/pipeline/QueryForm.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* parsing. Presentational; `onSubmit` fires on form submission.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ReactNode } from "react"
|
|
10
|
+
import type { InputHTMLAttributes, KeyboardEvent, ReactNode } from "react"
|
|
11
11
|
|
|
12
12
|
import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
|
|
13
13
|
|
|
@@ -18,9 +18,22 @@ export interface QueryFormProps {
|
|
|
18
18
|
disabled?: boolean
|
|
19
19
|
busy?: boolean
|
|
20
20
|
placeholder?: string
|
|
21
|
+
/** Keydown on the input — the seam a combobox (the demo's place-autocomplete) needs for arrow/Enter/Esc nav. */
|
|
22
|
+
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void
|
|
23
|
+
/** Escape hatch for extra `<input>` attributes (aria-combobox props, `autoComplete`, …). Merged last. */
|
|
24
|
+
inputProps?: InputHTMLAttributes<HTMLInputElement>
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export function QueryForm({
|
|
27
|
+
export function QueryForm({
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
onSubmit,
|
|
31
|
+
disabled,
|
|
32
|
+
busy,
|
|
33
|
+
placeholder,
|
|
34
|
+
onKeyDown,
|
|
35
|
+
inputProps,
|
|
36
|
+
}: QueryFormProps): ReactNode {
|
|
24
37
|
return (
|
|
25
38
|
<form
|
|
26
39
|
className="mw-field"
|
|
@@ -37,6 +50,8 @@ export function QueryForm({ value, onChange, onSubmit, disabled, busy, placehold
|
|
|
37
50
|
onChange={(e) => onChange(e.target.value)}
|
|
38
51
|
disabled={disabled}
|
|
39
52
|
placeholder={placeholder}
|
|
53
|
+
onKeyDown={onKeyDown}
|
|
54
|
+
{...inputProps}
|
|
40
55
|
/>
|
|
41
56
|
<button type="submit" disabled={disabled || busy}>
|
|
42
57
|
{busy ? (
|
package/poi/LiveResultsBlock.tsx
CHANGED
|
@@ -14,15 +14,15 @@ import { formatDistance } from "./runtime.ts"
|
|
|
14
14
|
import type { LiveSearchState } from "./types.ts"
|
|
15
15
|
|
|
16
16
|
export interface LiveResultsBlockProps {
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/** Subject label for empty-state copy — a category ("No hospital results near …") or a brand ("No chevron results …"). */
|
|
18
|
+
subjectLabel: string
|
|
19
19
|
/** The location anchor the search runs against (empty ⇒ the button is disabled with a hint). */
|
|
20
20
|
anchor: string
|
|
21
21
|
state: LiveSearchState
|
|
22
22
|
onSearch: () => void
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function LiveResultsBlock({
|
|
25
|
+
export function LiveResultsBlock({ subjectLabel, anchor, state, onSearch }: LiveResultsBlockProps): ReactNode {
|
|
26
26
|
const hasAnchor = anchor.trim().length > 0
|
|
27
27
|
|
|
28
28
|
return (
|
|
@@ -47,7 +47,7 @@ export function LiveResultsBlock({ categoryLabel, anchor, state, onSearch }: Liv
|
|
|
47
47
|
) : state.status === "success" ? (
|
|
48
48
|
state.hits.length === 0 ? (
|
|
49
49
|
<p className="mw-muted">
|
|
50
|
-
No {
|
|
50
|
+
No {subjectLabel.toLowerCase()} results near {state.centerName}.
|
|
51
51
|
</p>
|
|
52
52
|
) : (
|
|
53
53
|
<>
|
package/poi/POIExplorer.tsx
CHANGED
|
@@ -32,6 +32,12 @@ export interface POIExplorerProps {
|
|
|
32
32
|
loadRuntime?: LoadPOIRuntime
|
|
33
33
|
/** Live poi.db probe. Absent ⇒ intent-only (no live-results affordance). */
|
|
34
34
|
runLiveSearch?: POILiveSearch
|
|
35
|
+
/**
|
|
36
|
+
* Whether {@link runLiveSearch} can serve BRAND subjects (fetch by QID). Default false: a brand subject shows the
|
|
37
|
+
* intent + QID chip but no live block. The docs' httpvfs probe leaves this off (brand-wide byte-range hydration is
|
|
38
|
+
* pathological — measured); a server-side backend can enable it.
|
|
39
|
+
*/
|
|
40
|
+
brandLiveSearch?: boolean
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
interface POIExplorerInnerProps extends POIExplorerProps {
|
|
@@ -39,9 +45,30 @@ interface POIExplorerInnerProps extends POIExplorerProps {
|
|
|
39
45
|
presets: ReadonlyArray<Preset>
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
function POIExplorerInner({
|
|
48
|
+
function POIExplorerInner({
|
|
49
|
+
defaultText,
|
|
50
|
+
presets,
|
|
51
|
+
loadRuntime,
|
|
52
|
+
runLiveSearch,
|
|
53
|
+
brandLiveSearch,
|
|
54
|
+
}: POIExplorerInnerProps): ReactNode {
|
|
43
55
|
const [text, setText] = useState(defaultText)
|
|
44
|
-
const { result, liveSearch, searchLive } = usePOISearch({
|
|
56
|
+
const { result, liveSearch, searchLive } = usePOISearch({
|
|
57
|
+
text,
|
|
58
|
+
loadRuntime,
|
|
59
|
+
runLiveSearch,
|
|
60
|
+
brandLiveSearch,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const subject = result?.subject
|
|
64
|
+
// The live block appears whenever the subject COULD be live-searched with an anchor — i.e. capable minus the
|
|
65
|
+
// anchor-present requirement (`canSearchLive` also requires an anchor; the block itself renders the "add an anchor"
|
|
66
|
+
// hint, so it must show one step earlier). Category: not build-local. Brand: brand-capable probe + a QID.
|
|
67
|
+
const showLiveBlock = Boolean(
|
|
68
|
+
runLiveSearch &&
|
|
69
|
+
subject &&
|
|
70
|
+
(subject.kind === "brand" ? brandLiveSearch && subject.wikidata !== undefined : !subject.buildLocal)
|
|
71
|
+
)
|
|
45
72
|
|
|
46
73
|
return (
|
|
47
74
|
<div className="mw-poi-explorer">
|
|
@@ -52,15 +79,17 @@ function POIExplorerInner({ defaultText, presets, loadRuntime, runLiveSearch }:
|
|
|
52
79
|
<div className="mw-result">
|
|
53
80
|
<KindBadge kindResult={result.kindResult} />
|
|
54
81
|
|
|
55
|
-
{
|
|
82
|
+
{subject ? (
|
|
56
83
|
<>
|
|
57
|
-
<SubjectPanel subject={
|
|
58
|
-
|
|
84
|
+
<SubjectPanel subject={subject} />
|
|
85
|
+
{subject.kind === "category" ? (
|
|
86
|
+
<OverpassBlock overpassQL={result.overpassQL} overpassError={result.overpassError} />
|
|
87
|
+
) : null}
|
|
59
88
|
|
|
60
|
-
{
|
|
89
|
+
{showLiveBlock ? (
|
|
61
90
|
<LiveResultsBlock
|
|
62
|
-
|
|
63
|
-
anchor={
|
|
91
|
+
subjectLabel={subject.kind === "brand" ? subject.name : subject.category.label}
|
|
92
|
+
anchor={subject.remainder}
|
|
64
93
|
state={liveSearch}
|
|
65
94
|
onSearch={searchLive}
|
|
66
95
|
/>
|
|
@@ -80,6 +109,7 @@ export function POIExplorer({
|
|
|
80
109
|
presets = POI_PRESETS,
|
|
81
110
|
loadRuntime,
|
|
82
111
|
runLiveSearch,
|
|
112
|
+
brandLiveSearch,
|
|
83
113
|
}: POIExplorerProps): ReactNode {
|
|
84
114
|
return (
|
|
85
115
|
<ClientOnly
|
|
@@ -95,6 +125,7 @@ export function POIExplorer({
|
|
|
95
125
|
presets={presets}
|
|
96
126
|
loadRuntime={loadRuntime}
|
|
97
127
|
runLiveSearch={runLiveSearch}
|
|
128
|
+
brandLiveSearch={brandLiveSearch}
|
|
98
129
|
/>
|
|
99
130
|
)}
|
|
100
131
|
</ClientOnly>
|
package/poi/SubjectPanel.tsx
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* `SubjectPanel` — renders the detected POI subject: the category chip, the optional
|
|
7
|
-
* badge + note
|
|
6
|
+
* `SubjectPanel` — renders the detected POI subject. For a CATEGORY: the category chip, the optional
|
|
7
|
+
* build-local badge + note. For a BRAND: the brand-name chip + a Wikidata QID chip (linked). Both
|
|
8
|
+
* share the match-detail list (matched phrase, confidence, anchor). Presentational.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { ReactNode } from "react"
|
|
@@ -15,7 +16,50 @@ export interface SubjectPanelProps {
|
|
|
15
16
|
subject: POISubject
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** The shared match-detail list (matched phrase, confidence, anchor) — identical for category + brand subjects. */
|
|
20
|
+
function MatchDetail({ subject }: SubjectPanelProps): ReactNode {
|
|
21
|
+
return (
|
|
22
|
+
<dl className="mw-subject__detail">
|
|
23
|
+
<dt>matched phrase</dt>
|
|
24
|
+
<dd>
|
|
25
|
+
<code>{subject.matchedPhrase}</code>
|
|
26
|
+
</dd>
|
|
27
|
+
<dt>confidence</dt>
|
|
28
|
+
<dd>{Math.round(subject.confidence * 100)}%</dd>
|
|
29
|
+
<dt>anchor</dt>
|
|
30
|
+
<dd>{subject.remainder ? subject.remainder : <em>none — global query</em>}</dd>
|
|
31
|
+
</dl>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export function SubjectPanel({ subject }: SubjectPanelProps): ReactNode {
|
|
36
|
+
if (subject.kind === "brand") {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<div className="mw-subject__row">
|
|
40
|
+
<span className="mw-subject__chip">{subject.name}</span>
|
|
41
|
+
<span className="mw-subject__badge mw-subject__badge--brand">brand</span>
|
|
42
|
+
{subject.wikidata ? (
|
|
43
|
+
<a
|
|
44
|
+
className="mw-subject__qid"
|
|
45
|
+
href={`https://www.wikidata.org/wiki/${subject.wikidata}`}
|
|
46
|
+
target="_blank"
|
|
47
|
+
rel="noreferrer"
|
|
48
|
+
>
|
|
49
|
+
{subject.wikidata}
|
|
50
|
+
</a>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<MatchDetail subject={subject} />
|
|
55
|
+
|
|
56
|
+
<p className="mw-subject__note">
|
|
57
|
+
Matched as a chain brand — resolved by Wikidata QID against the layer's brand index.
|
|
58
|
+
</p>
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
return (
|
|
20
64
|
<>
|
|
21
65
|
<div className="mw-subject__row">
|
|
@@ -23,16 +67,7 @@ export function SubjectPanel({ subject }: SubjectPanelProps): ReactNode {
|
|
|
23
67
|
{subject.buildLocal ? <span className="mw-subject__badge">build-local</span> : null}
|
|
24
68
|
</div>
|
|
25
69
|
|
|
26
|
-
<
|
|
27
|
-
<dt>matched phrase</dt>
|
|
28
|
-
<dd>
|
|
29
|
-
<code>{subject.matchedPhrase}</code>
|
|
30
|
-
</dd>
|
|
31
|
-
<dt>confidence</dt>
|
|
32
|
-
<dd>{Math.round(subject.confidence * 100)}%</dd>
|
|
33
|
-
<dt>anchor</dt>
|
|
34
|
-
<dd>{subject.remainder ? subject.remainder : <em>none — global query</em>}</dd>
|
|
35
|
-
</dl>
|
|
70
|
+
<MatchDetail subject={subject} />
|
|
36
71
|
|
|
37
72
|
{subject.buildLocal ? (
|
|
38
73
|
<p className="mw-subject__note">
|
package/poi/runtime.ts
CHANGED
|
@@ -11,36 +11,61 @@
|
|
|
11
11
|
|
|
12
12
|
import { createKindClassifier } from "@mailwoman/kind-classifier"
|
|
13
13
|
import type { POIPhraseLookup } from "@mailwoman/kind-classifier"
|
|
14
|
-
import { createPOITaxonomyLookup } from "@mailwoman/poi-taxonomy/table"
|
|
14
|
+
import { createPOIBrandLookup, createPOITaxonomyLookup } from "@mailwoman/poi-taxonomy/table"
|
|
15
15
|
|
|
16
16
|
import type { POIRuntime } from "./types.ts"
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Build the POI runtime, dynamically importing the taxonomy JSON so the
|
|
19
|
+
* Build the POI runtime, dynamically importing the taxonomy + brand JSON so the tables land in their own chunk. A
|
|
20
20
|
* static import would inline the whole snapshot into every consumer's bundle.
|
|
21
|
+
*
|
|
22
|
+
* The lexicon unions categories then brands, mirroring the Node runtime's `poiTaxonomyLookup` precedence: a phrase that
|
|
23
|
+
* matches a taxonomy CATEGORY wins (the curated set); only on a category miss does the chain-brand table fire,
|
|
24
|
+
* returning a `kind: "brand"` match carrying the brand's canonical name + Wikidata QID. (The Node path also chains
|
|
25
|
+
* `@mailwoman/variant-aliases` regional slang; the browser tester leaves that out — one fewer package + data table for
|
|
26
|
+
* a demo, and the QID-keyed brand table already covers the headline brands.)
|
|
21
27
|
*/
|
|
22
28
|
export async function loadPOIRuntime(): Promise<POIRuntime> {
|
|
23
|
-
const table =
|
|
29
|
+
const [table, brandTable] = await Promise.all([
|
|
30
|
+
import("@mailwoman/poi-taxonomy/data/taxonomy.json").then((m) => m.default),
|
|
31
|
+
import("@mailwoman/poi-taxonomy/data/brands.json").then((m) => m.default),
|
|
32
|
+
])
|
|
24
33
|
const lookup = createPOITaxonomyLookup(table as unknown as Parameters<typeof createPOITaxonomyLookup>[0])
|
|
34
|
+
const brands = createPOIBrandLookup(brandTable as unknown as Parameters<typeof createPOIBrandLookup>[0])
|
|
35
|
+
|
|
36
|
+
// Adapt the taxonomy + brand lookups to the `POIPhraseLookup` shape the classifier expects: categories first, brands
|
|
37
|
+
// on a category miss.
|
|
38
|
+
const lexicon: POIPhraseLookup = (phrase, locale) => {
|
|
39
|
+
const categoryHits = lookup.lookupPOICategory(phrase, locale)
|
|
40
|
+
|
|
41
|
+
if (categoryHits.length > 0) {
|
|
42
|
+
return categoryHits.map((match) => ({
|
|
43
|
+
kind: "category",
|
|
44
|
+
categoryID: match.category.id,
|
|
45
|
+
matchedPhrase: match.matchedPhrase,
|
|
46
|
+
confidence: match.confidence,
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
25
49
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
categoryID: match.category.id,
|
|
50
|
+
return brands.lookupPOIBrand(phrase).map((match) => ({
|
|
51
|
+
kind: "brand",
|
|
52
|
+
categoryID: match.brand.name,
|
|
53
|
+
wikidata: match.brand.wikidata,
|
|
31
54
|
matchedPhrase: match.matchedPhrase,
|
|
32
55
|
confidence: match.confidence,
|
|
33
56
|
}))
|
|
57
|
+
}
|
|
34
58
|
|
|
35
59
|
return { lookup, lexicon, classify: createKindClassifier({ poiLexicon: lexicon }) }
|
|
36
60
|
}
|
|
37
61
|
|
|
38
|
-
/** Default example queries for the POI explorer. */
|
|
62
|
+
/** Default example queries for the POI explorer — a mix of category, build-local, and chain-brand subjects. */
|
|
39
63
|
export const POI_PRESETS = [
|
|
40
64
|
{ label: "Drinking fountain", value: "drinking fountain near Springfield" },
|
|
41
65
|
{ label: "Fire hydrant", value: "fire hydrant" },
|
|
42
66
|
{ label: "Hospital + address", value: "hospital, 350 5th Ave, New York" },
|
|
43
|
-
{ label: "
|
|
67
|
+
{ label: "Chevron (brand)", value: "chevron near Houston" },
|
|
68
|
+
{ label: "Applebee's (brand)", value: "applebee's near Chicago" },
|
|
44
69
|
] as const
|
|
45
70
|
|
|
46
71
|
export const POI_DEFAULT_TEXT = POI_PRESETS[0].value
|