@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/dist/editor.js CHANGED
@@ -1,20 +1,20 @@
1
1
  // src/editor/StoryEditor.tsx
2
- import { useCallback, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef7, useState as useState7 } from "react";
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 Button3,
7
+ Button as Button4,
8
8
  Divider,
9
- Group as Group4,
9
+ Group as Group5,
10
10
  Modal,
11
11
  Popover,
12
12
  Select,
13
- Stack as Stack5,
14
- Text as Text4,
15
- TextInput as TextInput3,
16
- Tooltip as Tooltip2,
17
- UnstyledButton as UnstyledButton2
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/SlideList.tsx
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] = useState6(null);
3258
- const [over, setOver] = useState6(null);
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__ */ jsx7(Stack3, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
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__ */ jsx7(
3267
- UnstyledButton,
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 useRef6 } from "react";
3307
- import { Button as Button2, Group as Group3, Stack as Stack4, Text as Text3, Textarea } from "@mantine/core";
3308
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
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 = useRef6(null);
3327
- return /* @__PURE__ */ jsxs6(Stack4, { gap: 4, children: [
3328
- /* @__PURE__ */ jsxs6(Group3, { gap: "xs", children: [
3329
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: label }),
3330
- /* @__PURE__ */ jsx8(
3331
- Button2,
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__ */ jsx8(
3343
- Button2,
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__ */ jsx8(
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 jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
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] = useState7(null);
3439
- useEffect6(() => {
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__ */ jsx9(
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__ */ jsx9("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
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 = useMemo2(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
3472
- const [history, setHistory] = useState7(() => startHistory(initialStory));
3473
- const [selected, setSelected] = useState7(0);
3474
- const [presenting, setPresenting] = useState7(false);
3475
- const [problemsOpen, setProblemsOpen] = useState7(false);
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] = useState7(() => new Map(initialFiles ?? []));
3479
- const [busy, setBusy] = useState7(false);
3480
- const [photoError, setPhotoError] = useState7(null);
3481
- const docRef = useRef7(doc);
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 = useRef7(files);
3761
+ const filesRef = useRef9(files);
3484
3762
  filesRef.current = files;
3485
- const urls = useRef7(/* @__PURE__ */ new Map());
3486
- useEffect6(() => () => {
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
- useEffect6(() => {
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 issues = useMemo2(() => validate2(doc, files).errors, [doc, files]);
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
- useEffect6(() => {
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
- useEffect6(() => {
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__ */ jsxs7(Group4, { gap: 6, children: [
3580
- /* @__PURE__ */ jsx9(Tooltip2, { label: strings.moveUp, children: /* @__PURE__ */ jsx9(
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__ */ jsx9(Tooltip2, { label: strings.moveDown, children: /* @__PURE__ */ jsx9(
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__ */ jsx9(Button3, { variant: "default", size: "xs", onClick: () => {
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__ */ jsx9(
3612
- Button3,
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__ */ jsxs7(Stack5, { gap: "md", children: [
3628
- /* @__PURE__ */ jsx9(Text4, { fw: 600, size: "lg", children: strings.story }),
3629
- /* @__PURE__ */ jsx9(
3630
- TextInput3,
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__ */ jsx9(
3639
- TextInput3,
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__ */ jsx9(
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__ */ jsxs7(Stack5, { gap: "md", children: [
3668
- /* @__PURE__ */ jsxs7(Group4, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
3669
- /* @__PURE__ */ jsx9(Text4, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
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__ */ jsx9(
3673
- TextInput3,
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__ */ jsx9(
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__ */ jsx9(
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
- slide.place && /* @__PURE__ */ jsxs7(Stack5, { gap: 2, children: [
3702
- /* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, children: strings.place }),
3703
- /* @__PURE__ */ jsxs7(Text4, { size: "sm", children: [
3704
- slide.place.name ?? "",
3705
- slide.place.lat !== void 0 && slide.place.lon !== void 0 && /* @__PURE__ */ jsxs7(Text4, { span: true, c: "dimmed", size: "sm", children: [
3706
- " ",
3707
- slide.place.lat.toFixed(4),
3708
- ", ",
3709
- slide.place.lon.toFixed(4)
3710
- ] })
3711
- ] })
3712
- ] }),
3713
- /* @__PURE__ */ jsx9(
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__ */ jsx9(Text4, { size: "xs", c: "red", role: "alert", children: photoError })
4016
+ photoError && /* @__PURE__ */ jsx11(Text5, { size: "xs", c: "red", role: "alert", children: photoError })
3727
4017
  ] }, slide.id);
3728
- const list = /* @__PURE__ */ jsxs7(Stack5, { gap: "xs", children: [
3729
- /* @__PURE__ */ jsxs7(
3730
- UnstyledButton2,
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__ */ jsx9(Text4, { fw: 600, size: "sm", children: strings.story }),
3736
- /* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", children: strings.storySettings })
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__ */ jsx9(Divider, { label: strings.slides, labelPosition: "left" }),
3741
- /* @__PURE__ */ jsx9(
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__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", children: [
3754
- /* @__PURE__ */ jsx9(Thumb, { doc, slide: s, resolveMedia }),
3755
- /* @__PURE__ */ jsxs7(Box2, { style: { minWidth: 0 }, children: [
3756
- /* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
3757
- /* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
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__ */ jsxs7(
3763
- Button3,
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__ */ jsx9(Box2, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx9(
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__ */ jsx9(
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__ */ jsxs7(Box2, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
3809
- /* @__PURE__ */ jsxs7(
3810
- Group4,
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__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
4109
+ /* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
3820
4110
  brand,
3821
- /* @__PURE__ */ jsx9(Text4, { fw: 600, truncate: true, dir: "auto", children: doc.title })
4111
+ /* @__PURE__ */ jsx11(Text5, { fw: 600, truncate: true, dir: "auto", children: doc.title })
3822
4112
  ] }),
3823
- /* @__PURE__ */ jsxs7(Group4, { gap: "xs", wrap: "nowrap", children: [
3824
- issues.length > 0 && /* @__PURE__ */ jsxs7(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
3825
- /* @__PURE__ */ jsx9(Popover.Target, { children: /* @__PURE__ */ jsx9(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
3826
- /* @__PURE__ */ jsx9(Popover.Dropdown, { children: /* @__PURE__ */ jsx9(Stack5, { gap: 6, children: issues.map((issue, i) => {
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__ */ jsxs7(UnstyledButton2, { onClick: () => {
4118
+ return /* @__PURE__ */ jsxs8(UnstyledButton3, { onClick: () => {
3829
4119
  setSelected(at);
3830
4120
  setProblemsOpen(false);
3831
4121
  }, children: [
3832
- /* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
3833
- /* @__PURE__ */ jsxs7(Text4, { size: "xs", c: "dimmed", children: [
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__ */ jsx9(Tooltip2, { label: strings.undo, children: /* @__PURE__ */ jsx9(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
3842
- /* @__PURE__ */ jsx9(Tooltip2, { label: strings.redo, children: /* @__PURE__ */ jsx9(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
3843
- /* @__PURE__ */ jsx9(Button3, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
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__ */ jsxs7(
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__ */ jsx9(Modal, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx9(Box2, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx9(
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