@mailwoman/react 7.4.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/common/PresetChips.tsx +4 -1
  2. package/index.ts +31 -0
  3. package/map/BackendControl.tsx +39 -0
  4. package/map/CompareToggle.tsx +81 -0
  5. package/map/DemoControls.tsx +173 -0
  6. package/map/DemoMap.tsx +98 -0
  7. package/map/GeocoderDemo.tsx +178 -0
  8. package/map/OverlayLayers.tsx +46 -0
  9. package/map/PlaceAutocomplete.tsx +69 -0
  10. package/map/PlaceMarker.tsx +40 -0
  11. package/map/ResolvedPlaceLayers.tsx +63 -0
  12. package/map/ResultCamera.tsx +60 -0
  13. package/map/ResultOverlay.tsx +61 -0
  14. package/map/ResultPanel.tsx +84 -0
  15. package/map/VersionPicker.tsx +62 -0
  16. package/map/geometry.ts +135 -0
  17. package/map/index.ts +73 -0
  18. package/map/place-render.ts +157 -0
  19. package/map/types.ts +207 -0
  20. package/map/useCompareState.ts +45 -0
  21. package/map/useDemoGeocode.ts +67 -0
  22. package/map/useMapPlaceRender.ts +21 -0
  23. package/map/usePlaceAutocomplete.ts +195 -0
  24. package/out/common/PresetChips.d.ts +3 -1
  25. package/out/common/PresetChips.d.ts.map +1 -1
  26. package/out/common/PresetChips.js +2 -2
  27. package/out/common/PresetChips.js.map +1 -1
  28. package/out/index.d.ts +4 -1
  29. package/out/index.d.ts.map +1 -1
  30. package/out/index.js +2 -0
  31. package/out/index.js.map +1 -1
  32. package/out/map/BackendControl.d.ts +24 -0
  33. package/out/map/BackendControl.d.ts.map +1 -0
  34. package/out/map/BackendControl.js +6 -0
  35. package/out/map/BackendControl.js.map +1 -0
  36. package/out/map/CompareToggle.d.ts +35 -0
  37. package/out/map/CompareToggle.d.ts.map +1 -0
  38. package/out/map/CompareToggle.js +9 -0
  39. package/out/map/CompareToggle.js.map +1 -0
  40. package/out/map/DemoControls.d.ts +44 -0
  41. package/out/map/DemoControls.d.ts.map +1 -0
  42. package/out/map/DemoControls.js +28 -0
  43. package/out/map/DemoControls.js.map +1 -0
  44. package/out/map/DemoMap.d.ts +59 -0
  45. package/out/map/DemoMap.d.ts.map +1 -0
  46. package/out/map/DemoMap.js +12 -0
  47. package/out/map/DemoMap.js.map +1 -0
  48. package/out/map/GeocoderDemo.d.ts +47 -0
  49. package/out/map/GeocoderDemo.d.ts.map +1 -0
  50. package/out/map/GeocoderDemo.js +73 -0
  51. package/out/map/GeocoderDemo.js.map +1 -0
  52. package/out/map/OverlayLayers.d.ts +23 -0
  53. package/out/map/OverlayLayers.d.ts.map +1 -0
  54. package/out/map/OverlayLayers.js +27 -0
  55. package/out/map/OverlayLayers.js.map +1 -0
  56. package/out/map/PlaceAutocomplete.d.ts +34 -0
  57. package/out/map/PlaceAutocomplete.d.ts.map +1 -0
  58. package/out/map/PlaceAutocomplete.js +9 -0
  59. package/out/map/PlaceAutocomplete.js.map +1 -0
  60. package/out/map/PlaceMarker.d.ts +27 -0
  61. package/out/map/PlaceMarker.d.ts.map +1 -0
  62. package/out/map/PlaceMarker.js +9 -0
  63. package/out/map/PlaceMarker.js.map +1 -0
  64. package/out/map/ResolvedPlaceLayers.d.ts +34 -0
  65. package/out/map/ResolvedPlaceLayers.d.ts.map +1 -0
  66. package/out/map/ResolvedPlaceLayers.js +11 -0
  67. package/out/map/ResolvedPlaceLayers.js.map +1 -0
  68. package/out/map/ResultCamera.d.ts +34 -0
  69. package/out/map/ResultCamera.d.ts.map +1 -0
  70. package/out/map/ResultCamera.js +43 -0
  71. package/out/map/ResultCamera.js.map +1 -0
  72. package/out/map/ResultOverlay.d.ts +34 -0
  73. package/out/map/ResultOverlay.d.ts.map +1 -0
  74. package/out/map/ResultOverlay.js +18 -0
  75. package/out/map/ResultOverlay.js.map +1 -0
  76. package/out/map/ResultPanel.d.ts +34 -0
  77. package/out/map/ResultPanel.d.ts.map +1 -0
  78. package/out/map/ResultPanel.js +12 -0
  79. package/out/map/ResultPanel.js.map +1 -0
  80. package/out/map/VersionPicker.d.ts +30 -0
  81. package/out/map/VersionPicker.d.ts.map +1 -0
  82. package/out/map/VersionPicker.js +8 -0
  83. package/out/map/VersionPicker.js.map +1 -0
  84. package/out/map/geometry.d.ts +55 -0
  85. package/out/map/geometry.d.ts.map +1 -0
  86. package/out/map/geometry.js +99 -0
  87. package/out/map/geometry.js.map +1 -0
  88. package/out/map/index.d.ts +52 -0
  89. package/out/map/index.d.ts.map +1 -0
  90. package/out/map/index.js +36 -0
  91. package/out/map/index.js.map +1 -0
  92. package/out/map/place-render.d.ts +85 -0
  93. package/out/map/place-render.d.ts.map +1 -0
  94. package/out/map/place-render.js +98 -0
  95. package/out/map/place-render.js.map +1 -0
  96. package/out/map/types.d.ts +193 -0
  97. package/out/map/types.d.ts.map +1 -0
  98. package/out/map/types.js +18 -0
  99. package/out/map/types.js.map +1 -0
  100. package/out/map/useCompareState.d.ts +25 -0
  101. package/out/map/useCompareState.d.ts.map +1 -0
  102. package/out/map/useCompareState.js +27 -0
  103. package/out/map/useCompareState.js.map +1 -0
  104. package/out/map/useDemoGeocode.d.ts +36 -0
  105. package/out/map/useDemoGeocode.d.ts.map +1 -0
  106. package/out/map/useDemoGeocode.js +44 -0
  107. package/out/map/useDemoGeocode.js.map +1 -0
  108. package/out/map/useMapPlaceRender.d.ts +15 -0
  109. package/out/map/useMapPlaceRender.d.ts.map +1 -0
  110. package/out/map/useMapPlaceRender.js +18 -0
  111. package/out/map/useMapPlaceRender.js.map +1 -0
  112. package/out/map/usePlaceAutocomplete.d.ts +61 -0
  113. package/out/map/usePlaceAutocomplete.d.ts.map +1 -0
  114. package/out/map/usePlaceAutocomplete.js +122 -0
  115. package/out/map/usePlaceAutocomplete.js.map +1 -0
  116. package/out/pipeline/QueryForm.d.ts +6 -2
  117. package/out/pipeline/QueryForm.d.ts.map +1 -1
  118. package/out/pipeline/QueryForm.js +2 -2
  119. package/out/pipeline/QueryForm.js.map +1 -1
  120. package/out/poi/LiveResultsBlock.d.ts +3 -3
  121. package/out/poi/LiveResultsBlock.d.ts.map +1 -1
  122. package/out/poi/LiveResultsBlock.js +2 -2
  123. package/out/poi/LiveResultsBlock.js.map +1 -1
  124. package/out/poi/POIExplorer.d.ts +7 -1
  125. package/out/poi/POIExplorer.d.ts.map +1 -1
  126. package/out/poi/POIExplorer.js +17 -5
  127. package/out/poi/POIExplorer.js.map +1 -1
  128. package/out/poi/SubjectPanel.d.ts +3 -2
  129. package/out/poi/SubjectPanel.d.ts.map +1 -1
  130. package/out/poi/SubjectPanel.js +8 -1
  131. package/out/poi/SubjectPanel.js.map +1 -1
  132. package/out/poi/runtime.d.ts +13 -4
  133. package/out/poi/runtime.d.ts.map +1 -1
  134. package/out/poi/runtime.js +36 -12
  135. package/out/poi/runtime.js.map +1 -1
  136. package/out/poi/types.d.ts +31 -4
  137. package/out/poi/types.d.ts.map +1 -1
  138. package/out/poi/usePOISearch.d.ts +8 -1
  139. package/out/poi/usePOISearch.d.ts.map +1 -1
  140. package/out/poi/usePOISearch.js +47 -12
  141. package/out/poi/usePOISearch.js.map +1 -1
  142. package/out/runtime/useDemoRuntime.d.ts +113 -0
  143. package/out/runtime/useDemoRuntime.d.ts.map +1 -0
  144. package/out/runtime/useDemoRuntime.js +135 -0
  145. package/out/runtime/useDemoRuntime.js.map +1 -0
  146. package/package.json +31 -10
  147. package/pipeline/QueryForm.tsx +17 -2
  148. package/poi/LiveResultsBlock.tsx +4 -4
  149. package/poi/POIExplorer.tsx +39 -8
  150. package/poi/SubjectPanel.tsx +47 -12
  151. package/poi/runtime.ts +35 -10
  152. package/poi/types.ts +34 -4
  153. package/poi/usePOISearch.ts +62 -11
  154. package/runtime/useDemoRuntime.ts +245 -0
  155. package/styles.css +166 -0
  156. package/vitest.node.config.ts +20 -0
