@gramps-connect/story 0.2.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,23 +1,71 @@
1
1
  // src/editor/StoryEditor.tsx
2
- import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useState as useState6 } from "react";
2
+ import { useCallback, useEffect as useEffect8, useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
3
3
  import {
4
- ActionIcon,
4
+ ActionIcon as ActionIcon2,
5
5
  Badge,
6
- Box,
7
- Button as Button2,
6
+ Box as Box2,
7
+ Button as Button4,
8
8
  Divider,
9
- Group as Group3,
9
+ Group as Group5,
10
10
  Modal,
11
11
  Popover,
12
12
  Select,
13
- Stack as Stack4,
14
- Text as Text3,
15
- TextInput as TextInput2,
16
- Tooltip,
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
 
21
+ // src/image.ts
22
+ var DEFAULT_MAX_EDGE = { photo: 1600, overlay: 2048 };
23
+ function fitWithin(width, height, maxEdge) {
24
+ const longEdge = Math.max(width, height);
25
+ if (longEdge <= maxEdge) return { width, height };
26
+ const scale = maxEdge / longEdge;
27
+ return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) };
28
+ }
29
+ function encodingCandidates(purpose, hasAlpha) {
30
+ if (hasAlpha) return purpose === "overlay" ? ["image/png"] : ["image/webp", "image/png"];
31
+ return ["image/webp", "image/jpeg"];
32
+ }
33
+ var EXT = {
34
+ "image/webp": "webp",
35
+ "image/jpeg": "jpg",
36
+ "image/png": "png"
37
+ };
38
+ function canvasHasAlpha(ctx, width, height) {
39
+ const data = ctx.getImageData(0, 0, width, height).data;
40
+ for (let i = 3; i < data.length; i += 4) {
41
+ if (data[i] < 255) return true;
42
+ }
43
+ return false;
44
+ }
45
+ async function optimizeImage(input, options = {}) {
46
+ const purpose = options.purpose ?? "photo";
47
+ const maxEdge = options.maxEdge ?? DEFAULT_MAX_EDGE[purpose];
48
+ const quality = options.quality ?? 0.82;
49
+ const bitmap = await createImageBitmap(input, { imageOrientation: "from-image" });
50
+ try {
51
+ const { width, height } = fitWithin(bitmap.width, bitmap.height, maxEdge);
52
+ const canvas = new OffscreenCanvas(width, height);
53
+ const ctx = canvas.getContext("2d");
54
+ if (!ctx) throw new Error("No 2D canvas context available.");
55
+ ctx.imageSmoothingQuality = "high";
56
+ ctx.drawImage(bitmap, 0, 0, width, height);
57
+ const hasAlpha = canvasHasAlpha(ctx, width, height);
58
+ for (const mime of encodingCandidates(purpose, hasAlpha)) {
59
+ const blob2 = await canvas.convertToBlob({ type: mime, quality });
60
+ if (blob2.type === mime) return { blob: blob2, mime, ext: EXT[mime], width, height };
61
+ }
62
+ const blob = await canvas.convertToBlob({ type: "image/png" });
63
+ return { blob, mime: "image/png", ext: "png", width, height };
64
+ } finally {
65
+ bitmap.close();
66
+ }
67
+ }
68
+
21
69
  // spec/1.0/story.schema.json
22
70
  var story_schema_default = {
23
71
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -1739,6 +1787,53 @@ function findUnknownFields(value, node, path, warnings, root = story_schema_defa
1739
1787
  }
1740
1788
  }
1741
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
+
1742
1837
  // src/player/StoryPlayer.tsx
1743
1838
  import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
1744
1839
 
@@ -1791,53 +1886,6 @@ function timelineYear(date) {
1791
1886
  return sortYear + (month - 1) / 12 + (day ? (day - 1) / 365 : 0);
1792
1887
  }
1793
1888
 
1794
- // src/player/geo.ts
1795
- function visit(coords, bounds) {
1796
- if (!Array.isArray(coords)) return;
1797
- if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
1798
- const [lon, lat] = coords;
1799
- bounds[0] = Math.min(bounds[0], lon);
1800
- bounds[1] = Math.min(bounds[1], lat);
1801
- bounds[2] = Math.max(bounds[2], lon);
1802
- bounds[3] = Math.max(bounds[3], lat);
1803
- return;
1804
- }
1805
- for (const child of coords) visit(child, bounds);
1806
- }
1807
- function visitGeoJson(node, bounds) {
1808
- if (!node || typeof node !== "object") return;
1809
- const obj = node;
1810
- if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
1811
- if (obj.geometry) visitGeoJson(obj.geometry, bounds);
1812
- for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
1813
- for (const f of obj.features ?? []) visitGeoJson(f, bounds);
1814
- }
1815
- function placeBounds(place) {
1816
- if (!place) return null;
1817
- const bounds = [Infinity, Infinity, -Infinity, -Infinity];
1818
- visitGeoJson(place.geometry, bounds);
1819
- for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
1820
- return Number.isFinite(bounds[0]) ? bounds : null;
1821
- }
1822
- function placeCenter(place) {
1823
- if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
1824
- const bounds = placeBounds(place);
1825
- return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
1826
- }
1827
- function storyPlaces(places) {
1828
- const bounds = [Infinity, Infinity, -Infinity, -Infinity];
1829
- const points = /* @__PURE__ */ new Map();
1830
- for (const place of places) {
1831
- const center = placeCenter(place);
1832
- if (!center) continue;
1833
- points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
1834
- visit(center, bounds);
1835
- const box = placeBounds(place);
1836
- if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
1837
- }
1838
- return points.size ? { points: [...points.values()], bounds } : null;
1839
- }
1840
-
1841
1889
  // src/player/MapBackground.tsx
1842
1890
  import { useEffect, useRef, useState } from "react";
1843
1891
  import * as maplibregl from "maplibre-gl";
@@ -2200,7 +2248,8 @@ var ENGLISH = {
2200
2248
  historical: "Historical",
2201
2249
  historicalMapYear: (year) => `Historical map, ${year}`,
2202
2250
  noDateStandardMap: "No date here \u2014 standard map",
2203
- historicalYear: "Historical map year"
2251
+ historicalYear: "Historical map year",
2252
+ photoOf: (n, total) => `Photo ${n} of ${total}`
2204
2253
  };
