@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/README.md +25 -14
- package/dist/editor.js +851 -181
- package/dist/editor.js.map +4 -4
- package/dist/embed.js +10 -10
- package/dist/player.js +63 -8
- package/dist/player.js.map +2 -2
- package/dist/types/editor/PhotosField.d.ts +16 -0
- package/dist/types/editor/PlaceField.d.ts +21 -0
- package/dist/types/editor/PlaceMap.d.ts +12 -0
- package/dist/types/editor/StoryEditor.d.ts +21 -12
- package/dist/types/editor/index.d.ts +5 -2
- package/dist/types/editor/model.d.ts +42 -1
- package/dist/types/editor/placeSearch.d.ts +22 -0
- package/dist/types/editor/strings.d.ts +27 -0
- package/dist/types/player/strings.d.ts +2 -0
- package/package.json +1 -1
- package/src/editor/PhotosField.tsx +134 -0
- package/src/editor/PlaceField.tsx +160 -0
- package/src/editor/PlaceMap.tsx +88 -0
- package/src/editor/StoryEditor.tsx +143 -40
- package/src/editor/index.ts +8 -3
- package/src/editor/model.ts +100 -1
- package/src/editor/placeSearch.ts +61 -0
- package/src/editor/strings.ts +54 -0
- package/src/player/StoryPlayer.tsx +57 -5
- package/src/player/strings.ts +7 -0
package/dist/editor.js
CHANGED
|
@@ -1,23 +1,71 @@
|
|
|
1
1
|
// src/editor/StoryEditor.tsx
|
|
2
|
-
import { useCallback, useEffect as
|
|
2
|
+
import { useCallback, useEffect as useEffect8, useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
|
|
3
3
|
import {
|
|
4
|
-
ActionIcon,
|
|
4
|
+
ActionIcon as ActionIcon2,
|
|
5
5
|
Badge,
|
|
6
|
-
Box,
|
|
7
|
-
Button as
|
|
6
|
+
Box as Box2,
|
|
7
|
+
Button as Button4,
|
|
8
8
|
Divider,
|
|
9
|
-
Group as
|
|
9
|
+
Group as Group5,
|
|
10
10
|
Modal,
|
|
11
11
|
Popover,
|
|
12
12
|
Select,
|
|
13
|
-
Stack as
|
|
14
|
-
Text as
|
|
15
|
-
TextInput as
|
|
16
|
-
Tooltip,
|
|
17
|
-
UnstyledButton as
|
|
13
|
+
Stack as Stack6,
|
|
14
|
+
Text as Text5,
|
|
15
|
+
TextInput as TextInput4,
|
|
16
|
+
Tooltip as Tooltip3,
|
|
17
|
+
UnstyledButton as UnstyledButton3
|
|
18
18
|
} from "@mantine/core";
|
|
19
19
|
import { useMediaQuery } from "@mantine/hooks";
|
|
20
20
|
|
|
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
|
|
2421
|
-
const
|
|
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
|
|
2984
|
-
import { Stack as
|
|
2985
|
-
import { jsx as
|
|
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] =
|
|
2988
|
-
const [over, setOver] =
|
|
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__ */
|
|
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__ */
|
|
2997
|
-
|
|
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
|
|
3037
|
-
import { Button, Group as
|
|
3038
|
-
import { jsx as
|
|
3568
|
+
import { useRef as useRef8 } from "react";
|
|
3569
|
+
import { Button as Button3, Group as Group4, Stack as Stack5, Text as Text4, Textarea } from "@mantine/core";
|
|
3570
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
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 =
|
|
3057
|
-
return /* @__PURE__ */
|
|
3058
|
-
/* @__PURE__ */
|
|
3059
|
-
/* @__PURE__ */
|
|
3060
|
-
/* @__PURE__ */
|
|
3061
|
-
|
|
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__ */
|
|
3073
|
-
|
|
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__ */
|
|
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
|
|
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] =
|
|
3153
|
-
|
|
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__ */
|
|
3164
|
-
|
|
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__ */
|
|
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
|
-
|
|
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 =
|
|
3186
|
-
const [history, setHistory] =
|
|
3187
|
-
const [selected, setSelected] =
|
|
3188
|
-
const [presenting, setPresenting] =
|
|
3189
|
-
const [problemsOpen, setProblemsOpen] =
|
|
3749
|
+
const strings = useMemo3(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
|
|
3750
|
+
const [history, setHistory] = useState8(() => startHistory(initialStory));
|
|
3751
|
+
const [selected, setSelected] = useState8(0);
|
|
3752
|
+
const [presenting, setPresenting] = useState8(false);
|
|
3753
|
+
const [problemsOpen, setProblemsOpen] = useState8(false);
|
|
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
|
-
|
|
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
|
|
3204
|
-
|
|
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__ */
|
|
3222
|
-
/* @__PURE__ */
|
|
3223
|
-
|
|
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__ */
|
|
3236
|
-
|
|
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__ */
|
|
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__ */
|
|
3254
|
-
|
|
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__ */
|
|
3270
|
-
/* @__PURE__ */
|
|
3271
|
-
/* @__PURE__ */
|
|
3272
|
-
|
|
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__ */
|
|
3281
|
-
|
|
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__ */
|
|
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__ */
|
|
3310
|
-
/* @__PURE__ */
|
|
3311
|
-
/* @__PURE__ */
|
|
3956
|
+
const slideForm = slide && /* @__PURE__ */ jsxs8(Stack6, { gap: "md", children: [
|
|
3957
|
+
/* @__PURE__ */ jsxs8(Group5, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
|
|
3958
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
|
|
3312
3959
|
slideActions
|
|
3313
3960
|
] }),
|
|
3314
|
-
/* @__PURE__ */
|
|
3315
|
-
|
|
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__ */
|
|
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__ */
|
|
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
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
slide.place
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
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__ */
|
|
3364
|
-
/* @__PURE__ */
|
|
3365
|
-
|
|
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__ */
|
|
3371
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3376
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3389
|
-
/* @__PURE__ */
|
|
3390
|
-
/* @__PURE__ */
|
|
3391
|
-
/* @__PURE__ */
|
|
3392
|
-
/* @__PURE__ */
|
|
4043
|
+
renderItem: (s, i) => /* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", children: [
|
|
4044
|
+
/* @__PURE__ */ jsx11(Thumb, { doc, slide: s, resolveMedia }),
|
|
4045
|
+
/* @__PURE__ */ jsxs8(Box2, { style: { minWidth: 0 }, children: [
|
|
4046
|
+
/* @__PURE__ */ jsx11(Text5, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
|
|
4047
|
+
/* @__PURE__ */ jsx11(Text5, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
|
|
3393
4048
|
] })
|
|
3394
4049
|
] })
|
|
3395
4050
|
}
|
|
3396
4051
|
),
|
|
3397
|
-
/* @__PURE__ */
|
|
3398
|
-
|
|
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__ */
|
|
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__ */
|
|
3430
|
-
|
|
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__ */
|
|
3444
|
-
/* @__PURE__ */
|
|
3445
|
-
|
|
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__ */
|
|
4109
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
3455
4110
|
brand,
|
|
3456
|
-
/* @__PURE__ */
|
|
4111
|
+
/* @__PURE__ */ jsx11(Text5, { fw: 600, truncate: true, dir: "auto", children: doc.title })
|
|
3457
4112
|
] }),
|
|
3458
|
-
/* @__PURE__ */
|
|
3459
|
-
issues.length > 0 && /* @__PURE__ */
|
|
3460
|
-
/* @__PURE__ */
|
|
3461
|
-
/* @__PURE__ */
|
|
4113
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: "xs", wrap: "nowrap", children: [
|
|
4114
|
+
issues.length > 0 && /* @__PURE__ */ jsxs8(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
|
|
4115
|
+
/* @__PURE__ */ jsx11(Popover.Target, { children: /* @__PURE__ */ jsx11(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
|
|
4116
|
+
/* @__PURE__ */ jsx11(Popover.Dropdown, { children: /* @__PURE__ */ jsx11(Stack6, { gap: 6, children: issues.map((issue, i) => {
|
|
3462
4117
|
const at = issueSlide(issue);
|
|
3463
|
-
return /* @__PURE__ */
|
|
4118
|
+
return /* @__PURE__ */ jsxs8(UnstyledButton3, { onClick: () => {
|
|
3464
4119
|
setSelected(at);
|
|
3465
4120
|
setProblemsOpen(false);
|
|
3466
4121
|
}, children: [
|
|
3467
|
-
/* @__PURE__ */
|
|
3468
|
-
/* @__PURE__ */
|
|
4122
|
+
/* @__PURE__ */ jsx11(Text5, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
|
|
4123
|
+
/* @__PURE__ */ jsxs8(Text5, { size: "xs", c: "dimmed", children: [
|
|
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__ */
|
|
3477
|
-
/* @__PURE__ */
|
|
3478
|
-
/* @__PURE__ */
|
|
4131
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.undo, children: /* @__PURE__ */ jsx11(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
|
|
4132
|
+
/* @__PURE__ */ jsx11(Tooltip3, { label: strings.redo, children: /* @__PURE__ */ jsx11(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
|
|
4133
|
+
/* @__PURE__ */ jsx11(Button4, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
|
|
3479
4134
|
toolbar
|
|
3480
4135
|
] })
|
|
3481
4136
|
]
|
|
3482
4137
|
}
|
|
3483
4138
|
),
|
|
3484
|
-
/* @__PURE__ */
|
|
3485
|
-
|
|
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__ */
|
|
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
|