@mailwoman/react 9.4.0 → 10.0.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/lib/common/About.tsx +87 -71
- package/lib/common/index.ts +22 -0
- package/lib/index.ts +4 -99
- package/lib/map/BackendControl.tsx +7 -3
- package/lib/map/CompareToggle.tsx +3 -2
- package/lib/map/Geocoder.tsx +48 -28
- package/lib/map/GeocoderControls.tsx +258 -99
- package/lib/map/MapChipRow.tsx +77 -0
- package/lib/map/MapCompass.tsx +77 -0
- package/lib/map/MapControlStack.tsx +98 -0
- package/lib/map/MapFooter.tsx +115 -0
- package/lib/map/MapProgressBar.tsx +58 -0
- package/lib/map/MapSearchBar.tsx +68 -0
- package/lib/map/MapSheet.tsx +62 -0
- package/lib/map/index.ts +19 -0
- package/lib/map/types.ts +18 -4
- package/lib/map/useMapBearing.ts +56 -0
- package/lib/map/useMapLabelPick.ts +123 -0
- package/lib/pipeline/index.ts +36 -0
- package/lib/pipeline/useParsePipeline.ts +29 -18
- package/lib/poi/index.ts +39 -0
- package/lib/runtime/index.ts +16 -0
- package/out/common/About.d.ts +13 -4
- package/out/common/About.d.ts.map +1 -1
- package/out/common/About.js +5 -2
- package/out/common/About.js.map +1 -1
- package/out/common/index.d.ts +22 -0
- package/out/common/index.d.ts.map +1 -0
- package/out/common/index.js +15 -0
- package/out/common/index.js.map +1 -0
- package/out/index.d.ts +4 -59
- package/out/index.d.ts.map +1 -1
- package/out/index.js +4 -36
- package/out/index.js.map +1 -1
- package/out/map/BackendControl.d.ts.map +1 -1
- package/out/map/BackendControl.js +1 -1
- package/out/map/BackendControl.js.map +1 -1
- package/out/map/CompareToggle.d.ts.map +1 -1
- package/out/map/CompareToggle.js +1 -1
- package/out/map/CompareToggle.js.map +1 -1
- package/out/map/Geocoder.d.ts +11 -1
- package/out/map/Geocoder.d.ts.map +1 -1
- package/out/map/Geocoder.js +26 -26
- package/out/map/Geocoder.js.map +1 -1
- package/out/map/GeocoderControls.d.ts +29 -11
- package/out/map/GeocoderControls.d.ts.map +1 -1
- package/out/map/GeocoderControls.js +65 -16
- package/out/map/GeocoderControls.js.map +1 -1
- package/out/map/MapChipRow.d.ts +47 -0
- package/out/map/MapChipRow.d.ts.map +1 -0
- package/out/map/MapChipRow.js +12 -0
- package/out/map/MapChipRow.js.map +1 -0
- package/out/map/MapCompass.d.ts +42 -0
- package/out/map/MapCompass.d.ts.map +1 -0
- package/out/map/MapCompass.js +11 -0
- package/out/map/MapCompass.js.map +1 -0
- package/out/map/MapControlStack.d.ts +57 -0
- package/out/map/MapControlStack.d.ts.map +1 -0
- package/out/map/MapControlStack.js +15 -0
- package/out/map/MapControlStack.js.map +1 -0
- package/out/map/MapFooter.d.ts +40 -0
- package/out/map/MapFooter.d.ts.map +1 -0
- package/out/map/MapFooter.js +51 -0
- package/out/map/MapFooter.js.map +1 -0
- package/out/map/MapProgressBar.d.ts +34 -0
- package/out/map/MapProgressBar.d.ts.map +1 -0
- package/out/map/MapProgressBar.js +8 -0
- package/out/map/MapProgressBar.js.map +1 -0
- package/out/map/MapSearchBar.d.ts +49 -0
- package/out/map/MapSearchBar.d.ts.map +1 -0
- package/out/map/MapSearchBar.js +6 -0
- package/out/map/MapSearchBar.js.map +1 -0
- package/out/map/MapSheet.d.ts +32 -0
- package/out/map/MapSheet.d.ts.map +1 -0
- package/out/map/MapSheet.js +33 -0
- package/out/map/MapSheet.js.map +1 -0
- package/out/map/OverlayLayers.js +1 -1
- package/out/map/ResolvedPlaceLayers.js +1 -1
- package/out/map/ResultPanel.js +1 -1
- package/out/map/fake-runtime.js.map +1 -1
- package/out/map/index.d.ts +17 -0
- package/out/map/index.d.ts.map +1 -1
- package/out/map/index.js +10 -0
- package/out/map/index.js.map +1 -1
- package/out/map/map-debug.d.ts.map +1 -1
- package/out/map/map-debug.js.map +1 -1
- package/out/map/types.d.ts +20 -4
- package/out/map/types.d.ts.map +1 -1
- package/out/map/useMapBearing.d.ts +28 -0
- package/out/map/useMapBearing.d.ts.map +1 -0
- package/out/map/useMapBearing.js +35 -0
- package/out/map/useMapBearing.js.map +1 -0
- package/out/map/useMapLabelPick.d.ts +23 -0
- package/out/map/useMapLabelPick.d.ts.map +1 -0
- package/out/map/useMapLabelPick.js +104 -0
- package/out/map/useMapLabelPick.js.map +1 -0
- package/out/pipeline/CandidatePicker.js +1 -1
- package/out/pipeline/PipelineExplorer.js +1 -1
- package/out/pipeline/QueryForm.js +1 -1
- package/out/pipeline/ResolvedPlace.js +1 -1
- package/out/pipeline/TimingPanel.js +1 -1
- package/out/pipeline/index.d.ts +33 -0
- package/out/pipeline/index.d.ts.map +1 -0
- package/out/pipeline/index.js +21 -0
- package/out/pipeline/index.js.map +1 -0
- package/out/pipeline/useParsePipeline.d.ts +5 -2
- package/out/pipeline/useParsePipeline.d.ts.map +1 -1
- package/out/pipeline/useParsePipeline.js +6 -2
- package/out/pipeline/useParsePipeline.js.map +1 -1
- package/out/poi/LiveResultsBlock.js +1 -1
- package/out/poi/POIExplorer.js +1 -1
- package/out/poi/SubjectPanel.js +1 -1
- package/out/poi/index.d.ts +23 -0
- package/out/poi/index.d.ts.map +1 -0
- package/out/poi/index.js +15 -0
- package/out/poi/index.js.map +1 -0
- package/out/poi/runtime.d.ts.map +1 -1
- package/out/runtime/index.d.ts +9 -0
- package/out/runtime/index.d.ts.map +1 -0
- package/out/runtime/index.js +8 -0
- package/out/runtime/index.js.map +1 -0
- package/package.json +87 -6
- package/styleframe.config.ts +9 -1
- package/styles.css +948 -26
- package/tokens/index.css +2 -1
- package/tokens/tokens.json +7 -0
- package/tokens/tokens.resolver.json +1 -1
|
@@ -3,28 +3,36 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* pipeline units (QueryForm / PresetChips / LoadingIndicator). It reads the injected {@link GeocoderRuntime}
|
|
9
|
-
* for ready/version/backend/loading state and threads the {@link useGeocode} + {@link usePlaceAutocomplete}
|
|
10
|
-
* + {@link useCompareState} hooks into the controls. Host-specific visualizers arrive via {@link GeocoderPanels}.
|
|
6
|
+
* The geocoder's chrome over the map: a search pill on top, example chips on one line beneath it, a control column
|
|
7
|
+
* down the right edge, and a result sheet that rises from the bottom when there is something to show.
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* It replaces a 400px full-height panel that carried every control at once. The layout is the one the reference
|
|
10
|
+
* map apps use, and the reason is the map: chrome that floats over it leaves the map the page, where a slab makes
|
|
11
|
+
* the map the leftover third.
|
|
12
|
+
*
|
|
13
|
+
* NODE-SAFE: pure React + the shared units, no maplibre.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type
|
|
16
|
+
import { type ReactNode, useCallback, useState } from "react"
|
|
17
|
+
import type { MapInstance } from "react-map-gl/maplibre"
|
|
17
18
|
|
|
18
19
|
import type { GeocoderPanels, GeocoderRuntime } from "#map/types"
|
|
19
20
|
import type { UseCompareState } from "#map/useCompareState"
|
|
20
21
|
import type { UseGeocode } from "#map/useGeocode"
|
|
22
|
+
import { useMapBearing } from "#map/useMapBearing"
|
|
23
|
+
import { useMapLabelPick } from "#map/useMapLabelPick"
|
|
21
24
|
import type { UsePlaceAutocomplete } from "#map/usePlaceAutocomplete"
|
|
22
25
|
|
|
23
26
|
import { LoadingIndicator } from "../common/LoadingIndicator.tsx"
|
|
24
|
-
import {
|
|
25
|
-
import { QueryForm } from "../pipeline/QueryForm.tsx"
|
|
27
|
+
import type { Preset } from "../common/PresetChips.tsx"
|
|
26
28
|
import { BackendControl } from "./BackendControl.tsx"
|
|
27
29
|
import { CompareToggle } from "./CompareToggle.tsx"
|
|
30
|
+
import { MapChipRow } from "./MapChipRow.tsx"
|
|
31
|
+
import { MapCompass } from "./MapCompass.tsx"
|
|
32
|
+
import { MapControlButton, MapControlGroup, MapControlStack } from "./MapControlStack.tsx"
|
|
33
|
+
import { MapProgressBar } from "./MapProgressBar.tsx"
|
|
34
|
+
import { MapSearchBar } from "./MapSearchBar.tsx"
|
|
35
|
+
import { MapSheet } from "./MapSheet.tsx"
|
|
28
36
|
import { PlaceAutocomplete } from "./PlaceAutocomplete.tsx"
|
|
29
37
|
import { ResultPanel } from "./ResultPanel.tsx"
|
|
30
38
|
import { VersionPicker } from "./VersionPicker.tsx"
|
|
@@ -58,6 +66,16 @@ export interface GeocoderControlsProps {
|
|
|
58
66
|
* Input placeholder.
|
|
59
67
|
*/
|
|
60
68
|
placeholder: string
|
|
69
|
+
/**
|
|
70
|
+
* The live map, for the controls that read it: the compass takes its bearing, the host's layer control reads its
|
|
71
|
+
* style. `null` until react-map-gl instantiates the map.
|
|
72
|
+
*/
|
|
73
|
+
map?: MapInstance | null
|
|
74
|
+
/**
|
|
75
|
+
* Fired with the query whenever one is submitted, before the parse starts. The host writes it into the URL; this
|
|
76
|
+
* package never touches `location`, because the address bar is the app's state, not a control's.
|
|
77
|
+
*/
|
|
78
|
+
onSubmitQuery?: (query: string) => void
|
|
61
79
|
/**
|
|
62
80
|
* Select a model version (the composed geocoder also clears a now-colliding compare selection).
|
|
63
81
|
*/
|
|
@@ -66,10 +84,22 @@ export interface GeocoderControlsProps {
|
|
|
66
84
|
* Toggle the forced WASM backend.
|
|
67
85
|
*/
|
|
68
86
|
onForceWASMChange: (forceWASM: boolean) => void
|
|
87
|
+
/**
|
|
88
|
+
* Open the developer panel on mount. The model version, the backend readout and compare live inside it: they read on
|
|
89
|
+
* the model rather than on an address, and above the query field they were the first thing every visitor met.
|
|
90
|
+
*
|
|
91
|
+
* @default false
|
|
92
|
+
*/
|
|
93
|
+
developer?: boolean
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
/**
|
|
72
|
-
*
|
|
97
|
+
* Which side sheet is open. At most one, because they share an edge and a phone gives each the whole panel.
|
|
98
|
+
*/
|
|
99
|
+
type SheetName = "about" | "layers" | "developer" | null
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The chrome. Everything positioned here floats over the map; nothing occupies a column of the page.
|
|
73
103
|
*/
|
|
74
104
|
export function GeocoderControls({
|
|
75
105
|
runtime,
|
|
@@ -79,115 +109,244 @@ export function GeocoderControls({
|
|
|
79
109
|
panels,
|
|
80
110
|
presets,
|
|
81
111
|
placeholder,
|
|
112
|
+
map = null,
|
|
113
|
+
onSubmitQuery,
|
|
82
114
|
onSelectVersion,
|
|
83
115
|
onForceWASMChange,
|
|
116
|
+
developer = false,
|
|
84
117
|
}: GeocoderControlsProps): ReactNode {
|
|
85
118
|
const versions = runtime.availableVersions ?? []
|
|
86
119
|
const { busy, result, selectedCandidate } = geocode
|
|
87
120
|
const loading = runtime.loading
|
|
88
121
|
const errorMessage = geocode.parseError ?? runtime.errorMessage ?? null
|
|
89
122
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{panels.releaseInfo}
|
|
94
|
-
|
|
95
|
-
<VersionPicker
|
|
96
|
-
versions={versions}
|
|
97
|
-
selected={runtime.selectedVersion ?? null}
|
|
98
|
-
onSelect={onSelectVersion}
|
|
99
|
-
disabled={busy}
|
|
100
|
-
/>
|
|
123
|
+
// One sheet at a time. Two open at once stack on the same edge, and on a phone each is the full panel.
|
|
124
|
+
const [openSheet, setOpenSheet] = useState<SheetName>(developer ? "developer" : null)
|
|
125
|
+
const { bearing, resetNorth } = useMapBearing(map)
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
forceWASM={runtime.forceWASM ?? false}
|
|
105
|
-
onForceWASMChange={onForceWASMChange}
|
|
106
|
-
/>
|
|
127
|
+
const toggleSheet = (name: Exclude<SheetName, null>) => setOpenSheet((current) => (current === name ? null : name))
|
|
128
|
+
const closeSheet = useCallback(() => setOpenSheet(null), [])
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
// Every path that starts a query goes through here, so the URL is written in exactly one place — the event that
|
|
131
|
+
// caused it, rather than an effect watching the result after the fact.
|
|
132
|
+
const runQuery = useCallback(
|
|
133
|
+
(query: string) => {
|
|
134
|
+
onSubmitQuery?.(query)
|
|
135
|
+
void geocode.submit(query)
|
|
136
|
+
},
|
|
137
|
+
[onSubmitQuery, geocode]
|
|
138
|
+
)
|
|
117
139
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/>
|
|
140
|
+
// A label on the map is a search a visitor already typed by pointing at it.
|
|
141
|
+
const pickLabel = useCallback(
|
|
142
|
+
(name: string) => {
|
|
143
|
+
geocode.setText(name)
|
|
144
|
+
geocode.reset()
|
|
145
|
+
runQuery(name)
|
|
146
|
+
},
|
|
147
|
+
[geocode, runQuery]
|
|
148
|
+
)
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
useMapLabelPick(map, pickLabel)
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/>
|
|
152
|
+
const chips = presets.map((preset) => ({ label: preset.label, value: preset.value }))
|
|
153
|
+
// The bundle load no longer opens the result sheet: it reports on the bar at the top of the viewport and in the
|
|
154
|
+
// footer, so an empty sheet does not sit over the map for the length of a 38 MB download.
|
|
155
|
+
const showSheet = Boolean(busy || result || errorMessage)
|
|
156
|
+
|
|
157
|
+
const bundleLoading = Boolean(loading && !runtime.ready)
|
|
158
|
+
const steps = loading?.stepLabels.length ?? 0
|
|
139
159
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}}
|
|
147
|
-
trailing={panels.permalink ? panels.permalink(geocode.text) : null}
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
<MapProgressBar
|
|
163
|
+
active={bundleLoading}
|
|
164
|
+
fraction={steps ? ((loading?.stepIndex ?? 0) + 1) / steps : null}
|
|
165
|
+
label="Loading the geocoder"
|
|
148
166
|
/>
|
|
149
167
|
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
<div className="mw-map-chrome mw-map-chrome--top">
|
|
169
|
+
<form
|
|
170
|
+
className="mw-map-chrome__search"
|
|
171
|
+
onSubmit={(event) => {
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
runQuery(geocode.text)
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{/*
|
|
177
|
+
* `type="search"` brings its own clear button, so the pill carries no trailing slot: a second cross
|
|
178
|
+
* beside the native one is two controls for one job, and the spinner that used to live there changed
|
|
179
|
+
* the field's height on every submit.
|
|
180
|
+
*/}
|
|
181
|
+
<MapSearchBar label="Search addresses" leading={<span aria-hidden="true">⌕</span>} busy={busy}>
|
|
182
|
+
<input
|
|
183
|
+
id="mw-pipeline-input"
|
|
184
|
+
type="search"
|
|
185
|
+
aria-label="Address"
|
|
186
|
+
value={geocode.text}
|
|
187
|
+
onChange={(event) => geocode.setText(event.target.value)}
|
|
188
|
+
disabled={!runtime.ready}
|
|
189
|
+
placeholder={placeholder}
|
|
190
|
+
onKeyDown={autocomplete.onInputKeyDown}
|
|
191
|
+
{...autocomplete.inputProps}
|
|
192
|
+
/>
|
|
193
|
+
</MapSearchBar>
|
|
194
|
+
</form>
|
|
195
|
+
|
|
196
|
+
<PlaceAutocomplete
|
|
197
|
+
suggestions={autocomplete.suggestions}
|
|
198
|
+
activeIndex={autocomplete.activeIndex}
|
|
199
|
+
onPick={autocomplete.pick}
|
|
200
|
+
onHover={autocomplete.setActiveIndex}
|
|
201
|
+
listboxID={autocomplete.listboxID}
|
|
202
|
+
optionID={autocomplete.optionID}
|
|
156
203
|
/>
|
|
204
|
+
|
|
205
|
+
<MapChipRow
|
|
206
|
+
chips={chips}
|
|
207
|
+
label="Example addresses"
|
|
208
|
+
activeValue={geocode.text}
|
|
209
|
+
disabled={!runtime.ready || busy}
|
|
210
|
+
onPick={(value) => {
|
|
211
|
+
geocode.setText(value)
|
|
212
|
+
geocode.reset()
|
|
213
|
+
runQuery(value)
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
|
|
217
|
+
{panels.bias}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/*
|
|
221
|
+
* Every floating control lives in this one column, so nothing can land on top of anything else: the layer
|
|
222
|
+
* control joins the capsule rather than sitting in the left column, and the compass takes its own capsule
|
|
223
|
+
* below because it comes and goes and would otherwise resize the one above it.
|
|
224
|
+
*/}
|
|
225
|
+
<MapControlStack label="Map controls">
|
|
226
|
+
<MapControlGroup>
|
|
227
|
+
<MapControlButton
|
|
228
|
+
label="About this geocoder"
|
|
229
|
+
active={openSheet === "about"}
|
|
230
|
+
onPress={() => toggleSheet("about")}
|
|
231
|
+
>
|
|
232
|
+
<span aria-hidden="true">i</span>
|
|
233
|
+
</MapControlButton>
|
|
234
|
+
|
|
235
|
+
{panels.layers ? (
|
|
236
|
+
<MapControlButton label="Map layers" active={openSheet === "layers"} onPress={() => toggleSheet("layers")}>
|
|
237
|
+
<span aria-hidden="true">≡</span>
|
|
238
|
+
</MapControlButton>
|
|
239
|
+
) : null}
|
|
240
|
+
|
|
241
|
+
<MapControlButton
|
|
242
|
+
label="Developer controls"
|
|
243
|
+
active={openSheet === "developer"}
|
|
244
|
+
onPress={() => toggleSheet("developer")}
|
|
245
|
+
>
|
|
246
|
+
<span aria-hidden="true">⚙</span>
|
|
247
|
+
</MapControlButton>
|
|
248
|
+
</MapControlGroup>
|
|
249
|
+
|
|
250
|
+
<MapControlGroup className="mw-map-control-group--compass">
|
|
251
|
+
<MapCompass bearing={bearing} onResetNorth={resetNorth} />
|
|
252
|
+
</MapControlGroup>
|
|
253
|
+
</MapControlStack>
|
|
254
|
+
|
|
255
|
+
{openSheet === "about" ? (
|
|
256
|
+
<MapSheet title="About" onClose={closeSheet}>
|
|
257
|
+
{panels.header}
|
|
258
|
+
</MapSheet>
|
|
157
259
|
) : null}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<div className="mw-result">
|
|
164
|
-
<LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
|
|
165
|
-
</div>
|
|
166
|
-
) : result ? (
|
|
167
|
-
panels.result ? (
|
|
168
|
-
panels.result({
|
|
169
|
-
result,
|
|
170
|
-
selectedCandidate,
|
|
171
|
-
selectedCandidateIndex: geocode.selectedCandidateIndex,
|
|
172
|
-
onSelectCandidate: geocode.selectCandidate,
|
|
173
|
-
})
|
|
174
|
-
) : (
|
|
175
|
-
<ResultPanel
|
|
176
|
-
result={result}
|
|
177
|
-
selectedCandidate={selectedCandidate}
|
|
178
|
-
selectedCandidateIndex={geocode.selectedCandidateIndex}
|
|
179
|
-
onSelectCandidate={geocode.selectCandidate}
|
|
180
|
-
extras={panels.extras}
|
|
181
|
-
failure={panels.failure}
|
|
182
|
-
/>
|
|
183
|
-
)
|
|
260
|
+
|
|
261
|
+
{openSheet === "layers" ? (
|
|
262
|
+
<MapSheet title="Layers" onClose={closeSheet}>
|
|
263
|
+
{panels.layers?.({ map })}
|
|
264
|
+
</MapSheet>
|
|
184
265
|
) : null}
|
|
185
266
|
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
267
|
+
{openSheet === "developer" ? (
|
|
268
|
+
<MapSheet title="Developer" onClose={closeSheet}>
|
|
269
|
+
{panels.releaseInfo ? (
|
|
270
|
+
<div className="mw-map-sheet__row">
|
|
271
|
+
<span className="mw-map-sheet__label">This release</span>
|
|
272
|
+
{panels.releaseInfo}
|
|
273
|
+
</div>
|
|
274
|
+
) : null}
|
|
275
|
+
|
|
276
|
+
<div className="mw-map-sheet__row">
|
|
277
|
+
<VersionPicker
|
|
278
|
+
versions={versions}
|
|
279
|
+
selected={runtime.selectedVersion ?? null}
|
|
280
|
+
onSelect={onSelectVersion}
|
|
281
|
+
disabled={busy}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div className="mw-map-sheet__row">
|
|
286
|
+
<BackendControl
|
|
287
|
+
activeBackend={runtime.activeBackend}
|
|
288
|
+
forceWASM={runtime.forceWASM ?? false}
|
|
289
|
+
onForceWASMChange={onForceWASMChange}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="mw-map-sheet__row">
|
|
294
|
+
<CompareToggle
|
|
295
|
+
versions={versions}
|
|
296
|
+
primaryVersion={runtime.selectedVersion ?? null}
|
|
297
|
+
compareMode={compare.compareMode}
|
|
298
|
+
onCompareModeChange={compare.setCompareMode}
|
|
299
|
+
compareVersion={compare.compareVersion}
|
|
300
|
+
onCompareVersionChange={compare.setCompareVersion}
|
|
301
|
+
disabled={busy}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{panels.developerExtras}
|
|
306
|
+
</MapSheet>
|
|
307
|
+
) : null}
|
|
308
|
+
|
|
309
|
+
{showSheet ? (
|
|
310
|
+
<section className="mw-map-sheet mw-map-sheet--bottom" aria-label="Result">
|
|
311
|
+
<div className="mw-map-sheet__handle" aria-hidden="true" />
|
|
312
|
+
|
|
313
|
+
{errorMessage ? <p className="mw-error">{errorMessage}</p> : null}
|
|
314
|
+
|
|
315
|
+
{panels.aboveResult ? panels.aboveResult({ result }) : null}
|
|
316
|
+
|
|
317
|
+
{busy ? (
|
|
318
|
+
<div className="mw-result">
|
|
319
|
+
<LoadingIndicator mode="staged" steps={runtime.parseStageLabels} activeStep={geocode.parseStage} />
|
|
320
|
+
</div>
|
|
321
|
+
) : result ? (
|
|
322
|
+
panels.result ? (
|
|
323
|
+
panels.result({
|
|
324
|
+
result,
|
|
325
|
+
selectedCandidate,
|
|
326
|
+
selectedCandidateIndex: geocode.selectedCandidateIndex,
|
|
327
|
+
onSelectCandidate: geocode.selectCandidate,
|
|
328
|
+
})
|
|
329
|
+
) : (
|
|
330
|
+
<ResultPanel
|
|
331
|
+
result={result}
|
|
332
|
+
selectedCandidate={selectedCandidate}
|
|
333
|
+
selectedCandidateIndex={geocode.selectedCandidateIndex}
|
|
334
|
+
onSelectCandidate={geocode.selectCandidate}
|
|
335
|
+
extras={panels.extras}
|
|
336
|
+
failure={panels.failure}
|
|
337
|
+
/>
|
|
338
|
+
)
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
{panels.compare
|
|
342
|
+
? panels.compare({ result, compareMode: compare.compareMode, compareVersion: compare.compareVersion })
|
|
343
|
+
: null}
|
|
344
|
+
|
|
345
|
+
{panels.permalink ? <div className="mw-map-sheet__actions">{panels.permalink(geocode.text)}</div> : null}
|
|
346
|
+
</section>
|
|
347
|
+
) : null}
|
|
189
348
|
|
|
190
349
|
{panels.footer}
|
|
191
|
-
|
|
350
|
+
</>
|
|
192
351
|
)
|
|
193
352
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `<MapChipRow>` — the row of example queries under a search bar.
|
|
7
|
+
*
|
|
8
|
+
* It SCROLLS horizontally and never wraps. Earth's twelve examples wrapped down five ragged rows (3/3/2/1/1/1) and
|
|
9
|
+
* took a third of the panel; one scrolling line is the shape the reference map apps use, and it costs a fixed
|
|
10
|
+
* height whatever the number of examples.
|
|
11
|
+
*
|
|
12
|
+
* NODE-SAFE: pure React, no maplibre.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react"
|
|
16
|
+
|
|
17
|
+
import { cx } from "#common/cx"
|
|
18
|
+
|
|
19
|
+
export interface MapChip {
|
|
20
|
+
/**
|
|
21
|
+
* What the chip reads.
|
|
22
|
+
*/
|
|
23
|
+
label: string
|
|
24
|
+
/**
|
|
25
|
+
* The query the chip stands for, handed back on press. Defaults to the label.
|
|
26
|
+
*/
|
|
27
|
+
value?: string
|
|
28
|
+
/**
|
|
29
|
+
* Rendered before the label — a category mark.
|
|
30
|
+
*/
|
|
31
|
+
icon?: ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MapChipRowProps {
|
|
35
|
+
chips: MapChip[]
|
|
36
|
+
/**
|
|
37
|
+
* Fired with the chip's `value` (or its label) when one is pressed.
|
|
38
|
+
*/
|
|
39
|
+
onPick: (value: string) => void
|
|
40
|
+
/**
|
|
41
|
+
* The chip matching this value reads as pressed.
|
|
42
|
+
*/
|
|
43
|
+
activeValue?: string
|
|
44
|
+
disabled?: boolean
|
|
45
|
+
className?: string
|
|
46
|
+
/**
|
|
47
|
+
* Accessible name for the row, e.g. "Example addresses".
|
|
48
|
+
*/
|
|
49
|
+
label?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function MapChipRow({ chips, onPick, activeValue, disabled, className, label }: MapChipRowProps): ReactNode {
|
|
53
|
+
if (!chips.length) return null
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cx("mw-map-chiprow", className)} role="group" aria-label={label}>
|
|
57
|
+
{chips.map((chip) => {
|
|
58
|
+
const value = chip.value ?? chip.label
|
|
59
|
+
const active = value === activeValue
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
key={value}
|
|
64
|
+
type="button"
|
|
65
|
+
className={cx("mw-map-chip", active && "mw-map-chip--active")}
|
|
66
|
+
aria-pressed={active}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
onClick={() => onPick(value)}
|
|
69
|
+
>
|
|
70
|
+
{chip.icon ? <span className="mw-map-chip__icon">{chip.icon}</span> : null}
|
|
71
|
+
{chip.label}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `<MapCompass>` — the needle that appears when the map leaves north and fades out when it returns.
|
|
7
|
+
*
|
|
8
|
+
* It is a compass ROSE, the way the reference map apps draw it: a ringed dial carrying a two-tone needle whose red
|
|
9
|
+
* half points north and whose pale half points south. A single-color arrow cannot say which end is north, so a
|
|
10
|
+
* reader has to already know the convention to read it; two tones say it outright. There is no `N` on the dial —
|
|
11
|
+
* at this size the letter and the needle's north tip want the same few pixels, and the tip is the clearer of them.
|
|
12
|
+
*
|
|
13
|
+
* The whole dial counter-rotates the bearing, so the needle keeps pointing at true north while the map turns under
|
|
14
|
+
* it. Pressing it returns the map to north, which is why the control is a button rather than an ornament.
|
|
15
|
+
*
|
|
16
|
+
* It stays mounted through the fade rather than unmounting on the bearing crossing zero, because a control that
|
|
17
|
+
* vanishes mid-gesture is the thing that reads as a glitch. `HIDE_BELOW_DEGREES` is the dead zone: a map settled by
|
|
18
|
+
* a snap-to-north lands a fraction off zero, and a compass that lingers over that fraction never goes away.
|
|
19
|
+
*
|
|
20
|
+
* Under `prefers-reduced-motion` it appears and disappears with no transition, and the dial still rotates —
|
|
21
|
+
* rotation IS the information, not decoration.
|
|
22
|
+
*
|
|
23
|
+
* NODE-SAFE: pure React, no maplibre. The host reads the bearing off its own map and passes it in.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ReactNode } from "react"
|
|
27
|
+
|
|
28
|
+
import { cx } from "#common/cx"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Below this many degrees off north the compass is treated as pointing north and fades out.
|
|
32
|
+
*/
|
|
33
|
+
const HIDE_BELOW_DEGREES = 0.5
|
|
34
|
+
|
|
35
|
+
export interface MapCompassProps {
|
|
36
|
+
/**
|
|
37
|
+
* The map's bearing in degrees, as MapLibre reports it: 0 is north, positive is counter-clockwise.
|
|
38
|
+
*/
|
|
39
|
+
bearing: number
|
|
40
|
+
/**
|
|
41
|
+
* Fired when the compass is pressed — the host resets its map to north.
|
|
42
|
+
*/
|
|
43
|
+
onResetNorth: () => void
|
|
44
|
+
/**
|
|
45
|
+
* The control's accessible name. @default "Reset bearing to north"
|
|
46
|
+
*/
|
|
47
|
+
label?: string
|
|
48
|
+
className?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function MapCompass({ bearing, onResetNorth, label, className }: MapCompassProps): ReactNode {
|
|
52
|
+
const facingNorth = Math.abs(bearing) < HIDE_BELOW_DEGREES
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
className={cx("mw-map-compass", facingNorth && "mw-map-compass--north", className)}
|
|
58
|
+
aria-label={label ?? "Reset bearing to north"}
|
|
59
|
+
title={label ?? "Reset bearing to north"}
|
|
60
|
+
// Out of the tab order and out of the accessibility tree while it is invisible, so a keyboard reaches only
|
|
61
|
+
// the controls a pointer can see.
|
|
62
|
+
aria-hidden={facingNorth}
|
|
63
|
+
tabIndex={facingNorth ? -1 : 0}
|
|
64
|
+
onClick={onResetNorth}
|
|
65
|
+
>
|
|
66
|
+
<svg viewBox="0 0 32 32" width="26" height="26" aria-hidden="true" focusable="false">
|
|
67
|
+
<g style={{ transform: `rotate(${-bearing}deg)`, transformOrigin: "16px 16px" }}>
|
|
68
|
+
<circle cx="16" cy="16" r="12.5" className="mw-map-compass__dial" />
|
|
69
|
+
|
|
70
|
+
{/* North, then south. Two triangles meeting at the hub rather than one arrow through it. */}
|
|
71
|
+
<path d="M16 5.5 L20.5 16 L16 16 Z M16 5.5 L11.5 16 L16 16 Z" className="mw-map-compass__north" />
|
|
72
|
+
<path d="M16 26.5 L20.5 16 L16 16 Z M16 26.5 L11.5 16 L16 16 Z" className="mw-map-compass__south" />
|
|
73
|
+
</g>
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `<MapControlStack>` — the column of map controls down one edge, and `<MapControlButton>`, the control inside it.
|
|
7
|
+
*
|
|
8
|
+
* Controls sit in GROUPS: a group is one glass capsule with hairlines between its buttons, and separate groups are
|
|
9
|
+
* separate capsules with a gap. That grouping is the whole layout idea — related controls read as one object, and
|
|
10
|
+
* an unrelated one (a compass that comes and goes) does not join a capsule it would resize.
|
|
11
|
+
*
|
|
12
|
+
* NODE-SAFE: pure React, no maplibre.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react"
|
|
16
|
+
|
|
17
|
+
import { cx } from "#common/cx"
|
|
18
|
+
|
|
19
|
+
export interface MapControlButtonProps {
|
|
20
|
+
/**
|
|
21
|
+
* The mark inside the control — an SVG icon or a single glyph.
|
|
22
|
+
*/
|
|
23
|
+
children: ReactNode
|
|
24
|
+
/**
|
|
25
|
+
* The control's accessible name. Required: the visible content is a mark, not a word.
|
|
26
|
+
*/
|
|
27
|
+
label: string
|
|
28
|
+
onPress: () => void
|
|
29
|
+
/**
|
|
30
|
+
* Reads as active, for a control that toggles a layer or a mode.
|
|
31
|
+
*/
|
|
32
|
+
active?: boolean
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MapControlButton({
|
|
38
|
+
children,
|
|
39
|
+
label,
|
|
40
|
+
onPress,
|
|
41
|
+
active,
|
|
42
|
+
disabled,
|
|
43
|
+
className,
|
|
44
|
+
}: MapControlButtonProps): ReactNode {
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
className={cx("mw-map-control", active && "mw-map-control--active", className)}
|
|
49
|
+
aria-label={label}
|
|
50
|
+
aria-pressed={active}
|
|
51
|
+
title={label}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
onClick={onPress}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MapControlGroupProps {
|
|
61
|
+
children: ReactNode
|
|
62
|
+
className?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One capsule. Put related controls in the same group; give an unrelated one its own.
|
|
67
|
+
*/
|
|
68
|
+
export function MapControlGroup({ children, className }: MapControlGroupProps): ReactNode {
|
|
69
|
+
return <div className={cx("mw-map-control-group", className)}>{children}</div>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MapControlStackProps {
|
|
73
|
+
/**
|
|
74
|
+
* Groups, top to bottom.
|
|
75
|
+
*/
|
|
76
|
+
children: ReactNode
|
|
77
|
+
/**
|
|
78
|
+
* Which edge the column sits against. @default "right"
|
|
79
|
+
*/
|
|
80
|
+
side?: "left" | "right"
|
|
81
|
+
className?: string
|
|
82
|
+
/**
|
|
83
|
+
* Accessible name for the column, e.g. "Map controls".
|
|
84
|
+
*/
|
|
85
|
+
label?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function MapControlStack({ children, side = "right", className, label }: MapControlStackProps): ReactNode {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={cx("mw-map-control-stack", `mw-map-control-stack--${side}`, className)}
|
|
92
|
+
role="group"
|
|
93
|
+
aria-label={label}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|