2205
2254
  var TABLE = {
2206
2255
  en: ENGLISH,
@@ -2214,7 +2263,8 @@ var TABLE = {
2214
2263
  historical: "Historisch",
2215
2264
  historicalMapYear: (year) => `Historische Karte, ${year}`,
2216
2265
  noDateStandardMap: "Kein Datum hier \u2014 Standardkarte",
2217
- historicalYear: "Jahr der historischen Karte"
2266
+ historicalYear: "Jahr der historischen Karte",
2267
+ photoOf: (n, total) => `Foto ${n} von ${total}`
2218
2268
  },
2219
2269
  es: {
2220
2270
  previous: "Anterior",
@@ -2226,7 +2276,8 @@ var TABLE = {
2226
2276
  historical: "Hist\xF3rico",
2227
2277
  historicalMapYear: (year) => `Mapa hist\xF3rico, ${year}`,
2228
2278
  noDateStandardMap: "Sin fecha aqu\xED \u2014 mapa est\xE1ndar",
2229
- historicalYear: "A\xF1o del mapa hist\xF3rico"
2279
+ historicalYear: "A\xF1o del mapa hist\xF3rico",
2280
+ photoOf: (n, total) => `Foto ${n} de ${total}`
2230
2281
  },
2231
2282
  fr: {
2232
2283
  previous: "Pr\xE9c\xE9dent",
@@ -2238,7 +2289,8 @@ var TABLE = {
2238
2289
  historical: "Historique",
2239
2290
  historicalMapYear: (year) => `Carte historique, ${year}`,
2240
2291
  noDateStandardMap: "Pas de date ici \u2014 carte standard",
2241
- historicalYear: "Ann\xE9e de la carte historique"
2292
+ historicalYear: "Ann\xE9e de la carte historique",
2293
+ photoOf: (n, total) => `Photo ${n} sur ${total}`
2242
2294
  },
2243
2295
  nl: {
2244
2296
  previous: "Vorige",
@@ -2250,7 +2302,8 @@ var TABLE = {
2250
2302
  historical: "Historisch",
2251
2303
  historicalMapYear: (year) => `Historische kaart, ${year}`,
2252
2304
  noDateStandardMap: "Geen datum hier \u2014 standaardkaart",
2253
- historicalYear: "Jaar van de historische kaart"
2305
+ historicalYear: "Jaar van de historische kaart",
2306
+ photoOf: (n, total) => `Foto ${n} van ${total}`
2254
2307
  }
2255
2308
  };
2256
2309
  function stringsFor(...languages) {
@@ -2417,8 +2470,45 @@ function SlidePhoto({ mediaKey, item, caption, credit, resolveMedia, colors }) {
2417
2470
  (caption || credit) && /* @__PURE__ */ jsx4("figcaption", { style: { fontSize: 13, color: colors.muted }, children: [caption, credit && `\xA9 ${credit}`].filter(Boolean).join(" \xB7 ") })
2418
2471
  ] });
2419
2472
  }
2420
- function SlideContent({ slide, story, resolveMedia, colors }) {
2421
- const photo = slide?.media?.find((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
2473
+ function PhotoThumb({ mediaKey, item, label, selected, onSelect, resolveMedia, colors }) {
2474
+ const [src, setSrc] = useState3(null);
2475
+ useEffect3(() => {
2476
+ let cancelled = false;
2477
+ resolveMedia(mediaKey, item).then((url) => {
2478
+ if (!cancelled) setSrc(url);
2479
+ }, () => {
2480
+ });
2481
+ return () => {
2482
+ cancelled = true;
2483
+ };
2484
+ }, [mediaKey, item, resolveMedia]);
2485
+ return /* @__PURE__ */ jsx4(
2486
+ "button",
2487
+ {
2488
+ type: "button",
2489
+ "aria-label": label,
2490
+ title: label,
2491
+ "aria-pressed": selected,
2492
+ onClick: onSelect,
2493
+ style: {
2494
+ width: 44,
2495
+ height: 44,
2496
+ padding: 0,
2497
+ borderRadius: 4,
2498
+ overflow: "hidden",
2499
+ cursor: "pointer",
2500
+ border: `2px solid ${selected ? colors.mark : "transparent"}`,
2501
+ background: colors.border,
2502
+ opacity: selected ? 1 : 0.7
2503
+ },
2504
+ children: src && /* @__PURE__ */ jsx4("img", { src, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
2505
+ }
2506
+ );
2507
+ }
2508
+ function SlideContent({ slide, story, resolveMedia, colors, strings }) {
2509
+ const photos = (slide?.media ?? []).filter((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
2510
+ const [shown, setShown] = useState3(0);
2511
+ const photo = photos[Math.min(shown, photos.length - 1)];
2422
2512
  const eyebrow = [slide?.date?.text, slide?.place?.name].filter(Boolean).join(" \xB7 ");
2423
2513
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
2424
2514
  photo && /* @__PURE__ */ jsx4(
@@ -2432,6 +2522,19 @@ function SlideContent({ slide, story, resolveMedia, colors }) {
2432
2522
  colors
2433
2523
  }
2434
2524
  ),
2525
+ photos.length > 1 && /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "center" }, children: photos.map((m, i) => /* @__PURE__ */ jsx4(
2526
+ PhotoThumb,
2527
+ {
2528
+ mediaKey: m.ref,
2529
+ item: story.media[m.ref],
2530
+ label: strings.photoOf(i + 1, photos.length),
2531
+ selected: photo === m,
2532
+ onSelect: () => setShown(i),
2533
+ resolveMedia,
2534
+ colors
2535
+ },
2536
+ `${i}:${m.ref}`
2537
+ )) }),
2435
2538
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "center", maxWidth: 640, textAlign: "center" }, children: [
2436
2539
  eyebrow && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 14, color: colors.muted }, children: eyebrow }),
2437
2540
  slide?.title && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 20, fontWeight: 700, color: colors.text }, children: slide.title }),
