@mailwoman/react 9.1.0 → 9.3.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/README.md +2 -2
- package/{common → lib/common}/ClientOnly.tsx +18 -6
- package/{common → lib/common}/CopyButton.tsx +1 -1
- package/{common → lib/common}/LoadingIndicator.tsx +1 -1
- package/{index.ts → lib/index.ts} +17 -17
- package/{map → lib/map}/CompareToggle.tsx +1 -1
- package/{map → lib/map}/DemoControls.tsx +5 -4
- package/{map → lib/map}/DemoMap.tsx +2 -2
- package/{map → lib/map}/GeocoderDemo.tsx +32 -8
- package/{map → lib/map}/OverlayLayers.tsx +3 -3
- package/{map → lib/map}/PlaceAutocomplete.tsx +2 -2
- package/{map → lib/map}/ResolvedPlaceLayers.tsx +2 -1
- package/{map → lib/map}/ResultCamera.tsx +2 -2
- package/{map → lib/map}/ResultOverlay.tsx +2 -2
- package/{map → lib/map}/ResultPanel.tsx +3 -2
- package/{map → lib/map}/VersionPicker.tsx +1 -1
- package/{map → lib/map}/geometry.ts +7 -7
- package/{map → lib/map}/index.ts +12 -12
- package/{map → lib/map}/place-render.ts +3 -3
- package/{map → lib/map}/types.ts +12 -9
- package/{map → lib/map}/useDemoGeocode.ts +4 -4
- package/{map → lib/map}/useMapPlaceRender.ts +2 -2
- package/{map → lib/map}/usePlaceAutocomplete.ts +39 -27
- package/{pipeline → lib/pipeline}/CandidatePicker.tsx +2 -2
- package/{pipeline → lib/pipeline}/ComponentTable.tsx +2 -1
- package/{pipeline → lib/pipeline}/ConfidenceCell.tsx +1 -1
- package/{pipeline → lib/pipeline}/PipelineExplorer.tsx +5 -4
- package/{pipeline → lib/pipeline}/QueryForm.tsx +2 -2
- package/{pipeline → lib/pipeline}/ResolvedPlace.tsx +1 -1
- package/{pipeline → lib/pipeline}/copy.ts +1 -1
- package/{pipeline → lib/pipeline}/presets.ts +2 -1
- package/{pipeline → lib/pipeline}/useParsePipeline.ts +1 -1
- package/{poi → lib/poi}/LiveResultsBlock.tsx +2 -2
- package/{poi → lib/poi}/POIExplorer.tsx +4 -3
- package/{poi → lib/poi}/SubjectPanel.tsx +1 -1
- package/{poi → lib/poi}/runtime.ts +3 -3
- package/{poi → lib/poi}/types.ts +1 -1
- package/{poi → lib/poi}/usePOISearch.ts +77 -51
- package/{runtime → lib/runtime}/useDemoRuntime.ts +8 -5
- package/out/common/ClientOnly.d.ts +5 -2
- package/out/common/ClientOnly.d.ts.map +1 -1
- package/out/common/ClientOnly.js +13 -5
- package/out/common/ClientOnly.js.map +1 -1
- package/out/common/CopyButton.d.ts.map +1 -1
- package/out/common/CopyButton.js +1 -1
- package/out/common/CopyButton.js.map +1 -1
- package/out/common/KindBadge.d.ts.map +1 -1
- package/out/common/KindBadge.js.map +1 -1
- package/out/common/LoadingIndicator.d.ts.map +1 -1
- package/out/common/LoadingIndicator.js +1 -1
- package/out/common/LoadingIndicator.js.map +1 -1
- package/out/common/PresetChips.d.ts.map +1 -1
- package/out/common/PresetChips.js.map +1 -1
- package/out/common/cx.d.ts.map +1 -1
- package/out/common/cx.js.map +1 -1
- package/out/common/useClipboard.d.ts.map +1 -1
- package/out/common/useClipboard.js.map +1 -1
- package/out/common/useDebouncedValue.d.ts.map +1 -1
- package/out/common/useDebouncedValue.js.map +1 -1
- package/out/index.d.ts +17 -17
- package/out/index.d.ts.map +1 -1
- package/out/index.js +9 -9
- package/out/index.js.map +1 -1
- package/out/map/BackendControl.d.ts.map +1 -1
- package/out/map/BackendControl.js.map +1 -1
- package/out/map/CompareToggle.d.ts +1 -1
- package/out/map/CompareToggle.d.ts.map +1 -1
- package/out/map/CompareToggle.js.map +1 -1
- package/out/map/DemoControls.d.ts +4 -4
- package/out/map/DemoControls.d.ts.map +1 -1
- package/out/map/DemoControls.js.map +1 -1
- package/out/map/DemoMap.d.ts +2 -2
- package/out/map/DemoMap.d.ts.map +1 -1
- package/out/map/DemoMap.js.map +1 -1
- package/out/map/GeocoderDemo.d.ts +3 -3
- package/out/map/GeocoderDemo.d.ts.map +1 -1
- package/out/map/GeocoderDemo.js +25 -6
- package/out/map/GeocoderDemo.js.map +1 -1
- package/out/map/OverlayLayers.d.ts +3 -3
- package/out/map/OverlayLayers.d.ts.map +1 -1
- package/out/map/OverlayLayers.js +2 -2
- package/out/map/OverlayLayers.js.map +1 -1
- package/out/map/PlaceAutocomplete.d.ts +1 -1
- package/out/map/PlaceAutocomplete.d.ts.map +1 -1
- package/out/map/PlaceAutocomplete.js +1 -1
- package/out/map/PlaceAutocomplete.js.map +1 -1
- package/out/map/PlaceMarker.d.ts.map +1 -1
- package/out/map/PlaceMarker.js.map +1 -1
- package/out/map/ResolvedPlaceLayers.d.ts +1 -1
- package/out/map/ResolvedPlaceLayers.d.ts.map +1 -1
- package/out/map/ResolvedPlaceLayers.js.map +1 -1
- package/out/map/ResultCamera.d.ts +2 -2
- package/out/map/ResultCamera.d.ts.map +1 -1
- package/out/map/ResultCamera.js +1 -1
- package/out/map/ResultCamera.js.map +1 -1
- package/out/map/ResultOverlay.d.ts +2 -2
- package/out/map/ResultOverlay.d.ts.map +1 -1
- package/out/map/ResultOverlay.js.map +1 -1
- package/out/map/ResultPanel.d.ts +1 -1
- package/out/map/ResultPanel.d.ts.map +1 -1
- package/out/map/ResultPanel.js +1 -1
- package/out/map/ResultPanel.js.map +1 -1
- package/out/map/VersionPicker.d.ts +1 -1
- package/out/map/VersionPicker.d.ts.map +1 -1
- package/out/map/VersionPicker.js.map +1 -1
- package/out/map/geometry.d.ts.map +1 -1
- package/out/map/geometry.js +7 -7
- package/out/map/geometry.js.map +1 -1
- package/out/map/index.d.ts +12 -12
- package/out/map/index.d.ts.map +1 -1
- package/out/map/index.js +6 -6
- package/out/map/index.js.map +1 -1
- package/out/map/place-render.d.ts +2 -2
- package/out/map/place-render.d.ts.map +1 -1
- package/out/map/place-render.js +1 -1
- package/out/map/place-render.js.map +1 -1
- package/out/map/types.d.ts +11 -9
- package/out/map/types.d.ts.map +1 -1
- package/out/map/types.js +2 -2
- package/out/map/types.js.map +1 -1
- package/out/map/useCompareState.d.ts.map +1 -1
- package/out/map/useCompareState.js.map +1 -1
- package/out/map/useDemoGeocode.d.ts +3 -3
- package/out/map/useDemoGeocode.d.ts.map +1 -1
- package/out/map/useDemoGeocode.js +1 -1
- package/out/map/useDemoGeocode.js.map +1 -1
- package/out/map/useMapPlaceRender.d.ts +1 -1
- package/out/map/useMapPlaceRender.d.ts.map +1 -1
- package/out/map/useMapPlaceRender.js +1 -1
- package/out/map/useMapPlaceRender.js.map +1 -1
- package/out/map/usePlaceAutocomplete.d.ts +1 -1
- package/out/map/usePlaceAutocomplete.d.ts.map +1 -1
- package/out/map/usePlaceAutocomplete.js +34 -23
- package/out/map/usePlaceAutocomplete.js.map +1 -1
- package/out/pipeline/CandidatePicker.d.ts +1 -1
- package/out/pipeline/CandidatePicker.d.ts.map +1 -1
- package/out/pipeline/CandidatePicker.js +1 -1
- package/out/pipeline/CandidatePicker.js.map +1 -1
- package/out/pipeline/ComponentTable.d.ts +1 -1
- package/out/pipeline/ComponentTable.d.ts.map +1 -1
- package/out/pipeline/ComponentTable.js.map +1 -1
- package/out/pipeline/ConfidenceCell.d.ts.map +1 -1
- package/out/pipeline/ConfidenceCell.js +1 -1
- package/out/pipeline/ConfidenceCell.js.map +1 -1
- package/out/pipeline/PipelineExplorer.d.ts +1 -1
- package/out/pipeline/PipelineExplorer.d.ts.map +1 -1
- package/out/pipeline/PipelineExplorer.js +3 -3
- package/out/pipeline/PipelineExplorer.js.map +1 -1
- package/out/pipeline/QueryForm.d.ts +2 -2
- package/out/pipeline/QueryForm.d.ts.map +1 -1
- package/out/pipeline/QueryForm.js.map +1 -1
- package/out/pipeline/ResolvedPlace.d.ts +1 -1
- package/out/pipeline/ResolvedPlace.d.ts.map +1 -1
- package/out/pipeline/ResolvedPlace.js.map +1 -1
- package/out/pipeline/copy.d.ts +1 -1
- package/out/pipeline/copy.d.ts.map +1 -1
- package/out/pipeline/copy.js.map +1 -1
- package/out/pipeline/presets.d.ts.map +1 -1
- package/out/pipeline/presets.js +2 -1
- package/out/pipeline/presets.js.map +1 -1
- package/out/pipeline/types.d.ts.map +1 -1
- package/out/pipeline/types.js.map +1 -1
- package/out/pipeline/useParsePipeline.d.ts +1 -1
- package/out/pipeline/useParsePipeline.d.ts.map +1 -1
- package/out/pipeline/useParsePipeline.js.map +1 -1
- package/out/poi/AbstainPanel.d.ts.map +1 -1
- package/out/poi/AbstainPanel.js.map +1 -1
- package/out/poi/LiveResultsBlock.d.ts +1 -1
- package/out/poi/LiveResultsBlock.d.ts.map +1 -1
- package/out/poi/LiveResultsBlock.js +1 -1
- package/out/poi/LiveResultsBlock.js.map +1 -1
- package/out/poi/OverpassBlock.d.ts.map +1 -1
- package/out/poi/OverpassBlock.js.map +1 -1
- package/out/poi/POIExplorer.d.ts +1 -1
- package/out/poi/POIExplorer.d.ts.map +1 -1
- package/out/poi/POIExplorer.js +2 -2
- package/out/poi/POIExplorer.js.map +1 -1
- package/out/poi/QueryInput.d.ts.map +1 -1
- package/out/poi/QueryInput.js.map +1 -1
- package/out/poi/SubjectPanel.d.ts +1 -1
- package/out/poi/SubjectPanel.d.ts.map +1 -1
- package/out/poi/SubjectPanel.js.map +1 -1
- package/out/poi/runtime.d.ts +1 -1
- package/out/poi/runtime.d.ts.map +1 -1
- package/out/poi/runtime.js.map +1 -1
- package/out/poi/types.d.ts +1 -1
- package/out/poi/types.d.ts.map +1 -1
- package/out/poi/types.js +1 -1
- package/out/poi/types.js.map +1 -1
- package/out/poi/usePOISearch.d.ts +1 -1
- package/out/poi/usePOISearch.d.ts.map +1 -1
- package/out/poi/usePOISearch.js +75 -48
- package/out/poi/usePOISearch.js.map +1 -1
- package/out/runtime/useDemoRuntime.d.ts +2 -2
- package/out/runtime/useDemoRuntime.d.ts.map +1 -1
- package/out/runtime/useDemoRuntime.js +7 -5
- package/out/runtime/useDemoRuntime.js.map +1 -1
- package/package.json +338 -18
- package/styles.css +49 -19
- /package/{common → lib/common}/KindBadge.tsx +0 -0
- /package/{common → lib/common}/PresetChips.tsx +0 -0
- /package/{common → lib/common}/cx.ts +0 -0
- /package/{common → lib/common}/useClipboard.ts +0 -0
- /package/{common → lib/common}/useDebouncedValue.ts +0 -0
- /package/{map → lib/map}/BackendControl.tsx +0 -0
- /package/{map → lib/map}/PlaceMarker.tsx +0 -0
- /package/{map → lib/map}/useCompareState.ts +0 -0
- /package/{pipeline → lib/pipeline}/types.ts +0 -0
- /package/{poi → lib/poi}/AbstainPanel.tsx +0 -0
- /package/{poi → lib/poi}/OverpassBlock.tsx +0 -0
- /package/{poi → lib/poi}/QueryInput.tsx +0 -0
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
* the presentational {@link PlaceAutocomplete} listbox renders `suggestions` / `activeIndex`.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { type KeyboardEvent, useCallback, useEffect,
|
|
18
|
+
import { type KeyboardEvent, useCallback, useEffect, useState } from "react"
|
|
19
19
|
|
|
20
|
-
import { useDebouncedValue } from "
|
|
21
|
-
import type { Suggestion } from "
|
|
20
|
+
import { useDebouncedValue } from "#common/useDebouncedValue"
|
|
21
|
+
import type { Suggestion } from "#map/types"
|
|
22
22
|
|
|
23
23
|
export interface UsePlaceAutocompleteOptions {
|
|
24
24
|
/**
|
|
@@ -95,6 +95,12 @@ export interface UsePlaceAutocomplete {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const LISTBOX_ID = "mw-demo-suggest-list"
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stable empty list for the derived no-suggestions state — a fresh `[]` per render would churn the identity every
|
|
101
|
+
* consumer and every useCallback dependency list sees.
|
|
102
|
+
*/
|
|
103
|
+
const NO_SUGGESTIONS: Suggestion[] = []
|
|
98
104
|
const optionID = (index: number) => `mw-demo-suggest-${index}`
|
|
99
105
|
|
|
100
106
|
/**
|
|
@@ -118,30 +124,33 @@ export function usePlaceAutocomplete({
|
|
|
118
124
|
minChars = 2,
|
|
119
125
|
debounceMs = 150,
|
|
120
126
|
}: UsePlaceAutocompleteOptions): UsePlaceAutocomplete {
|
|
121
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The last COMPLETED fetch, keyed by the query that produced it. Visibility is DERIVED from this during render rather
|
|
129
|
+
* than pushed through sync setStates in the effect — the effect's only job is the async fetch, so every state write
|
|
130
|
+
* in it happens after an await and the react(set-state-in-effect) rule is satisfied by structure, not by exception.
|
|
131
|
+
*/
|
|
132
|
+
const [fetched, setFetched] = useState<{ query: string; suggestions: Suggestion[] } | null>(null)
|
|
133
|
+
/**
|
|
134
|
+
* The query whose list was dismissed or picked. Suppression as DATA, replacing the one-shot ref flag: a pick stores
|
|
135
|
+
* the query the rewritten text will produce, so the post-pick fetch is skipped and the place just chosen is never
|
|
136
|
+
* re-suggested. One divergence from the ref version, accepted and small: after Esc, retyping the exact same string
|
|
137
|
+
* keeps the list hidden until the query changes.
|
|
138
|
+
*/
|
|
139
|
+
const [dismissed, setDismissed] = useState<string | null>(null)
|
|
122
140
|
const [activeIndex, setActiveIndex] = useState(-1)
|
|
123
|
-
// One-shot guard: a pick rewrites `text` to the chosen name, which would otherwise re-trigger the fetch and
|
|
124
|
-
// immediately re-suggest the place just chosen. Set on pick, consumed by the next effect run.
|
|
125
|
-
const suppressRef = useRef(false)
|
|
126
141
|
|
|
127
142
|
const query = localitySegment(text)
|
|
128
143
|
const debouncedQuery = useDebouncedValue(query, debounceMs)
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
if (suppressRef.current) {
|
|
132
|
-
suppressRef.current = false
|
|
133
|
-
setSuggestions([])
|
|
134
|
-
setActiveIndex(-1)
|
|
145
|
+
const eligible = Boolean(autocomplete) && debouncedQuery.length >= minChars && !/^\d/.test(debouncedQuery)
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
const suggestions =
|
|
148
|
+
eligible && fetched?.query === debouncedQuery && dismissed !== debouncedQuery ? fetched.suggestions : NO_SUGGESTIONS
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
setActiveIndex(-1)
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!autocomplete || debouncedQuery.length < minChars || /^\d/.test(debouncedQuery)) return
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
}
|
|
153
|
+
if (dismissed === debouncedQuery) return
|
|
145
154
|
|
|
146
155
|
let cancelled = false
|
|
147
156
|
|
|
@@ -150,11 +159,11 @@ export function usePlaceAutocomplete({
|
|
|
150
159
|
const next = await autocomplete(debouncedQuery)
|
|
151
160
|
|
|
152
161
|
if (cancelled) return
|
|
153
|
-
|
|
162
|
+
setFetched({ query: debouncedQuery, suggestions: next })
|
|
154
163
|
setActiveIndex(-1)
|
|
155
164
|
} catch {
|
|
156
165
|
if (cancelled) return
|
|
157
|
-
|
|
166
|
+
setFetched({ query: debouncedQuery, suggestions: [] })
|
|
158
167
|
setActiveIndex(-1)
|
|
159
168
|
}
|
|
160
169
|
})()
|
|
@@ -162,22 +171,25 @@ export function usePlaceAutocomplete({
|
|
|
162
171
|
return () => {
|
|
163
172
|
cancelled = true
|
|
164
173
|
}
|
|
165
|
-
}, [debouncedQuery, autocomplete, minChars])
|
|
174
|
+
}, [debouncedQuery, autocomplete, minChars, dismissed])
|
|
166
175
|
|
|
167
176
|
const pick = useCallback(
|
|
168
177
|
(value: string) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
const next = replaceSegment(text, value)
|
|
179
|
+
|
|
180
|
+
setText(next)
|
|
181
|
+
// Suppress the fetch the rewritten text would trigger — the segment being typed IS the picked name.
|
|
182
|
+
setDismissed(localitySegment(next))
|
|
183
|
+
setFetched(null)
|
|
172
184
|
setActiveIndex(-1)
|
|
173
185
|
},
|
|
174
186
|
[text, setText]
|
|
175
187
|
)
|
|
176
188
|
|
|
177
189
|
const dismiss = useCallback(() => {
|
|
178
|
-
|
|
190
|
+
setDismissed(debouncedQuery)
|
|
179
191
|
setActiveIndex(-1)
|
|
180
|
-
}, [])
|
|
192
|
+
}, [debouncedQuery])
|
|
181
193
|
|
|
182
194
|
const onInputKeyDown = useCallback(
|
|
183
195
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from "react"
|
|
11
11
|
|
|
12
|
-
import { cx } from "
|
|
13
|
-
import type { ResolvedPlaceView } from "
|
|
12
|
+
import { cx } from "#common/cx"
|
|
13
|
+
import type { ResolvedPlaceView } from "#pipeline/types"
|
|
14
14
|
|
|
15
15
|
export interface CandidatePickerProps {
|
|
16
16
|
candidates: ResolvedPlaceView[]
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ReactNode } from "react"
|
|
10
10
|
|
|
11
|
+
import type { ParsedComponent } from "#pipeline/types"
|
|
12
|
+
|
|
11
13
|
import { ConfidenceCell } from "./ConfidenceCell.tsx"
|
|
12
|
-
import type { ParsedComponent } from "./types.ts"
|
|
13
14
|
|
|
14
15
|
export interface ComponentTableProps {
|
|
15
16
|
nodes: ParsedComponent[]
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ReactNode } from "react"
|
|
13
13
|
|
|
14
|
+
import { buildParsePayload } from "#pipeline/copy"
|
|
15
|
+
import { PIPELINE_DEFAULT_ADDRESS, PIPELINE_PRESETS } from "#pipeline/presets"
|
|
16
|
+
import type { PipelinePanels, PipelineRuntime } from "#pipeline/types"
|
|
17
|
+
import { useParsePipeline } from "#pipeline/useParsePipeline"
|
|
18
|
+
|
|
14
19
|
import { ClientOnly } from "../common/ClientOnly.tsx"
|
|
15
20
|
import { CopyButton } from "../common/CopyButton.tsx"
|
|
16
21
|
import { KindBadge } from "../common/KindBadge.tsx"
|
|
@@ -18,12 +23,8 @@ import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
|
|
|
18
23
|
import { PresetChips, type Preset } from "../common/PresetChips.tsx"
|
|
19
24
|
import { CandidatePicker } from "./CandidatePicker.tsx"
|
|
20
25
|
import { ComponentTable } from "./ComponentTable.tsx"
|
|
21
|
-
import { buildParsePayload } from "./copy.ts"
|
|
22
|
-
import { PIPELINE_DEFAULT_ADDRESS, PIPELINE_PRESETS } from "./presets.ts"
|
|
23
26
|
import { QueryForm } from "./QueryForm.tsx"
|
|
24
27
|
import { ResolvedPlace } from "./ResolvedPlace.tsx"
|
|
25
|
-
import type { PipelinePanels, PipelineRuntime } from "./types.ts"
|
|
26
|
-
import { useParsePipeline } from "./useParsePipeline.ts"
|
|
27
28
|
|
|
28
29
|
export interface PipelineExplorerProps {
|
|
29
30
|
/**
|
|
@@ -19,11 +19,11 @@ export interface QueryFormProps {
|
|
|
19
19
|
busy?: boolean
|
|
20
20
|
placeholder?: string
|
|
21
21
|
/**
|
|
22
|
-
* Keydown on the input — the
|
|
22
|
+
* Keydown on the input — the hook a combobox (the demo's place-autocomplete) needs for arrow/Enter/Esc nav.
|
|
23
23
|
*/
|
|
24
24
|
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Override for extra `<input>` attributes (aria-combobox props, `autoComplete`, …). Merged last.
|
|
27
27
|
*/
|
|
28
28
|
inputProps?: InputHTMLAttributes<HTMLInputElement>
|
|
29
29
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Fragment, type ReactNode } from "react"
|
|
11
11
|
|
|
12
|
-
import type { DualRoleView, ResolvedPlaceView } from "
|
|
12
|
+
import type { DualRoleView, ResolvedPlaceView } from "#pipeline/types"
|
|
13
13
|
|
|
14
14
|
export interface ResolvedPlaceProps {
|
|
15
15
|
place: ResolvedPlaceView
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* pretty-printed. Pure; shared by the explorer's copy button.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ParseResult, ResolvedPlaceView } from "
|
|
10
|
+
import type { ParseResult, ResolvedPlaceView } from "#pipeline/types"
|
|
11
11
|
|
|
12
12
|
export function buildParsePayload(result: ParseResult, selected: ResolvedPlaceView | null): string {
|
|
13
13
|
return JSON.stringify(
|
|
@@ -30,6 +30,7 @@ export const PIPELINE_PRESETS: ReadonlyArray<Preset> = [
|
|
|
30
30
|
{ label: "Space Needle", value: "400 Broad St, Seattle, WA 98109", country: "us" },
|
|
31
31
|
{ label: "ZIP only", value: "90210", country: "us" },
|
|
32
32
|
{ label: "Berlin (native order)", value: "Straußstraße 27, 12623 Berlin", country: "de" },
|
|
33
|
+
{ label: "Berlin city-state (int'l order)", value: "5 Hauptstraße, Berlin, Berlin 10115", country: "de" },
|
|
33
34
|
{ label: "Paris (street fall-through)", value: "181 Rue du Chevaleret, Paris", country: "fr" },
|
|
34
35
|
// GB dependent_locality (placetype-pair-prior arc) — "Henbury" flips to dependent_locality via the en-gb pair-index
|
|
35
36
|
// prior; the UK postcode is also structurally detectable (parity with docs EXAMPLE_ADDRESSES).
|
|
@@ -40,7 +41,7 @@ export const PIPELINE_PRESETS: ReadonlyArray<Preset> = [
|
|
|
40
41
|
},
|
|
41
42
|
// NZ dependent_locality (en-nz pair-prior arc) — Plimmerton is a suburb (dependent_locality) of Porirua. Postcode
|
|
42
43
|
// DELIBERATELY OMITTED: a trailing "Porirua 5026" folds "porirua 5026" in segment mode and misses the index's bare
|
|
43
|
-
// "porirua" key (tracked as #1308). The `country: "nz"` pin is
|
|
44
|
+
// "porirua" key (tracked as #1308). The `country: "nz"` pin is required — locale-hint can't structurally detect
|
|
44
45
|
// NZ (4-digit postcode isn't distinctive), so only the pin selects the nz index; free-typed NZ stays unfired.
|
|
45
46
|
{ label: "Plimmerton (NZ dependent_locality)", value: "35 Steyne Avenue, Plimmerton, Porirua", country: "nz" },
|
|
46
47
|
]
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { useCallback, useState } from "react"
|
|
13
13
|
|
|
14
|
-
import type { ParseResult, PipelineRuntime, ResolvedPlaceView } from "
|
|
14
|
+
import type { ParseResult, PipelineRuntime, ResolvedPlaceView } from "#pipeline/types"
|
|
15
15
|
|
|
16
16
|
export interface UseParsePipelineOptions {
|
|
17
17
|
runtime: PipelineRuntime
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ReactNode } from "react"
|
|
12
12
|
|
|
13
|
-
import { formatDistance } from "
|
|
14
|
-
import type { LiveSearchState } from "
|
|
13
|
+
import { formatDistance } from "#poi/runtime"
|
|
14
|
+
import type { LiveSearchState } from "#poi/types"
|
|
15
15
|
|
|
16
16
|
export interface LiveResultsBlockProps {
|
|
17
17
|
/**
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { type ReactNode, useState } from "react"
|
|
13
13
|
|
|
14
|
+
import { POI_DEFAULT_TEXT, POI_PRESETS } from "#poi/runtime"
|
|
15
|
+
import type { LoadPOIRuntime, POILiveSearch } from "#poi/types"
|
|
16
|
+
import { usePOISearch } from "#poi/usePOISearch"
|
|
17
|
+
|
|
14
18
|
import { ClientOnly } from "../common/ClientOnly.tsx"
|
|
15
19
|
import { KindBadge } from "../common/KindBadge.tsx"
|
|
16
20
|
import { PresetChips, type Preset } from "../common/PresetChips.tsx"
|
|
@@ -18,10 +22,7 @@ import { AbstainPanel } from "./AbstainPanel.tsx"
|
|
|
18
22
|
import { LiveResultsBlock } from "./LiveResultsBlock.tsx"
|
|
19
23
|
import { OverpassBlock } from "./OverpassBlock.tsx"
|
|
20
24
|
import { QueryInput } from "./QueryInput.tsx"
|
|
21
|
-
import { POI_DEFAULT_TEXT, POI_PRESETS } from "./runtime.ts"
|
|
22
25
|
import { SubjectPanel } from "./SubjectPanel.tsx"
|
|
23
|
-
import type { LoadPOIRuntime, POILiveSearch } from "./types.ts"
|
|
24
|
-
import { usePOISearch } from "./usePOISearch.ts"
|
|
25
26
|
|
|
26
27
|
export interface POIExplorerProps {
|
|
27
28
|
/**
|
|
@@ -13,7 +13,7 @@ import { createKindClassifier } from "@mailwoman/kind-classifier"
|
|
|
13
13
|
import type { POIPhraseLookup } from "@mailwoman/kind-classifier"
|
|
14
14
|
import { createPOIBrandLookup, createPOITaxonomyLookup } from "@mailwoman/poi-taxonomy/table"
|
|
15
15
|
|
|
16
|
-
import type { POIRuntime } from "
|
|
16
|
+
import type { POIRuntime } from "#poi/types"
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Build the POI runtime, dynamically importing the taxonomy + brand JSON so the tables land in their own chunk. A
|
|
@@ -31,8 +31,8 @@ export async function loadPOIRuntime(): Promise<POIRuntime> {
|
|
|
31
31
|
import("@mailwoman/poi-taxonomy/data/brands.json").then((m) => m.default),
|
|
32
32
|
])
|
|
33
33
|
|
|
34
|
-
const lookup = createPOITaxonomyLookup(table as
|
|
35
|
-
const brands = createPOIBrandLookup(brandTable as
|
|
34
|
+
const lookup = createPOITaxonomyLookup(table as Parameters<typeof createPOITaxonomyLookup>[0])
|
|
35
|
+
const brands = createPOIBrandLookup(brandTable as Parameters<typeof createPOIBrandLookup>[0])
|
|
36
36
|
|
|
37
37
|
// Adapt the taxonomy + brand lookups to the `POIPhraseLookup` shape the classifier expects: categories first, brands
|
|
38
38
|
// on a category miss.
|
package/{poi → lib/poi}/types.ts
RENAMED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Types for the POI explorer. The intent path (classification → subject → OverpassQL) is fully
|
|
7
7
|
* self-contained over the pure `@mailwoman/*` packages; the live poi.db path is expressed only as an
|
|
8
|
-
* INJECTED async function ({@link POILiveSearch}) so the httpvfs/worker
|
|
8
|
+
* INJECTED async function ({@link POILiveSearch}) so the httpvfs/worker implementation stays out of this
|
|
9
9
|
* package's browser graph (it lives in the docs site, which knows where the layer is served).
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -13,11 +13,11 @@ import { matchPOISubject } from "@mailwoman/kind-classifier"
|
|
|
13
13
|
import { emitOverpassQL } from "@mailwoman/poi-taxonomy/overpass"
|
|
14
14
|
import type { OverpassIntentLike } from "@mailwoman/poi-taxonomy/overpass"
|
|
15
15
|
import { computeQueryShape } from "@mailwoman/query-shape"
|
|
16
|
-
import { useCallback, useEffect,
|
|
16
|
+
import { useCallback, useEffect, useEffectEvent, useState } from "react"
|
|
17
17
|
|
|
18
|
-
import { useDebouncedValue } from "
|
|
19
|
-
import { loadPOIRuntime } from "
|
|
20
|
-
import type { LiveSearchState, LoadPOIRuntime, POIExplorerResult, POILiveSearch, POIRuntime } from "
|
|
18
|
+
import { useDebouncedValue } from "#common/useDebouncedValue"
|
|
19
|
+
import { loadPOIRuntime } from "#poi/runtime"
|
|
20
|
+
import type { LiveSearchState, LoadPOIRuntime, POIExplorerResult, POILiveSearch, POIRuntime } from "#poi/types"
|
|
21
21
|
|
|
22
22
|
export interface UsePOISearchOptions {
|
|
23
23
|
/**
|
|
@@ -84,12 +84,12 @@ function buildOverpass(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const intent: OverpassIntentLike = {
|
|
87
|
-
subject: { kind: "category", categoryID, matched: matchedPhrase },
|
|
87
|
+
subject: { kind: "category", categoryIDs: [categoryID], matched: matchedPhrase },
|
|
88
88
|
...(remainder ? { anchor: { text: remainder } } : {}),
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
try {
|
|
92
|
-
return { overpassQL: emitOverpassQL(intent, category.osmTag ? {
|
|
92
|
+
return { overpassQL: emitOverpassQL(intent, category.osmTag ? { osmTags: [category.osmTag] } : {}) }
|
|
93
93
|
} catch (error) {
|
|
94
94
|
return { overpassError: error instanceof Error ? error.message : String(error) }
|
|
95
95
|
}
|
|
@@ -103,21 +103,31 @@ export function usePOISearch({
|
|
|
103
103
|
debounceMs = 250,
|
|
104
104
|
}: UsePOISearchOptions): UsePOISearch {
|
|
105
105
|
const [runtime, setRuntime] = useState<POIRuntime | null>(null)
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The classify result KEYED BY the query that produced it. The visible result is derived during render
|
|
108
|
+
* (`storedResult.query === trimmedText ? … : null`), so a new query invalidates the old answer by derivation — the
|
|
109
|
+
* effect never writes state synchronously to "reset", which is the react(set-state-in-effect) shape the lint bump
|
|
110
|
+
* rightly flags.
|
|
111
|
+
*/
|
|
112
|
+
const [storedResult, setStoredResult] = useState<{ query: string; value: POIExplorerResult } | null>(null)
|
|
113
|
+
/**
|
|
114
|
+
* Live-search state, keyed the same way: launched FOR a query, visible only while that query stands.
|
|
115
|
+
*/
|
|
116
|
+
const [storedLive, setStoredLive] = useState<{ query: string; state: LiveSearchState } | null>(null)
|
|
108
117
|
|
|
109
118
|
const debouncedText = useDebouncedValue(text, debounceMs)
|
|
119
|
+
const trimmedText = debouncedText.trim()
|
|
110
120
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
// The load fires exactly ONCE on mount regardless of whether the caller passes a fresh `loadRuntime`
|
|
122
|
+
// closure each render (an inline `async () => …` would otherwise retrigger the effect → reload →
|
|
123
|
+
// re-render loop). `useEffectEvent` reads the LATEST closure without joining the dependency list —
|
|
124
|
+
// the runtime is a load-once resource.
|
|
125
|
+
const loadRuntimeEvent = useEffectEvent(() => loadRuntime())
|
|
116
126
|
|
|
117
127
|
useEffect(() => {
|
|
118
128
|
let cancelled = false
|
|
119
129
|
|
|
120
|
-
|
|
130
|
+
void loadRuntimeEvent().then((loaded) => {
|
|
121
131
|
if (!cancelled) {
|
|
122
132
|
setRuntime(loaded)
|
|
123
133
|
}
|
|
@@ -129,20 +139,16 @@ export function usePOISearch({
|
|
|
129
139
|
}, [])
|
|
130
140
|
|
|
131
141
|
// Classify the debounced query and derive the subject + OverpassQL (async, so it lives in an effect).
|
|
142
|
+
// Every state write below happens after an await — invalidation on query change is handled by the
|
|
143
|
+
// key derivation above, not by a synchronous reset here.
|
|
132
144
|
useEffect(() => {
|
|
133
145
|
if (!runtime) return
|
|
134
146
|
|
|
135
|
-
|
|
136
|
-
const trimmed = debouncedText.trim()
|
|
137
|
-
|
|
138
|
-
// A new query invalidates live results from the previous one.
|
|
139
|
-
setLiveSearch({ status: "idle" })
|
|
147
|
+
const trimmed = trimmedText
|
|
140
148
|
|
|
141
|
-
if (!trimmed)
|
|
142
|
-
setResult(null)
|
|
149
|
+
if (!trimmed) return
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
}
|
|
151
|
+
let cancelled = false
|
|
146
152
|
|
|
147
153
|
const input = { raw: trimmed, normalized: trimmed }
|
|
148
154
|
const shape = computeQueryShape(trimmed)
|
|
@@ -153,7 +159,7 @@ export function usePOISearch({
|
|
|
153
159
|
const matched = kindResult.kind === "poi_query" ? matchPOISubject(trimmed, undefined, runtime.lexicon) : null
|
|
154
160
|
|
|
155
161
|
if (!matched) {
|
|
156
|
-
|
|
162
|
+
setStoredResult({ query: trimmed, value: { kindResult } })
|
|
157
163
|
|
|
158
164
|
return
|
|
159
165
|
}
|
|
@@ -161,15 +167,18 @@ export function usePOISearch({
|
|
|
161
167
|
// Brand subject: the lexicon carries the brand's canonical name as `categoryID` + its Wikidata QID. No category
|
|
162
168
|
// record, no OverpassQL (brands are searched by QID against the layer's `brand_wikidata` index, not OSM tags).
|
|
163
169
|
if ((matched.match.kind ?? "category") === "brand") {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
setStoredResult({
|
|
171
|
+
query: trimmed,
|
|
172
|
+
value: {
|
|
173
|
+
kindResult,
|
|
174
|
+
subject: {
|
|
175
|
+
kind: "brand",
|
|
176
|
+
name: matched.match.categoryID,
|
|
177
|
+
...(matched.match.wikidata ? { wikidata: matched.match.wikidata } : {}),
|
|
178
|
+
matchedPhrase: matched.match.matchedPhrase,
|
|
179
|
+
confidence: matched.match.confidence,
|
|
180
|
+
remainder: matched.remainder,
|
|
181
|
+
},
|
|
173
182
|
},
|
|
174
183
|
})
|
|
175
184
|
|
|
@@ -179,29 +188,37 @@ export function usePOISearch({
|
|
|
179
188
|
const category = runtime.lookup.getPOICategory(matched.match.categoryID)
|
|
180
189
|
|
|
181
190
|
if (!category) {
|
|
182
|
-
|
|
191
|
+
setStoredResult({ query: trimmed, value: { kindResult } })
|
|
183
192
|
|
|
184
193
|
return
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
setStoredResult({
|
|
197
|
+
query: trimmed,
|
|
198
|
+
value: {
|
|
199
|
+
kindResult,
|
|
200
|
+
subject: {
|
|
201
|
+
kind: "category",
|
|
202
|
+
category,
|
|
203
|
+
matchedPhrase: matched.match.matchedPhrase,
|
|
204
|
+
confidence: matched.match.confidence,
|
|
205
|
+
remainder: matched.remainder,
|
|
206
|
+
buildLocal: runtime.lookup.requiresBuildLocalLayer(category),
|
|
207
|
+
},
|
|
208
|
+
...buildOverpass(runtime, matched.match.categoryID, matched.match.matchedPhrase, matched.remainder),
|
|
196
209
|
},
|
|
197
|
-
...buildOverpass(runtime, matched.match.categoryID, matched.match.matchedPhrase, matched.remainder),
|
|
198
210
|
})
|
|
199
211
|
})
|
|
200
212
|
|
|
201
213
|
return () => {
|
|
202
214
|
cancelled = true
|
|
203
215
|
}
|
|
204
|
-
}, [
|
|
216
|
+
}, [trimmedText, runtime])
|
|
217
|
+
|
|
218
|
+
// Derived visibility: an answer stands only while its query does. A new query reads as null/idle
|
|
219
|
+
// with no reset write anywhere.
|
|
220
|
+
const result = storedResult?.query === trimmedText ? storedResult.value : null
|
|
221
|
+
const liveSearch: LiveSearchState = storedLive?.query === trimmedText ? storedLive.state : { status: "idle" }
|
|
205
222
|
|
|
206
223
|
const subject = result?.subject
|
|
207
224
|
|
|
@@ -221,7 +238,7 @@ export function usePOISearch({
|
|
|
221
238
|
|
|
222
239
|
if (subject.kind === "category" ? subject.buildLocal : !(brandLiveSearch && subject.wikidata)) return
|
|
223
240
|
|
|
224
|
-
|
|
241
|
+
setStoredLive({ query: trimmedText, state: { status: "loading" } })
|
|
225
242
|
|
|
226
243
|
try {
|
|
227
244
|
const outcome = await runLiveSearch(
|
|
@@ -241,16 +258,25 @@ export function usePOISearch({
|
|
|
241
258
|
)
|
|
242
259
|
|
|
243
260
|
if (outcome.status === "success") {
|
|
244
|
-
|
|
261
|
+
setStoredLive({
|
|
262
|
+
query: trimmedText,
|
|
263
|
+
state: { status: "success", hits: outcome.hits, centerName: outcome.centerName },
|
|
264
|
+
})
|
|
245
265
|
} else if (outcome.status === "unplaced") {
|
|
246
|
-
|
|
266
|
+
setStoredLive({ query: trimmedText, state: { status: "error", message: `couldn't place "${outcome.anchor}"` } })
|
|
247
267
|
} else {
|
|
248
|
-
|
|
268
|
+
setStoredLive({
|
|
269
|
+
query: trimmedText,
|
|
270
|
+
state: { status: "error", message: "the published POI layer isn't reachable" },
|
|
271
|
+
})
|
|
249
272
|
}
|
|
250
273
|
} catch {
|
|
251
|
-
|
|
274
|
+
setStoredLive({
|
|
275
|
+
query: trimmedText,
|
|
276
|
+
state: { status: "error", message: "the published POI layer isn't reachable" },
|
|
277
|
+
})
|
|
252
278
|
}
|
|
253
|
-
}, [runLiveSearch, runtime, subject, brandLiveSearch])
|
|
279
|
+
}, [runLiveSearch, runtime, subject, brandLiveSearch, trimmedText])
|
|
254
280
|
|
|
255
281
|
return { runtimeReady: runtime !== null, result, liveSearch, canSearchLive, searchLive }
|
|
256
282
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* WOF opener, the FST fetch, the releases.json fetch) are INJECTED by the host as async functions, so
|
|
11
11
|
* this module imports only React: no `onnxruntime-web`, no `sql.js-httpvfs`, no `maplibre-gl`, no
|
|
12
12
|
* `fetch`-specific plumbing. That keeps it node-import-safe and root-exportable from
|
|
13
|
-
* `@mailwoman/react` — the exact
|
|
13
|
+
* `@mailwoman/react` — the exact interface `PipelineRuntime` established, generalized to the loader itself.
|
|
14
14
|
*
|
|
15
15
|
* The hook is generic over `TAssets` (the opaque bundle the host's `loadAssets` returns — classifier,
|
|
16
16
|
* FST, WOF lookup, calibrator, …) and `TRelease` (the host's release-manifest entry). The package
|
|
@@ -169,7 +169,7 @@ export interface DemoLoaderState<TAssets, TRelease extends DemoReleaseBase = Dem
|
|
|
169
169
|
*
|
|
170
170
|
* Sequence: on mount `loadManifest` runs and its `defaultVersion` becomes the selection; each version (or `forceWASM`)
|
|
171
171
|
* change reloads the bundle via `loadAssets`, the previous load aborted first. The assets are revealed ATOMICALLY when
|
|
172
|
-
* `loadAssets` resolves (so `ready` flips exactly once per load), and consumers
|
|
172
|
+
* `loadAssets` resolves (so `ready` flips exactly once per load), and consumers wait on `ready`.
|
|
173
173
|
*/
|
|
174
174
|
export function useDemoRuntime<TAssets, TRelease extends DemoReleaseBase = DemoReleaseBase>(
|
|
175
175
|
config: DemoRuntimeConfig<TAssets, TRelease>
|
|
@@ -189,15 +189,18 @@ export function useDemoRuntime<TAssets, TRelease extends DemoReleaseBase = DemoR
|
|
|
189
189
|
// Latest-ref the injected loaders: a host that re-creates them each render (an inline arrow) must NOT retrigger the
|
|
190
190
|
// load effects, which key ONLY on version/backend. The effects read `.current` at run time.
|
|
191
191
|
const loadManifestRef = useRef(config.loadManifest)
|
|
192
|
-
loadManifestRef.current = config.loadManifest
|
|
193
192
|
const loadAssetsRef = useRef(config.loadAssets)
|
|
194
|
-
loadAssetsRef.current = config.loadAssets
|
|
195
193
|
|
|
196
194
|
// Latest manifest for the version-load effect, so it can resolve the release WITHOUT depending on `manifest`
|
|
197
195
|
// identity — which would double-fire the load the instant the manifest first arrives (the selection transition
|
|
198
196
|
// null → defaultVersion already fires it once).
|
|
199
197
|
const manifestRef = useRef<DemoManifest<TRelease> | null>(null)
|
|
200
|
-
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
loadManifestRef.current = config.loadManifest
|
|
201
|
+
loadAssetsRef.current = config.loadAssets
|
|
202
|
+
manifestRef.current = manifest
|
|
203
|
+
}, [config.loadAssets, config.loadManifest, manifest])
|
|
201
204
|
|
|
202
205
|
// Mount: fetch the manifest, then select the default version.
|
|
203
206
|
useEffect(() => {
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
|
|
9
9
|
* `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
|
|
10
10
|
*
|
|
11
|
-
* The mount flag
|
|
12
|
-
*
|
|
11
|
+
* The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
|
|
12
|
+
* server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
|
|
13
|
+
* earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
|
|
14
|
+
* render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
|
|
15
|
+
* mechanism, not an accident, and the store form gets the same second paint without a set-state.
|
|
13
16
|
*/
|
|
14
17
|
import { type ReactNode } from "react";
|
|
15
18
|
export interface ClientOnlyProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../common/ClientOnly.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ClientOnly.d.ts","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAA;AAS5D,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,QAAQ,EAAE,MAAM,SAAS,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAe,EAAE,EAAE,eAAe,GAAG,SAAS,CAQpF"}
|
package/out/common/ClientOnly.js
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
* Remix, Docusaurus). It renders `fallback` on the server and the first client paint, then swaps to
|
|
9
9
|
* `children()` once mounted — keeping timers, clipboard, and dynamic imports off the server render.
|
|
10
10
|
*
|
|
11
|
-
* The mount flag
|
|
12
|
-
*
|
|
11
|
+
* The mount flag comes from `useSyncExternalStore` with a constant snapshot pair — `false` on the
|
|
12
|
+
* server, `true` in the browser — which is the store-shaped statement of exactly this boundary. The
|
|
13
|
+
* earlier `useState(false)` + `useEffect(() => setMounted(true))` said the same thing with a
|
|
14
|
+
* render-cascade the react(set-state-in-effect) rule rightly flags: the second render was the
|
|
15
|
+
* mechanism, not an accident, and the store form gets the same second paint without a set-state.
|
|
13
16
|
*/
|
|
14
|
-
import {
|
|
17
|
+
import { useSyncExternalStore } from "react";
|
|
18
|
+
/**
|
|
19
|
+
* The boundary never changes after mount, so nothing ever notifies.
|
|
20
|
+
*/
|
|
21
|
+
function subscribeNever() {
|
|
22
|
+
return () => { };
|
|
23
|
+
}
|
|
15
24
|
export function ClientOnly({ children, fallback = null }) {
|
|
16
|
-
const
|
|
17
|
-
useEffect(() => setMounted(true), []);
|
|
25
|
+
const mounted = useSyncExternalStore(subscribeNever, () => true, () => false);
|
|
18
26
|
return mounted ? children() : fallback;
|
|
19
27
|
}
|
|
20
28
|
//# sourceMappingURL=ClientOnly.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../common/ClientOnly.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ClientOnly.js","sourceRoot":"","sources":["../../lib/common/ClientOnly.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAkB,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5D;;GAEG;AACH,SAAS,cAAc;IACtB,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;AAChB,CAAC;AAaD,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,EAAmB;IACxE,MAAM,OAAO,GAAG,oBAAoB,CACnC,cAAc,EACd,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACX,CAAA;IAED,OAAO,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AACvC,CAAC"}
|