@@ -0,0 +1,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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/react",
3
- "version": "7.4.0",
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.4.0",
62
- "@mailwoman/poi-taxonomy": "7.4.0",
63
- "@mailwoman/query-shape": "7.4.0"
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.0",
67
- "@storybook/react-vite": "^10.5.0",
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
- "storybook": "^10.5.0",
77
- "vite": "^8.0.16",
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
  }
@@ -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({ value, onChange, onSubmit, disabled, busy, placeholder }: QueryFormProps): ReactNode {
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 ? (
@@ -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
- /** Category label for empty-state copy ("No hospital results near …"). */
18
- categoryLabel: string
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({ categoryLabel, anchor, state, onSearch }: LiveResultsBlockProps): ReactNode {
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 {categoryLabel.toLowerCase()} results near {state.centerName}.
50
+ No {subjectLabel.toLowerCase()} results near {state.centerName}.
51
51
  </p>
52
52
  ) : (
53
53
  <>
@@ -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({ defaultText, presets, loadRuntime, runLiveSearch }: POIExplorerInnerProps): ReactNode {
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({ text, loadRuntime, runLiveSearch })
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
- {result.subject ? (
82
+ {subject ? (
56
83
  <>
57
- <SubjectPanel subject={result.subject} />
58
- <OverpassBlock overpassQL={result.overpassQL} overpassError={result.overpassError} />
84
+ <SubjectPanel subject={subject} />
85
+ {subject.kind === "category" ? (
86
+ <OverpassBlock overpassQL={result.overpassQL} overpassError={result.overpassError} />
87
+ ) : null}
59
88
 
60
- {runLiveSearch && !result.subject.buildLocal ? (
89
+ {showLiveBlock ? (
61
90
  <LiveResultsBlock
62
- categoryLabel={result.subject.category.label}
63
- anchor={result.subject.remainder}
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>
@@ -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 build-local
7
- * badge + note, and the match-detail list (matched phrase, confidence, anchor). Presentational.
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
- <dl className="mw-subject__detail">
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 ~2k-record table lands in its own chunk. A
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 = (await import("@mailwoman/poi-taxonomy/data/taxonomy.json")).default
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
- // Adapt `POITaxonomyLookup.lookupPOICategory` to the `POIPhraseLookup` shape the classifier expects.
27
- const lexicon: POIPhraseLookup = (phrase, locale) =>
28
- lookup.lookupPOICategory(phrase, locale).map((match) => ({
29
- kind: "category",
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: "Biking trails", value: "biking trails near Portland" },
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