@@ -2609,7 +2712,7 @@ function StoryPlayer({
2609
2712
  const item = Object.prototype.hasOwnProperty.call(story.media, key) ? story.media[key] : void 0;
2610
2713
  return item ? resolveMedia(key, item) : Promise.resolve(null);
2611
2714
  }, [story, resolveMedia]);
2612
- const content = /* @__PURE__ */ jsx4(SlideContent, { slide, story, resolveMedia, colors });
2715
+ const content = /* @__PURE__ */ jsx4(SlideContent, { slide, story, resolveMedia, colors, strings }, slide?.id ?? index);
2613
2716
  const timeline = showTimeline && /* @__PURE__ */ jsx4(TimelineStrip, { points: datedSlides, currentIndex: index, onSelect: setIndex, colors });
2614
2717
  return /* @__PURE__ */ jsxs3(
2615
2718
  "div",
@@ -2864,6 +2967,68 @@ function slideDate(previous, text, edtf) {
2864
2967
  if (next.edtf !== previous?.edtf) delete next.sortYear;
2865
2968
  return next;
2866
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
+ }
2994
+ function sameBytes(a, b) {
2995
+ if (!a || a.length !== b.length) return false;
2996
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
2997
+ return true;
2998
+ }
2999
+ function findMedia(doc, files, data) {
3000
+ for (const [key, item] of Object.entries(doc.media)) {
3001
+ if (sameBytes(files.get(item.src), data)) return key;
3002
+ }
3003
+ return void 0;
3004
+ }
3005
+ function addImage(doc, files, image, stem = "photo") {
3006
+ const existing = findMedia(doc, files, image.data);
3007
+ if (existing) return { doc, files, key: existing };
3008
+ let n = 1;
3009
+ const taken = (key2) => Object.prototype.hasOwnProperty.call(doc.media, key2) || files.has(`media/${key2}.${image.ext}`);
3010
+ while (taken(`${stem}-${n}`)) n += 1;
3011
+ const key = `${stem}-${n}`;
3012
+ const src = `media/${key}.${image.ext}`;
3013
+ const item = {
3014
+ src,
3015
+ mime: image.mime,
3016
+ ...image.width ? { width: image.width } : {},
3017
+ ...image.height ? { height: image.height } : {}
3018
+ };
3019
+ return {
3020
+ doc: { ...doc, media: { ...doc.media, [key]: item } },
3021
+ files: new Map(files).set(src, image.data),
3022
+ key
3023
+ };
3024
+ }
3025
+ function setSlidePhotos(doc, index, media) {
3026
+ return updateSlide(doc, index, { media: media.length ? media : void 0 });
3027
+ }
3028
+ function updateMedia(doc, key, patch) {
3029
+ if (!Object.prototype.hasOwnProperty.call(doc.media, key)) return doc;
3030
+ return { ...doc, media: { ...doc.media, [key]: patched(doc.media[key], patch) } };
3031
+ }
2867
3032
  function usedMedia(doc) {
2868
3033
  const used = /* @__PURE__ */ new Set();
2869
3034
  for (const slide of doc.slides) {
@@ -2979,22 +3144,389 @@ function DateField({ value, language, parse, strings, onChange }) {
2979
3144
  ] });
2980
3145
  }
2981
3146
 
