@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/dist/editor.js CHANGED
@@ -1,23 +1,71 @@
1
1
  // src/editor/StoryEditor.tsx
2
- import { useCallback, useEffect as useEffect5, useMemo as useMemo2, useState as useState6 } from "react";
2
+ import { useCallback, useEffect as 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 Button2,
6
+ Box as Box2,
7
+ Button as Button3,
8
8
  Divider,
9
- Group as Group3,
9
+ Group as Group4,
10
10
  Modal,
11
11
  Popover,
12
12
  Select,
13
- Stack as Stack4,
14
- Text as Text3,
15
- TextInput as TextInput2,
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 SlideContent({ slide, story, resolveMedia, colors }) {
2421
- const photo = slide?.media?.find((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
2473
+ function PhotoThumb({ mediaKey, item, label, selected, onSelect, resolveMedia, colors }) {
2474
+ const [src, setSrc] = useState3(null);
2475
+ useEffect3(() => {
2476
+ let cancelled = false;
2477
+ resolveMedia(mediaKey, item).then((url) => {
2478
+ if (!cancelled) setSrc(url);
2479
+ }, () => {
2480
+ });
2481
+ return () => {
2482
+ cancelled = true;
2483
+ };
2484
+ }, [mediaKey, item, resolveMedia]);
2485
+ return /* @__PURE__ */ jsx4(
2486
+ "button",
2487
+ {
2488
+ type: "button",
2489
+ "aria-label": label,
2490
+ title: label,
2491
+ "aria-pressed": selected,
2492
+ onClick: onSelect,
2493
+ style: {
2494
+ width: 44,
2495
+ height: 44,
2496
+ padding: 0,
2497
+ borderRadius: 4,
2498
+ overflow: "hidden",
2499
+ cursor: "pointer",
2500
+ border: `2px solid ${selected ? colors.mark : "transparent"}`,
2501
+ background: colors.border,
2502
+ opacity: selected ? 1 : 0.7
2503
+ },
2504
+ children: src && /* @__PURE__ */ jsx4("img", { src, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
2505
+ }
2506
+ );
2507
+ }
2508
+ function SlideContent({ slide, story, resolveMedia, colors, strings }) {
2509
+ const photos = (slide?.media ?? []).filter((m) => Object.prototype.hasOwnProperty.call(story.media, m.ref));
2510
+ const [shown, setShown] = useState3(0);
2511
+ const photo = photos[Math.min(shown, photos.length - 1)];
2422
2512
  const eyebrow = [slide?.date?.text, slide?.place?.name].filter(Boolean).join(" \xB7 ");
2423
2513
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
2424
2514
  photo && /* @__PURE__ */ jsx4(
@@ -2432,6 +2522,19 @@ function SlideContent({ slide, story, resolveMedia, colors }) {
2432
2522
  colors
2433
2523
  }
2434
2524
  ),
2525
+ photos.length > 1 && /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "center" }, children: photos.map((m, i) => /* @__PURE__ */ jsx4(
2526
+ PhotoThumb,
2527
+ {
2528
+ mediaKey: m.ref,
2529
+ item: story.media[m.ref],
2530
+ label: strings.photoOf(i + 1, photos.length),
2531
+ selected: photo === m,
2532
+ onSelect: () => setShown(i),
2533
+ resolveMedia,
2534
+ colors
2535
+ },
2536
+ `${i}:${m.ref}`
2537
+ )) }),
2435
2538
  /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "center", maxWidth: 640, textAlign: "center" }, children: [
2436
2539
  eyebrow && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 14, color: colors.muted }, children: eyebrow }),
2437
2540
  slide?.title && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 20, fontWeight: 700, color: colors.text }, children: slide.title }),
@@ -2609,7 +2712,7 @@ function StoryPlayer({
2609
2712
  const item = Object.prototype.hasOwnProperty.call(story.media, key) ? story.media[key] : void 0;
2610
2713
  return item ? resolveMedia(key, item) : Promise.resolve(null);
2611
2714
  }, [story, resolveMedia]);
2612
- const content = /* @__PURE__ */ jsx4(SlideContent, { slide, story, resolveMedia, colors });
2715
+ const content = /* @__PURE__ */ jsx4(SlideContent, { slide, story, resolveMedia, colors, strings }, slide?.id ?? index);
2613
2716
  const timeline = showTimeline && /* @__PURE__ */ jsx4(TimelineStrip, { points: datedSlides, currentIndex: index, onSelect: setIndex, colors });
