@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
package/poi/types.ts
CHANGED
|
@@ -23,18 +23,39 @@ export interface POIRuntime {
|
|
|
23
23
|
classify: ReturnType<typeof createKindClassifier>
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
export interface
|
|
28
|
-
category: CategoryRecord
|
|
26
|
+
/** Fields shared by every resolved POI subject, category or brand. */
|
|
27
|
+
export interface POISubjectBase {
|
|
29
28
|
matchedPhrase: string
|
|
30
29
|
confidence: number
|
|
31
30
|
/** The non-subject remainder of the query (the location anchor, e.g. "near Springfield"). */
|
|
32
31
|
remainder: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A resolved POI subject that names a taxonomy CATEGORY (`cafe`, `hospital`, `drinking fountain`). */
|
|
35
|
+
export interface POICategorySubject extends POISubjectBase {
|
|
36
|
+
kind: "category"
|
|
37
|
+
category: CategoryRecord
|
|
33
38
|
/** Whether this category needs the locally-built OSM (ODbL) layer — precomputed off the runtime. */
|
|
34
39
|
buildLocal: boolean
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A resolved POI subject that names a chain BRAND (`chevron`, `applebee's`). Brands carry a Wikidata QID and are
|
|
44
|
+
* searched by that QID, NOT by category k-ring — see `@mailwoman/poi-taxonomy`'s brand table + the layer's
|
|
45
|
+
* `brand_wikidata` index.
|
|
46
|
+
*/
|
|
47
|
+
export interface POIBrandSubject extends POISubjectBase {
|
|
48
|
+
kind: "brand"
|
|
49
|
+
/** The brand's canonical display name. */
|
|
50
|
+
name: string
|
|
51
|
+
/** Wikidata QID, when the lexicon carried one (`Q319642` = Chevron). Absent ⇒ matched by name alone. */
|
|
52
|
+
wikidata?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A resolved POI subject — a taxonomy category or a chain brand — plus its match metadata. */
|
|
56
|
+
export type POISubject = POICategorySubject | POIBrandSubject
|
|
57
|
+
|
|
58
|
+
/** The intent-only result: the kind verdict plus (when a subject was detected) its OverpassQL export (category only). */
|
|
38
59
|
export interface POIExplorerResult {
|
|
39
60
|
kindResult: QueryKindResult
|
|
40
61
|
subject?: POISubject
|
|
@@ -64,11 +85,20 @@ export type POILiveSearchResult =
|
|
|
64
85
|
/**
|
|
65
86
|
* The injected live-search function. Given the resolved category (+ its Overture leaf fan-out) and the anchor text, it
|
|
66
87
|
* probes the published poi.db and returns hits. Absent ⇒ the explorer runs intent-only (no "Search live" button).
|
|
88
|
+
*
|
|
89
|
+
* Brand support is ADDITIVE: when the resolved subject is a chain brand, `brandWikidata` carries its QID and the probe
|
|
90
|
+
* fetches by that QID instead of a category k-ring (`categoryID`/`overtureCategoryIDs` are then the brand name / empty
|
|
91
|
+
* and unused). The category path is byte-identical to before. A probe that can't serve brands simply leaves brand live
|
|
92
|
+
* search unwired at the call site (see `usePOISearch`'s `brandLiveSearch` option) — the docs' httpvfs probe does
|
|
93
|
+
* exactly that, brand-wide row hydration being pathological over byte-range (measured; the brand path is server-side
|
|
94
|
+
* only).
|
|
67
95
|
*/
|
|
68
96
|
export type POILiveSearch = (params: {
|
|
69
97
|
categoryID: string
|
|
70
98
|
overtureCategoryIDs: string[]
|
|
71
99
|
anchor: string
|
|
100
|
+
/** Present when the subject is a chain brand — the probe fetches by this QID, not a category k-ring. */
|
|
101
|
+
brandWikidata?: string
|
|
72
102
|
}) => Promise<POILiveSearchResult>
|
|
73
103
|
|
|
74
104
|
/** "Search live" state machine. */
|
package/poi/usePOISearch.ts
CHANGED
|
@@ -26,6 +26,13 @@ export interface UsePOISearchOptions {
|
|
|
26
26
|
loadRuntime?: LoadPOIRuntime
|
|
27
27
|
/** Injected live-search probe. Absent ⇒ the live-results affordance is disabled. */
|
|
28
28
|
runLiveSearch?: POILiveSearch
|
|
29
|
+
/**
|
|
30
|
+
* Whether the injected probe can serve BRAND subjects (fetch by Wikidata QID). Default false: brand subjects show the
|
|
31
|
+
* intent + QID chip but no live-search affordance. The docs' httpvfs probe leaves this off — brand-wide row hydration
|
|
32
|
+
* is pathological over byte-range (measured) — so brand live search is a server-side-backend capability. Category
|
|
33
|
+
* live search is unaffected either way.
|
|
34
|
+
*/
|
|
35
|
+
brandLiveSearch?: boolean
|
|
29
36
|
/** Debounce before (re)classifying. @default 250 */
|
|
30
37
|
debounceMs?: number
|
|
31
38
|
}
|
|
@@ -72,6 +79,7 @@ export function usePOISearch({
|
|
|
72
79
|
text,
|
|
73
80
|
loadRuntime = loadPOIRuntime,
|
|
74
81
|
runLiveSearch,
|
|
82
|
+
brandLiveSearch = false,
|
|
75
83
|
debounceMs = 250,
|
|
76
84
|
}: UsePOISearchOptions): UsePOISearch {
|
|
77
85
|
const [runtime, setRuntime] = useState<POIRuntime | null>(null)
|
|
@@ -123,9 +131,34 @@ export function usePOISearch({
|
|
|
123
131
|
if (cancelled) return
|
|
124
132
|
|
|
125
133
|
const matched = kindResult.kind === "poi_query" ? matchPOISubject(trimmed, undefined, runtime.lexicon) : null
|
|
126
|
-
const category = matched ? runtime.lookup.getPOICategory(matched.match.categoryID) : undefined
|
|
127
134
|
|
|
128
|
-
if (!matched
|
|
135
|
+
if (!matched) {
|
|
136
|
+
setResult({ kindResult })
|
|
137
|
+
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Brand subject: the lexicon carries the brand's canonical name as `categoryID` + its Wikidata QID. No category
|
|
142
|
+
// record, no OverpassQL (brands are searched by QID against the layer's `brand_wikidata` index, not OSM tags).
|
|
143
|
+
if ((matched.match.kind ?? "category") === "brand") {
|
|
144
|
+
setResult({
|
|
145
|
+
kindResult,
|
|
146
|
+
subject: {
|
|
147
|
+
kind: "brand",
|
|
148
|
+
name: matched.match.categoryID,
|
|
149
|
+
...(matched.match.wikidata ? { wikidata: matched.match.wikidata } : {}),
|
|
150
|
+
matchedPhrase: matched.match.matchedPhrase,
|
|
151
|
+
confidence: matched.match.confidence,
|
|
152
|
+
remainder: matched.remainder,
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const category = runtime.lookup.getPOICategory(matched.match.categoryID)
|
|
160
|
+
|
|
161
|
+
if (!category) {
|
|
129
162
|
setResult({ kindResult })
|
|
130
163
|
|
|
131
164
|
return
|
|
@@ -134,6 +167,7 @@ export function usePOISearch({
|
|
|
134
167
|
setResult({
|
|
135
168
|
kindResult,
|
|
136
169
|
subject: {
|
|
170
|
+
kind: "category",
|
|
137
171
|
category,
|
|
138
172
|
matchedPhrase: matched.match.matchedPhrase,
|
|
139
173
|
confidence: matched.match.confidence,
|
|
@@ -150,22 +184,39 @@ export function usePOISearch({
|
|
|
150
184
|
}, [debouncedText, runtime])
|
|
151
185
|
|
|
152
186
|
const subject = result?.subject
|
|
187
|
+
// A subject is live-searchable when a probe is wired, it has an anchor, and: a CATEGORY that isn't build-local, or a
|
|
188
|
+
// BRAND with a QID AND a brand-capable probe (`brandLiveSearch`). Brands without a QID / without a brand probe show
|
|
189
|
+
// the intent + QID chip but no live affordance.
|
|
190
|
+
const subjectLiveCapable =
|
|
191
|
+
subject !== undefined &&
|
|
192
|
+
(subject.kind === "brand" ? brandLiveSearch && subject.wikidata !== undefined : !subject.buildLocal)
|
|
153
193
|
const canSearchLive = Boolean(
|
|
154
|
-
runLiveSearch && runtime && subject &&
|
|
194
|
+
runLiveSearch && runtime && subject && subjectLiveCapable && subject.remainder.trim().length > 0
|
|
155
195
|
)
|
|
156
196
|
|
|
157
197
|
const searchLive = useCallback(async () => {
|
|
158
|
-
if (!runLiveSearch || !runtime || !subject ||
|
|
198
|
+
if (!runLiveSearch || !runtime || !subject || !subject.remainder.trim()) return
|
|
199
|
+
|
|
200
|
+
if (subject.kind === "category" ? subject.buildLocal : !(brandLiveSearch && subject.wikidata)) return
|
|
159
201
|
|
|
160
202
|
setLiveSearch({ status: "loading" })
|
|
161
203
|
|
|
162
204
|
try {
|
|
163
|
-
const outcome = await runLiveSearch(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
const outcome = await runLiveSearch(
|
|
206
|
+
subject.kind === "brand"
|
|
207
|
+
? {
|
|
208
|
+
categoryID: subject.name,
|
|
209
|
+
overtureCategoryIDs: [],
|
|
210
|
+
anchor: subject.remainder,
|
|
211
|
+
brandWikidata: subject.wikidata,
|
|
212
|
+
}
|
|
213
|
+
: {
|
|
214
|
+
categoryID: subject.category.id,
|
|
215
|
+
// Fan the canonical seed id out over its Overture leaves — the same translation the Node reader uses.
|
|
216
|
+
overtureCategoryIDs: runtime.lookup.resolveOvertureCategories(subject.category.id),
|
|
217
|
+
anchor: subject.remainder,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
169
220
|
|
|
170
221
|
if (outcome.status === "success") {
|
|
171
222
|
setLiveSearch({ status: "success", hits: outcome.hits, centerName: outcome.centerName })
|
|
@@ -177,7 +228,7 @@ export function usePOISearch({
|
|
|
177
228
|
} catch {
|
|
178
229
|
setLiveSearch({ status: "error", message: "the published POI layer isn't reachable" })
|
|
179
230
|
}
|
|
180
|
-
}, [runLiveSearch, runtime, subject])
|
|
231
|
+
}, [runLiveSearch, runtime, subject, brandLiveSearch])
|
|
181
232
|
|
|
182
233
|
return { runtimeReady: runtime !== null, result, liveSearch, canSearchLive, searchLive }
|
|
183
234
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
21
|
+
|
|
22
|
+
/** The minimal contract a release-manifest entry must satisfy. Hosts extend this with their own fields. */
|
|
23
|
+
export interface DemoReleaseBase {
|
|
24
|
+
/** The version tag this entry describes (matched against `selectedVersion`). */
|
|
25
|
+
version: string
|
|
26
|
+
/** Optional display label a version picker shows; falls back to `version`. */
|
|
27
|
+
label?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The releases manifest the host fetches — the default version plus the selectable release entries. */
|
|
31
|
+
export interface DemoManifest<TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
32
|
+
/** The version selected on first load (before the user picks another). */
|
|
33
|
+
defaultVersion: string
|
|
34
|
+
/** Every selectable release. */
|
|
35
|
+
releases: TRelease[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The progress channel handed to the host's `loadAssets`. The host reports load progress + the resolved backend + the
|
|
40
|
+
* staged step labels/index THROUGH these setters (all no-op once the load is superseded/aborted), while the hook owns
|
|
41
|
+
* the terminal state (revealing the assets + clearing progress on success, surfacing the error on failure).
|
|
42
|
+
*/
|
|
43
|
+
export interface DemoAssetsLoadContext {
|
|
44
|
+
/** Aborts when this load is superseded (version/backend switch) or the provider unmounts. */
|
|
45
|
+
signal: AbortSignal
|
|
46
|
+
/** Whether the host should force the CPU/WASM backend (opt out of WebGPU) for this load. */
|
|
47
|
+
forceWASM: boolean
|
|
48
|
+
/** Set the human-readable progress line (e.g. `Loading v7 model (~28 MB)…`). */
|
|
49
|
+
setProgress: (progress: string) => void
|
|
50
|
+
/** Set the staged-loader step labels (e.g. `["Loading classifier", "Loading FST gazetteer"]`). */
|
|
51
|
+
setStepLabels: (labels: string[]) => void
|
|
52
|
+
/** Advance the staged-loader step index (0-based). */
|
|
53
|
+
setStepIndex: (index: number) => void
|
|
54
|
+
/** Report the backend the neural runtime resolved to (e.g. `webgpu (27 MB int8)`). */
|
|
55
|
+
setBackend: (backend: string) => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The injected loaders the hook orchestrates. Nothing here is model- or map-aware — the host owns all that. */
|
|
59
|
+
export interface DemoRuntimeConfig<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
60
|
+
/**
|
|
61
|
+
* Fetch + normalize the releases manifest. Returns `null` when no manifest is available (the demo then shows nothing
|
|
62
|
+
* selectable). Rejecting surfaces `errorMessage`. Runs once on mount.
|
|
63
|
+
*/
|
|
64
|
+
loadManifest: (signal: AbortSignal) => Promise<DemoManifest<TRelease> | null>
|
|
65
|
+
/**
|
|
66
|
+
* Load the full asset bundle for one release — the classifier, FST, WOF lookup, calibrator, whatever the host needs.
|
|
67
|
+
* Runs on every version or `forceWASM` change. Report progress via `ctx`; return the bundle. Rejecting surfaces
|
|
68
|
+
* `errorMessage`. Bail early when `ctx.signal.aborted` — the hook discards a superseded result regardless.
|
|
69
|
+
*/
|
|
70
|
+
loadAssets: (release: TRelease, ctx: DemoAssetsLoadContext) => Promise<TAssets>
|
|
71
|
+
/** The progress line shown before the manifest arrives. @default "Loading releases…" */
|
|
72
|
+
initialProgress?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The state `useDemoRuntime` produces — the load-orchestration state a demo surface renders + re-projects.
|
|
77
|
+
*
|
|
78
|
+
* NAMING (phase 4): this is the LOADER STATE, deliberately distinct from {@link DemoRuntime} (the injected runtime
|
|
79
|
+
* contract `<GeocoderDemo>` consumes). The two were same-stem before (`UseDemoRuntime` vs `DemoRuntime`); phase 4
|
|
80
|
+
* disambiguates by renaming the loader-state type to `DemoLoaderState`. A host builds a {@link DemoRuntime} by pairing
|
|
81
|
+
* this loader state (assets + backend + version) with the map surface (style, overlays, bias, parse) — see the map
|
|
82
|
+
* subpath's `DemoRuntime`.
|
|
83
|
+
*/
|
|
84
|
+
export interface DemoLoaderState<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase> {
|
|
85
|
+
/** The releases manifest, once fetched. */
|
|
86
|
+
manifest: DemoManifest<TRelease> | null
|
|
87
|
+
/** The currently-selected version, or `null` before the manifest resolves. */
|
|
88
|
+
selectedVersion: string | null
|
|
89
|
+
/** The release entry matching `selectedVersion` (convenience over `manifest.releases.find`). */
|
|
90
|
+
selectedRelease: TRelease | null
|
|
91
|
+
/** The loaded asset bundle for the selected version, or `null` while (re)loading. */
|
|
92
|
+
assets: TAssets | null
|
|
93
|
+
/** Whether the asset bundle is loaded and ready to use. */
|
|
94
|
+
ready: boolean
|
|
95
|
+
/** Human-readable load progress (`""` when idle/ready). */
|
|
96
|
+
loadingProgress: string
|
|
97
|
+
/** Staged-loader step index (0-based; `-1` before the first step). */
|
|
98
|
+
loadingStepIndex: number
|
|
99
|
+
/** Staged-loader step labels. */
|
|
100
|
+
loadingStepLabels: string[]
|
|
101
|
+
/** A load error (manifest or asset), distinct from any per-parse error a consumer tracks separately. */
|
|
102
|
+
errorMessage: string | null
|
|
103
|
+
/** The backend the neural runtime resolved to (e.g. `webgpu (27 MB int8)`), or `""` before it's known. */
|
|
104
|
+
activeBackend: string
|
|
105
|
+
/** Whether the CPU/WASM backend is currently forced. */
|
|
106
|
+
forceWASM: boolean
|
|
107
|
+
/** Switch to a different version (clears any error, then reloads the asset bundle). */
|
|
108
|
+
selectVersion: (version: string) => void
|
|
109
|
+
/** Force (or unforce) the CPU/WASM backend — reloads the asset bundle. */
|
|
110
|
+
setForceWASM: (forceWASM: boolean) => void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drive the shared version → asset-bundle load state machine over a host-injected loader.
|
|
115
|
+
*
|
|
116
|
+
* Sequence: on mount `loadManifest` runs and its `defaultVersion` becomes the selection; each version (or `forceWASM`)
|
|
117
|
+
* change reloads the bundle via `loadAssets`, the previous load aborted first. The assets are revealed ATOMICALLY when
|
|
118
|
+
* `loadAssets` resolves (so `ready` flips exactly once per load), and consumers gate on `ready`.
|
|
119
|
+
*/
|
|
120
|
+
export function useDemoRuntime<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase>(
|
|
121
|
+
config: DemoRuntimeConfig<TAssets, TRelease>
|
|
122
|
+
): DemoLoaderState<TAssets, TRelease> {
|
|
123
|
+
const { initialProgress = "Loading releases…" } = config
|
|
124
|
+
|
|
125
|
+
const [manifest, setManifest] = useState<DemoManifest<TRelease> | null>(null)
|
|
126
|
+
const [selectedVersion, setSelectedVersion] = useState<string | null>(null)
|
|
127
|
+
const [assets, setAssets] = useState<TAssets | null>(null)
|
|
128
|
+
const [loadingProgress, setLoadingProgress] = useState<string>(initialProgress)
|
|
129
|
+
const [loadingStepIndex, setLoadingStepIndex] = useState(-1)
|
|
130
|
+
const [loadingStepLabels, setLoadingStepLabels] = useState<string[]>([])
|
|
131
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
132
|
+
const [activeBackend, setActiveBackend] = useState<string>("")
|
|
133
|
+
const [forceWASM, setForceWASMState] = useState(false)
|
|
134
|
+
|
|
135
|
+
// Latest-ref the injected loaders: a host that re-creates them each render (an inline arrow) must NOT retrigger the
|
|
136
|
+
// load effects, which key ONLY on version/backend. The effects read `.current` at run time.
|
|
137
|
+
const loadManifestRef = useRef(config.loadManifest)
|
|
138
|
+
loadManifestRef.current = config.loadManifest
|
|
139
|
+
const loadAssetsRef = useRef(config.loadAssets)
|
|
140
|
+
loadAssetsRef.current = config.loadAssets
|
|
141
|
+
|
|
142
|
+
// Latest manifest for the version-load effect, so it can resolve the release WITHOUT depending on `manifest`
|
|
143
|
+
// identity — which would double-fire the load the instant the manifest first arrives (the selection transition
|
|
144
|
+
// null → defaultVersion already fires it once).
|
|
145
|
+
const manifestRef = useRef<DemoManifest<TRelease> | null>(null)
|
|
146
|
+
manifestRef.current = manifest
|
|
147
|
+
|
|
148
|
+
// Mount: fetch the manifest, then select the default version.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const controller = new AbortController()
|
|
151
|
+
|
|
152
|
+
void (async () => {
|
|
153
|
+
try {
|
|
154
|
+
const data = await loadManifestRef.current(controller.signal)
|
|
155
|
+
|
|
156
|
+
if (controller.signal.aborted) return
|
|
157
|
+
|
|
158
|
+
if (data) {
|
|
159
|
+
setManifest(data)
|
|
160
|
+
setSelectedVersion(data.defaultVersion)
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (controller.signal.aborted) return
|
|
164
|
+
setErrorMessage(error instanceof Error ? error.message : String(error))
|
|
165
|
+
}
|
|
166
|
+
})()
|
|
167
|
+
|
|
168
|
+
return () => controller.abort()
|
|
169
|
+
}, [])
|
|
170
|
+
|
|
171
|
+
// Load the per-version asset bundle when the version (or the backend force) changes.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!selectedVersion) return
|
|
174
|
+
|
|
175
|
+
const release = manifestRef.current?.releases.find((r) => r.version === selectedVersion) ?? null
|
|
176
|
+
|
|
177
|
+
if (!release) return
|
|
178
|
+
|
|
179
|
+
const controller = new AbortController()
|
|
180
|
+
const { signal } = controller
|
|
181
|
+
const guard = (fn: () => void) => {
|
|
182
|
+
if (!signal.aborted) {
|
|
183
|
+
fn()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
void (async () => {
|
|
188
|
+
try {
|
|
189
|
+
setAssets(null)
|
|
190
|
+
setLoadingStepIndex(-1)
|
|
191
|
+
setLoadingStepLabels([])
|
|
192
|
+
setActiveBackend("")
|
|
193
|
+
|
|
194
|
+
const ctx: DemoAssetsLoadContext = {
|
|
195
|
+
signal,
|
|
196
|
+
forceWASM,
|
|
197
|
+
setProgress: (progress) => guard(() => setLoadingProgress(progress)),
|
|
198
|
+
setStepLabels: (labels) => guard(() => setLoadingStepLabels(labels)),
|
|
199
|
+
setStepIndex: (index) => guard(() => setLoadingStepIndex(index)),
|
|
200
|
+
setBackend: (backend) => guard(() => setActiveBackend(backend)),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const loaded = await loadAssetsRef.current(release, ctx)
|
|
204
|
+
|
|
205
|
+
if (signal.aborted) return
|
|
206
|
+
setAssets(loaded)
|
|
207
|
+
setLoadingProgress("")
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (signal.aborted) return
|
|
210
|
+
setErrorMessage(error instanceof Error ? error.message : String(error))
|
|
211
|
+
setLoadingProgress("")
|
|
212
|
+
}
|
|
213
|
+
})()
|
|
214
|
+
|
|
215
|
+
return () => controller.abort()
|
|
216
|
+
}, [selectedVersion, forceWASM])
|
|
217
|
+
|
|
218
|
+
const selectVersion = useCallback((version: string) => {
|
|
219
|
+
setSelectedVersion(version)
|
|
220
|
+
setErrorMessage(null)
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
const setForceWASM = useCallback((next: boolean) => setForceWASMState(next), [])
|
|
224
|
+
|
|
225
|
+
const selectedRelease = useMemo(
|
|
226
|
+
() => manifest?.releases.find((r) => r.version === selectedVersion) ?? null,
|
|
227
|
+
[manifest, selectedVersion]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
manifest,
|
|
232
|
+
selectedVersion,
|
|
233
|
+
selectedRelease,
|
|
234
|
+
assets,
|
|
235
|
+
ready: assets !== null,
|
|
236
|
+
loadingProgress,
|
|
237
|
+
loadingStepIndex,
|
|
238
|
+
loadingStepLabels,
|
|
239
|
+
errorMessage,
|
|
240
|
+
activeBackend,
|
|
241
|
+
forceWASM,
|
|
242
|
+
selectVersion,
|
|
243
|
+
setForceWASM,
|
|
244
|
+
}
|
|
245
|
+
}
|
package/styles.css
CHANGED
|
@@ -214,6 +214,27 @@
|
|
|
214
214
|
letter-spacing: 0.02em;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
.mw-subject__badge--brand {
|
|
218
|
+
background: var(--ifm-color-info-contrast-background, rgba(84, 199, 236, 0.15));
|
|
219
|
+
color: var(--ifm-color-info-darkest, #14657d);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.mw-subject__qid {
|
|
223
|
+
display: inline-block;
|
|
224
|
+
padding: 0.2rem 0.5rem;
|
|
225
|
+
border-radius: 999px;
|
|
226
|
+
background: var(--ifm-color-emphasis-200, #ebedf0);
|
|
227
|
+
color: var(--ifm-color-emphasis-800, #303846);
|
|
228
|
+
font-family: var(--ifm-font-family-monospace, monospace);
|
|
229
|
+
font-size: 0.75rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
text-decoration: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.mw-subject__qid:hover {
|
|
235
|
+
text-decoration: underline;
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
.mw-subject__note {
|
|
218
239
|
margin: 0 0 0.75rem;
|
|
219
240
|
font-size: 0.85rem;
|
|
@@ -625,3 +646,148 @@
|
|
|
625
646
|
border-width: 0 2px 2px 0;
|
|
626
647
|
transform: translateY(-60%) rotate(45deg);
|
|
627
648
|
}
|
|
649
|
+
|
|
650
|
+
/* ── Demo map (react-map-gl/maplibre shell) ──────────────────────────────── */
|
|
651
|
+
|
|
652
|
+
/*
|
|
653
|
+
* The <DemoMap> wrapper. Sizing is the host's call (pass `style`/`className`), but a min-height keeps
|
|
654
|
+
* the map from collapsing to zero when a consumer forgets. The inner <Map> fills 100% of this box.
|
|
655
|
+
* The host must ALSO import `maplibre-gl/dist/maplibre-gl.css` for the map's own control/canvas styles.
|
|
656
|
+
*/
|
|
657
|
+
.mw-demo-map {
|
|
658
|
+
position: relative;
|
|
659
|
+
width: 100%;
|
|
660
|
+
min-height: 320px;
|
|
661
|
+
overflow: hidden;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/* ── Geocoder demo (composed: full-viewport map + floating control panel) ──── */
|
|
665
|
+
|
|
666
|
+
/*
|
|
667
|
+
* The demo fills its positioning context; the host sizes it (a full-viewport wrapper in docs). The map is the
|
|
668
|
+
* background layer, the control panel floats over the top-left (bottom on narrow screens). A host that wants a
|
|
669
|
+
* different layout can restyle these classes without touching the components.
|
|
670
|
+
*/
|
|
671
|
+
.mw-geocoder-demo {
|
|
672
|
+
position: relative;
|
|
673
|
+
width: 100%;
|
|
674
|
+
height: 100%;
|
|
675
|
+
min-height: 480px;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.mw-geocoder-demo__map {
|
|
679
|
+
position: absolute;
|
|
680
|
+
inset: 0;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.mw-demo-controls {
|
|
684
|
+
position: absolute;
|
|
685
|
+
top: 1rem;
|
|
686
|
+
left: 1rem;
|
|
687
|
+
z-index: 2;
|
|
688
|
+
width: min(360px, calc(100% - 2rem));
|
|
689
|
+
max-height: calc(100% - 2rem);
|
|
690
|
+
overflow-y: auto;
|
|
691
|
+
padding: 1rem;
|
|
692
|
+
border-radius: 10px;
|
|
693
|
+
background: color-mix(in srgb, var(--ifm-background-surface-color, #fff) 92%, transparent);
|
|
694
|
+
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.18);
|
|
695
|
+
font-size: 0.95rem;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
@media (max-width: 600px) {
|
|
699
|
+
.mw-demo-controls {
|
|
700
|
+
top: auto;
|
|
701
|
+
bottom: 1rem;
|
|
702
|
+
left: 1rem;
|
|
703
|
+
right: 1rem;
|
|
704
|
+
width: auto;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ── Demo control field (version / compare-version selects) ──────────────── */
|
|
709
|
+
|
|
710
|
+
.mw-demo-control {
|
|
711
|
+
margin-bottom: 0.75rem;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.mw-demo-control__label {
|
|
715
|
+
display: block;
|
|
716
|
+
margin-bottom: 0.25rem;
|
|
717
|
+
font-weight: 600;
|
|
718
|
+
font-size: 0.9rem;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.mw-demo-control__select {
|
|
722
|
+
width: 100%;
|
|
723
|
+
padding: 0.4rem;
|
|
724
|
+
font: inherit;
|
|
725
|
+
border: 1px solid var(--ifm-color-emphasis-300, #d1d5db);
|
|
726
|
+
border-radius: 6px;
|
|
727
|
+
background: var(--ifm-background-surface-color, #fff);
|
|
728
|
+
color: var(--ifm-font-color-base, inherit);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.mw-demo-control__select:disabled {
|
|
732
|
+
opacity: 0.5;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* ── Backend indicator + WASM toggle / compare toggle ────────────────────── */
|
|
736
|
+
|
|
737
|
+
.mw-demo-backend,
|
|
738
|
+
.mw-demo-compare {
|
|
739
|
+
display: flex;
|
|
740
|
+
align-items: center;
|
|
741
|
+
gap: 0.75rem;
|
|
742
|
+
flex-wrap: wrap;
|
|
743
|
+
margin-bottom: 0.75rem;
|
|
744
|
+
font-size: 0.85rem;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.mw-demo-compare {
|
|
748
|
+
display: block;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.mw-demo-backend__active {
|
|
752
|
+
opacity: 0.7;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.mw-demo-backend__toggle,
|
|
756
|
+
.mw-demo-compare__toggle {
|
|
757
|
+
display: inline-flex;
|
|
758
|
+
align-items: center;
|
|
759
|
+
gap: 0.25rem;
|
|
760
|
+
cursor: pointer;
|
|
761
|
+
opacity: 0.8;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.mw-demo-compare__toggle {
|
|
765
|
+
margin-bottom: 0.5rem;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.mw-demo-compare__status {
|
|
769
|
+
margin: 0.25rem 0 0;
|
|
770
|
+
font-size: 0.8rem;
|
|
771
|
+
color: var(--ifm-color-emphasis-600, #6e6e6e);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* ── Place autocomplete listbox (reuses .mw-chip) ────────────────────────── */
|
|
775
|
+
|
|
776
|
+
.mw-demo-suggest {
|
|
777
|
+
margin-top: 0.75rem;
|
|
778
|
+
display: flex;
|
|
779
|
+
flex-wrap: wrap;
|
|
780
|
+
gap: 0.4rem;
|
|
781
|
+
align-items: baseline;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.mw-demo-suggest__label {
|
|
785
|
+
font-size: 0.85rem;
|
|
786
|
+
color: var(--ifm-color-emphasis-600, #6e6e6e);
|
|
787
|
+
margin-right: 0.25rem;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.mw-chip--active {
|
|
791
|
+
outline: 2px solid var(--ifm-color-primary, #e0367c);
|
|
792
|
+
outline-offset: 1px;
|
|
793
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Node-mode vitest for `@mailwoman/react` — runs the PURE `*.node.test.ts` suites under bare node (no
|
|
7
|
+
* browser, no WebGL, no map). Their existence is the proof that the geometry (`map/geometry.ts`) and
|
|
8
|
+
* render-spec (`map/place-render.ts`) modules are side-effect-free and node-safe: if either grew a
|
|
9
|
+
* `react-map-gl` / `maplibre-gl` / DOM import, these tests would fail to even load here. The browser
|
|
10
|
+
* suite (`vitest.config.ts`) excludes these files, so this is the only entry that runs them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { defineConfig } from "vitest/config"
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
test: {
|
|
17
|
+
environment: "node",
|
|
18
|
+
include: ["**/*.node.test.ts"],
|
|
19
|
+
},
|
|
20
|
+
})
|