@gramps-connect/story 0.3.0 → 0.4.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 +14 -5
- package/dist/editor.js +464 -165
- package/dist/editor.js.map +4 -4
- package/dist/types/editor/PlaceField.d.ts +21 -0
- package/dist/types/editor/PlaceMap.d.ts +12 -0
- package/dist/types/editor/StoryEditor.d.ts +5 -1
- package/dist/types/editor/index.d.ts +3 -1
- package/dist/types/editor/model.d.ts +17 -1
- package/dist/types/editor/placeSearch.d.ts +22 -0
- package/dist/types/editor/strings.d.ts +15 -0
- package/package.json +1 -1
- package/src/editor/PlaceField.tsx +160 -0
- package/src/editor/PlaceMap.tsx +88 -0
- package/src/editor/StoryEditor.tsx +27 -14
- package/src/editor/index.ts +5 -2
- package/src/editor/model.ts +38 -1
- package/src/editor/placeSearch.ts +61 -0
- package/src/editor/strings.ts +30 -0
package/dist/editor.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
// src/editor/StoryEditor.tsx
|
|
2
|
-
import { useCallback, useEffect as
|
|
2
|
+
import { useCallback, useEffect as useEffect8, useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
|
|
3
3
|
import {
|
|
4
4
|
ActionIcon as ActionIcon2,
|
|
5
5
|
Badge,
|
|
6
6
|
Box as Box2,
|
|
7
|
-
Button as
|
|
7
|
+
Button as Button4,
|
|
8
8
|
Divider,
|
|
9
|
-
Group as
|
|
9
|
+
Group as Group5,
|
|
10
10
|
Modal,
|
|
11
11
|
Popover,
|
|
12
12
|
Select,
|
|
13
|
-
Stack as
|
|
14
|
-
Text as
|
|
15
|
-
TextInput as
|
|
16
|
-
Tooltip as
|
|
17
|
-
UnstyledButton as
|
|
13
|
+
Stack as Stack6,
|
|
14
|
+
Text as Text5,
|
|
15
|
+
TextInput as TextInput4,
|
|
16
|
+
Tooltip as Tooltip3,
|
|
17
|
+
UnstyledButton as UnstyledButton3
|
|
18
18
|
} from "@mantine/core";
|
|
19
19
|
import { useMediaQuery } from "@mantine/hooks";
|
|
20
20
|
|
|
@@ -1787,6 +1787,53 @@ function findUnknownFields(value, node, path, warnings, root = story_schema_defa
|
|
|
1787
1787
|
}
|
|
1788
1788
|
}
|
|
1789
1789
|
|
|
1790
|
+
// src/player/geo.ts
|
|
1791
|
+
function visit(coords, bounds) {
|
|
1792
|
+
if (!Array.isArray(coords)) return;
|
|
1793
|
+
if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
|
|
1794
|
+
const [lon, lat] = coords;
|
|
1795
|
+
bounds[0] = Math.min(bounds[0], lon);
|
|
1796
|
+
bounds[1] = Math.min(bounds[1], lat);
|
|
1797
|
+
bounds[2] = Math.max(bounds[2], lon);
|
|
1798
|
+
bounds[3] = Math.max(bounds[3], lat);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
for (const child of coords) visit(child, bounds);
|
|
1802
|
+
}
|
|
1803
|
+
function visitGeoJson(node, bounds) {
|
|
1804
|
+
if (!node || typeof node !== "object") return;
|
|
1805
|
+
const obj = node;
|
|
1806
|
+
if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
|
|
1807
|
+
if (obj.geometry) visitGeoJson(obj.geometry, bounds);
|
|
1808
|
+
for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
|
|
1809
|
+
for (const f of obj.features ?? []) visitGeoJson(f, bounds);
|
|
1810
|
+
}
|
|
1811
|
+
function placeBounds(place) {
|
|
1812
|
+
if (!place) return null;
|
|
1813
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1814
|
+
visitGeoJson(place.geometry, bounds);
|
|
1815
|
+
for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
|
|
1816
|
+
return Number.isFinite(bounds[0]) ? bounds : null;
|
|
1817
|
+
}
|
|
1818
|
+
function placeCenter(place) {
|
|
1819
|
+
if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
|
|
1820
|
+
const bounds = placeBounds(place);
|
|
1821
|
+
return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
|
|
1822
|
+
}
|
|
1823
|
+
function storyPlaces(places) {
|
|
1824
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1825
|
+
const points = /* @__PURE__ */ new Map();
|
|
1826
|
+
for (const place of places) {
|
|
1827
|
+
const center = placeCenter(place);
|
|
1828
|
+
if (!center) continue;
|
|
1829
|
+
points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
|
|
1830
|
+
visit(center, bounds);
|
|
1831
|
+
const box = placeBounds(place);
|
|
1832
|
+
if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
|
|
1833
|
+
}
|
|
1834
|
+
return points.size ? { points: [...points.values()], bounds } : null;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1790
1837
|
// src/player/StoryPlayer.tsx
|
|
1791
1838
|
import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1792
1839
|
|
|
@@ -1839,53 +1886,6 @@ function timelineYear(date) {
|
|
|
1839
1886
|
return sortYear + (month - 1) / 12 + (day ? (day - 1) / 365 : 0);
|
|
1840
1887
|
}
|
|
1841
1888
|
|
|
1842
|
-
// src/player/geo.ts
|
|
1843
|
-
function visit(coords, bounds) {
|
|
1844
|
-
if (!Array.isArray(coords)) return;
|
|
1845
|
-
if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
|
|
1846
|
-
const [lon, lat] = coords;
|
|
1847
|
-
bounds[0] = Math.min(bounds[0], lon);
|
|
1848
|
-
bounds[1] = Math.min(bounds[1], lat);
|
|
1849
|
-
bounds[2] = Math.max(bounds[2], lon);
|
|
1850
|
-
bounds[3] = Math.max(bounds[3], lat);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
for (const child of coords) visit(child, bounds);
|
|
1854
|
-
}
|
|
1855
|
-
function visitGeoJson(node, bounds) {
|
|
1856
|
-
if (!node || typeof node !== "object") return;
|
|
1857
|
-
const obj = node;
|
|
1858
|
-
if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
|
|
1859
|
-
if (obj.geometry) visitGeoJson(obj.geometry, bounds);
|
|
1860
|
-
for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
|
|
1861
|
-
for (const f of obj.features ?? []) visitGeoJson(f, bounds);
|
|
1862
|
-
}
|
|
1863
|
-
function placeBounds(place) {
|
|
1864
|
-
if (!place) return null;
|
|
1865
|
-
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1866
|
-
visitGeoJson(place.geometry, bounds);
|
|
1867
|
-
for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
|
|
1868
|
-
return Number.isFinite(bounds[0]) ? bounds : null;
|
|
1869
|
-
}
|
|
1870
|
-
function placeCenter(place) {
|
|
1871
|
-
if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
|
|
1872
|
-
const bounds = placeBounds(place);
|
|
1873
|
-
return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
|
|
1874
|
-
}
|
|
1875
|
-
function storyPlaces(places) {
|
|
1876
|
-
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1877
|
-
const points = /* @__PURE__ */ new Map();
|
|
1878
|
-
for (const place of places) {
|
|
1879
|
-
const center = placeCenter(place);
|
|
1880
|
-
if (!center) continue;
|
|
1881
|
-
points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
|
|
1882
|
-
visit(center, bounds);
|
|
1883
|
-
const box = placeBounds(place);
|
|
1884
|
-
if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
|
|
1885
|
-
}
|
|
1886
|
-
return points.size ? { points: [...points.values()], bounds } : null;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
1889
|
// src/player/MapBackground.tsx
|
|
1890
1890
|
import { useEffect, useRef, useState } from "react";
|
|
1891
1891
|
import * as maplibregl from "maplibre-gl";
|
|
@@ -2967,6 +2967,30 @@ function slideDate(previous, text, edtf) {
|
|
|
2967
2967
|
if (next.edtf !== previous?.edtf) delete next.sortYear;
|
|
2968
2968
|
return next;
|
|
2969
2969
|
}
|
|
2970
|
+
function patchPlace(place, patch) {
|
|
2971
|
+
if (place && Object.entries(patch).every(([key, value]) => place[key] === value)) return place;
|
|
2972
|
+
const next = patched(place ?? {}, patch);
|
|
2973
|
+
return Object.keys(next).length ? next : void 0;
|
|
2974
|
+
}
|
|
2975
|
+
var roundDegrees = (value) => Math.round(value * 1e5) / 1e5;
|
|
2976
|
+
function parseLatLon(text) {
|
|
2977
|
+
const parts = text.trim().split(/\s*[,;\s]\s*/);
|
|
2978
|
+
if (parts.length !== 2 || parts.some((p) => !/^[-+]?(\d+\.?\d*|\.\d+)$/.test(p))) return null;
|
|
2979
|
+
const [lat, lon] = parts.map(Number);
|
|
2980
|
+
if (Math.abs(lat) > 90 || Math.abs(lon) > 180) return null;
|
|
2981
|
+
return { lat, lon };
|
|
2982
|
+
}
|
|
2983
|
+
function formatLatLon(lat, lon) {
|
|
2984
|
+
return `${roundDegrees(lat)}, ${roundDegrees(lon)}`;
|
|
2985
|
+
}
|
|
2986
|
+
function storyPlaceChoices(doc, exceptIndex) {
|
|
2987
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2988
|
+
doc.slides.forEach((slide, i) => {
|
|
2989
|
+
const name = slide.place?.name?.trim();
|
|
2990
|
+
if (i !== exceptIndex && name && !byName.has(name)) byName.set(name, slide.place);
|
|
2991
|
+
});
|
|
2992
|
+
return [...byName.values()];
|
|
2993
|
+
}
|
|
2970
2994
|
function sameBytes(a, b) {
|
|
2971
2995
|
if (!a || a.length !== b.length) return false;
|
|
2972
2996
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
@@ -3249,22 +3273,260 @@ function PhotosField({ photos, media, urlFor, strings, busy, onChange, onFiles,
|
|
|
3249
3273
|
);
|
|
3250
3274
|
}
|
|
3251
3275
|
|
|
3252
|
-
// src/editor/
|
|
3253
|
-
import { useState as useState6 } from "react";
|
|
3254
|
-
import { Stack as Stack3, UnstyledButton } from "@mantine/core";
|
|
3276
|
+
// src/editor/PlaceField.tsx
|
|
3277
|
+
import { useEffect as useEffect7, useMemo as useMemo2, useRef as useRef7, useState as useState6 } from "react";
|
|
3278
|
+
import { Autocomplete, Button as Button2, Group as Group3, Loader as Loader2, Stack as Stack3, Text as Text3, TextInput as TextInput3, Tooltip as Tooltip2, UnstyledButton } from "@mantine/core";
|
|
3279
|
+
|
|
3280
|
+
// src/editor/PlaceMap.tsx
|
|
3281
|
+
import { useEffect as useEffect6, useRef as useRef6 } from "react";
|
|
3282
|
+
import * as maplibregl2 from "maplibre-gl";
|
|
3283
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
3255
3284
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
3285
|
+
function PlaceMap({ point, around, dark = false, label, onPick }) {
|
|
3286
|
+
const container = useRef6(null);
|
|
3287
|
+
const mapRef = useRef6(null);
|
|
3288
|
+
const markerRef = useRef6(null);
|
|
3289
|
+
const onPickRef = useRef6(onPick);
|
|
3290
|
+
onPickRef.current = onPick;
|
|
3291
|
+
const pick = (lngLat) => onPickRef.current([roundDegrees(lngLat.lng), roundDegrees(lngLat.lat)]);
|
|
3292
|
+
const start = useRef6({ point, around });
|
|
3293
|
+
useEffect6(() => {
|
|
3294
|
+
if (!container.current) return;
|
|
3295
|
+
const { point: first, around: box } = start.current;
|
|
3296
|
+
const map = new maplibregl2.Map({
|
|
3297
|
+
container: container.current,
|
|
3298
|
+
style: mapStyleUrl(dark, null),
|
|
3299
|
+
...first ? { center: first, zoom: 9 } : box ? { bounds: box, fitBoundsOptions: { padding: 30, maxZoom: 9 } } : { center: [10, 30], zoom: 0.5 },
|
|
3300
|
+
attributionControl: { compact: true },
|
|
3301
|
+
// Scrolling the form past the map shouldn't zoom it.
|
|
3302
|
+
cooperativeGestures: true
|
|
3303
|
+
});
|
|
3304
|
+
map.addControl(new maplibregl2.NavigationControl({ showCompass: false }), "top-right");
|
|
3305
|
+
map.getCanvas().style.cursor = "crosshair";
|
|
3306
|
+
map.on("click", (e) => pick(e.lngLat));
|
|
3307
|
+
mapRef.current = map;
|
|
3308
|
+
return () => {
|
|
3309
|
+
markerRef.current?.remove();
|
|
3310
|
+
markerRef.current = null;
|
|
3311
|
+
map.remove();
|
|
3312
|
+
mapRef.current = null;
|
|
3313
|
+
};
|
|
3314
|
+
}, [dark]);
|
|
3315
|
+
useEffect6(() => {
|
|
3316
|
+
const map = mapRef.current;
|
|
3317
|
+
if (!map) return;
|
|
3318
|
+
if (!point) {
|
|
3319
|
+
markerRef.current?.remove();
|
|
3320
|
+
markerRef.current = null;
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (!markerRef.current) {
|
|
3324
|
+
const el = document.createElement("div");
|
|
3325
|
+
const c = defaultColors(dark);
|
|
3326
|
+
el.innerHTML = `<svg viewBox="0 0 24 24" width="28" height="28" style="display:block;cursor:grab"><path d="${PIN_PATH_D}" fill="${c.mark}" stroke="${c.surface}" stroke-width="1.5" /></svg>`;
|
|
3327
|
+
const marker = new maplibregl2.Marker({ element: el, anchor: "bottom", draggable: true }).setLngLat(point).addTo(map);
|
|
3328
|
+
marker.on("dragend", () => pick(marker.getLngLat()));
|
|
3329
|
+
markerRef.current = marker;
|
|
3330
|
+
} else {
|
|
3331
|
+
markerRef.current.setLngLat(point);
|
|
3332
|
+
}
|
|
3333
|
+
if (!map.getBounds().contains(point)) map.easeTo({ center: point, zoom: Math.max(map.getZoom(), 8) });
|
|
3334
|
+
}, [point?.[0], point?.[1], dark]);
|
|
3335
|
+
return /* @__PURE__ */ jsx7(
|
|
3336
|
+
"div",
|
|
3337
|
+
{
|
|
3338
|
+
ref: container,
|
|
3339
|
+
role: "application",
|
|
3340
|
+
"aria-label": label,
|
|
3341
|
+
style: { height: 240, borderRadius: 6, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }
|
|
3342
|
+
}
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/editor/PlaceField.tsx
|
|
3347
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3348
|
+
function PlaceField({ place, choices, around, language, search, dark, strings, onChange }) {
|
|
3349
|
+
const lat = place?.lat;
|
|
3350
|
+
const lon = place?.lon;
|
|
3351
|
+
const point = lat !== void 0 && lon !== void 0 ? [lon, lat] : null;
|
|
3352
|
+
const name = place?.name ?? "";
|
|
3353
|
+
const [coords, setCoords] = useState6(point ? formatLatLon(point[1], point[0]) : "");
|
|
3354
|
+
useEffect7(() => {
|
|
3355
|
+
setCoords((typed) => {
|
|
3356
|
+
const parsed = parseLatLon(typed);
|
|
3357
|
+
if (lat === void 0 || lon === void 0) return parsed ? "" : typed;
|
|
3358
|
+
return parsed && parsed.lat === lat && parsed.lon === lon ? typed : formatLatLon(lat, lon);
|
|
3359
|
+
});
|
|
3360
|
+
}, [lat, lon]);
|
|
3361
|
+
const coordsInvalid = coords.trim() !== "" && !parseLatLon(coords);
|
|
3362
|
+
const [found, setFound] = useState6({ state: "idle" });
|
|
3363
|
+
const searching = useRef7(null);
|
|
3364
|
+
useEffect7(() => () => searching.current?.abort(), []);
|
|
3365
|
+
const find = async () => {
|
|
3366
|
+
if (!search || !name.trim()) return;
|
|
3367
|
+
searching.current?.abort();
|
|
3368
|
+
const controller = new AbortController();
|
|
3369
|
+
searching.current = controller;
|
|
3370
|
+
setFound({ state: "searching" });
|
|
3371
|
+
try {
|
|
3372
|
+
const results = await search.search(name.trim(), language, controller.signal);
|
|
3373
|
+
setFound(results.length ? { state: "results", results } : { state: "message", message: strings.findPlaceNone });
|
|
3374
|
+
} catch {
|
|
3375
|
+
if (!controller.signal.aborted) setFound({ state: "message", message: strings.findPlaceFailed });
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
const choose = async (result) => {
|
|
3379
|
+
if (!search) return;
|
|
3380
|
+
searching.current?.abort();
|
|
3381
|
+
const controller = new AbortController();
|
|
3382
|
+
searching.current = controller;
|
|
3383
|
+
setFound({ state: "searching" });
|
|
3384
|
+
try {
|
|
3385
|
+
const at = await search.point(result, controller.signal);
|
|
3386
|
+
if (!at) {
|
|
3387
|
+
setFound({ state: "message", message: strings.findPlaceNoPoint(result.label) });
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
setFound({ state: "idle" });
|
|
3391
|
+
onChange((p) => patchPlace(p, { name: result.label, lat: roundDegrees(at.lat), lon: roundDegrees(at.lon) }), "place");
|
|
3392
|
+
} catch {
|
|
3393
|
+
if (!controller.signal.aborted) setFound({ state: "message", message: strings.findPlaceFailed });
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
const byName = useMemo2(() => new Map(choices.map((c) => [c.name.trim(), c])), [choices]);
|
|
3397
|
+
const keeps = [
|
|
3398
|
+
place?.geometry ? strings.placeShape : null,
|
|
3399
|
+
place?.overlays?.length ? strings.placeOverlays(place.overlays.length) : null
|
|
3400
|
+
].filter(Boolean);
|
|
3401
|
+
return /* @__PURE__ */ jsxs6(Stack3, { gap: "xs", children: [
|
|
3402
|
+
/* @__PURE__ */ jsxs6(Group3, { justify: "space-between", children: [
|
|
3403
|
+
/* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.place }),
|
|
3404
|
+
place && /* @__PURE__ */ jsx8(Button2, { variant: "subtle", size: "compact-xs", color: "red", onClick: () => {
|
|
3405
|
+
setFound({ state: "idle" });
|
|
3406
|
+
onChange(() => void 0, "place");
|
|
3407
|
+
}, children: strings.removePlace })
|
|
3408
|
+
] }),
|
|
3409
|
+
/* @__PURE__ */ jsxs6(Group3, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
|
|
3410
|
+
/* @__PURE__ */ jsx8(
|
|
3411
|
+
Autocomplete,
|
|
3412
|
+
{
|
|
3413
|
+
size: "xs",
|
|
3414
|
+
label: strings.placeName,
|
|
3415
|
+
style: { flex: 1 },
|
|
3416
|
+
value: name,
|
|
3417
|
+
data: [...byName.keys()],
|
|
3418
|
+
comboboxProps: { withinPortal: true },
|
|
3419
|
+
onChange: (value) => onChange((p) => patchPlace(p, { name: value || void 0 }), "place:name"),
|
|
3420
|
+
onOptionSubmit: (value) => {
|
|
3421
|
+
const chosen = byName.get(value);
|
|
3422
|
+
if (chosen) onChange(() => structuredClone(chosen), "place");
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
),
|
|
3426
|
+
search && /* @__PURE__ */ jsx8(Tooltip2, { label: strings.findPlaceHint, multiline: true, w: 260, children: /* @__PURE__ */ jsx8(Button2, { variant: "default", size: "xs", disabled: !name.trim() || found.state === "searching", onClick: () => void find(), children: strings.findPlace }) })
|
|
3427
|
+
] }),
|
|
3428
|
+
found.state === "searching" && /* @__PURE__ */ jsx8(Loader2, { size: "xs" }),
|
|
3429
|
+
found.state === "message" && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", role: "status", children: found.message }),
|
|
3430
|
+
found.state === "results" && /* @__PURE__ */ jsx8(Stack3, { gap: 2, role: "list", "aria-label": strings.findPlace, children: found.results.map((result) => /* @__PURE__ */ jsxs6(
|
|
3431
|
+
UnstyledButton,
|
|
3432
|
+
{
|
|
3433
|
+
role: "listitem",
|
|
3434
|
+
onClick: () => void choose(result),
|
|
3435
|
+
style: { padding: "4px 8px", borderRadius: 4, border: "1px solid var(--mantine-color-default-border)" },
|
|
3436
|
+
children: [
|
|
3437
|
+
/* @__PURE__ */ jsx8(Text3, { size: "sm", children: result.label }),
|
|
3438
|
+
result.description && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: result.description })
|
|
3439
|
+
]
|
|
3440
|
+
},
|
|
3441
|
+
result.id
|
|
3442
|
+
)) }),
|
|
3443
|
+
/* @__PURE__ */ jsx8(
|
|
3444
|
+
TextInput3,
|
|
3445
|
+
{
|
|
3446
|
+
size: "xs",
|
|
3447
|
+
label: strings.coordinates,
|
|
3448
|
+
placeholder: strings.coordinatesHint,
|
|
3449
|
+
w: 260,
|
|
3450
|
+
value: coords,
|
|
3451
|
+
error: coordsInvalid ? strings.coordinatesInvalid : void 0,
|
|
3452
|
+
onChange: (e) => {
|
|
3453
|
+
const text = e.currentTarget.value;
|
|
3454
|
+
setCoords(text);
|
|
3455
|
+
const parsed = parseLatLon(text);
|
|
3456
|
+
if (!text.trim()) onChange((p) => patchPlace(p, { lat: void 0, lon: void 0 }), "place:point");
|
|
3457
|
+
else if (parsed) onChange((p) => patchPlace(p, parsed), "place:point");
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
),
|
|
3461
|
+
/* @__PURE__ */ jsx8(
|
|
3462
|
+
PlaceMap,
|
|
3463
|
+
{
|
|
3464
|
+
point,
|
|
3465
|
+
around,
|
|
3466
|
+
dark,
|
|
3467
|
+
label: strings.placeMap,
|
|
3468
|
+
onPick: ([pickedLon, pickedLat]) => onChange((p) => patchPlace(p, { lat: pickedLat, lon: pickedLon }), "place:point")
|
|
3469
|
+
}
|
|
3470
|
+
),
|
|
3471
|
+
/* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeMapHint }),
|
|
3472
|
+
keeps.length > 0 && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeKeeps(keeps.join(", ")) })
|
|
3473
|
+
] });
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/editor/placeSearch.ts
|
|
3477
|
+
var WIKIDATA_API = "https://www.wikidata.org/w/api.php";
|
|
3478
|
+
var WIKIDATA_ENTITY_DATA = "https://www.wikidata.org/wiki/Special:EntityData";
|
|
3479
|
+
function wikidataLanguage(language) {
|
|
3480
|
+
return (language ?? "").trim().toLowerCase().replace(/_/g, "-") || "en";
|
|
3481
|
+
}
|
|
3482
|
+
async function searchWikidataPlaces(term, language, signal) {
|
|
3483
|
+
const code = wikidataLanguage(language);
|
|
3484
|
+
const params = new URLSearchParams({
|
|
3485
|
+
action: "wbsearchentities",
|
|
3486
|
+
search: term,
|
|
3487
|
+
language: code,
|
|
3488
|
+
uselang: code,
|
|
3489
|
+
format: "json",
|
|
3490
|
+
limit: "7",
|
|
3491
|
+
origin: "*"
|
|
3492
|
+
// what makes an anonymous api.php GET answer cross-origin
|
|
3493
|
+
});
|
|
3494
|
+
const res = await fetch(`${WIKIDATA_API}?${params}`, { signal });
|
|
3495
|
+
if (!res.ok) throw new Error(`Wikidata search failed (${res.status})`);
|
|
3496
|
+
const data = await res.json();
|
|
3497
|
+
return (data.search ?? []).map((hit) => ({
|
|
3498
|
+
id: hit.id,
|
|
3499
|
+
label: hit.label ?? hit.id,
|
|
3500
|
+
...hit.description ? { description: hit.description } : {}
|
|
3501
|
+
}));
|
|
3502
|
+
}
|
|
3503
|
+
async function wikidataPoint(result, signal) {
|
|
3504
|
+
const res = await fetch(`${WIKIDATA_ENTITY_DATA}/${encodeURIComponent(result.id)}.json`, { signal });
|
|
3505
|
+
if (!res.ok) throw new Error(`Wikidata fetch failed for ${result.id} (${res.status})`);
|
|
3506
|
+
const data = await res.json();
|
|
3507
|
+
const entity = data.entities?.[result.id] ?? Object.values(data.entities ?? {})[0];
|
|
3508
|
+
const value = entity?.claims?.P625?.[0]?.mainsnak?.datavalue?.value;
|
|
3509
|
+
if (typeof value?.latitude !== "number" || typeof value.longitude !== "number") return null;
|
|
3510
|
+
return { lat: value.latitude, lon: value.longitude };
|
|
3511
|
+
}
|
|
3512
|
+
var WIKIDATA_PLACE_SEARCH = { search: searchWikidataPlaces, point: wikidataPoint };
|
|
3513
|
+
|
|
3514
|
+
// src/editor/SlideList.tsx
|
|
3515
|
+
import { useState as useState7 } from "react";
|
|
3516
|
+
import { Stack as Stack4, UnstyledButton as UnstyledButton2 } from "@mantine/core";
|
|
3517
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
3256
3518
|
function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
|
|
3257
|
-
const [dragging, setDragging] =
|
|
3258
|
-
const [over, setOver] =
|
|
3519
|
+
const [dragging, setDragging] = useState7(null);
|
|
3520
|
+
const [over, setOver] = useState7(null);
|
|
3259
3521
|
const end = () => {
|
|
3260
3522
|
setDragging(null);
|
|
3261
3523
|
setOver(null);
|
|
3262
3524
|
};
|
|
3263
|
-
return /* @__PURE__ */
|
|
3525
|
+
return /* @__PURE__ */ jsx9(Stack4, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
|
|
3264
3526
|
const isSelected = i === selected;
|
|
3265
3527
|
const dropLine = over === i && dragging !== null && dragging !== i ? dragging < i ? { borderBottom: "2px solid var(--mantine-primary-color-filled)" } : { borderTop: "2px solid var(--mantine-primary-color-filled)" } : {};
|
|
3266
|
-
return /* @__PURE__ */
|
|
3267
|
-
|
|
3528
|
+
return /* @__PURE__ */ jsx9(
|
|
3529
|
+
UnstyledButton2,
|
|
3268
3530
|
{
|
|
3269
3531
|
role: "option",
|
|
3270
3532
|
"aria-selected": isSelected,
|
|
@@ -3303,9 +3565,9 @@ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, labe
|
|
|
3303
3565
|
}
|
|
3304
3566
|
|
|
3305
3567
|
// src/editor/StoryTextField.tsx
|
|
3306
|
-
import { useRef as
|
|
3307
|
-
import { Button as
|
|
3308
|
-
import { jsx as
|
|
3568
|
+
import { useRef as useRef8 } from "react";
|
|
3569
|
+
import { Button as Button3, Group as Group4, Stack as Stack5, Text as Text4, Textarea } from "@mantine/core";
|
|
3570
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3309
3571
|
function applyMarker(el, marker, value, onChange) {
|
|
3310
3572
|
const start = el.selectionStart ?? value.length;
|
|
3311
3573
|
const end = el.selectionEnd ?? value.length;
|
|
@@ -3323,12 +3585,12 @@ function applyMarker(el, marker, value, onChange) {
|
|
|
3323
3585
|
onChange(value.slice(0, start) + replacement + value.slice(end));
|
|
3324
3586
|
}
|
|
3325
3587
|
function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
|
|
3326
|
-
const ref =
|
|
3327
|
-
return /* @__PURE__ */
|
|
3328
|
-
/* @__PURE__ */
|
|
3329
|
-
/* @__PURE__ */
|
|
3330
|
-
/* @__PURE__ */
|
|
3331
|
-
|
|
3588
|
+
const ref = useRef8(null);
|
|
3589
|
+
return /* @__PURE__ */ jsxs7(Stack5, { gap: 4, children: [
|
|
3590
|
+
/* @__PURE__ */ jsxs7(Group4, { gap: "xs", children: [
|
|
3591
|
+
/* @__PURE__ */ jsx10(Text4, { size: "sm", fw: 500, children: label }),
|
|
3592
|
+
/* @__PURE__ */ jsx10(
|
|
3593
|
+
Button3,
|
|
3332
3594
|
{
|
|
3333
3595
|
variant: "default",
|
|
3334
3596
|
size: "compact-xs",
|
|
@@ -3339,8 +3601,8 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3339
3601
|
children: "B"
|
|
3340
3602
|
}
|
|
3341
3603
|
),
|
|
3342
|
-
/* @__PURE__ */
|
|
3343
|
-
|
|
3604
|
+
/* @__PURE__ */ jsx10(
|
|
3605
|
+
Button3,
|
|
3344
3606
|
{
|
|
3345
3607
|
variant: "default",
|
|
3346
3608
|
size: "compact-xs",
|
|
@@ -3352,7 +3614,7 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3352
3614
|
}
|
|
3353
3615
|
)
|
|
3354
3616
|
] }),
|
|
3355
|
-
/* @__PURE__ */
|
|
3617
|
+
/* @__PURE__ */ jsx10(
|
|
3356
3618
|
Textarea,
|
|
3357
3619
|
{
|
|
3358
3620
|
ref,
|
|
@@ -3399,6 +3661,21 @@ var EDITOR_ENGLISH = {
|
|
|
3399
3661
|
matchDateAsWritten: "Work it out from the date",
|
|
3400
3662
|
noPreciseDate: "None: this slide stays off the timeline.",
|
|
3401
3663
|
place: "Place",
|
|
3664
|
+
placeName: "Name",
|
|
3665
|
+
removePlace: "Remove place",
|
|
3666
|
+
findPlace: "Find on Wikidata",
|
|
3667
|
+
findPlaceHint: "Looks the name up on Wikidata, to put the pin there. The name is all that's sent, and only when you ask.",
|
|
3668
|
+
findPlaceNone: "Wikidata has no place by that name.",
|
|
3669
|
+
findPlaceFailed: "Couldn't reach Wikidata.",
|
|
3670
|
+
findPlaceNoPoint: (name) => `Wikidata has no map position for ${name}.`,
|
|
3671
|
+
coordinates: "Latitude, longitude",
|
|
3672
|
+
coordinatesHint: "e.g. 47.3769, 8.5417",
|
|
3673
|
+
coordinatesInvalid: "Two numbers: latitude (-90 to 90), then longitude (-180 to 180).",
|
|
3674
|
+
placeMap: "Map: click to put the pin",
|
|
3675
|
+
placeMapHint: "Click the map to put the pin there, or drag the pin.",
|
|
3676
|
+
placeShape: "a shape",
|
|
3677
|
+
placeOverlays: (count) => count === 1 ? "a map overlay" : `${count} map overlays`,
|
|
3678
|
+
placeKeeps: (what) => `This place also has ${what}, kept as they are.`,
|
|
3402
3679
|
photos: "Photos",
|
|
3403
3680
|
photosHint: "Add, drop or paste a photo. It's resized for the screen, and hidden details like camera and location are left out.",
|
|
3404
3681
|
addPhoto: "Add photo",
|
|
@@ -3421,7 +3698,7 @@ var EDITOR_ENGLISH = {
|
|
|
3421
3698
|
};
|
|
3422
3699
|
|
|
3423
3700
|
// src/editor/StoryEditor.tsx
|
|
3424
|
-
import { jsx as
|
|
3701
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3425
3702
|
var BAR_HEIGHT2 = 56;
|
|
3426
3703
|
var STORY = -1;
|
|
3427
3704
|
function issueSlide(issue) {
|
|
@@ -3435,8 +3712,8 @@ async function prepareImage(file) {
|
|
|
3435
3712
|
function Thumb({ doc, slide, resolveMedia }) {
|
|
3436
3713
|
const ref = slide.media?.[0]?.ref;
|
|
3437
3714
|
const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
|
|
3438
|
-
const [url, setUrl] =
|
|
3439
|
-
|
|
3715
|
+
const [url, setUrl] = useState8(null);
|
|
3716
|
+
useEffect8(() => {
|
|
3440
3717
|
let live = true;
|
|
3441
3718
|
setUrl(null);
|
|
3442
3719
|
if (ref && item) void resolveMedia(ref, item).then((u) => {
|
|
@@ -3446,13 +3723,13 @@ function Thumb({ doc, slide, resolveMedia }) {
|
|
|
3446
3723
|
live = false;
|
|
3447
3724
|
};
|
|
3448
3725
|
}, [ref, item, resolveMedia]);
|
|
3449
|
-
return /* @__PURE__ */
|
|
3726
|
+
return /* @__PURE__ */ jsx11(
|
|
3450
3727
|
Box2,
|
|
3451
3728
|
{
|
|
3452
3729
|
w: 40,
|
|
3453
3730
|
h: 40,
|
|
3454
3731
|
style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
|
|
3455
|
-
children: url && /* @__PURE__ */
|
|
3732
|
+
children: url && /* @__PURE__ */ jsx11("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
|
|
3456
3733
|
}
|
|
3457
3734
|
);
|
|
3458
3735
|
}
|
|
@@ -3466,24 +3743,25 @@ function StoryEditor({
|
|
|
3466
3743
|
dark = false,
|
|
3467
3744
|
strings: stringOverrides,
|
|
3468
3745
|
playerStrings,
|
|
3469
|
-
parseDateText = suggestEdtf
|
|
3746
|
+
parseDateText = suggestEdtf,
|
|
3747
|
+
placeSearch = WIKIDATA_PLACE_SEARCH
|
|
3470
3748
|
}) {
|
|
3471
|
-
const strings =
|
|
3472
|
-
const [history, setHistory] =
|
|
3473
|
-
const [selected, setSelected] =
|
|
3474
|
-
const [presenting, setPresenting] =
|
|
3475
|
-
const [problemsOpen, setProblemsOpen] =
|
|
3749
|
+
const strings = useMemo3(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
|
|
3750
|
+
const [history, setHistory] = useState8(() => startHistory(initialStory));
|
|
3751
|
+
const [selected, setSelected] = useState8(0);
|
|
3752
|
+
const [presenting, setPresenting] = useState8(false);
|
|
3753
|
+
const [problemsOpen, setProblemsOpen] = useState8(false);
|
|
3476
3754
|
const wide = useMediaQuery("(min-width: 1000px)", true);
|
|
3477
3755
|
const doc = history.present;
|
|
3478
|
-
const [files, setFiles] =
|
|
3479
|
-
const [busy, setBusy] =
|
|
3480
|
-
const [photoError, setPhotoError] =
|
|
3481
|
-
const docRef =
|
|
3756
|
+
const [files, setFiles] = useState8(() => new Map(initialFiles ?? []));
|
|
3757
|
+
const [busy, setBusy] = useState8(false);
|
|
3758
|
+
const [photoError, setPhotoError] = useState8(null);
|
|
3759
|
+
const docRef = useRef9(doc);
|
|
3482
3760
|
docRef.current = doc;
|
|
3483
|
-
const filesRef =
|
|
3761
|
+
const filesRef = useRef9(files);
|
|
3484
3762
|
filesRef.current = files;
|
|
3485
|
-
const urls =
|
|
3486
|
-
|
|
3763
|
+
const urls = useRef9(/* @__PURE__ */ new Map());
|
|
3764
|
+
useEffect8(() => () => {
|
|
3487
3765
|
for (const url of urls.current.values()) URL.revokeObjectURL(url);
|
|
3488
3766
|
}, []);
|
|
3489
3767
|
const resolveMedia = useCallback(async (_key, item) => {
|
|
@@ -3502,7 +3780,7 @@ function StoryEditor({
|
|
|
3502
3780
|
const apply = useCallback((next, key) => {
|
|
3503
3781
|
setHistory((h) => record(h, next, key));
|
|
3504
3782
|
}, []);
|
|
3505
|
-
|
|
3783
|
+
useEffect8(() => {
|
|
3506
3784
|
onChange?.(doc, files);
|
|
3507
3785
|
}, [doc, files, onChange]);
|
|
3508
3786
|
const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
|
|
@@ -3510,7 +3788,18 @@ function StoryEditor({
|
|
|
3510
3788
|
const setSlide = (patch, key) => {
|
|
3511
3789
|
if (index !== STORY) apply(updateSlide(history.present, index, patch), `${slide.id}:${key}`);
|
|
3512
3790
|
};
|
|
3513
|
-
const
|
|
3791
|
+
const setPlace = (slideId2, update, key) => {
|
|
3792
|
+
setHistory((h) => {
|
|
3793
|
+
const at = h.present.slides.findIndex((s) => s.id === slideId2);
|
|
3794
|
+
if (at < 0) return h;
|
|
3795
|
+
const before = h.present.slides[at].place;
|
|
3796
|
+
const after = update(before);
|
|
3797
|
+
return after === before ? h : record(h, updateSlide(h.present, at, { place: after }), `${slideId2}:${key}`);
|
|
3798
|
+
});
|
|
3799
|
+
};
|
|
3800
|
+
const placeChoices = useMemo3(() => storyPlaceChoices(doc, index), [doc, index]);
|
|
3801
|
+
const around = useMemo3(() => storyPlaces(doc.slides.map((s) => s.place))?.bounds ?? null, [doc.slides]);
|
|
3802
|
+
const issues = useMemo3(() => validate2(doc, files).errors, [doc, files]);
|
|
3514
3803
|
const addPhotos = useCallback(async (chosen, slideId2, replace) => {
|
|
3515
3804
|
setBusy(true);
|
|
3516
3805
|
setPhotoError(null);
|
|
@@ -3547,7 +3836,7 @@ function StoryEditor({
|
|
|
3547
3836
|
apply(setSlidePhotos(next, at, photos));
|
|
3548
3837
|
}, [apply, prepare, strings]);
|
|
3549
3838
|
const slideId = slide?.id;
|
|
3550
|
-
|
|
3839
|
+
useEffect8(() => {
|
|
3551
3840
|
if (!slideId) return;
|
|
3552
3841
|
const onPaste = (e) => {
|
|
3553
3842
|
if (e.target?.closest("input, textarea, [contenteditable=true]")) return;
|
|
@@ -3559,7 +3848,7 @@ function StoryEditor({
|
|
|
3559
3848
|
window.addEventListener("paste", onPaste);
|
|
3560
3849
|
return () => window.removeEventListener("paste", onPaste);
|
|
3561
3850
|
}, [slideId, addPhotos]);
|
|
3562
|
-
|
|
3851
|
+
useEffect8(() => {
|
|
3563
3852
|
const onKey = (e) => {
|
|
3564
3853
|
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
|
|
3565
3854
|
const target = e.target;
|
|
@@ -3576,8 +3865,8 @@ function StoryEditor({
|
|
|
3576
3865
|
window.addEventListener("keydown", onKey);
|
|
3577
3866
|
return () => window.removeEventListener("keydown", onKey);
|
|
3578
3867
|
}, []);
|
|
3579
|
-
const slideActions = slide && /* @__PURE__ */
|
|
3580
|
-
/* @__PURE__ */
|
|
3868
|
+
const slideActions = slide && /* @__PURE__ */ jsxs8(Group5, { gap: 6, children: [
|
|
3869
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.moveUp, children: /* @__PURE__ */ jsx11(
|
|
3581
3870
|
ActionIcon2,
|
|
3582
3871
|
{
|
|
3583
3872
|
variant: "default",
|
|
@@ -3590,7 +3879,7 @@ function StoryEditor({
|
|
|
3590
3879
|
children: "\u2191"
|
|
3591
3880
|
}
|
|
3592
3881
|
) }),
|
|
3593
|
-
/* @__PURE__ */
|
|
3882
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.moveDown, children: /* @__PURE__ */ jsx11(
|
|
3594
3883
|
ActionIcon2,
|
|
3595
3884
|
{
|
|
3596
3885
|
variant: "default",
|
|
@@ -3603,13 +3892,13 @@ function StoryEditor({
|
|
|
3603
3892
|
children: "\u2193"
|
|
3604
3893
|
}
|
|
3605
3894
|
) }),
|
|
3606
|
-
/* @__PURE__ */
|
|
3895
|
+
/* @__PURE__ */ jsx11(Button4, { variant: "default", size: "xs", onClick: () => {
|
|
3607
3896
|
const r = duplicateSlide(doc, index);
|
|
3608
3897
|
apply(r.doc);
|
|
3609
3898
|
setSelected(r.index);
|
|
3610
3899
|
}, children: strings.duplicateSlide }),
|
|
3611
|
-
/* @__PURE__ */
|
|
3612
|
-
|
|
3900
|
+
/* @__PURE__ */ jsx11(
|
|
3901
|
+
Button4,
|
|
3613
3902
|
{
|
|
3614
3903
|
variant: "default",
|
|
3615
3904
|
size: "xs",
|
|
@@ -3624,10 +3913,10 @@ function StoryEditor({
|
|
|
3624
3913
|
}
|
|
3625
3914
|
)
|
|
3626
3915
|
] });
|
|
3627
|
-
const storyForm = /* @__PURE__ */
|
|
3628
|
-
/* @__PURE__ */
|
|
3629
|
-
/* @__PURE__ */
|
|
3630
|
-
|
|
3916
|
+
const storyForm = /* @__PURE__ */ jsxs8(Stack6, { gap: "md", children: [
|
|
3917
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, size: "lg", children: strings.story }),
|
|
3918
|
+
/* @__PURE__ */ jsx11(
|
|
3919
|
+
TextInput4,
|
|
3631
3920
|
{
|
|
3632
3921
|
label: strings.title,
|
|
3633
3922
|
required: true,
|
|
@@ -3635,8 +3924,8 @@ function StoryEditor({
|
|
|
3635
3924
|
onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
|
|
3636
3925
|
}
|
|
3637
3926
|
),
|
|
3638
|
-
/* @__PURE__ */
|
|
3639
|
-
|
|
3927
|
+
/* @__PURE__ */ jsx11(
|
|
3928
|
+
TextInput4,
|
|
3640
3929
|
{
|
|
3641
3930
|
label: strings.language,
|
|
3642
3931
|
description: strings.languageHint,
|
|
@@ -3645,7 +3934,7 @@ function StoryEditor({
|
|
|
3645
3934
|
onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
|
|
3646
3935
|
}
|
|
3647
3936
|
),
|
|
3648
|
-
/* @__PURE__ */
|
|
3937
|
+
/* @__PURE__ */ jsx11(
|
|
3649
3938
|
Select,
|
|
3650
3939
|
{
|
|
3651
3940
|
label: strings.map,
|
|
@@ -3664,20 +3953,20 @@ function StoryEditor({
|
|
|
3664
3953
|
}
|
|
3665
3954
|
)
|
|
3666
3955
|
] });
|
|
3667
|
-
const slideForm = slide && /* @__PURE__ */
|
|
3668
|
-
/* @__PURE__ */
|
|
3669
|
-
/* @__PURE__ */
|
|
3956
|
+
const slideForm = slide && /* @__PURE__ */ jsxs8(Stack6, { gap: "md", children: [
|
|
3957
|
+
/* @__PURE__ */ jsxs8(Group5, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
|
|
3958
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
|
|
3670
3959
|
slideActions
|
|
3671
3960
|
] }),
|
|
3672
|
-
/* @__PURE__ */
|
|
3673
|
-
|
|
3961
|
+
/* @__PURE__ */ jsx11(
|
|
3962
|
+
TextInput4,
|
|
3674
3963
|
{
|
|
3675
3964
|
label: strings.slideTitle,
|
|
3676
3965
|
value: slide.title ?? "",
|
|
3677
3966
|
onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
|
|
3678
3967
|
}
|
|
3679
3968
|
),
|
|
3680
|
-
/* @__PURE__ */
|
|
3969
|
+
/* @__PURE__ */ jsx11(
|
|
3681
3970
|
DateField,
|
|
3682
3971
|
{
|
|
3683
3972
|
value: slide.date,
|
|
@@ -3687,7 +3976,7 @@ function StoryEditor({
|
|
|
3687
3976
|
onChange: (date) => setSlide({ date }, "date")
|
|
3688
3977
|
}
|
|
3689
3978
|
),
|
|
3690
|
-
/* @__PURE__ */
|
|
3979
|
+
/* @__PURE__ */ jsx11(
|
|
3691
3980
|
StoryTextField,
|
|
3692
3981
|
{
|
|
3693
3982
|
label: strings.text,
|
|
@@ -3698,19 +3987,20 @@ function StoryEditor({
|
|
|
3698
3987
|
onChange: (text) => setSlide({ text: text || void 0 }, "text")
|
|
3699
3988
|
}
|
|
3700
3989
|
),
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
slide.place
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3990
|
+
/* @__PURE__ */ jsx11(
|
|
3991
|
+
PlaceField,
|
|
3992
|
+
{
|
|
3993
|
+
place: slide.place,
|
|
3994
|
+
choices: placeChoices,
|
|
3995
|
+
around,
|
|
3996
|
+
language: doc.language,
|
|
3997
|
+
search: placeSearch,
|
|
3998
|
+
dark,
|
|
3999
|
+
strings,
|
|
4000
|
+
onChange: (update, key) => setPlace(slide.id, update, key)
|
|
4001
|
+
}
|
|
4002
|
+
),
|
|
4003
|
+
/* @__PURE__ */ jsx11(
|
|
3714
4004
|
PhotosField,
|
|
3715
4005
|
{
|
|
3716
4006
|
photos: slide.media ?? [],
|
|
@@ -3723,22 +4013,22 @@ function StoryEditor({
|
|
|
3723
4013
|
onDescription: (key, alt) => apply(updateMedia(doc, key, { alt: alt || void 0 }), `media:${key}:alt`)
|
|
3724
4014
|
}
|
|
3725
4015
|
),
|
|
3726
|
-
photoError && /* @__PURE__ */
|
|
4016
|
+
photoError && /* @__PURE__ */ jsx11(Text5, { size: "xs", c: "red", role: "alert", children: photoError })
|
|
3727
4017
|
] }, slide.id);
|
|
3728
|
-
const list = /* @__PURE__ */
|
|
3729
|
-
/* @__PURE__ */
|
|
3730
|
-
|
|
4018
|
+
const list = /* @__PURE__ */ jsxs8(Stack6, { gap: "xs", children: [
|
|
4019
|
+
/* @__PURE__ */ jsxs8(
|
|
4020
|
+
UnstyledButton3,
|
|
3731
4021
|
{
|
|
3732
4022
|
onClick: () => setSelected(STORY),
|
|
3733
4023
|
style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
|
|
3734
4024
|
children: [
|
|
3735
|
-
/* @__PURE__ */
|
|
3736
|
-
/* @__PURE__ */
|
|
4025
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, size: "sm", children: strings.story }),
|
|
4026
|
+
/* @__PURE__ */ jsx11(Text5, { size: "xs", c: "dimmed", children: strings.storySettings })
|
|
3737
4027
|
]
|
|
3738
4028
|
}
|
|
3739
4029
|
),
|
|
3740
|
-
/* @__PURE__ */
|
|
3741
|
-
/* @__PURE__ */
|
|
4030
|
+
/* @__PURE__ */ jsx11(Divider, { label: strings.slides, labelPosition: "left" }),
|
|
4031
|
+
/* @__PURE__ */ jsx11(
|
|
3742
4032
|
SlideList,
|
|
3743
4033
|
{
|
|
3744
4034
|
label: strings.slides,
|
|
@@ -3750,17 +4040,17 @@ function StoryEditor({
|
|
|
3750
4040
|
setSelected(to);
|
|
3751
4041
|
},
|
|
3752
4042
|
getKey: (s) => s.id,
|
|
3753
|
-
renderItem: (s, i) => /* @__PURE__ */
|
|
3754
|
-
/* @__PURE__ */
|
|
3755
|
-
/* @__PURE__ */
|
|
3756
|
-
/* @__PURE__ */
|
|
3757
|
-
/* @__PURE__ */
|
|
4043
|
+
renderItem: (s, i) => /* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", children: [
|
|
4044
|
+
/* @__PURE__ */ jsx11(Thumb, { doc, slide: s, resolveMedia }),
|
|
4045
|
+
/* @__PURE__ */ jsxs8(Box2, { style: { minWidth: 0 }, children: [
|
|
4046
|
+
/* @__PURE__ */ jsx11(Text5, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
|
|
4047
|
+
/* @__PURE__ */ jsx11(Text5, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
|
|
3758
4048
|
] })
|
|
3759
4049
|
] })
|
|
3760
4050
|
}
|
|
3761
4051
|
),
|
|
3762
|
-
/* @__PURE__ */
|
|
3763
|
-
|
|
4052
|
+
/* @__PURE__ */ jsxs8(
|
|
4053
|
+
Button4,
|
|
3764
4054
|
{
|
|
3765
4055
|
variant: "light",
|
|
3766
4056
|
size: "xs",
|
|
@@ -3777,7 +4067,7 @@ function StoryEditor({
|
|
|
3777
4067
|
}
|
|
3778
4068
|
)
|
|
3779
4069
|
] });
|
|
3780
|
-
const preview = /* @__PURE__ */
|
|
4070
|
+
const preview = /* @__PURE__ */ jsx11(Box2, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx11(
|
|
3781
4071
|
StoryPlayer,
|
|
3782
4072
|
{
|
|
3783
4073
|
story: doc,
|
|
@@ -3791,7 +4081,7 @@ function StoryEditor({
|
|
|
3791
4081
|
}
|
|
3792
4082
|
}
|
|
3793
4083
|
) });
|
|
3794
|
-
const column = (children, width) => /* @__PURE__ */
|
|
4084
|
+
const column = (children, width) => /* @__PURE__ */ jsx11(
|
|
3795
4085
|
Box2,
|
|
3796
4086
|
{
|
|
3797
4087
|
style: {
|
|
@@ -3805,9 +4095,9 @@ function StoryEditor({
|
|
|
3805
4095
|
children
|
|
3806
4096
|
}
|
|
3807
4097
|
);
|
|
3808
|
-
return /* @__PURE__ */
|
|
3809
|
-
/* @__PURE__ */
|
|
3810
|
-
|
|
4098
|
+
return /* @__PURE__ */ jsxs8(Box2, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
|
|
4099
|
+
/* @__PURE__ */ jsxs8(
|
|
4100
|
+
Group5,
|
|
3811
4101
|
{
|
|
3812
4102
|
h: BAR_HEIGHT2,
|
|
3813
4103
|
px: "md",
|
|
@@ -3816,21 +4106,21 @@ function StoryEditor({
|
|
|
3816
4106
|
justify: "space-between",
|
|
3817
4107
|
style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
|
|
3818
4108
|
children: [
|
|
3819
|
-
/* @__PURE__ */
|
|
4109
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
3820
4110
|
brand,
|
|
3821
|
-
/* @__PURE__ */
|
|
4111
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, truncate: true, dir: "auto", children: doc.title })
|
|
3822
4112
|
] }),
|
|
3823
|
-
/* @__PURE__ */
|
|
3824
|
-
issues.length > 0 && /* @__PURE__ */
|
|
3825
|
-
/* @__PURE__ */
|
|
3826
|
-
/* @__PURE__ */
|
|
4113
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: "xs", wrap: "nowrap", children: [
|
|
4114
|
+
issues.length > 0 && /* @__PURE__ */ jsxs8(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
|
|
4115
|
+
/* @__PURE__ */ jsx11(Popover.Target, { children: /* @__PURE__ */ jsx11(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
|
|
4116
|
+
/* @__PURE__ */ jsx11(Popover.Dropdown, { children: /* @__PURE__ */ jsx11(Stack6, { gap: 6, children: issues.map((issue, i) => {
|
|
3827
4117
|
const at = issueSlide(issue);
|
|
3828
|
-
return /* @__PURE__ */
|
|
4118
|
+
return /* @__PURE__ */ jsxs8(UnstyledButton3, { onClick: () => {
|
|
3829
4119
|
setSelected(at);
|
|
3830
4120
|
setProblemsOpen(false);
|
|
3831
4121
|
}, children: [
|
|
3832
|
-
/* @__PURE__ */
|
|
3833
|
-
/* @__PURE__ */
|
|
4122
|
+
/* @__PURE__ */ jsx11(Text5, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
|
|
4123
|
+
/* @__PURE__ */ jsxs8(Text5, { size: "xs", c: "dimmed", children: [
|
|
3834
4124
|
issue.path,
|
|
3835
4125
|
" ",
|
|
3836
4126
|
issue.message
|
|
@@ -3838,15 +4128,15 @@ function StoryEditor({
|
|
|
3838
4128
|
] }, i);
|
|
3839
4129
|
}) }) })
|
|
3840
4130
|
] }),
|
|
3841
|
-
/* @__PURE__ */
|
|
3842
|
-
/* @__PURE__ */
|
|
3843
|
-
/* @__PURE__ */
|
|
4131
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.undo, children: /* @__PURE__ */ jsx11(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
|
|
4132
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.redo, children: /* @__PURE__ */ jsx11(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
|
|
4133
|
+
/* @__PURE__ */ jsx11(Button4, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
|
|
3844
4134
|
toolbar
|
|
3845
4135
|
] })
|
|
3846
4136
|
]
|
|
3847
4137
|
}
|
|
3848
4138
|
),
|
|
3849
|
-
/* @__PURE__ */
|
|
4139
|
+
/* @__PURE__ */ jsxs8(
|
|
3850
4140
|
Box2,
|
|
3851
4141
|
{
|
|
3852
4142
|
style: {
|
|
@@ -3863,7 +4153,7 @@ function StoryEditor({
|
|
|
3863
4153
|
]
|
|
3864
4154
|
}
|
|
3865
4155
|
),
|
|
3866
|
-
/* @__PURE__ */
|
|
4156
|
+
/* @__PURE__ */ jsx11(Modal, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx11(Box2, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx11(
|
|
3867
4157
|
StoryPlayer,
|
|
3868
4158
|
{
|
|
3869
4159
|
story: doc,
|
|
@@ -3881,30 +4171,39 @@ export {
|
|
|
3881
4171
|
DateField,
|
|
3882
4172
|
EDITOR_ENGLISH,
|
|
3883
4173
|
PhotosField,
|
|
4174
|
+
PlaceField,
|
|
3884
4175
|
SlideList,
|
|
3885
4176
|
StoryEditor,
|
|
3886
4177
|
StoryTextField,
|
|
4178
|
+
WIKIDATA_PLACE_SEARCH,
|
|
3887
4179
|
addImage,
|
|
3888
4180
|
addSlide,
|
|
3889
4181
|
blankStory,
|
|
3890
4182
|
deleteSlide,
|
|
3891
4183
|
duplicateSlide,
|
|
3892
4184
|
findMedia,
|
|
4185
|
+
formatLatLon,
|
|
3893
4186
|
moveSlide,
|
|
3894
4187
|
nextSlideId,
|
|
4188
|
+
parseLatLon,
|
|
4189
|
+
patchPlace,
|
|
3895
4190
|
prepareImage,
|
|
3896
4191
|
pruneMedia,
|
|
3897
4192
|
record,
|
|
3898
4193
|
redo,
|
|
4194
|
+
roundDegrees,
|
|
4195
|
+
searchWikidataPlaces,
|
|
3899
4196
|
setSlidePhotos,
|
|
3900
4197
|
slideDate,
|
|
3901
4198
|
startHistory,
|
|
3902
4199
|
storyFileName,
|
|
4200
|
+
storyPlaceChoices,
|
|
3903
4201
|
suggestEdtf,
|
|
3904
4202
|
undo,
|
|
3905
4203
|
updateMedia,
|
|
3906
4204
|
updateSlide,
|
|
3907
4205
|
updateStory,
|
|
3908
|
-
usedMedia
|
|
4206
|
+
usedMedia,
|
|
4207
|
+
wikidataPoint
|
|
3909
4208
|
};
|
|
3910
4209
|
//# sourceMappingURL=editor.js.map
|