2614
2717
  return /* @__PURE__ */ jsxs3(
2615
2718
  "div",
@@ -2864,6 +2967,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 useState5 } from "react";
2984
- import { Stack as Stack2, UnstyledButton } from "@mantine/core";
2985
- import { jsx as jsx6 } from "react/jsx-runtime";
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] = useState5(null);
2988
- const [over, setOver] = useState5(null);
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__ */ jsx6(Stack2, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
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__ */ jsx6(
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 useRef5 } from "react";
3037
- import { Button, Group as Group2, Stack as Stack3, Text as Text2, Textarea } from "@mantine/core";
3038
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
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 = useRef5(null);
3057
- return /* @__PURE__ */ jsxs5(Stack3, { gap: 4, children: [
3058
- /* @__PURE__ */ jsxs5(Group2, { gap: "xs", children: [
3059
- /* @__PURE__ */ jsx7(Text2, { size: "sm", fw: 500, children: label }),
3060
- /* @__PURE__ */ jsx7(
3061
- Button,
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__ */ jsx7(
3073
- Button,
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__ */ jsx7(
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 jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
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] = useState6(null);
3153
- useEffect5(() => {
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__ */ jsx8(
3164
- Box,
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__ */ jsx8("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
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
- files,
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] = useState6(() => startHistory(initialStory));
3187
- const [selected, setSelected] = useState6(0);
3188
- const [presenting, setPresenting] = useState6(false);
3189
- const [problemsOpen, setProblemsOpen] = useState6(false);
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
- useEffect5(() => {
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
- useEffect5(() => {
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__ */ jsxs6(Group3, { gap: 6, children: [
3222
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.moveUp, children: /* @__PURE__ */ jsx8(
3223
- ActionIcon,
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__ */ jsx8(Tooltip, { label: strings.moveDown, children: /* @__PURE__ */ jsx8(
3236
- ActionIcon,
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__ */ jsx8(Button2, { variant: "default", size: "xs", onClick: () => {
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__ */ jsx8(
3254
- Button2,
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__ */ jsxs6(Stack4, { gap: "md", children: [
3270
- /* @__PURE__ */ jsx8(Text3, { fw: 600, size: "lg", children: strings.story }),
3271
- /* @__PURE__ */ jsx8(
3272
- TextInput2,
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__ */ jsx8(
3281
- TextInput2,
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__ */ jsx8(
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__ */ jsxs6(Stack4, { gap: "md", children: [
3310
- /* @__PURE__ */ jsxs6(Group3, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
3311
- /* @__PURE__ */ jsx8(Text3, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
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__ */ jsx8(
3315
- TextInput2,
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__ */ jsx8(
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__ */ jsx8(
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__ */ jsxs6(Stack4, { gap: 2, children: [
3344
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.place }),
3345
- /* @__PURE__ */ jsxs6(Text3, { size: "sm", children: [
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__ */ jsxs6(Text3, { span: true, c: "dimmed", size: "sm", children: [
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
- slide.media && slide.media.length > 0 && /* @__PURE__ */ jsxs6(Stack4, { gap: 4, children: [
3356
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.photos }),
3357
- /* @__PURE__ */ jsx8(Group3, { gap: "sm", children: slide.media.map((m, i) => /* @__PURE__ */ jsxs6(Group3, { gap: "xs", wrap: "nowrap", children: [
3358
- /* @__PURE__ */ jsx8(Thumb, { doc, slide: { id: "", media: [m] }, resolveMedia }),
3359
- /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", maw: 200, lineClamp: 2, children: m.caption ?? "" })
3360
- ] }, i)) })
3361
- ] })
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__ */ jsxs6(Stack4, { gap: "xs", children: [
3364
- /* @__PURE__ */ jsxs6(
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__ */ jsx8(Text3, { fw: 600, size: "sm", children: strings.story }),
3371
- /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.storySettings })
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__ */ jsx8(Divider, { label: strings.slides, labelPosition: "left" }),
3376
- /* @__PURE__ */ jsx8(
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__ */ jsxs6(Group3, { gap: "sm", wrap: "nowrap", children: [
3389
- /* @__PURE__ */ jsx8(Thumb, { doc, slide: s, resolveMedia }),
3390
- /* @__PURE__ */ jsxs6(Box, { style: { minWidth: 0 }, children: [
3391
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
3392
- /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
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__ */ jsxs6(
3398
- Button2,
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__ */ jsx8(Box, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx8(
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__ */ jsx8(
3430
- Box,
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__ */ jsxs6(Box, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
3444
- /* @__PURE__ */ jsxs6(
3445
- Group3,
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__ */ jsxs6(Group3, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
3819
+ /* @__PURE__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
3455
3820
  brand,
3456
- /* @__PURE__ */ jsx8(Text3, { fw: 600, truncate: true, dir: "auto", children: doc.title })
3821
+ /* @__PURE__ */ jsx9(Text4, { fw: 600, truncate: true, dir: "auto", children: doc.title })
3457
3822
  ] }),
3458
- /* @__PURE__ */ jsxs6(Group3, { gap: "xs", wrap: "nowrap", children: [
3459
- issues.length > 0 && /* @__PURE__ */ jsxs6(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
3460
- /* @__PURE__ */ jsx8(Popover.Target, { children: /* @__PURE__ */ jsx8(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
3461
- /* @__PURE__ */ jsx8(Popover.Dropdown, { children: /* @__PURE__ */ jsx8(Stack4, { gap: 6, children: issues.map((issue, i) => {
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__ */ jsxs6(UnstyledButton2, { onClick: () => {
3828
+ return /* @__PURE__ */ jsxs7(UnstyledButton2, { onClick: () => {
3464
3829
  setSelected(at);
3465
3830
  setProblemsOpen(false);
3466
3831
  }, children: [
3467
- /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
3468
- /* @__PURE__ */ jsxs6(Text3, { size: "xs", c: "dimmed", children: [
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__ */ jsx8(Tooltip, { label: strings.undo, children: /* @__PURE__ */ jsx8(ActionIcon, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
3477
- /* @__PURE__ */ jsx8(Tooltip, { label: strings.redo, children: /* @__PURE__ */ jsx8(ActionIcon, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
3478
- /* @__PURE__ */ jsx8(Button2, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
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__ */ jsxs6(
3485
- Box,
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__ */ jsx8(Modal, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx8(Box, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx8(
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