@vespercms/admin 0.0.0-stage → 0.1.0-next.1
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 +119 -2
- package/dist/BodyEditor-_mkh0BSQ.js +3047 -0
- package/dist/a11y/FieldNotes.d.ts +23 -0
- package/dist/a11y/NotesDrawer.d.ts +23 -0
- package/dist/a11y/pdf.d.ts +3 -0
- package/dist/a11y/run.d.ts +3 -0
- package/dist/a11y/useNotes.d.ts +28 -0
- package/dist/app/App.d.ts +2 -0
- package/dist/app/Screen.d.ts +88 -0
- package/dist/app/UserMenu.d.ts +7 -0
- package/dist/app/api.d.ts +75 -0
- package/dist/app/appearance.d.ts +10 -0
- package/dist/app/browser-api.d.ts +17 -0
- package/dist/app/collection-icon.d.ts +12 -0
- package/dist/app/context.d.ts +23 -0
- package/dist/app/entries-changed.d.ts +11 -0
- package/dist/app/file-url.d.ts +13 -0
- package/dist/app/format.d.ts +4 -0
- package/dist/app/idb.d.ts +7 -0
- package/dist/app/load.d.ts +16 -0
- package/dist/app/media.d.ts +5 -0
- package/dist/app/navigation.d.ts +4 -0
- package/dist/app/pane-context.d.ts +19 -0
- package/dist/app/panes.d.ts +14 -0
- package/dist/app/router.d.ts +62 -0
- package/dist/body/BodyEditor.d.ts +42 -0
- package/dist/body/TableView.d.ts +2 -0
- package/dist/body/catalog.d.ts +47 -0
- package/dist/body/checks.d.ts +15 -0
- package/dist/body/chips.d.ts +31 -0
- package/dist/body/commands.d.ts +31 -0
- package/dist/body/document.d.ts +42 -0
- package/dist/body/insert.d.ts +69 -0
- package/dist/body/nodes.d.ts +38 -0
- package/dist/body/reveal.d.ts +13 -0
- package/dist/body/scroll.d.ts +16 -0
- package/dist/body/settings.d.ts +19 -0
- package/dist/body/shortcuts.d.ts +13 -0
- package/dist/body/table.d.ts +31 -0
- package/dist/body/ui.d.ts +30 -0
- package/dist/body/views.d.ts +9 -0
- package/dist/checks-C-0xDu-I.js +57 -0
- package/dist/cloud/CloudGate.d.ts +14 -0
- package/dist/cloud/access.d.ts +14 -0
- package/dist/cloud/changes.d.ts +26 -0
- package/dist/cloud/connection.d.ts +100 -0
- package/dist/cloud/editor.d.ts +44 -0
- package/dist/cloud/session.d.ts +62 -0
- package/dist/cloud/storage.d.ts +45 -0
- package/dist/editor/Field.d.ts +24 -0
- package/dist/editor/PagePicker.d.ts +20 -0
- package/dist/editor/arrange.d.ts +13 -0
- package/dist/editor/context.d.ts +23 -0
- package/dist/editor/copy.d.ts +9 -0
- package/dist/editor/reveal.d.ts +7 -0
- package/dist/editor/target.d.ts +21 -0
- package/dist/editor/values.d.ts +18 -0
- package/dist/geo-map-CT3OGD7O.js +110 -0
- package/dist/github/GitHubGate.d.ts +15 -0
- package/dist/github/api.d.ts +20 -0
- package/dist/github/cache.d.ts +2 -0
- package/dist/github/session.d.ts +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/markdown-4fVeej3c.js +211 -0
- package/dist/media/MediaPicker.d.ts +28 -0
- package/dist/media/file-icon.d.ts +3 -0
- package/dist/media/formats.d.ts +23 -0
- package/dist/media/hygiene.d.ts +37 -0
- package/dist/media/upload.d.ts +31 -0
- package/dist/mount.d.ts +3 -0
- package/dist/pdf-m5s_uXJ3.js +24 -0
- package/dist/run-y4XRkg17.js +7 -0
- package/dist/schedule/ScheduleDialog.d.ts +28 -0
- package/dist/schedule/ScheduleSummary.d.ts +12 -0
- package/dist/schedule/ScheduledView.d.ts +8 -0
- package/dist/schedule/cloud.d.ts +21 -0
- package/dist/schedule/now.d.ts +5 -0
- package/dist/schedule/times.d.ts +35 -0
- package/dist/screens/EntryEditor.d.ts +13 -0
- package/dist/screens/EntryList.d.ts +16 -0
- package/dist/screens/Home.d.ts +6 -0
- package/dist/screens/ListPane.d.ts +6 -0
- package/dist/screens/MediaBrowser.d.ts +7 -0
- package/dist/screens/NotFound.d.ts +3 -0
- package/dist/screens/PageTree.d.ts +10 -0
- package/dist/screens/Redirects.d.ts +1 -0
- package/dist/screens/Unavailable.d.ts +8 -0
- package/dist/screens/status.d.ts +36 -0
- package/dist/shortcuts-CarKeDqp.js +4666 -0
- package/dist/studio.css +3 -0
- package/dist/studio.js +6449 -0
- package/dist/types.d.ts +76 -0
- package/dist/widgets/basic.d.ts +25 -0
- package/dist/widgets/blank-date.d.ts +8 -0
- package/dist/widgets/crops.d.ts +43 -0
- package/dist/widgets/focal-point.d.ts +15 -0
- package/dist/widgets/geo-map.d.ts +10 -0
- package/dist/widgets/geo.d.ts +16 -0
- package/dist/widgets/group.d.ts +41 -0
- package/dist/widgets/icon.d.ts +9 -0
- package/dist/widgets/image.d.ts +16 -0
- package/dist/widgets/markdown-lazy.d.ts +2 -0
- package/dist/widgets/markdown.d.ts +2 -0
- package/dist/widgets/reference.d.ts +12 -0
- package/dist/widgets/registry.d.ts +32 -0
- package/dist/widgets/special.d.ts +20 -0
- package/dist/widgets/union.d.ts +36 -0
- package/package.json +63 -4
- package/src/a11y/FieldNotes.tsx +56 -0
- package/src/a11y/NotesDrawer.tsx +81 -0
- package/src/a11y/pdf.ts +45 -0
- package/src/a11y/run.ts +12 -0
- package/src/a11y/useNotes.ts +139 -0
- package/src/app/App.tsx +156 -0
- package/src/app/Screen.tsx +416 -0
- package/src/app/UserMenu.tsx +95 -0
- package/src/app/api.ts +176 -0
- package/src/app/appearance.ts +54 -0
- package/src/app/browser-api.ts +173 -0
- package/src/app/collection-icon.ts +15 -0
- package/src/app/context.tsx +32 -0
- package/src/app/entries-changed.ts +48 -0
- package/src/app/file-url.tsx +44 -0
- package/src/app/format.ts +39 -0
- package/src/app/idb.ts +82 -0
- package/src/app/load.ts +40 -0
- package/src/app/media.ts +20 -0
- package/src/app/navigation.ts +4 -0
- package/src/app/pane-context.ts +25 -0
- package/src/app/panes.tsx +513 -0
- package/src/app/router.ts +158 -0
- package/src/body/BodyEditor.tsx +1139 -0
- package/src/body/TableView.tsx +700 -0
- package/src/body/catalog.ts +198 -0
- package/src/body/checks.ts +83 -0
- package/src/body/chips.ts +150 -0
- package/src/body/commands.ts +191 -0
- package/src/body/document.ts +388 -0
- package/src/body/insert.tsx +301 -0
- package/src/body/nodes.ts +287 -0
- package/src/body/reveal.ts +93 -0
- package/src/body/scroll.ts +53 -0
- package/src/body/settings.tsx +510 -0
- package/src/body/shortcuts.ts +31 -0
- package/src/body/table.ts +192 -0
- package/src/body/ui.ts +77 -0
- package/src/body/views.tsx +268 -0
- package/src/build.ts +6 -0
- package/src/cloud/CloudGate.tsx +245 -0
- package/src/cloud/access.ts +52 -0
- package/src/cloud/changes.tsx +474 -0
- package/src/cloud/connection.ts +281 -0
- package/src/cloud/editor.tsx +250 -0
- package/src/cloud/session.ts +289 -0
- package/src/cloud/storage.ts +212 -0
- package/src/editor/Field.tsx +182 -0
- package/src/editor/PagePicker.tsx +102 -0
- package/src/editor/arrange.ts +98 -0
- package/src/editor/context.tsx +31 -0
- package/src/editor/copy.ts +30 -0
- package/src/editor/reveal.ts +38 -0
- package/src/editor/target.ts +104 -0
- package/src/editor/values.ts +71 -0
- package/src/github/GitHubGate.tsx +298 -0
- package/src/github/api.ts +81 -0
- package/src/github/cache.ts +64 -0
- package/src/github/session.ts +146 -0
- package/src/index.ts +10 -0
- package/src/media/MediaPicker.tsx +302 -0
- package/src/media/file-icon.ts +17 -0
- package/src/media/formats.ts +262 -0
- package/src/media/hygiene.ts +220 -0
- package/src/media/upload.ts +47 -0
- package/src/mount.tsx +20 -0
- package/src/schedule/ScheduleDialog.tsx +303 -0
- package/src/schedule/ScheduleSummary.tsx +59 -0
- package/src/schedule/ScheduledView.tsx +294 -0
- package/src/schedule/cloud.ts +66 -0
- package/src/schedule/now.ts +18 -0
- package/src/schedule/times.ts +122 -0
- package/src/screens/EntryEditor.tsx +1817 -0
- package/src/screens/EntryList.tsx +272 -0
- package/src/screens/Home.tsx +131 -0
- package/src/screens/ListPane.tsx +477 -0
- package/src/screens/MediaBrowser.tsx +253 -0
- package/src/screens/NotFound.tsx +19 -0
- package/src/screens/PageTree.tsx +738 -0
- package/src/screens/Redirects.tsx +522 -0
- package/src/screens/Unavailable.tsx +27 -0
- package/src/screens/status.tsx +104 -0
- package/src/studio.css +168 -0
- package/src/types.ts +82 -0
- package/src/widgets/basic.tsx +315 -0
- package/src/widgets/blank-date.ts +32 -0
- package/src/widgets/crops.tsx +399 -0
- package/src/widgets/focal-point.tsx +240 -0
- package/src/widgets/geo-map.tsx +160 -0
- package/src/widgets/geo.tsx +170 -0
- package/src/widgets/group.tsx +827 -0
- package/src/widgets/icon.tsx +47 -0
- package/src/widgets/image.tsx +407 -0
- package/src/widgets/markdown-lazy.tsx +22 -0
- package/src/widgets/markdown.tsx +267 -0
- package/src/widgets/reference.tsx +196 -0
- package/src/widgets/registry.ts +104 -0
- package/src/widgets/special.tsx +290 -0
- package/src/widgets/union.tsx +233 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
2
|
+
import { Map as MapLibre, Marker, setWorkerUrl } from 'maplibre-gl';
|
|
3
|
+
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import type { LatLng } from './geo.tsx';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The geo field's map (DECISIONS.md: MapLibre with OpenFreeMap tiles). Loaded
|
|
9
|
+
* on demand. The pin is a real button: Tab reaches it, and the arrow keys
|
|
10
|
+
* move it (Shift moves ten times as far), with each move announced.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
setWorkerUrl(workerUrl);
|
|
14
|
+
|
|
15
|
+
const STYLE = 'https://tiles.openfreemap.org/styles/liberty';
|
|
16
|
+
const WORLD: LatLng = { lat: 20, lng: 0 };
|
|
17
|
+
|
|
18
|
+
export default function GeoMap({
|
|
19
|
+
label,
|
|
20
|
+
point,
|
|
21
|
+
center,
|
|
22
|
+
zoom,
|
|
23
|
+
disabled,
|
|
24
|
+
onChange,
|
|
25
|
+
}: {
|
|
26
|
+
label: string;
|
|
27
|
+
point: LatLng | null;
|
|
28
|
+
center: LatLng | undefined;
|
|
29
|
+
zoom: number | undefined;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
onChange: (point: LatLng) => void;
|
|
32
|
+
}) {
|
|
33
|
+
const container = useRef<HTMLDivElement>(null);
|
|
34
|
+
const map = useRef<MapLibre | null>(null);
|
|
35
|
+
const marker = useRef<Marker | null>(null);
|
|
36
|
+
const onChangeRef = useRef(onChange);
|
|
37
|
+
const [failed, setFailed] = useState(false);
|
|
38
|
+
const [announcement, setAnnouncement] = useState('');
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
onChangeRef.current = onChange;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!container.current) return;
|
|
46
|
+
const start = point ?? center ?? WORLD;
|
|
47
|
+
let instance: MapLibre;
|
|
48
|
+
try {
|
|
49
|
+
instance = new MapLibre({
|
|
50
|
+
container: container.current,
|
|
51
|
+
style: STYLE,
|
|
52
|
+
center: [start.lng, start.lat],
|
|
53
|
+
zoom: zoom ?? (point || center ? 13 : 1),
|
|
54
|
+
attributionControl: { compact: true },
|
|
55
|
+
// The map itself isn't a tab stop; the pin and the number fields are.
|
|
56
|
+
keyboard: false,
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// No WebGL, most likely. Reported after this effect, not during it.
|
|
60
|
+
void Promise.resolve().then(() => {
|
|
61
|
+
setFailed(true);
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
instance.on('error', () => {
|
|
66
|
+
setFailed(true);
|
|
67
|
+
});
|
|
68
|
+
instance.getCanvas().setAttribute('tabindex', '-1');
|
|
69
|
+
instance.getCanvas().setAttribute('aria-hidden', 'true');
|
|
70
|
+
map.current = instance;
|
|
71
|
+
|
|
72
|
+
const pin = document.createElement('button');
|
|
73
|
+
pin.type = 'button';
|
|
74
|
+
pin.className = 'vesper-map-pin';
|
|
75
|
+
pin.setAttribute('aria-label', `${label} pin. Use the arrow keys to move it.`);
|
|
76
|
+
const created = new Marker({ element: pin, draggable: !disabled })
|
|
77
|
+
.setLngLat([start.lng, start.lat])
|
|
78
|
+
.addTo(instance);
|
|
79
|
+
marker.current = created;
|
|
80
|
+
|
|
81
|
+
const commit = (next: LatLng, announce: boolean) => {
|
|
82
|
+
const rounded = { lat: round(next.lat), lng: round(next.lng) };
|
|
83
|
+
created.setLngLat([rounded.lng, rounded.lat]);
|
|
84
|
+
onChangeRef.current(rounded);
|
|
85
|
+
if (announce)
|
|
86
|
+
setAnnouncement(`Latitude ${String(rounded.lat)}, longitude ${String(rounded.lng)}.`);
|
|
87
|
+
};
|
|
88
|
+
created.on('dragend', () => {
|
|
89
|
+
const { lat, lng } = created.getLngLat();
|
|
90
|
+
commit({ lat, lng }, true);
|
|
91
|
+
});
|
|
92
|
+
instance.on('click', (event) => {
|
|
93
|
+
if (!disabled) commit({ lat: event.lngLat.lat, lng: event.lngLat.lng }, true);
|
|
94
|
+
});
|
|
95
|
+
pin.addEventListener('keydown', (event) => {
|
|
96
|
+
const steps: Record<string, [number, number]> = {
|
|
97
|
+
ArrowUp: [1, 0],
|
|
98
|
+
ArrowDown: [-1, 0],
|
|
99
|
+
ArrowRight: [0, 1],
|
|
100
|
+
ArrowLeft: [0, -1],
|
|
101
|
+
};
|
|
102
|
+
const step = steps[event.key];
|
|
103
|
+
if (!step || disabled) return;
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
// About 20 screen pixels at the current zoom; Shift moves ten times as far.
|
|
106
|
+
const degrees = (360 / 2 ** instance.getZoom() / 512) * 20 * (event.shiftKey ? 10 : 1);
|
|
107
|
+
const { lat, lng } = created.getLngLat();
|
|
108
|
+
const next = {
|
|
109
|
+
lat: Math.max(-85, Math.min(85, lat + step[0] * degrees)),
|
|
110
|
+
lng: ((lng + step[1] * degrees + 540) % 360) - 180,
|
|
111
|
+
};
|
|
112
|
+
commit(next, true);
|
|
113
|
+
instance.panTo([next.lng, next.lat]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
created.remove();
|
|
118
|
+
instance.remove();
|
|
119
|
+
map.current = null;
|
|
120
|
+
marker.current = null;
|
|
121
|
+
};
|
|
122
|
+
// The map is created once; later point changes are applied below.
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
// Values typed into the number fields move the pin.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!point || !marker.current) return;
|
|
129
|
+
const current = marker.current.getLngLat();
|
|
130
|
+
if (current.lat === point.lat && current.lng === point.lng) return;
|
|
131
|
+
marker.current.setLngLat([point.lng, point.lat]);
|
|
132
|
+
map.current?.panTo([point.lng, point.lat]);
|
|
133
|
+
}, [point]);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="flex flex-col gap-2">
|
|
137
|
+
<div
|
|
138
|
+
ref={container}
|
|
139
|
+
className="h-64 w-full overflow-hidden rounded-md border border-border-strong"
|
|
140
|
+
/>
|
|
141
|
+
{failed && (
|
|
142
|
+
<p className="text-small text-content-muted">
|
|
143
|
+
The map couldn’t load (it needs an internet connection). The number fields above work
|
|
144
|
+
without it.
|
|
145
|
+
</p>
|
|
146
|
+
)}
|
|
147
|
+
<p className="text-small text-content-muted">
|
|
148
|
+
Drag the pin, click the map, or focus the pin and use the arrow keys.
|
|
149
|
+
</p>
|
|
150
|
+
<p role="status" className="sr-only">
|
|
151
|
+
{announcement}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Six decimal places is about 10 cm: more is noise in a content file. */
|
|
158
|
+
function round(n: number): number {
|
|
159
|
+
return Math.round(n * 1e6) / 1e6;
|
|
160
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { isPlainObject, type DataValue } from '@vespercms/core';
|
|
2
|
+
import { Button, cx } from '@vespercms/ui';
|
|
3
|
+
import { Suspense, lazy, useState } from 'react';
|
|
4
|
+
import type { ValuePath } from '../editor/values.ts';
|
|
5
|
+
import type { WidgetProps } from '../types.ts';
|
|
6
|
+
import { inputClass } from './basic.tsx';
|
|
7
|
+
|
|
8
|
+
const GeoMap = lazy(() => import('./geo-map.tsx'));
|
|
9
|
+
|
|
10
|
+
export type LatLng = { lat: number; lng: number };
|
|
11
|
+
|
|
12
|
+
function pointOf(value: DataValue): Partial<LatLng> {
|
|
13
|
+
if (!isPlainObject(value)) return {};
|
|
14
|
+
const { lat, lng } = value;
|
|
15
|
+
return {
|
|
16
|
+
...(typeof lat === 'number' ? { lat } : {}),
|
|
17
|
+
...(typeof lng === 'number' ? { lng } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A place (spec §6.3.2): latitude and longitude as two number fields, which
|
|
23
|
+
* are the real, always-visible controls. A map with a draggable,
|
|
24
|
+
* keyboard-movable pin loads only when asked for: it fetches map tiles from
|
|
25
|
+
* the internet, and it's a convenience, not the way in.
|
|
26
|
+
*/
|
|
27
|
+
export function GeoWidget({
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
field,
|
|
31
|
+
id,
|
|
32
|
+
disabled,
|
|
33
|
+
invalid,
|
|
34
|
+
describedBy,
|
|
35
|
+
}: WidgetProps<DataValue> & { path: ValuePath }) {
|
|
36
|
+
const point = pointOf(value);
|
|
37
|
+
const [showMap, setShowMap] = useState(false);
|
|
38
|
+
const set = (key: 'lat' | 'lng', next: number | undefined) => {
|
|
39
|
+
const base = isPlainObject(value) ? value : {};
|
|
40
|
+
const merged: Record<string, DataValue> = { ...base, [key]: next };
|
|
41
|
+
if (next === undefined) Reflect.deleteProperty(merged, key);
|
|
42
|
+
onChange(Object.keys(merged).length === 0 && !field.required ? undefined : merged);
|
|
43
|
+
};
|
|
44
|
+
const options = field.widgetOptions as { defaultCenter?: LatLng; zoom?: number } | undefined;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex flex-col gap-3">
|
|
48
|
+
<div className="flex flex-wrap gap-4">
|
|
49
|
+
<Coordinate
|
|
50
|
+
id={id}
|
|
51
|
+
label="Latitude"
|
|
52
|
+
hint="−90 to 90, north is positive"
|
|
53
|
+
value={point.lat}
|
|
54
|
+
min={-90}
|
|
55
|
+
max={90}
|
|
56
|
+
onChange={(next) => {
|
|
57
|
+
set('lat', next);
|
|
58
|
+
}}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
invalid={invalid}
|
|
61
|
+
describedBy={describedBy}
|
|
62
|
+
/>
|
|
63
|
+
<Coordinate
|
|
64
|
+
id={`${id}-lng`}
|
|
65
|
+
label="Longitude"
|
|
66
|
+
hint="−180 to 180, east is positive"
|
|
67
|
+
value={point.lng}
|
|
68
|
+
min={-180}
|
|
69
|
+
max={180}
|
|
70
|
+
onChange={(next) => {
|
|
71
|
+
set('lng', next);
|
|
72
|
+
}}
|
|
73
|
+
disabled={disabled}
|
|
74
|
+
invalid={invalid}
|
|
75
|
+
describedBy={describedBy}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
{showMap ? (
|
|
79
|
+
<Suspense fallback={<p className="text-small text-content-muted">Loading the map…</p>}>
|
|
80
|
+
<GeoMap
|
|
81
|
+
label={field.label}
|
|
82
|
+
point={point.lat !== undefined && point.lng !== undefined ? (point as LatLng) : null}
|
|
83
|
+
center={options?.defaultCenter}
|
|
84
|
+
zoom={options?.zoom}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
onChange={(next) => {
|
|
87
|
+
const base = isPlainObject(value) ? value : {};
|
|
88
|
+
onChange({ ...base, lat: next.lat, lng: next.lng });
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</Suspense>
|
|
92
|
+
) : (
|
|
93
|
+
<div>
|
|
94
|
+
<Button
|
|
95
|
+
variant="quiet"
|
|
96
|
+
onPress={() => {
|
|
97
|
+
setShowMap(true);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
Show on a map
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Coordinate({
|
|
109
|
+
id,
|
|
110
|
+
label,
|
|
111
|
+
hint,
|
|
112
|
+
value,
|
|
113
|
+
min,
|
|
114
|
+
max,
|
|
115
|
+
onChange,
|
|
116
|
+
disabled,
|
|
117
|
+
invalid,
|
|
118
|
+
describedBy,
|
|
119
|
+
}: {
|
|
120
|
+
id: string;
|
|
121
|
+
label: string;
|
|
122
|
+
hint: string;
|
|
123
|
+
value: number | undefined;
|
|
124
|
+
min: number;
|
|
125
|
+
max: number;
|
|
126
|
+
onChange: (value: number | undefined) => void;
|
|
127
|
+
disabled: boolean;
|
|
128
|
+
invalid: boolean;
|
|
129
|
+
describedBy: string | undefined;
|
|
130
|
+
}) {
|
|
131
|
+
// Text state lets "-8" or "36." be typed on the way to a number. A value
|
|
132
|
+
// that changes from outside (the map) replaces it.
|
|
133
|
+
const [text, setText] = useState(value === undefined ? '' : String(value));
|
|
134
|
+
const [shown, setShown] = useState(value);
|
|
135
|
+
if (value !== shown) {
|
|
136
|
+
setShown(value);
|
|
137
|
+
setText(value === undefined ? '' : String(value));
|
|
138
|
+
}
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex flex-col gap-1">
|
|
141
|
+
<label htmlFor={id} className="text-small text-content">
|
|
142
|
+
{label}
|
|
143
|
+
</label>
|
|
144
|
+
<input
|
|
145
|
+
id={id}
|
|
146
|
+
type="number"
|
|
147
|
+
inputMode="decimal"
|
|
148
|
+
step="any"
|
|
149
|
+
min={min}
|
|
150
|
+
max={max}
|
|
151
|
+
value={text}
|
|
152
|
+
onChange={(event) => {
|
|
153
|
+
setText(event.target.value);
|
|
154
|
+
const next = event.target.value === '' ? undefined : event.target.valueAsNumber;
|
|
155
|
+
if (next === undefined || Number.isFinite(next)) {
|
|
156
|
+
setShown(next);
|
|
157
|
+
onChange(next);
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
160
|
+
aria-describedby={[describedBy, `${id}-hint`].filter(Boolean).join(' ')}
|
|
161
|
+
aria-invalid={invalid || undefined}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
className={cx(inputClass, 'w-44')}
|
|
164
|
+
/>
|
|
165
|
+
<p id={`${id}-hint`} className="text-small text-content-muted">
|
|
166
|
+
{hint}
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|