3147
+ // src/editor/PhotosField.tsx
3148
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
3149
+ import { ActionIcon, Box, Button, Group as Group2, Loader, Stack as Stack2, Text as Text2, TextInput as TextInput2, Tooltip } from "@mantine/core";
3150
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3151
+ var imagesIn = (list) => [...list ?? []].filter((f) => f.type.startsWith("image/"));
3152
+ function Preview({ mediaKey, urlFor }) {
3153
+ const [url, setUrl] = useState5(null);
3154
+ useEffect5(() => {
3155
+ let live = true;
3156
+ void urlFor(mediaKey).then((u) => {
3157
+ if (live) setUrl(u);
3158
+ });
3159
+ return () => {
3160
+ live = false;
3161
+ };
3162
+ }, [mediaKey, urlFor]);
3163
+ return /* @__PURE__ */ jsx6(Box, { w: 96, h: 96, style: { flex: "none", borderRadius: 6, overflow: "hidden", background: "var(--mantine-color-default-hover)" }, children: url && /* @__PURE__ */ jsx6("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) });
3164
+ }
3165
+ function PhotosField({ photos, media, urlFor, strings, busy, onChange, onFiles, onDescription }) {
3166
+ const input = useRef5(null);
3167
+ const replacing = useRef5(void 0);
3168
+ const [dropping, setDropping] = useState5(false);
3169
+ const pick = (replace) => {
3170
+ replacing.current = replace;
3171
+ input.current?.click();
3172
+ };
3173
+ const set = (i, patch, field) => {
3174
+ const next = photos.map((p, j) => {
3175
+ if (j !== i) return p;
3176
+ const changed = { ...p, ...patch };
3177
+ for (const [k, v] of Object.entries(patch)) if (!v) delete changed[k];
3178
+ return changed;
3179
+ });
3180
+ onChange(next, `photo:${i}:${field}`);
3181
+ };
3182
+ const move = (from, to) => {
3183
+ const next = [...photos];
3184
+ const [p] = next.splice(from, 1);
3185
+ next.splice(to, 0, p);
3186
+ onChange(next, "photos");
3187
+ };
3188
+ const onDrop = (e) => {
3189
+ e.preventDefault();
3190
+ setDropping(false);
3191
+ const files = imagesIn(e.dataTransfer.files);
3192
+ if (files.length) onFiles(files);
3193
+ };
3194
+ return /* @__PURE__ */ jsxs5(
3195
+ Stack2,
3196
+ {
3197
+ gap: "sm",
3198
+ onDragOver: (e) => {
3199
+ if ([...e.dataTransfer.types].includes("Files")) {
3200
+ e.preventDefault();
3201
+ setDropping(true);
3202
+ }
3203
+ },
3204
+ onDragLeave: (e) => {
3205
+ if (e.currentTarget === e.target) setDropping(false);
3206
+ },
3207
+ onDrop,
3208
+ style: {
3209
+ borderRadius: 8,
3210
+ padding: dropping ? 8 : 0,
3211
+ margin: dropping ? -8 : 0,
3212
+ outline: dropping ? "2px dashed var(--mantine-primary-color-filled)" : void 0
3213
+ },
3214
+ children: [
3215
+ /* @__PURE__ */ jsxs5(Group2, { justify: "space-between", children: [
3216
+ /* @__PURE__ */ jsx6(Text2, { size: "sm", fw: 500, children: strings.photos }),
3217
+ /* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
3218
+ busy && /* @__PURE__ */ jsx6(Loader, { size: "xs" }),
3219
+ /* @__PURE__ */ jsxs5(Button, { variant: "default", size: "xs", onClick: () => pick(), disabled: busy, children: [
3220
+ "+ ",
3221
+ strings.addPhoto
3222
+ ] })
3223
+ ] })
3224
+ ] }),
3225
+ /* @__PURE__ */ jsx6(
3226
+ "input",
3227
+ {
3228
+ ref: input,
3229
+ type: "file",
3230
+ accept: "image/*",
3231
+ multiple: true,
3232
+ hidden: true,
3233
+ onChange: (e) => {
3234
+ const files = imagesIn(e.currentTarget.files);
3235
+ e.currentTarget.value = "";
3236
+ if (files.length) onFiles(replacing.current === void 0 ? files : files.slice(0, 1), replacing.current);
3237
+ }
3238
+ }
3239
+ ),
3240
+ photos.length === 0 && /* @__PURE__ */ jsx6(Text2, { size: "xs", c: "dimmed", children: strings.photosHint }),
3241
+ photos.map((photo, i) => {
3242
+ const item = Object.prototype.hasOwnProperty.call(media, photo.ref) ? media[photo.ref] : void 0;
3243
+ return /* @__PURE__ */ jsxs5(Group2, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
3244
+ /* @__PURE__ */ jsxs5(Stack2, { gap: 4, align: "center", children: [
3245
+ /* @__PURE__ */ jsx6(Preview, { mediaKey: photo.ref, urlFor }),
3246
+ /* @__PURE__ */ jsxs5(Group2, { gap: 4, children: [
3247
+ /* @__PURE__ */ jsx6(Tooltip, { label: strings.movePhotoUp, children: /* @__PURE__ */ jsx6(ActionIcon, { size: "sm", variant: "default", "aria-label": strings.movePhotoUp, disabled: i === 0, onClick: () => move(i, i - 1), children: "\u2191" }) }),
3248
+ /* @__PURE__ */ jsx6(Tooltip, { label: strings.movePhotoDown, children: /* @__PURE__ */ jsx6(ActionIcon, { size: "sm", variant: "default", "aria-label": strings.movePhotoDown, disabled: i === photos.length - 1, onClick: () => move(i, i + 1), children: "\u2193" }) })
3249
+ ] })
3250
+ ] }),
3251
+ /* @__PURE__ */ jsxs5(Stack2, { gap: 6, style: { flex: 1, minWidth: 0 }, children: [
3252
+ /* @__PURE__ */ jsx6(TextInput2, { size: "xs", label: strings.caption, value: photo.caption ?? "", onChange: (e) => set(i, { caption: e.currentTarget.value }, "caption") }),
3253
+ /* @__PURE__ */ jsx6(TextInput2, { size: "xs", label: strings.credit, placeholder: strings.creditHint, value: photo.credit ?? "", onChange: (e) => set(i, { credit: e.currentTarget.value }, "credit") }),
3254
+ item && /* @__PURE__ */ jsx6(
3255
+ TextInput2,
3256
+ {
3257
+ size: "xs",
3258
+ label: strings.description,
3259
+ description: strings.descriptionHint,
3260
+ value: item.alt ?? "",
3261
+ onChange: (e) => onDescription(photo.ref, e.currentTarget.value)
3262
+ }
3263
+ ),
3264
+ /* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
3265
+ /* @__PURE__ */ jsx6(Button, { variant: "subtle", size: "compact-xs", onClick: () => pick(i), disabled: busy, children: strings.replacePhoto }),
3266
+ /* @__PURE__ */ jsx6(Button, { variant: "subtle", size: "compact-xs", color: "red", onClick: () => onChange(photos.filter((_, j) => j !== i), "photos"), children: strings.removePhoto })
3267
+ ] })
3268
+ ] })
3269
+ ] }, i);
3270
+ })
3271
+ ]
3272
+ }
3273
+ );
3274
+ }
3275
+
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";
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
+
2982
3514
  // src/editor/SlideList.tsx
2983
- import { useState as useState5 } from "react";
2984
- import { Stack as Stack2, UnstyledButton } from "@mantine/core";
2985
- import { jsx as jsx6 } from "react/jsx-runtime";
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";
2986
3518
  function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
2987
- const [dragging, setDragging] = useState5(null);
2988
- const [over, setOver] = useState5(null);
3519
+ const [dragging, setDragging] = useState7(null);
3520
+ const [over, setOver] = useState7(null);
2989
3521
  const end = () => {
2990
3522
  setDragging(null);
2991
3523
  setOver(null);
2992
3524
  };
