@mailwoman/react 7.5.0 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/common/PresetChips.tsx +4 -1
  2. package/index.ts +28 -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 +3 -0
  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/runtime/useDemoRuntime.d.ts +113 -0
  121. package/out/runtime/useDemoRuntime.d.ts.map +1 -0
  122. package/out/runtime/useDemoRuntime.js +135 -0
  123. package/out/runtime/useDemoRuntime.js.map +1 -0
  124. package/package.json +27 -6
  125. package/pipeline/QueryForm.tsx +17 -2
  126. package/runtime/useDemoRuntime.ts +245 -0
  127. package/styles.css +145 -0
  128. package/vitest.node.config.ts +20 -0
@@ -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
@@ -646,3 +646,148 @@
646
646
  border-width: 0 2px 2px 0;
647
647
  transform: translateY(-60%) rotate(45deg);
648
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
+ })