@gramps-connect/story 0.2.0 → 0.3.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 +15 -13
- package/dist/editor.js +490 -119
- 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/StoryEditor.d.ts +17 -12
- package/dist/types/editor/index.d.ts +3 -2
- package/dist/types/editor/model.d.ts +26 -1
- package/dist/types/editor/strings.d.ts +12 -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/StoryEditor.tsx +119 -29
- package/src/editor/index.ts +5 -3
- package/src/editor/model.ts +63 -1
- package/src/editor/strings.ts +24 -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 useEffect6, useMemo as useMemo2, useRef as useRef7, useState as useState7 } 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 Button3,
|
|
8
8
|
Divider,
|
|
9
|
-
Group as
|
|
9
|
+
Group as Group4,
|
|
10
10
|
Modal,
|
|
11
11
|
Popover,
|
|
12
12
|
Select,
|
|
13
|
-
Stack as
|
|
14
|
-
Text as
|
|
15
|
-
TextInput as
|
|
16
|
-
Tooltip,
|
|
13
|
+
Stack as Stack5,
|
|
14
|
+
Text as Text4,
|
|
15
|
+
TextInput as TextInput3,
|
|
16
|
+
Tooltip as Tooltip2,
|
|
17
17
|
UnstyledButton as UnstyledButton2
|
|
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",
|
|
@@ -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,44 @@ function slideDate(previous, text, edtf) {
|
|
|
2864
2967
|
if (next.edtf !== previous?.edtf) delete next.sortYear;
|
|
2865
2968
|
return next;
|
|
2866
2969
|
}
|
|
2970
|
+
function sameBytes(a, b) {
|
|
2971
|
+
if (!a || a.length !== b.length) return false;
|
|
2972
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2973
|
+
return true;
|
|
2974
|
+
}
|
|
2975
|
+
function findMedia(doc, files, data) {
|
|
2976
|
+
for (const [key, item] of Object.entries(doc.media)) {
|
|
2977
|
+
if (sameBytes(files.get(item.src), data)) return key;
|
|
2978
|
+
}
|
|
2979
|
+
return void 0;
|
|
2980
|
+
}
|
|
2981
|
+
function addImage(doc, files, image, stem = "photo") {
|
|
2982
|
+
const existing = findMedia(doc, files, image.data);
|
|
2983
|
+
if (existing) return { doc, files, key: existing };
|
|
2984
|
+
let n = 1;
|
|
2985
|
+
const taken = (key2) => Object.prototype.hasOwnProperty.call(doc.media, key2) || files.has(`media/${key2}.${image.ext}`);
|
|
2986
|
+
while (taken(`${stem}-${n}`)) n += 1;
|
|
2987
|
+
const key = `${stem}-${n}`;
|
|
2988
|
+
const src = `media/${key}.${image.ext}`;
|
|
2989
|
+
const item = {
|
|
2990
|
+
src,
|
|
2991
|
+
mime: image.mime,
|
|
2992
|
+
...image.width ? { width: image.width } : {},
|
|
2993
|
+
...image.height ? { height: image.height } : {}
|
|
2994
|
+
};
|
|
2995
|
+
return {
|
|
2996
|
+
doc: { ...doc, media: { ...doc.media, [key]: item } },
|
|
2997
|
+
files: new Map(files).set(src, image.data),
|
|
2998
|
+
key
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function setSlidePhotos(doc, index, media) {
|
|
3002
|
+
return updateSlide(doc, index, { media: media.length ? media : void 0 });
|
|
3003
|
+
}
|
|
3004
|
+
function updateMedia(doc, key, patch) {
|
|
3005
|
+
if (!Object.prototype.hasOwnProperty.call(doc.media, key)) return doc;
|
|
3006
|
+
return { ...doc, media: { ...doc.media, [key]: patched(doc.media[key], patch) } };
|
|
3007
|
+
}
|
|
2867
3008
|
function usedMedia(doc) {
|
|
2868
3009
|
const used = /* @__PURE__ */ new Set();
|
|
2869
3010
|
for (const slide of doc.slides) {
|
|
@@ -2979,21 +3120,150 @@ function DateField({ value, language, parse, strings, onChange }) {
|
|
|
2979
3120
|
] });
|
|
2980
3121
|
}
|
|
2981
3122
|
|
|
3123
|
+
// src/editor/PhotosField.tsx
|
|
3124
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
|
|
3125
|
+
import { ActionIcon, Box, Button, Group as Group2, Loader, Stack as Stack2, Text as Text2, TextInput as TextInput2, Tooltip } from "@mantine/core";
|
|
3126
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3127
|
+
var imagesIn = (list) => [...list ?? []].filter((f) => f.type.startsWith("image/"));
|
|
3128
|
+
function Preview({ mediaKey, urlFor }) {
|
|
3129
|
+
const [url, setUrl] = useState5(null);
|
|
3130
|
+
useEffect5(() => {
|
|
3131
|
+
let live = true;
|
|
3132
|
+
void urlFor(mediaKey).then((u) => {
|
|
3133
|
+
if (live) setUrl(u);
|
|
3134
|
+
});
|
|
3135
|
+
return () => {
|
|
3136
|
+
live = false;
|
|
3137
|
+
};
|
|
3138
|
+
}, [mediaKey, urlFor]);
|
|
3139
|
+
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" } }) });
|
|
3140
|
+
}
|
|
3141
|
+
function PhotosField({ photos, media, urlFor, strings, busy, onChange, onFiles, onDescription }) {
|
|
3142
|
+
const input = useRef5(null);
|
|
3143
|
+
const replacing = useRef5(void 0);
|
|
3144
|
+
const [dropping, setDropping] = useState5(false);
|
|
3145
|
+
const pick = (replace) => {
|
|
3146
|
+
replacing.current = replace;
|
|
3147
|
+
input.current?.click();
|
|
3148
|
+
};
|
|
3149
|
+
const set = (i, patch, field) => {
|
|
3150
|
+
const next = photos.map((p, j) => {
|
|
3151
|
+
if (j !== i) return p;
|
|
3152
|
+
const changed = { ...p, ...patch };
|
|
3153
|
+
for (const [k, v] of Object.entries(patch)) if (!v) delete changed[k];
|
|
3154
|
+
return changed;
|
|
3155
|
+
});
|
|
3156
|
+
onChange(next, `photo:${i}:${field}`);
|
|
3157
|
+
};
|
|
3158
|
+
const move = (from, to) => {
|
|
3159
|
+
const next = [...photos];
|
|
3160
|
+
const [p] = next.splice(from, 1);
|
|
3161
|
+
next.splice(to, 0, p);
|
|
3162
|
+
onChange(next, "photos");
|
|
3163
|
+
};
|
|
3164
|
+
const onDrop = (e) => {
|
|
3165
|
+
e.preventDefault();
|
|
3166
|
+
setDropping(false);
|
|
3167
|
+
const files = imagesIn(e.dataTransfer.files);
|
|
3168
|
+
if (files.length) onFiles(files);
|
|
3169
|
+
};
|
|
3170
|
+
return /* @__PURE__ */ jsxs5(
|
|
3171
|
+
Stack2,
|
|
3172
|
+
{
|
|
3173
|
+
gap: "sm",
|
|
3174
|
+
onDragOver: (e) => {
|
|
3175
|
+
if ([...e.dataTransfer.types].includes("Files")) {
|
|
3176
|
+
e.preventDefault();
|
|
3177
|
+
setDropping(true);
|
|
3178
|
+
}
|
|
3179
|
+
},
|
|
3180
|
+
onDragLeave: (e) => {
|
|
3181
|
+
if (e.currentTarget === e.target) setDropping(false);
|
|
3182
|
+
},
|
|
3183
|
+
onDrop,
|
|
3184
|
+
style: {
|
|
3185
|
+
borderRadius: 8,
|
|
3186
|
+
padding: dropping ? 8 : 0,
|
|
3187
|
+
margin: dropping ? -8 : 0,
|
|
3188
|
+
outline: dropping ? "2px dashed var(--mantine-primary-color-filled)" : void 0
|
|
3189
|
+
},
|
|
3190
|
+
children: [
|
|
3191
|
+
/* @__PURE__ */ jsxs5(Group2, { justify: "space-between", children: [
|
|
3192
|
+
/* @__PURE__ */ jsx6(Text2, { size: "sm", fw: 500, children: strings.photos }),
|
|
3193
|
+
/* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
|
|
3194
|
+
busy && /* @__PURE__ */ jsx6(Loader, { size: "xs" }),
|
|
3195
|
+
/* @__PURE__ */ jsxs5(Button, { variant: "default", size: "xs", onClick: () => pick(), disabled: busy, children: [
|
|
3196
|
+
"+ ",
|
|
3197
|
+
strings.addPhoto
|
|
3198
|
+
] })
|
|
3199
|
+
] })
|
|
3200
|
+
] }),
|
|
3201
|
+
/* @__PURE__ */ jsx6(
|
|
3202
|
+
"input",
|
|
3203
|
+
{
|
|
3204
|
+
ref: input,
|
|
3205
|
+
type: "file",
|
|
3206
|
+
accept: "image/*",
|
|
3207
|
+
multiple: true,
|
|
3208
|
+
hidden: true,
|
|
3209
|
+
onChange: (e) => {
|
|
3210
|
+
const files = imagesIn(e.currentTarget.files);
|
|
3211
|
+
e.currentTarget.value = "";
|
|
3212
|
+
if (files.length) onFiles(replacing.current === void 0 ? files : files.slice(0, 1), replacing.current);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
),
|
|
3216
|
+
photos.length === 0 && /* @__PURE__ */ jsx6(Text2, { size: "xs", c: "dimmed", children: strings.photosHint }),
|
|
3217
|
+
photos.map((photo, i) => {
|
|
3218
|
+
const item = Object.prototype.hasOwnProperty.call(media, photo.ref) ? media[photo.ref] : void 0;
|
|
3219
|
+
return /* @__PURE__ */ jsxs5(Group2, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
|
|
3220
|
+
/* @__PURE__ */ jsxs5(Stack2, { gap: 4, align: "center", children: [
|
|
3221
|
+
/* @__PURE__ */ jsx6(Preview, { mediaKey: photo.ref, urlFor }),
|
|
3222
|
+
/* @__PURE__ */ jsxs5(Group2, { gap: 4, children: [
|
|
3223
|
+
/* @__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" }) }),
|
|
3224
|
+
/* @__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" }) })
|
|
3225
|
+
] })
|
|
3226
|
+
] }),
|
|
3227
|
+
/* @__PURE__ */ jsxs5(Stack2, { gap: 6, style: { flex: 1, minWidth: 0 }, children: [
|
|
3228
|
+
/* @__PURE__ */ jsx6(TextInput2, { size: "xs", label: strings.caption, value: photo.caption ?? "", onChange: (e) => set(i, { caption: e.currentTarget.value }, "caption") }),
|
|
3229
|
+
/* @__PURE__ */ jsx6(TextInput2, { size: "xs", label: strings.credit, placeholder: strings.creditHint, value: photo.credit ?? "", onChange: (e) => set(i, { credit: e.currentTarget.value }, "credit") }),
|
|
3230
|
+
item && /* @__PURE__ */ jsx6(
|
|
3231
|
+
TextInput2,
|
|
3232
|
+
{
|
|
3233
|
+
size: "xs",
|
|
3234
|
+
label: strings.description,
|
|
3235
|
+
description: strings.descriptionHint,
|
|
3236
|
+
value: item.alt ?? "",
|
|
3237
|
+
onChange: (e) => onDescription(photo.ref, e.currentTarget.value)
|
|
3238
|
+
}
|
|
3239
|
+
),
|
|
3240
|
+
/* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
|
|
3241
|
+
/* @__PURE__ */ jsx6(Button, { variant: "subtle", size: "compact-xs", onClick: () => pick(i), disabled: busy, children: strings.replacePhoto }),
|
|
3242
|
+
/* @__PURE__ */ jsx6(Button, { variant: "subtle", size: "compact-xs", color: "red", onClick: () => onChange(photos.filter((_, j) => j !== i), "photos"), children: strings.removePhoto })
|
|
3243
|
+
] })
|
|
3244
|
+
] })
|
|
3245
|
+
] }, i);
|
|
3246
|
+
})
|
|
3247
|
+
]
|
|
3248
|
+
}
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
2982
3252
|
// src/editor/SlideList.tsx
|
|
2983
|
-
import { useState as
|
|
2984
|
-
import { Stack as
|
|
2985
|
-
import { jsx as
|
|
3253
|
+
import { useState as useState6 } from "react";
|
|
3254
|
+
import { Stack as Stack3, UnstyledButton } from "@mantine/core";
|
|
3255
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2986
3256
|
function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
|
|
2987
|
-
const [dragging, setDragging] =
|
|
2988
|
-
const [over, setOver] =
|
|
3257
|
+
const [dragging, setDragging] = useState6(null);
|
|
3258
|
+
const [over, setOver] = useState6(null);
|
|
2989
3259
|
const end = () => {
|
|
2990
3260
|
setDragging(null);
|
|
2991
3261
|
setOver(null);
|
|
2992
3262
|
};
|
|
2993
|
-
return /* @__PURE__ */
|
|
3263
|
+
return /* @__PURE__ */ jsx7(Stack3, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
|
|
2994
3264
|
const isSelected = i === selected;
|
|
2995
3265
|
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__ */
|
|
3266
|
+
return /* @__PURE__ */ jsx7(
|
|
2997
3267
|
UnstyledButton,
|
|
2998
3268
|
{
|
|
2999
3269
|
role: "option",
|
|
@@ -3033,9 +3303,9 @@ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, labe
|
|
|
3033
3303
|
}
|
|
3034
3304
|
|
|
3035
3305
|
// src/editor/StoryTextField.tsx
|
|
3036
|
-
import { useRef as
|
|
3037
|
-
import { Button, Group as
|
|
3038
|
-
import { jsx as
|
|
3306
|
+
import { useRef as useRef6 } from "react";
|
|
3307
|
+
import { Button as Button2, Group as Group3, Stack as Stack4, Text as Text3, Textarea } from "@mantine/core";
|
|
3308
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3039
3309
|
function applyMarker(el, marker, value, onChange) {
|
|
3040
3310
|
const start = el.selectionStart ?? value.length;
|
|
3041
3311
|
const end = el.selectionEnd ?? value.length;
|
|
@@ -3053,12 +3323,12 @@ function applyMarker(el, marker, value, onChange) {
|
|
|
3053
3323
|
onChange(value.slice(0, start) + replacement + value.slice(end));
|
|
3054
3324
|
}
|
|
3055
3325
|
function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
|
|
3056
|
-
const ref =
|
|
3057
|
-
return /* @__PURE__ */
|
|
3058
|
-
/* @__PURE__ */
|
|
3059
|
-
/* @__PURE__ */
|
|
3060
|
-
/* @__PURE__ */
|
|
3061
|
-
|
|
3326
|
+
const ref = useRef6(null);
|
|
3327
|
+
return /* @__PURE__ */ jsxs6(Stack4, { gap: 4, children: [
|
|
3328
|
+
/* @__PURE__ */ jsxs6(Group3, { gap: "xs", children: [
|
|
3329
|
+
/* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: label }),
|
|
3330
|
+
/* @__PURE__ */ jsx8(
|
|
3331
|
+
Button2,
|
|
3062
3332
|
{
|
|
3063
3333
|
variant: "default",
|
|
3064
3334
|
size: "compact-xs",
|
|
@@ -3069,8 +3339,8 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3069
3339
|
children: "B"
|
|
3070
3340
|
}
|
|
3071
3341
|
),
|
|
3072
|
-
/* @__PURE__ */
|
|
3073
|
-
|
|
3342
|
+
/* @__PURE__ */ jsx8(
|
|
3343
|
+
Button2,
|
|
3074
3344
|
{
|
|
3075
3345
|
variant: "default",
|
|
3076
3346
|
size: "compact-xs",
|
|
@@ -3082,7 +3352,7 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3082
3352
|
}
|
|
3083
3353
|
)
|
|
3084
3354
|
] }),
|
|
3085
|
-
/* @__PURE__ */
|
|
3355
|
+
/* @__PURE__ */ jsx8(
|
|
3086
3356
|
Textarea,
|
|
3087
3357
|
{
|
|
3088
3358
|
ref,
|
|
@@ -3130,6 +3400,18 @@ var EDITOR_ENGLISH = {
|
|
|
3130
3400
|
noPreciseDate: "None: this slide stays off the timeline.",
|
|
3131
3401
|
place: "Place",
|
|
3132
3402
|
photos: "Photos",
|
|
3403
|
+
photosHint: "Add, drop or paste a photo. It's resized for the screen, and hidden details like camera and location are left out.",
|
|
3404
|
+
addPhoto: "Add photo",
|
|
3405
|
+
replacePhoto: "Replace",
|
|
3406
|
+
movePhotoUp: "Move photo up",
|
|
3407
|
+
movePhotoDown: "Move photo down",
|
|
3408
|
+
removePhoto: "Remove",
|
|
3409
|
+
caption: "Caption",
|
|
3410
|
+
credit: "Credit",
|
|
3411
|
+
creditHint: "Who took it, or where it's from",
|
|
3412
|
+
description: "Description",
|
|
3413
|
+
descriptionHint: "What the photo shows, for anyone who can't see it.",
|
|
3414
|
+
photoFailed: (name) => `Couldn't use ${name}: this browser can't read it as an image.`,
|
|
3133
3415
|
undo: "Undo",
|
|
3134
3416
|
redo: "Redo",
|
|
3135
3417
|
present: "Present",
|
|
@@ -3139,18 +3421,22 @@ var EDITOR_ENGLISH = {
|
|
|
3139
3421
|
};
|
|
3140
3422
|
|
|
3141
3423
|
// src/editor/StoryEditor.tsx
|
|
3142
|
-
import { jsx as
|
|
3424
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3143
3425
|
var BAR_HEIGHT2 = 56;
|
|
3144
3426
|
var STORY = -1;
|
|
3145
3427
|
function issueSlide(issue) {
|
|
3146
3428
|
const match = /^\/slides\/(\d+)/.exec(issue.path);
|
|
3147
3429
|
return match ? Number(match[1]) : STORY;
|
|
3148
3430
|
}
|
|
3431
|
+
async function prepareImage(file) {
|
|
3432
|
+
const image = await optimizeImage(file);
|
|
3433
|
+
return { data: new Uint8Array(await image.blob.arrayBuffer()), mime: image.mime, ext: image.ext, width: image.width, height: image.height };
|
|
3434
|
+
}
|
|
3149
3435
|
function Thumb({ doc, slide, resolveMedia }) {
|
|
3150
3436
|
const ref = slide.media?.[0]?.ref;
|
|
3151
3437
|
const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
|
|
3152
|
-
const [url, setUrl] =
|
|
3153
|
-
|
|
3438
|
+
const [url, setUrl] = useState7(null);
|
|
3439
|
+
useEffect6(() => {
|
|
3154
3440
|
let live = true;
|
|
3155
3441
|
setUrl(null);
|
|
3156
3442
|
if (ref && item) void resolveMedia(ref, item).then((u) => {
|
|
@@ -3160,21 +3446,21 @@ function Thumb({ doc, slide, resolveMedia }) {
|
|
|
3160
3446
|
live = false;
|
|
3161
3447
|
};
|
|
3162
3448
|
}, [ref, item, resolveMedia]);
|
|
3163
|
-
return /* @__PURE__ */
|
|
3164
|
-
|
|
3449
|
+
return /* @__PURE__ */ jsx9(
|
|
3450
|
+
Box2,
|
|
3165
3451
|
{
|
|
3166
3452
|
w: 40,
|
|
3167
3453
|
h: 40,
|
|
3168
3454
|
style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
|
|
3169
|
-
children: url && /* @__PURE__ */
|
|
3455
|
+
children: url && /* @__PURE__ */ jsx9("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
|
|
3170
3456
|
}
|
|
3171
3457
|
);
|
|
3172
3458
|
}
|
|
3173
3459
|
function StoryEditor({
|
|
3174
3460
|
initialStory,
|
|
3175
|
-
|
|
3176
|
-
resolveMedia,
|
|
3461
|
+
initialFiles,
|
|
3177
3462
|
onChange,
|
|
3463
|
+
prepareImage: prepare = prepareImage,
|
|
3178
3464
|
toolbar,
|
|
3179
3465
|
brand,
|
|
3180
3466
|
dark = false,
|
|
@@ -3183,25 +3469,97 @@ function StoryEditor({
|
|
|
3183
3469
|
parseDateText = suggestEdtf
|
|
3184
3470
|
}) {
|
|
3185
3471
|
const strings = useMemo2(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
|
|
3186
|
-
const [history, setHistory] =
|
|
3187
|
-
const [selected, setSelected] =
|
|
3188
|
-
const [presenting, setPresenting] =
|
|
3189
|
-
const [problemsOpen, setProblemsOpen] =
|
|
3472
|
+
const [history, setHistory] = useState7(() => startHistory(initialStory));
|
|
3473
|
+
const [selected, setSelected] = useState7(0);
|
|
3474
|
+
const [presenting, setPresenting] = useState7(false);
|
|
3475
|
+
const [problemsOpen, setProblemsOpen] = useState7(false);
|
|
3190
3476
|
const wide = useMediaQuery("(min-width: 1000px)", true);
|
|
3191
3477
|
const doc = history.present;
|
|
3478
|
+
const [files, setFiles] = useState7(() => new Map(initialFiles ?? []));
|
|
3479
|
+
const [busy, setBusy] = useState7(false);
|
|
3480
|
+
const [photoError, setPhotoError] = useState7(null);
|
|
3481
|
+
const docRef = useRef7(doc);
|
|
3482
|
+
docRef.current = doc;
|
|
3483
|
+
const filesRef = useRef7(files);
|
|
3484
|
+
filesRef.current = files;
|
|
3485
|
+
const urls = useRef7(/* @__PURE__ */ new Map());
|
|
3486
|
+
useEffect6(() => () => {
|
|
3487
|
+
for (const url of urls.current.values()) URL.revokeObjectURL(url);
|
|
3488
|
+
}, []);
|
|
3489
|
+
const resolveMedia = useCallback(async (_key, item) => {
|
|
3490
|
+
const cached = urls.current.get(item.src);
|
|
3491
|
+
if (cached) return cached;
|
|
3492
|
+
const data = filesRef.current.get(item.src);
|
|
3493
|
+
if (!data) return null;
|
|
3494
|
+
const url = URL.createObjectURL(new Blob([data], { type: item.mime }));
|
|
3495
|
+
urls.current.set(item.src, url);
|
|
3496
|
+
return url;
|
|
3497
|
+
}, []);
|
|
3498
|
+
const urlFor = useCallback((key) => {
|
|
3499
|
+
const item = Object.prototype.hasOwnProperty.call(docRef.current.media, key) ? docRef.current.media[key] : void 0;
|
|
3500
|
+
return item ? resolveMedia(key, item) : Promise.resolve(null);
|
|
3501
|
+
}, [resolveMedia]);
|
|
3192
3502
|
const apply = useCallback((next, key) => {
|
|
3193
3503
|
setHistory((h) => record(h, next, key));
|
|
3194
3504
|
}, []);
|
|
3195
|
-
|
|
3196
|
-
onChange?.(doc);
|
|
3197
|
-
}, [doc, onChange]);
|
|
3505
|
+
useEffect6(() => {
|
|
3506
|
+
onChange?.(doc, files);
|
|
3507
|
+
}, [doc, files, onChange]);
|
|
3198
3508
|
const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
|
|
3199
3509
|
const slide = index === STORY ? void 0 : doc.slides[index];
|
|
3200
3510
|
const setSlide = (patch, key) => {
|
|
3201
3511
|
if (index !== STORY) apply(updateSlide(history.present, index, patch), `${slide.id}:${key}`);
|
|
3202
3512
|
};
|
|
3203
3513
|
const issues = useMemo2(() => validate2(doc, files).errors, [doc, files]);
|
|
3204
|
-
|
|
3514
|
+
const addPhotos = useCallback(async (chosen, slideId2, replace) => {
|
|
3515
|
+
setBusy(true);
|
|
3516
|
+
setPhotoError(null);
|
|
3517
|
+
const chosenImages = [];
|
|
3518
|
+
for (const file of chosen) {
|
|
3519
|
+
try {
|
|
3520
|
+
const known = findMedia(docRef.current, filesRef.current, new Uint8Array(await file.arrayBuffer()));
|
|
3521
|
+
chosenImages.push(known ?? await prepare(file));
|
|
3522
|
+
} catch {
|
|
3523
|
+
setPhotoError(strings.photoFailed(file.name || "the image"));
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
setBusy(false);
|
|
3527
|
+
if (!chosenImages.length) return;
|
|
3528
|
+
let next = docRef.current;
|
|
3529
|
+
let nextFiles = filesRef.current;
|
|
3530
|
+
const keys = [];
|
|
3531
|
+
for (const image of chosenImages) {
|
|
3532
|
+
if (typeof image === "string") {
|
|
3533
|
+
keys.push(image);
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
const added = addImage(next, nextFiles, image);
|
|
3537
|
+
next = added.doc;
|
|
3538
|
+
nextFiles = added.files;
|
|
3539
|
+
keys.push(added.key);
|
|
3540
|
+
}
|
|
3541
|
+
const at = next.slides.findIndex((s) => s.id === slideId2);
|
|
3542
|
+
if (at < 0) return;
|
|
3543
|
+
const photos = [...next.slides[at].media ?? []];
|
|
3544
|
+
if (replace !== void 0 && photos[replace]) photos[replace] = { ...photos[replace], ref: keys[0] };
|
|
3545
|
+
else photos.push(...keys.map((ref) => ({ ref })));
|
|
3546
|
+
setFiles(nextFiles);
|
|
3547
|
+
apply(setSlidePhotos(next, at, photos));
|
|
3548
|
+
}, [apply, prepare, strings]);
|
|
3549
|
+
const slideId = slide?.id;
|
|
3550
|
+
useEffect6(() => {
|
|
3551
|
+
if (!slideId) return;
|
|
3552
|
+
const onPaste = (e) => {
|
|
3553
|
+
if (e.target?.closest("input, textarea, [contenteditable=true]")) return;
|
|
3554
|
+
const images = [...e.clipboardData?.files ?? []].filter((f) => f.type.startsWith("image/"));
|
|
3555
|
+
if (!images.length) return;
|
|
3556
|
+
e.preventDefault();
|
|
3557
|
+
void addPhotos(images, slideId);
|
|
3558
|
+
};
|
|
3559
|
+
window.addEventListener("paste", onPaste);
|
|
3560
|
+
return () => window.removeEventListener("paste", onPaste);
|
|
3561
|
+
}, [slideId, addPhotos]);
|
|
3562
|
+
useEffect6(() => {
|
|
3205
3563
|
const onKey = (e) => {
|
|
3206
3564
|
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
|
|
3207
3565
|
const target = e.target;
|
|
@@ -3218,9 +3576,9 @@ function StoryEditor({
|
|
|
3218
3576
|
window.addEventListener("keydown", onKey);
|
|
3219
3577
|
return () => window.removeEventListener("keydown", onKey);
|
|
3220
3578
|
}, []);
|
|
3221
|
-
const slideActions = slide && /* @__PURE__ */
|
|
3222
|
-
/* @__PURE__ */
|
|
3223
|
-
|
|
3579
|
+
const slideActions = slide && /* @__PURE__ */ jsxs7(Group4, { gap: 6, children: [
|
|
3580
|
+
/* @__PURE__ */ jsx9(Tooltip2, { label: strings.moveUp, children: /* @__PURE__ */ jsx9(
|
|
3581
|
+
ActionIcon2,
|
|
3224
3582
|
{
|
|
3225
3583
|
variant: "default",
|
|
3226
3584
|
"aria-label": strings.moveUp,
|
|
@@ -3232,8 +3590,8 @@ function StoryEditor({
|
|
|
3232
3590
|
children: "\u2191"
|
|
3233
3591
|
}
|
|
3234
3592
|
) }),
|
|
3235
|
-
/* @__PURE__ */
|
|
3236
|
-
|
|
3593
|
+
/* @__PURE__ */ jsx9(Tooltip2, { label: strings.moveDown, children: /* @__PURE__ */ jsx9(
|
|
3594
|
+
ActionIcon2,
|
|
3237
3595
|
{
|
|
3238
3596
|
variant: "default",
|
|
3239
3597
|
"aria-label": strings.moveDown,
|
|
@@ -3245,13 +3603,13 @@ function StoryEditor({
|
|
|
3245
3603
|
children: "\u2193"
|
|
3246
3604
|
}
|
|
3247
3605
|
) }),
|
|
3248
|
-
/* @__PURE__ */
|
|
3606
|
+
/* @__PURE__ */ jsx9(Button3, { variant: "default", size: "xs", onClick: () => {
|
|
3249
3607
|
const r = duplicateSlide(doc, index);
|
|
3250
3608
|
apply(r.doc);
|
|
3251
3609
|
setSelected(r.index);
|
|
3252
3610
|
}, children: strings.duplicateSlide }),
|
|
3253
|
-
/* @__PURE__ */
|
|
3254
|
-
|
|
3611
|
+
/* @__PURE__ */ jsx9(
|
|
3612
|
+
Button3,
|
|
3255
3613
|
{
|
|
3256
3614
|
variant: "default",
|
|
3257
3615
|
size: "xs",
|
|
@@ -3266,10 +3624,10 @@ function StoryEditor({
|
|
|
3266
3624
|
}
|
|
3267
3625
|
)
|
|
3268
3626
|
] });
|
|
3269
|
-
const storyForm = /* @__PURE__ */
|
|
3270
|
-
/* @__PURE__ */
|
|
3271
|
-
/* @__PURE__ */
|
|
3272
|
-
|
|
3627
|
+
const storyForm = /* @__PURE__ */ jsxs7(Stack5, { gap: "md", children: [
|
|
3628
|
+
/* @__PURE__ */ jsx9(Text4, { fw: 600, size: "lg", children: strings.story }),
|
|
3629
|
+
/* @__PURE__ */ jsx9(
|
|
3630
|
+
TextInput3,
|
|
3273
3631
|
{
|
|
3274
3632
|
label: strings.title,
|
|
3275
3633
|
required: true,
|
|
@@ -3277,8 +3635,8 @@ function StoryEditor({
|
|
|
3277
3635
|
onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
|
|
3278
3636
|
}
|
|
3279
3637
|
),
|
|
3280
|
-
/* @__PURE__ */
|
|
3281
|
-
|
|
3638
|
+
/* @__PURE__ */ jsx9(
|
|
3639
|
+
TextInput3,
|
|
3282
3640
|
{
|
|
3283
3641
|
label: strings.language,
|
|
3284
3642
|
description: strings.languageHint,
|
|
@@ -3287,7 +3645,7 @@ function StoryEditor({
|
|
|
3287
3645
|
onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
|
|
3288
3646
|
}
|
|
3289
3647
|
),
|
|
3290
|
-
/* @__PURE__ */
|
|
3648
|
+
/* @__PURE__ */ jsx9(
|
|
3291
3649
|
Select,
|
|
3292
3650
|
{
|
|
3293
3651
|
label: strings.map,
|
|
@@ -3306,20 +3664,20 @@ function StoryEditor({
|
|
|
3306
3664
|
}
|
|
3307
3665
|
)
|
|
3308
3666
|
] });
|
|
3309
|
-
const slideForm = slide && /* @__PURE__ */
|
|
3310
|
-
/* @__PURE__ */
|
|
3311
|
-
/* @__PURE__ */
|
|
3667
|
+
const slideForm = slide && /* @__PURE__ */ jsxs7(Stack5, { gap: "md", children: [
|
|
3668
|
+
/* @__PURE__ */ jsxs7(Group4, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
|
|
3669
|
+
/* @__PURE__ */ jsx9(Text4, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
|
|
3312
3670
|
slideActions
|
|
3313
3671
|
] }),
|
|
3314
|
-
/* @__PURE__ */
|
|
3315
|
-
|
|
3672
|
+
/* @__PURE__ */ jsx9(
|
|
3673
|
+
TextInput3,
|
|
3316
3674
|
{
|
|
3317
3675
|
label: strings.slideTitle,
|
|
3318
3676
|
value: slide.title ?? "",
|
|
3319
3677
|
onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
|
|
3320
3678
|
}
|
|
3321
3679
|
),
|
|
3322
|
-
/* @__PURE__ */
|
|
3680
|
+
/* @__PURE__ */ jsx9(
|
|
3323
3681
|
DateField,
|
|
3324
3682
|
{
|
|
3325
3683
|
value: slide.date,
|
|
@@ -3329,7 +3687,7 @@ function StoryEditor({
|
|
|
3329
3687
|
onChange: (date) => setSlide({ date }, "date")
|
|
3330
3688
|
}
|
|
3331
3689
|
),
|
|
3332
|
-
/* @__PURE__ */
|
|
3690
|
+
/* @__PURE__ */ jsx9(
|
|
3333
3691
|
StoryTextField,
|
|
3334
3692
|
{
|
|
3335
3693
|
label: strings.text,
|
|
@@ -3340,11 +3698,11 @@ function StoryEditor({
|
|
|
3340
3698
|
onChange: (text) => setSlide({ text: text || void 0 }, "text")
|
|
3341
3699
|
}
|
|
3342
3700
|
),
|
|
3343
|
-
slide.place && /* @__PURE__ */
|
|
3344
|
-
/* @__PURE__ */
|
|
3345
|
-
/* @__PURE__ */
|
|
3701
|
+
slide.place && /* @__PURE__ */ jsxs7(Stack5, { gap: 2, children: [
|
|
3702
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, children: strings.place }),
|
|
3703
|
+
/* @__PURE__ */ jsxs7(Text4, { size: "sm", children: [
|
|
3346
3704
|
slide.place.name ?? "",
|
|
3347
|
-
slide.place.lat !== void 0 && slide.place.lon !== void 0 && /* @__PURE__ */
|
|
3705
|
+
slide.place.lat !== void 0 && slide.place.lon !== void 0 && /* @__PURE__ */ jsxs7(Text4, { span: true, c: "dimmed", size: "sm", children: [
|
|
3348
3706
|
" ",
|
|
3349
3707
|
slide.place.lat.toFixed(4),
|
|
3350
3708
|
", ",
|
|
@@ -3352,28 +3710,35 @@ function StoryEditor({
|
|
|
3352
3710
|
] })
|
|
3353
3711
|
] })
|
|
3354
3712
|
] }),
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3713
|
+
/* @__PURE__ */ jsx9(
|
|
3714
|
+
PhotosField,
|
|
3715
|
+
{
|
|
3716
|
+
photos: slide.media ?? [],
|
|
3717
|
+
media: doc.media,
|
|
3718
|
+
urlFor,
|
|
3719
|
+
strings,
|
|
3720
|
+
busy,
|
|
3721
|
+
onChange: (photos, key) => apply(setSlidePhotos(doc, index, photos), `${slide.id}:${key}`),
|
|
3722
|
+
onFiles: (chosen, replace) => void addPhotos(chosen, slide.id, replace),
|
|
3723
|
+
onDescription: (key, alt) => apply(updateMedia(doc, key, { alt: alt || void 0 }), `media:${key}:alt`)
|
|
3724
|
+
}
|
|
3725
|
+
),
|
|
3726
|
+
photoError && /* @__PURE__ */ jsx9(Text4, { size: "xs", c: "red", role: "alert", children: photoError })
|
|
3362
3727
|
] }, slide.id);
|
|
3363
|
-
const list = /* @__PURE__ */
|
|
3364
|
-
/* @__PURE__ */
|
|
3728
|
+
const list = /* @__PURE__ */ jsxs7(Stack5, { gap: "xs", children: [
|
|
3729
|
+
/* @__PURE__ */ jsxs7(
|
|
3365
3730
|
UnstyledButton2,
|
|
3366
3731
|
{
|
|
3367
3732
|
onClick: () => setSelected(STORY),
|
|
3368
3733
|
style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
|
|
3369
3734
|
children: [
|
|
3370
|
-
/* @__PURE__ */
|
|
3371
|
-
/* @__PURE__ */
|
|
3735
|
+
/* @__PURE__ */ jsx9(Text4, { fw: 600, size: "sm", children: strings.story }),
|
|
3736
|
+
/* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", children: strings.storySettings })
|
|
3372
3737
|
]
|
|
3373
3738
|
}
|
|
3374
3739
|
),
|
|
3375
|
-
/* @__PURE__ */
|
|
3376
|
-
/* @__PURE__ */
|
|
3740
|
+
/* @__PURE__ */ jsx9(Divider, { label: strings.slides, labelPosition: "left" }),
|
|
3741
|
+
/* @__PURE__ */ jsx9(
|
|
3377
3742
|
SlideList,
|
|
3378
3743
|
{
|
|
3379
3744
|
label: strings.slides,
|
|
@@ -3385,17 +3750,17 @@ function StoryEditor({
|
|
|
3385
3750
|
setSelected(to);
|
|
3386
3751
|
},
|
|
3387
3752
|
getKey: (s) => s.id,
|
|
3388
|
-
renderItem: (s, i) => /* @__PURE__ */
|
|
3389
|
-
/* @__PURE__ */
|
|
3390
|
-
/* @__PURE__ */
|
|
3391
|
-
/* @__PURE__ */
|
|
3392
|
-
/* @__PURE__ */
|
|
3753
|
+
renderItem: (s, i) => /* @__PURE__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", children: [
|
|
3754
|
+
/* @__PURE__ */ jsx9(Thumb, { doc, slide: s, resolveMedia }),
|
|
3755
|
+
/* @__PURE__ */ jsxs7(Box2, { style: { minWidth: 0 }, children: [
|
|
3756
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
|
|
3757
|
+
/* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
|
|
3393
3758
|
] })
|
|
3394
3759
|
] })
|
|
3395
3760
|
}
|
|
3396
3761
|
),
|
|
3397
|
-
/* @__PURE__ */
|
|
3398
|
-
|
|
3762
|
+
/* @__PURE__ */ jsxs7(
|
|
3763
|
+
Button3,
|
|
3399
3764
|
{
|
|
3400
3765
|
variant: "light",
|
|
3401
3766
|
size: "xs",
|
|
@@ -3412,7 +3777,7 @@ function StoryEditor({
|
|
|
3412
3777
|
}
|
|
3413
3778
|
)
|
|
3414
3779
|
] });
|
|
3415
|
-
const preview = /* @__PURE__ */
|
|
3780
|
+
const preview = /* @__PURE__ */ jsx9(Box2, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx9(
|
|
3416
3781
|
StoryPlayer,
|
|
3417
3782
|
{
|
|
3418
3783
|
story: doc,
|
|
@@ -3426,8 +3791,8 @@ function StoryEditor({
|
|
|
3426
3791
|
}
|
|
3427
3792
|
}
|
|
3428
3793
|
) });
|
|
3429
|
-
const column = (children, width) => /* @__PURE__ */
|
|
3430
|
-
|
|
3794
|
+
const column = (children, width) => /* @__PURE__ */ jsx9(
|
|
3795
|
+
Box2,
|
|
3431
3796
|
{
|
|
3432
3797
|
style: {
|
|
3433
3798
|
width,
|
|
@@ -3440,9 +3805,9 @@ function StoryEditor({
|
|
|
3440
3805
|
children
|
|
3441
3806
|
}
|
|
3442
3807
|
);
|
|
3443
|
-
return /* @__PURE__ */
|
|
3444
|
-
/* @__PURE__ */
|
|
3445
|
-
|
|
3808
|
+
return /* @__PURE__ */ jsxs7(Box2, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
|
|
3809
|
+
/* @__PURE__ */ jsxs7(
|
|
3810
|
+
Group4,
|
|
3446
3811
|
{
|
|
3447
3812
|
h: BAR_HEIGHT2,
|
|
3448
3813
|
px: "md",
|
|
@@ -3451,21 +3816,21 @@ function StoryEditor({
|
|
|
3451
3816
|
justify: "space-between",
|
|
3452
3817
|
style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
|
|
3453
3818
|
children: [
|
|
3454
|
-
/* @__PURE__ */
|
|
3819
|
+
/* @__PURE__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
3455
3820
|
brand,
|
|
3456
|
-
/* @__PURE__ */
|
|
3821
|
+
/* @__PURE__ */ jsx9(Text4, { fw: 600, truncate: true, dir: "auto", children: doc.title })
|
|
3457
3822
|
] }),
|
|
3458
|
-
/* @__PURE__ */
|
|
3459
|
-
issues.length > 0 && /* @__PURE__ */
|
|
3460
|
-
/* @__PURE__ */
|
|
3461
|
-
/* @__PURE__ */
|
|
3823
|
+
/* @__PURE__ */ jsxs7(Group4, { gap: "xs", wrap: "nowrap", children: [
|
|
3824
|
+
issues.length > 0 && /* @__PURE__ */ jsxs7(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
|
|
3825
|
+
/* @__PURE__ */ jsx9(Popover.Target, { children: /* @__PURE__ */ jsx9(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
|
|
3826
|
+
/* @__PURE__ */ jsx9(Popover.Dropdown, { children: /* @__PURE__ */ jsx9(Stack5, { gap: 6, children: issues.map((issue, i) => {
|
|
3462
3827
|
const at = issueSlide(issue);
|
|
3463
|
-
return /* @__PURE__ */
|
|
3828
|
+
return /* @__PURE__ */ jsxs7(UnstyledButton2, { onClick: () => {
|
|
3464
3829
|
setSelected(at);
|
|
3465
3830
|
setProblemsOpen(false);
|
|
3466
3831
|
}, children: [
|
|
3467
|
-
/* @__PURE__ */
|
|
3468
|
-
/* @__PURE__ */
|
|
3832
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
|
|
3833
|
+
/* @__PURE__ */ jsxs7(Text4, { size: "xs", c: "dimmed", children: [
|
|
3469
3834
|
issue.path,
|
|
3470
3835
|
" ",
|
|
3471
3836
|
issue.message
|
|
@@ -3473,16 +3838,16 @@ function StoryEditor({
|
|
|
3473
3838
|
] }, i);
|
|
3474
3839
|
}) }) })
|
|
3475
3840
|
] }),
|
|
3476
|
-
/* @__PURE__ */
|
|
3477
|
-
/* @__PURE__ */
|
|
3478
|
-
/* @__PURE__ */
|
|
3841
|
+
/* @__PURE__ */ jsx9(Tooltip2, { label: strings.undo, children: /* @__PURE__ */ jsx9(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
|
|
3842
|
+
/* @__PURE__ */ jsx9(Tooltip2, { label: strings.redo, children: /* @__PURE__ */ jsx9(ActionIcon2, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
|
|
3843
|
+
/* @__PURE__ */ jsx9(Button3, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
|
|
3479
3844
|
toolbar
|
|
3480
3845
|
] })
|
|
3481
3846
|
]
|
|
3482
3847
|
}
|
|
3483
3848
|
),
|
|
3484
|
-
/* @__PURE__ */
|
|
3485
|
-
|
|
3849
|
+
/* @__PURE__ */ jsxs7(
|
|
3850
|
+
Box2,
|
|
3486
3851
|
{
|
|
3487
3852
|
style: {
|
|
3488
3853
|
flex: 1,
|
|
@@ -3498,7 +3863,7 @@ function StoryEditor({
|
|
|
3498
3863
|
]
|
|
3499
3864
|
}
|
|
3500
3865
|
),
|
|
3501
|
-
/* @__PURE__ */
|
|
3866
|
+
/* @__PURE__ */ jsx9(Modal, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx9(Box2, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx9(
|
|
3502
3867
|
StoryPlayer,
|
|
3503
3868
|
{
|
|
3504
3869
|
story: doc,
|
|
@@ -3515,23 +3880,29 @@ export {
|
|
|
3515
3880
|
COALESCE_MS,
|
|
3516
3881
|
DateField,
|
|
3517
3882
|
EDITOR_ENGLISH,
|
|
3883
|
+
PhotosField,
|
|
3518
3884
|
SlideList,
|
|
3519
3885
|
StoryEditor,
|
|
3520
3886
|
StoryTextField,
|
|
3887
|
+
addImage,
|
|
3521
3888
|
addSlide,
|
|
3522
3889
|
blankStory,
|
|
3523
3890
|
deleteSlide,
|
|
3524
3891
|
duplicateSlide,
|
|
3892
|
+
findMedia,
|
|
3525
3893
|
moveSlide,
|
|
3526
3894
|
nextSlideId,
|
|
3895
|
+
prepareImage,
|
|
3527
3896
|
pruneMedia,
|
|
3528
3897
|
record,
|
|
3529
3898
|
redo,
|
|
3899
|
+
setSlidePhotos,
|
|
3530
3900
|
slideDate,
|
|
3531
3901
|
startHistory,
|
|
3532
3902
|
storyFileName,
|
|
3533
3903
|
suggestEdtf,
|
|
3534
3904
|
undo,
|
|
3905
|
+
updateMedia,
|
|
3535
3906
|
updateSlide,
|
|
3536
3907
|
updateStory,
|
|
3537
3908
|
usedMedia
|