2993
- return /* @__PURE__ */ jsx6(Stack2, { 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) => {
2994
3526
  const isSelected = i === selected;
2995
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)" } : {};
2996
- return /* @__PURE__ */ jsx6(
2997
- UnstyledButton,
3528
+ return /* @__PURE__ */ jsx9(
3529
+ UnstyledButton2,
2998
3530
  {
2999
3531
  role: "option",
3000
3532
  "aria-selected": isSelected,
@@ -3033,9 +3565,9 @@ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, labe
3033
3565
  }
3034
3566
 
3035
3567
  // src/editor/StoryTextField.tsx
3036
- import { useRef as useRef5 } from "react";
3037
- import { Button, Group as Group2, Stack as Stack3, Text as Text2, Textarea } from "@mantine/core";
3038
- import { jsx as jsx7, jsxs as jsxs5 } 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";
3039
3571
  function applyMarker(el, marker, value, onChange) {
3040
3572
  const start = el.selectionStart ?? value.length;
3041
3573
  const end = el.selectionEnd ?? value.length;
@@ -3053,12 +3585,12 @@ function applyMarker(el, marker, value, onChange) {
3053
3585
  onChange(value.slice(0, start) + replacement + value.slice(end));
3054
3586
  }
3055
3587
  function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
3056
- const ref = useRef5(null);
3057
- return /* @__PURE__ */ jsxs5(Stack3, { gap: 4, children: [
3058
- /* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
3059
- /* @__PURE__ */ jsx7(Text2, { size: "sm", fw: 500, children: label }),
3060
- /* @__PURE__ */ jsx7(
3061
- Button,
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,
3062
3594
  {
3063
3595
  variant: "default",
3064
3596
  size: "compact-xs",
@@ -3069,8 +3601,8 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
3069
3601
  children: "B"
3070
3602
  }
3071
3603
  ),
3072
- /* @__PURE__ */ jsx7(
3073
- Button,
3604
+ /* @__PURE__ */ jsx10(
3605
+ Button3,
3074
3606
  {
3075
3607
  variant: "default",
3076
3608
  size: "compact-xs",
@@ -3082,7 +3614,7 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
3082
3614
  }
3083
3615
  )
3084
3616
  ] }),
3085
- /* @__PURE__ */ jsx7(
3617
+ /* @__PURE__ */ jsx10(
3086
3618
  Textarea,
3087
3619
  {
3088
3620
  ref,
@@ -3129,7 +3661,34 @@ var EDITOR_ENGLISH = {
3129
3661
  matchDateAsWritten: "Work it out from the date",
3130
3662
  noPreciseDate: "None: this slide stays off the timeline.",
3131
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.`,
3132
3679
  photos: "Photos",
3680
+ photosHint: "Add, drop or paste a photo. It's resized for the screen, and hidden details like camera and location are left out.",
3681
+ addPhoto: "Add photo",
3682
+ replacePhoto: "Replace",
3683
+ movePhotoUp: "Move photo up",
3684
+ movePhotoDown: "Move photo down",
3685
+ removePhoto: "Remove",
3686
+ caption: "Caption",
3687
+ credit: "Credit",
3688
+ creditHint: "Who took it, or where it's from",
3689
+ description: "Description",
3690
+ descriptionHint: "What the photo shows, for anyone who can't see it.",
3691
+ photoFailed: (name) => `Couldn't use ${name}: this browser can't read it as an image.`,
3133
3692
  undo: "Undo",
3134
3693
  redo: "Redo",
3135
3694
  present: "Present",
@@ -3139,18 +3698,22 @@ var EDITOR_ENGLISH = {
3139
3698
  };
3140
3699
 
3141
3700
  // src/editor/StoryEditor.tsx
3142
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3701
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
3143
3702
  var BAR_HEIGHT2 = 56;
3144
3703
  var STORY = -1;
3145
3704
  function issueSlide(issue) {
3146
3705
  const match = /^\/slides\/(\d+)/.exec(issue.path);
3147
3706
  return match ? Number(match[1]) : STORY;
3148
3707
  }
3708
+ async function prepareImage(file) {
3709
+ const image = await optimizeImage(file);
3710
+ return { data: new Uint8Array(await image.blob.arrayBuffer()), mime: image.mime, ext: image.ext, width: image.width, height: image.height };
3711
+ }
3149
3712
  function Thumb({ doc, slide, resolveMedia }) {
3150
3713
  const ref = slide.media?.[0]?.ref;
3151
3714
  const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
3152
- const [url, setUrl] = useState6(null);
3153
- useEffect5(() => {
3715
+ const [url, setUrl] = useState8(null);
3716
+ useEffect8(() => {
3154
3717
  let live = true;
3155
3718
  setUrl(null);
3156
3719
  if (ref && item) void resolveMedia(ref, item).then((u) => {
@@ -3160,48 +3723,132 @@ function Thumb({ doc, slide, resolveMedia }) {
3160
3723
  live = false;
3161
3724
  };
3162
3725
  }, [ref, item, resolveMedia]);
3163
- return /* @__PURE__ */ jsx8(
3164
- Box,
3726
+ return /* @__PURE__ */ jsx11(
3727
+ Box2,
3165
3728
  {
3166
3729
  w: 40,
3167
3730
  h: 40,
3168
3731
  style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
3169
- children: url && /* @__PURE__ */ jsx8("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" } })
3170
3733
  }
3171
3734
  );
3172
3735
  }
3173
3736
  function StoryEditor({
3174
3737
  initialStory,
3175
- files,
3176
- resolveMedia,
3738
+ initialFiles,
3177
3739
  onChange,
3740
+ prepareImage: prepare = prepareImage,
3178
3741
  toolbar,
3179
3742
  brand,
3180
3743
  dark = false,
3181
3744
  strings: stringOverrides,
3182
3745
  playerStrings,
3183
- parseDateText = suggestEdtf
3746
+ parseDateText = suggestEdtf,
3747
+ placeSearch = WIKIDATA_PLACE_SEARCH
3184
3748
  }) {
3185
- const strings = useMemo2(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
3186
- const [history, setHistory] = useState6(() => startHistory(initialStory));
3187
- const [selected, setSelected] = useState6(0);
3188
- const [presenting, setPresenting] = useState6(false);
3189
- const [problemsOpen, setProblemsOpen] = useState6(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);
3190
3754
  const wide = useMediaQuery("(min-width: 1000px)", true);
3191
3755
  const doc = history.present;
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);
3760
+ docRef.current = doc;
3761
+ const filesRef = useRef9(files);
3762
+ filesRef.current = files;
3763
+ const urls = useRef9(/* @__PURE__ */ new Map());
3764
+ useEffect8(() => () => {
3765
+ for (const url of urls.current.values()) URL.revokeObjectURL(url);
3766
+ }, []);
3767
+ const resolveMedia = useCallback(async (_key, item) => {
3768
+ const cached = urls.current.get(item.src);
3769
+ if (cached) return cached;
3770
+ const data = filesRef.current.get(item.src);
3771
+ if (!data) return null;
3772
+ const url = URL.createObjectURL(new Blob([data], { type: item.mime }));
3773
+ urls.current.set(item.src, url);
3774
+ return url;
3775
+ }, []);
3776
+ const urlFor = useCallback((key) => {
3777
+ const item = Object.prototype.hasOwnProperty.call(docRef.current.media, key) ? docRef.current.media[key] : void 0;
3778
+ return item ? resolveMedia(key, item) : Promise.resolve(null);
3779
+ }, [resolveMedia]);
3192
3780
  const apply = useCallback((next, key) => {
3193
3781
  setHistory((h) => record(h, next, key));
3194
3782
  }, []);
3195
- useEffect5(() => {
3196
- onChange?.(doc);
3197
- }, [doc, onChange]);
3783
+ useEffect8(() => {
3784
+ onChange?.(doc, files);
3785
+ }, [doc, files, onChange]);
3198
3786
  const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
3199
3787
  const slide = index === STORY ? void 0 : doc.slides[index];
3200
3788
  const setSlide = (patch, key) => {
3201
3789
  if (index !== STORY) apply(updateSlide(history.present, index, patch), `${slide.id}:${key}`);
3202
3790
  };
3203
- const issues = useMemo2(() => validate2(doc, files).errors, [doc, files]);
3204
- useEffect5(() => {
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]);
3803
+ const addPhotos = useCallback(async (chosen, slideId2, replace) => {
3804
+ setBusy(true);
3805
+ setPhotoError(null);
3806
+ const chosenImages = [];
3807
+ for (const file of chosen) {
3808
+ try {
3809
+ const known = findMedia(docRef.current, filesRef.current, new Uint8Array(await file.arrayBuffer()));
3810
+ chosenImages.push(known ?? await prepare(file));
3811
+ } catch {
3812
+ setPhotoError(strings.photoFailed(file.name || "the image"));
3813
+ }
3814
+ }
3815
+ setBusy(false);
3816
+ if (!chosenImages.length) return;
3817
+ let next = docRef.current;
3818
+ let nextFiles = filesRef.current;
3819
+ const keys = [];
3820
+ for (const image of chosenImages) {
3821
+ if (typeof image === "string") {
3822
+ keys.push(image);
3823
+ continue;
3824
+ }
3825
+ const added = addImage(next, nextFiles, image);
3826
+ next = added.doc;
3827
+ nextFiles = added.files;
3828
+ keys.push(added.key);
3829
+ }
3830
+ const at = next.slides.findIndex((s) => s.id === slideId2);
3831
+ if (at < 0) return;
3832
+ const photos = [...next.slides[at].media ?? []];
3833
+ if (replace !== void 0 && photos[replace]) photos[replace] = { ...photos[replace], ref: keys[0] };
3834
+ else photos.push(...keys.map((ref) => ({ ref })));
3835
+ setFiles(nextFiles);
3836
+ apply(setSlidePhotos(next, at, photos));
3837
+ }, [apply, prepare, strings]);
3838
+ const slideId = slide?.id;
3839
+ useEffect8(() => {
3840
+ if (!slideId) return;
3841
+ const onPaste = (e) => {
3842
+ if (e.target?.closest("input, textarea, [contenteditable=true]")) return;
3843
+ const images = [...e.clipboardData?.files ?? []].filter((f) => f.type.startsWith("image/"));
3844
+ if (!images.length) return;
3845
+ e.preventDefault();
3846
+ void addPhotos(images, slideId);
3847
+ };
3848
+ window.addEventListener("paste", onPaste);
3849
+ return () => window.removeEventListener("paste", onPaste);
3850
+ }, [slideId, addPhotos]);
3851
+ useEffect8(() => {
3205
3852
  const onKey = (e) => {
3206
3853
  if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
3207
3854
  const target = e.target;
@@ -3218,9 +3865,9 @@ function StoryEditor({
3218
3865
  window.addEventListener("keydown", onKey);
3219
3866
  return () => window.removeEventListener("keydown", onKey);
3220
3867
  }, []);
3221
- const slideActions = slide && /* @__PURE__ */ jsxs6(Group3, { gap: 6, children: [
3222
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.moveUp, children: /* @__PURE__ */ jsx8(
3223
- ActionIcon,
3868
+ const slideActions = slide && /* @__PURE__ */ jsxs8(Group5, { gap: 6, children: [
3869
+ /* @__PURE__ */ jsx11(Tooltip3, { label: strings.moveUp, children: /* @__PURE__ */ jsx11(
3870
+ ActionIcon2,
3224
3871
  {
3225
3872
  variant: "default",
3226
3873
  "aria-label": strings.moveUp,
@@ -3232,8 +3879,8 @@ function StoryEditor({
3232
3879
  children: "\u2191"
3233
3880
  }
3234
3881
  ) }),
3235
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.moveDown, children: /* @__PURE__ */ jsx8(
3236
- ActionIcon,
3882
+ /* @__PURE__ */ jsx11(Tooltip3, { label: strings.moveDown, children: /* @__PURE__ */ jsx11(
3883
+ ActionIcon2,
3237
3884
  {
3238
3885
  variant: "default",
3239
3886
  "aria-label": strings.moveDown,
@@ -3245,13 +3892,13 @@ function StoryEditor({
3245
3892
  children: "\u2193"
3246
3893
  }
3247
3894
  ) }),
3248
- /* @__PURE__ */ jsx8(Button2, { variant: "default", size: "xs", onClick: () => {
3895
+ /* @__PURE__ */ jsx11(Button4, { variant: "default", size: "xs", onClick: () => {
3249
3896
  const r = duplicateSlide(doc, index);
3250
3897
  apply(r.doc);
3251
3898
  setSelected(r.index);
3252
3899
  }, children: strings.duplicateSlide }),
3253
- /* @__PURE__ */ jsx8(
3254
- Button2,
3900
+ /* @__PURE__ */ jsx11(
3901
+ Button4,
3255
3902
  {
3256
3903
  variant: "default",
3257
3904
  size: "xs",
@@ -3266,10 +3913,10 @@ function StoryEditor({
3266
3913
  }
3267
3914
  )
3268
3915
  ] });
3269
- const storyForm = /* @__PURE__ */ jsxs6(Stack4, { gap: "md", children: [
3270
- /* @__PURE__ */ jsx8(Text3, { fw: 600, size: "lg", children: strings.story }),
3271
- /* @__PURE__ */ jsx8(
3272
- TextInput2,
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,
3273
3920
  {
3274
3921
  label: strings.title,
3275
3922
  required: true,
@@ -3277,8 +3924,8 @@ function StoryEditor({
3277
3924
  onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
3278
3925
  }
3279
3926
  ),
3280
- /* @__PURE__ */ jsx8(
3281
- TextInput2,
3927
+ /* @__PURE__ */ jsx11(
3928
+ TextInput4,
3282
3929
  {
3283
3930
  label: strings.language,
3284
3931
  description: strings.languageHint,
@@ -3287,7 +3934,7 @@ function StoryEditor({
3287
3934
  onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
3288
3935
  }
3289
3936
  ),
3290
- /* @__PURE__ */ jsx8(
3937
+ /* @__PURE__ */ jsx11(
3291
3938
  Select,
3292
3939
  {
3293
3940
  label: strings.map,
@@ -3306,20 +3953,20 @@ function StoryEditor({
3306
3953
  }
3307
3954
  )
3308
3955
  ] });
3309
- const slideForm = slide && /* @__PURE__ */ jsxs6(Stack4, { gap: "md", children: [
3310
- /* @__PURE__ */ jsxs6(Group3, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
3311
- /* @__PURE__ */ jsx8(Text3, { 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}` }),
3312
3959
  slideActions
3313
3960
  ] }),
3314
- /* @__PURE__ */ jsx8(
3315
- TextInput2,
3961
+ /* @__PURE__ */ jsx11(
3962
+ TextInput4,
3316
3963
  {
3317
3964
  label: strings.slideTitle,
3318
3965
  value: slide.title ?? "",
3319
3966
  onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
3320
3967
  }
3321
3968
  ),
3322
- /* @__PURE__ */ jsx8(
3969
+ /* @__PURE__ */ jsx11(
3323
3970
  DateField,
3324
3971
  {
3325
3972
  value: slide.date,
@@ -3329,7 +3976,7 @@ function StoryEditor({
3329
3976
  onChange: (date) => setSlide({ date }, "date")
3330
3977
  }
3331
3978
  ),
3332
- /* @__PURE__ */ jsx8(
3979
+ /* @__PURE__ */ jsx11(
3333
3980
  StoryTextField,
3334
3981
  {
3335
3982
  label: strings.text,
@@ -3340,40 +3987,48 @@ function StoryEditor({
3340
3987
  onChange: (text) => setSlide({ text: text || void 0 }, "text")
3341
3988
  }
3342
3989
  ),
3343
- slide.place && /* @__PURE__ */ jsxs6(Stack4, { gap: 2, children: [
3344
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.place }),
3345
- /* @__PURE__ */ jsxs6(Text3, { size: "sm", children: [
3346
- slide.place.name ?? "",
3347
- slide.place.lat !== void 0 && slide.place.lon !== void 0 && /* @__PURE__ */ jsxs6(Text3, { span: true, c: "dimmed", size: "sm", children: [
3348
- " ",
3349
- slide.place.lat.toFixed(4),
3350
- ", ",
3351
- slide.place.lon.toFixed(4)
3352
- ] })
3353
- ] })
3354
- ] }),
3355
- slide.media && slide.media.length > 0 && /* @__PURE__ */ jsxs6(Stack4, { gap: 4, children: [
3356
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.photos }),
3357
- /* @__PURE__ */ jsx8(Group3, { gap: "sm", children: slide.media.map((m, i) => /* @__PURE__ */ jsxs6(Group3, { gap: "xs", wrap: "nowrap", children: [
3358
- /* @__PURE__ */ jsx8(Thumb, { doc, slide: { id: "", media: [m] }, resolveMedia }),
3359
- /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", maw: 200, lineClamp: 2, children: m.caption ?? "" })
3360
- ] }, i)) })
3361
- ] })
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(
4004
+ PhotosField,
4005
+ {
4006
+ photos: slide.media ?? [],
4007
+ media: doc.media,
4008
+ urlFor,
4009
+ strings,
4010
+ busy,
4011
+ onChange: (photos, key) => apply(setSlidePhotos(doc, index, photos), `${slide.id}:${key}`),
4012
+ onFiles: (chosen, replace) => void addPhotos(chosen, slide.id, replace),
4013
+ onDescription: (key, alt) => apply(updateMedia(doc, key, { alt: alt || void 0 }), `media:${key}:alt`)
4014
+ }
4015
+ ),
4016
+ photoError && /* @__PURE__ */ jsx11(Text5, { size: "xs", c: "red", role: "alert", children: photoError })
3362
4017
  ] }, slide.id);
3363
- const list = /* @__PURE__ */ jsxs6(Stack4, { gap: "xs", children: [
3364
- /* @__PURE__ */ jsxs6(
3365
- UnstyledButton2,
4018
+ const list = /* @__PURE__ */ jsxs8(Stack6, { gap: "xs", children: [
4019
+ /* @__PURE__ */ jsxs8(
4020
+ UnstyledButton3,
3366
4021
  {
3367
4022
  onClick: () => setSelected(STORY),
3368
4023
  style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
3369
4024
  children: [
3370
- /* @__PURE__ */ jsx8(Text3, { fw: 600, size: "sm", children: strings.story }),
3371
- /* @__PURE__ */ jsx8(Text3, { 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 })
3372
4027
  ]
3373
4028
  }
3374
4029
  ),
3375
- /* @__PURE__ */ jsx8(Divider, { label: strings.slides, labelPosition: "left" }),
3376
- /* @__PURE__ */ jsx8(
4030
+ /* @__PURE__ */ jsx11(Divider, { label: strings.slides, labelPosition: "left" }),
4031
+ /* @__PURE__ */ jsx11(
3377
4032
  SlideList,
3378
4033
  {
3379
4034
  label: strings.slides,
@@ -3385,17 +4040,17 @@ function StoryEditor({
3385
4040
  setSelected(to);
3386
4041
  },
3387
4042
  getKey: (s) => s.id,
3388
- renderItem: (s, i) => /* @__PURE__ */ jsxs6(Group3, { gap: "sm", wrap: "nowrap", children: [
3389
- /* @__PURE__ */ jsx8(Thumb, { doc, slide: s, resolveMedia }),
3390
- /* @__PURE__ */ jsxs6(Box, { style: { minWidth: 0 }, children: [
3391
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
3392
- /* @__PURE__ */ jsx8(Text3, { 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 ?? "" })
3393
4048
  ] })
3394
4049
  ] })
3395
4050
  }
3396
4051
  ),
3397
- /* @__PURE__ */ jsxs6(
3398
- Button2,
4052
+ /* @__PURE__ */ jsxs8(
4053
+ Button4,
3399
4054
  {
3400
4055
  variant: "light",
3401
4056
  size: "xs",
@@ -3412,7 +4067,7 @@ function StoryEditor({
3412
4067
  }
3413
4068
  )
3414
4069
  ] });
3415
- const preview = /* @__PURE__ */ jsx8(Box, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx8(
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(
3416
4071
  StoryPlayer,
3417
4072
  {
3418
4073
  story: doc,
@@ -3426,8 +4081,8 @@ function StoryEditor({
3426
4081
  }
3427
4082
  }
3428
4083
  ) });
3429
- const column = (children, width) => /* @__PURE__ */ jsx8(
3430
- Box,
4084
+ const column = (children, width) => /* @__PURE__ */ jsx11(
4085
+ Box2,
3431
4086
  {
3432
4087
  style: {
3433
4088
  width,
@@ -3440,9 +4095,9 @@ function StoryEditor({
3440
4095
  children
3441
4096
  }
3442
4097
  );
3443
- return /* @__PURE__ */ jsxs6(Box, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
3444
- /* @__PURE__ */ jsxs6(
3445
- Group3,
4098
+ return /* @__PURE__ */ jsxs8(Box2, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
4099
+ /* @__PURE__ */ jsxs8(
4100
+ Group5,
3446
4101
  {
3447
4102
  h: BAR_HEIGHT2,
3448
4103
  px: "md",
@@ -3451,21 +4106,21 @@ function StoryEditor({
3451
4106
  justify: "space-between",
3452
4107
  style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
3453
4108
  children: [
3454
- /* @__PURE__ */ jsxs6(Group3, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
4109
+ /* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
3455
4110
  brand,
3456
- /* @__PURE__ */ jsx8(Text3, { fw: 600, truncate: true, dir: "auto", children: doc.title })
4111
+ /* @__PURE__ */ jsx11(Text5, { fw: 600, truncate: true, dir: "auto", children: doc.title })
3457
4112
  ] }),
3458
- /* @__PURE__ */ jsxs6(Group3, { gap: "xs", wrap: "nowrap", children: [
3459
- issues.length > 0 && /* @__PURE__ */ jsxs6(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
3460
- /* @__PURE__ */ jsx8(Popover.Target, { children: /* @__PURE__ */ jsx8(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
3461
- /* @__PURE__ */ jsx8(Popover.Dropdown, { children: /* @__PURE__ */ jsx8(Stack4, { 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) => {
3462
4117
  const at = issueSlide(issue);
3463
- return /* @__PURE__ */ jsxs6(UnstyledButton2, { onClick: () => {
4118
+ return /* @__PURE__ */ jsxs8(UnstyledButton3, { onClick: () => {
3464
4119
  setSelected(at);
3465
4120
  setProblemsOpen(false);
3466
4121
  }, children: [
3467
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
3468
- /* @__PURE__ */ jsxs6(Text3, { 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: [
3469
4124
  issue.path,
3470
4125
  " ",
3471
4126
  issue.message
@@ -3473,16 +4128,16 @@ function StoryEditor({
3473
4128
  ] }, i);
3474
4129
  }) }) })
3475
4130
  ] }),
3476
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.undo, children: /* @__PURE__ */ jsx8(ActionIcon, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
3477
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.redo, children: /* @__PURE__ */ jsx8(ActionIcon, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
3478
- /* @__PURE__ */ jsx8(Button2, { 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 }),
3479
4134
  toolbar
3480
4135
  ] })
3481
4136
  ]
3482
4137
  }
3483
4138
  ),
3484
- /* @__PURE__ */ jsxs6(
3485
- Box,
4139
+ /* @__PURE__ */ jsxs8(
4140
+ Box2,
3486
4141
  {
3487
4142
  style: {
3488
4143
  flex: 1,
@@ -3498,7 +4153,7 @@ function StoryEditor({
3498
4153
  ]
3499
4154
  }
3500
4155
  ),
3501
- /* @__PURE__ */ jsx8(Modal, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx8(Box, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx8(
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(
3502
4157
  StoryPlayer,
3503
4158
  {
3504
4159
  story: doc,
@@ -3515,25 +4170,40 @@ export {
3515
4170
  COALESCE_MS,
3516
4171
  DateField,
3517
4172
  EDITOR_ENGLISH,
4173
+ PhotosField,
4174
+ PlaceField,
3518
4175
  SlideList,
3519
4176
  StoryEditor,
3520
4177
  StoryTextField,
4178
+ WIKIDATA_PLACE_SEARCH,
4179
+ addImage,
3521
4180
  addSlide,
3522
4181
  blankStory,
3523
4182
  deleteSlide,
3524
4183
  duplicateSlide,
4184
+ findMedia,
4185
+ formatLatLon,
3525
4186
  moveSlide,
3526
4187
  nextSlideId,
4188
+ parseLatLon,
4189
+ patchPlace,
4190
+ prepareImage,
3527
4191
  pruneMedia,
3528
4192
  record,
3529
4193
  redo,
4194
+ roundDegrees,
4195
+ searchWikidataPlaces,
4196
+ setSlidePhotos,
3530
4197
  slideDate,
3531
4198
  startHistory,
3532
4199
  storyFileName,
4200
+ storyPlaceChoices,
3533
4201
  suggestEdtf,
3534
4202
  undo,
4203
+ updateMedia,
3535
4204
  updateSlide,
3536
4205
  updateStory,
3537
- usedMedia
4206
+ usedMedia,
4207
+ wikidataPoint
3538
4208
  };
3539
4209
  //# sourceMappingURL=editor.js.map