@gramps-connect/story 0.1.3 → 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 ADDED
@@ -0,0 +1,3910 @@
1
+ // src/editor/StoryEditor.tsx
2
+ import { useCallback, useEffect as useEffect6, useMemo as useMemo2, useRef as useRef7, useState as useState7 } from "react";
3
+ import {
4
+ ActionIcon as ActionIcon2,
5
+ Badge,
6
+ Box as Box2,
7
+ Button as Button3,
8
+ Divider,
9
+ Group as Group4,
10
+ Modal,
11
+ Popover,
12
+ Select,
13
+ Stack as Stack5,
14
+ Text as Text4,
15
+ TextInput as TextInput3,
16
+ Tooltip as Tooltip2,
17
+ UnstyledButton as UnstyledButton2
18
+ } from "@mantine/core";
19
+ import { useMediaQuery } from "@mantine/hooks";
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
+
69
+ // spec/1.0/story.schema.json
70
+ var story_schema_default = {
71
+ $schema: "https://json-schema.org/draft/2020-12/schema",
72
+ $id: "https://dsblank.github.io/gramps-connect-stories/spec/1.0/story.schema.json",
73
+ title: "Gramps Connect Story, format 1.0",
74
+ description: "The story.json at the root of a .story.zip. See spec/1.0/README.md for the rules this schema can't express (every ref resolves, every src exists in the zip, unknown fields are ignored).",
75
+ type: "object",
76
+ required: ["format", "version", "title", "slides", "media"],
77
+ properties: {
78
+ $schema: { type: "string" },
79
+ format: { const: "gramps-connect-story" },
80
+ version: {
81
+ description: "MAJOR.MINOR. This schema validates any 1.x file: newer minor versions only add optional fields.",
82
+ type: "string",
83
+ pattern: "^1\\.(0|[1-9][0-9]*)$"
84
+ },
85
+ title: { type: "string", pattern: "\\S" },
86
+ language: {
87
+ description: `BCP 47 language tag of the story's text, e.g. "en", "de-CH".`,
88
+ type: "string",
89
+ pattern: "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$"
90
+ },
91
+ created: {
92
+ description: "When the file was written, as an RFC 3339 date-time.",
93
+ type: "string",
94
+ pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$"
95
+ },
96
+ generator: {
97
+ type: "object",
98
+ required: ["name"],
99
+ properties: {
100
+ name: { type: "string", pattern: "\\S" },
101
+ version: { type: "string" },
102
+ url: { type: "string" }
103
+ }
104
+ },
105
+ authors: {
106
+ type: "array",
107
+ items: {
108
+ type: "object",
109
+ required: ["name"],
110
+ properties: {
111
+ name: { type: "string", pattern: "\\S" },
112
+ url: { type: "string" }
113
+ }
114
+ }
115
+ },
116
+ license: {
117
+ description: "License of the story's content (text and media), as an SPDX identifier or free text.",
118
+ type: "string"
119
+ },
120
+ presentation: {
121
+ type: "object",
122
+ properties: {
123
+ basemap: { enum: ["standard", "historical", "auto", "none"] }
124
+ }
125
+ },
126
+ slides: {
127
+ type: "array",
128
+ minItems: 1,
129
+ items: { $ref: "#/$defs/slide" }
130
+ },
131
+ media: {
132
+ description: "Every media file in the story, keyed by an id that slides and overlays refer to.",
133
+ type: "object",
134
+ propertyNames: { $ref: "#/$defs/id" },
135
+ additionalProperties: { $ref: "#/$defs/mediaItem" }
136
+ },
137
+ extensions: {
138
+ description: "Producer-specific data, keyed by namespace. Viewers ignore it.",
139
+ type: "object",
140
+ additionalProperties: { type: "object" }
141
+ }
142
+ },
143
+ $defs: {
144
+ id: {
145
+ type: "string",
146
+ pattern: "^[A-Za-z0-9_-]{1,64}$"
147
+ },
148
+ storyDate: {
149
+ type: "object",
150
+ required: ["text"],
151
+ properties: {
152
+ text: {
153
+ description: "The date as it should be shown, in the story's language.",
154
+ type: "string",
155
+ pattern: "\\S"
156
+ },
157
+ edtf: {
158
+ description: "Optional machine-readable form, in Extended Date/Time Format (ISO 8601-2).",
159
+ type: "string",
160
+ pattern: "\\S"
161
+ },
162
+ sortYear: {
163
+ description: "Optional year (proleptic Gregorian) that places the slide on the timeline.",
164
+ type: "integer"
165
+ }
166
+ }
167
+ },
168
+ slide: {
169
+ type: "object",
170
+ required: ["id"],
171
+ properties: {
172
+ id: { $ref: "#/$defs/id" },
173
+ title: { type: "string" },
174
+ text: {
175
+ description: "Body text. Markup: **bold**, *italic*, line breaks. Nothing else is interpreted.",
176
+ type: "string"
177
+ },
178
+ date: { $ref: "#/$defs/storyDate" },
179
+ place: { $ref: "#/$defs/place" },
180
+ media: {
181
+ type: "array",
182
+ items: { $ref: "#/$defs/slideMedia" }
183
+ },
184
+ people: {
185
+ type: "array",
186
+ items: {
187
+ type: "object",
188
+ required: ["name"],
189
+ properties: {
190
+ name: { type: "string", pattern: "\\S" },
191
+ role: { type: "string" }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ },
197
+ slideMedia: {
198
+ type: "object",
199
+ required: ["ref"],
200
+ properties: {
201
+ ref: { $ref: "#/$defs/id" },
202
+ caption: { type: "string" },
203
+ credit: { type: "string" }
204
+ }
205
+ },
206
+ place: {
207
+ type: "object",
208
+ properties: {
209
+ name: { type: "string" },
210
+ lat: { type: "number", minimum: -90, maximum: 90 },
211
+ lon: { type: "number", minimum: -180, maximum: 180 },
212
+ geometry: {
213
+ description: "Optional GeoJSON (RFC 7946) object drawn under the slide's pin: a FeatureCollection, Feature, or bare geometry.",
214
+ type: "object",
215
+ required: ["type"],
216
+ properties: { type: { type: "string" } },
217
+ additionalProperties: true
218
+ },
219
+ overlays: {
220
+ type: "array",
221
+ items: { $ref: "#/$defs/overlay" }
222
+ }
223
+ },
224
+ dependentRequired: {
225
+ lat: ["lon"],
226
+ lon: ["lat"]
227
+ }
228
+ },
229
+ lonLat: {
230
+ type: "array",
231
+ prefixItems: [
232
+ { type: "number", minimum: -180, maximum: 180 },
233
+ { type: "number", minimum: -90, maximum: 90 }
234
+ ],
235
+ minItems: 2,
236
+ maxItems: 2
237
+ },
238
+ overlay: {
239
+ type: "object",
240
+ required: ["media", "corners"],
241
+ properties: {
242
+ media: { $ref: "#/$defs/id" },
243
+ corners: {
244
+ description: "[lon, lat] of the image's top-left, top-right, bottom-right and bottom-left corners.",
245
+ type: "array",
246
+ items: { $ref: "#/$defs/lonLat" },
247
+ minItems: 4,
248
+ maxItems: 4
249
+ },
250
+ opacity: { type: "number", minimum: 0, maximum: 1 },
251
+ order: {
252
+ description: "Stacking order among a slide's overlays; lower is drawn first.",
253
+ type: "integer"
254
+ },
255
+ date: { $ref: "#/$defs/storyDate" }
256
+ }
257
+ },
258
+ mediaItem: {
259
+ type: "object",
260
+ required: ["src", "mime"],
261
+ properties: {
262
+ src: {
263
+ description: 'Path of the file inside the zip, relative to its root. No leading slash, no ".." segments.',
264
+ type: "string",
265
+ pattern: "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+$"
266
+ },
267
+ mime: {
268
+ type: "string",
269
+ pattern: "^[a-z]+/[A-Za-z0-9.+-]+$"
270
+ },
271
+ width: { type: "integer", minimum: 1 },
272
+ height: { type: "integer", minimum: 1 },
273
+ alt: { type: "string" }
274
+ }
275
+ }
276
+ }
277
+ };
278
+
279
+ // src/generated/schemaValidator.js
280
+ var validate = validate20;
281
+ var schema31 = { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://dsblank.github.io/gramps-connect-stories/spec/1.0/story.schema.json", "title": "Gramps Connect Story, format 1.0", "description": "The story.json at the root of a .story.zip. See spec/1.0/README.md for the rules this schema can't express (every ref resolves, every src exists in the zip, unknown fields are ignored).", "type": "object", "required": ["format", "version", "title", "slides", "media"], "properties": { "$schema": { "type": "string" }, "format": { "const": "gramps-connect-story" }, "version": { "description": "MAJOR.MINOR. This schema validates any 1.x file: newer minor versions only add optional fields.", "type": "string", "pattern": "^1\\.(0|[1-9][0-9]*)$" }, "title": { "type": "string", "pattern": "\\S" }, "language": { "description": `BCP 47 language tag of the story's text, e.g. "en", "de-CH".`, "type": "string", "pattern": "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$" }, "created": { "description": "When the file was written, as an RFC 3339 date-time.", "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$" }, "generator": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "pattern": "\\S" }, "version": { "type": "string" }, "url": { "type": "string" } } }, "authors": { "type": "array", "items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "pattern": "\\S" }, "url": { "type": "string" } } } }, "license": { "description": "License of the story's content (text and media), as an SPDX identifier or free text.", "type": "string" }, "presentation": { "type": "object", "properties": { "basemap": { "enum": ["standard", "historical", "auto", "none"] } } }, "slides": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/slide" } }, "media": { "description": "Every media file in the story, keyed by an id that slides and overlays refer to.", "type": "object", "propertyNames": { "$ref": "#/$defs/id" }, "additionalProperties": { "$ref": "#/$defs/mediaItem" } }, "extensions": { "description": "Producer-specific data, keyed by namespace. Viewers ignore it.", "type": "object", "additionalProperties": { "type": "object" } } }, "$defs": { "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$" }, "storyDate": { "type": "object", "required": ["text"], "properties": { "text": { "description": "The date as it should be shown, in the story's language.", "type": "string", "pattern": "\\S" }, "edtf": { "description": "Optional machine-readable form, in Extended Date/Time Format (ISO 8601-2).", "type": "string", "pattern": "\\S" }, "sortYear": { "description": "Optional year (proleptic Gregorian) that places the slide on the timeline.", "type": "integer" } } }, "slide": { "type": "object", "required": ["id"], "properties": { "id": { "$ref": "#/$defs/id" }, "title": { "type": "string" }, "text": { "description": "Body text. Markup: **bold**, *italic*, line breaks. Nothing else is interpreted.", "type": "string" }, "date": { "$ref": "#/$defs/storyDate" }, "place": { "$ref": "#/$defs/place" }, "media": { "type": "array", "items": { "$ref": "#/$defs/slideMedia" } }, "people": { "type": "array", "items": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string", "pattern": "\\S" }, "role": { "type": "string" } } } } } }, "slideMedia": { "type": "object", "required": ["ref"], "properties": { "ref": { "$ref": "#/$defs/id" }, "caption": { "type": "string" }, "credit": { "type": "string" } } }, "place": { "type": "object", "properties": { "name": { "type": "string" }, "lat": { "type": "number", "minimum": -90, "maximum": 90 }, "lon": { "type": "number", "minimum": -180, "maximum": 180 }, "geometry": { "description": "Optional GeoJSON (RFC 7946) object drawn under the slide's pin: a FeatureCollection, Feature, or bare geometry.", "type": "object", "required": ["type"], "properties": { "type": { "type": "string" } }, "additionalProperties": true }, "overlays": { "type": "array", "items": { "$ref": "#/$defs/overlay" } } }, "dependentRequired": { "lat": ["lon"], "lon": ["lat"] } }, "lonLat": { "type": "array", "prefixItems": [{ "type": "number", "minimum": -180, "maximum": 180 }, { "type": "number", "minimum": -90, "maximum": 90 }], "minItems": 2, "maxItems": 2 }, "overlay": { "type": "object", "required": ["media", "corners"], "properties": { "media": { "$ref": "#/$defs/id" }, "corners": { "description": "[lon, lat] of the image's top-left, top-right, bottom-right and bottom-left corners.", "type": "array", "items": { "$ref": "#/$defs/lonLat" }, "minItems": 4, "maxItems": 4 }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "order": { "description": "Stacking order among a slide's overlays; lower is drawn first.", "type": "integer" }, "date": { "$ref": "#/$defs/storyDate" } } }, "mediaItem": { "type": "object", "required": ["src", "mime"], "properties": { "src": { "description": 'Path of the file inside the zip, relative to its root. No leading slash, no ".." segments.', "type": "string", "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+$" }, "mime": { "type": "string", "pattern": "^[a-z]+/[A-Za-z0-9.+-]+$" }, "width": { "type": "integer", "minimum": 1 }, "height": { "type": "integer", "minimum": 1 }, "alt": { "type": "string" } } } } };
282
+ var pattern4 = new RegExp("^1\\.(0|[1-9][0-9]*)$", "u");
283
+ var pattern5 = new RegExp("\\S", "u");
284
+ var pattern6 = new RegExp("^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$", "u");
285
+ var pattern7 = new RegExp("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", "u");
286
+ var pattern10 = new RegExp("^[A-Za-z0-9_-]{1,64}$", "u");
287
+ var pattern19 = new RegExp("^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+$", "u");
288
+ var pattern20 = new RegExp("^[a-z]+/[A-Za-z0-9.+-]+$", "u");
289
+ function validate23(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
290
+ let vErrors = null;
291
+ let errors = 0;
292
+ const evaluated0 = validate23.evaluated;
293
+ if (evaluated0.dynamicProps) {
294
+ evaluated0.props = void 0;
295
+ }
296
+ if (evaluated0.dynamicItems) {
297
+ evaluated0.items = void 0;
298
+ }
299
+ if (data && typeof data == "object" && !Array.isArray(data)) {
300
+ if (data.media === void 0) {
301
+ const err0 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "media" }, message: "must have required property 'media'" };
302
+ if (vErrors === null) {
303
+ vErrors = [err0];
304
+ } else {
305
+ vErrors.push(err0);
306
+ }
307
+ errors++;
308
+ }
309
+ if (data.corners === void 0) {
310
+ const err1 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "corners" }, message: "must have required property 'corners'" };
311
+ if (vErrors === null) {
312
+ vErrors = [err1];
313
+ } else {
314
+ vErrors.push(err1);
315
+ }
316
+ errors++;
317
+ }
318
+ if (data.media !== void 0) {
319
+ let data0 = data.media;
320
+ if (typeof data0 === "string") {
321
+ if (!pattern10.test(data0)) {
322
+ const err2 = { instancePath: instancePath + "/media", schemaPath: "#/$defs/id/pattern", keyword: "pattern", params: { pattern: "^[A-Za-z0-9_-]{1,64}$" }, message: 'must match pattern "^[A-Za-z0-9_-]{1,64}$"' };
323
+ if (vErrors === null) {
324
+ vErrors = [err2];
325
+ } else {
326
+ vErrors.push(err2);
327
+ }
328
+ errors++;
329
+ }
330
+ } else {
331
+ const err3 = { instancePath: instancePath + "/media", schemaPath: "#/$defs/id/type", keyword: "type", params: { type: "string" }, message: "must be string" };
332
+ if (vErrors === null) {
333
+ vErrors = [err3];
334
+ } else {
335
+ vErrors.push(err3);
336
+ }
337
+ errors++;
338
+ }
339
+ }
340
+ if (data.corners !== void 0) {
341
+ let data1 = data.corners;
342
+ if (Array.isArray(data1)) {
343
+ if (data1.length > 4) {
344
+ const err4 = { instancePath: instancePath + "/corners", schemaPath: "#/properties/corners/maxItems", keyword: "maxItems", params: { limit: 4 }, message: "must NOT have more than 4 items" };
345
+ if (vErrors === null) {
346
+ vErrors = [err4];
347
+ } else {
348
+ vErrors.push(err4);
349
+ }
350
+ errors++;
351
+ }
352
+ if (data1.length < 4) {
353
+ const err5 = { instancePath: instancePath + "/corners", schemaPath: "#/properties/corners/minItems", keyword: "minItems", params: { limit: 4 }, message: "must NOT have fewer than 4 items" };
354
+ if (vErrors === null) {
355
+ vErrors = [err5];
356
+ } else {
357
+ vErrors.push(err5);
358
+ }
359
+ errors++;
360
+ }
361
+ const len0 = data1.length;
362
+ for (let i0 = 0; i0 < len0; i0++) {
363
+ let data2 = data1[i0];
364
+ if (Array.isArray(data2)) {
365
+ if (data2.length > 2) {
366
+ const err6 = { instancePath: instancePath + "/corners/" + i0, schemaPath: "#/$defs/lonLat/maxItems", keyword: "maxItems", params: { limit: 2 }, message: "must NOT have more than 2 items" };
367
+ if (vErrors === null) {
368
+ vErrors = [err6];
369
+ } else {
370
+ vErrors.push(err6);
371
+ }
372
+ errors++;
373
+ }
374
+ if (data2.length < 2) {
375
+ const err7 = { instancePath: instancePath + "/corners/" + i0, schemaPath: "#/$defs/lonLat/minItems", keyword: "minItems", params: { limit: 2 }, message: "must NOT have fewer than 2 items" };
376
+ if (vErrors === null) {
377
+ vErrors = [err7];
378
+ } else {
379
+ vErrors.push(err7);
380
+ }
381
+ errors++;
382
+ }
383
+ const len1 = data2.length;
384
+ if (len1 > 0) {
385
+ let data3 = data2[0];
386
+ if (typeof data3 == "number" && isFinite(data3)) {
387
+ if (data3 > 180 || isNaN(data3)) {
388
+ const err8 = { instancePath: instancePath + "/corners/" + i0 + "/0", schemaPath: "#/$defs/lonLat/prefixItems/0/maximum", keyword: "maximum", params: { comparison: "<=", limit: 180 }, message: "must be <= 180" };
389
+ if (vErrors === null) {
390
+ vErrors = [err8];
391
+ } else {
392
+ vErrors.push(err8);
393
+ }
394
+ errors++;
395
+ }
396
+ if (data3 < -180 || isNaN(data3)) {
397
+ const err9 = { instancePath: instancePath + "/corners/" + i0 + "/0", schemaPath: "#/$defs/lonLat/prefixItems/0/minimum", keyword: "minimum", params: { comparison: ">=", limit: -180 }, message: "must be >= -180" };
398
+ if (vErrors === null) {
399
+ vErrors = [err9];
400
+ } else {
401
+ vErrors.push(err9);
402
+ }
403
+ errors++;
404
+ }
405
+ } else {
406
+ const err10 = { instancePath: instancePath + "/corners/" + i0 + "/0", schemaPath: "#/$defs/lonLat/prefixItems/0/type", keyword: "type", params: { type: "number" }, message: "must be number" };
407
+ if (vErrors === null) {
408
+ vErrors = [err10];
409
+ } else {
410
+ vErrors.push(err10);
411
+ }
412
+ errors++;
413
+ }
414
+ }
415
+ if (len1 > 1) {
416
+ let data4 = data2[1];
417
+ if (typeof data4 == "number" && isFinite(data4)) {
418
+ if (data4 > 90 || isNaN(data4)) {
419
+ const err11 = { instancePath: instancePath + "/corners/" + i0 + "/1", schemaPath: "#/$defs/lonLat/prefixItems/1/maximum", keyword: "maximum", params: { comparison: "<=", limit: 90 }, message: "must be <= 90" };
420
+ if (vErrors === null) {
421
+ vErrors = [err11];
422
+ } else {
423
+ vErrors.push(err11);
424
+ }
425
+ errors++;
426
+ }
427
+ if (data4 < -90 || isNaN(data4)) {
428
+ const err12 = { instancePath: instancePath + "/corners/" + i0 + "/1", schemaPath: "#/$defs/lonLat/prefixItems/1/minimum", keyword: "minimum", params: { comparison: ">=", limit: -90 }, message: "must be >= -90" };
429
+ if (vErrors === null) {
430
+ vErrors = [err12];
431
+ } else {
432
+ vErrors.push(err12);
433
+ }
434
+ errors++;
435
+ }
436
+ } else {
437
+ const err13 = { instancePath: instancePath + "/corners/" + i0 + "/1", schemaPath: "#/$defs/lonLat/prefixItems/1/type", keyword: "type", params: { type: "number" }, message: "must be number" };
438
+ if (vErrors === null) {
439
+ vErrors = [err13];
440
+ } else {
441
+ vErrors.push(err13);
442
+ }
443
+ errors++;
444
+ }
445
+ }
446
+ } else {
447
+ const err14 = { instancePath: instancePath + "/corners/" + i0, schemaPath: "#/$defs/lonLat/type", keyword: "type", params: { type: "array" }, message: "must be array" };
448
+ if (vErrors === null) {
449
+ vErrors = [err14];
450
+ } else {
451
+ vErrors.push(err14);
452
+ }
453
+ errors++;
454
+ }
455
+ }
456
+ } else {
457
+ const err15 = { instancePath: instancePath + "/corners", schemaPath: "#/properties/corners/type", keyword: "type", params: { type: "array" }, message: "must be array" };
458
+ if (vErrors === null) {
459
+ vErrors = [err15];
460
+ } else {
461
+ vErrors.push(err15);
462
+ }
463
+ errors++;
464
+ }
465
+ }
466
+ if (data.opacity !== void 0) {
467
+ let data5 = data.opacity;
468
+ if (typeof data5 == "number" && isFinite(data5)) {
469
+ if (data5 > 1 || isNaN(data5)) {
470
+ const err16 = { instancePath: instancePath + "/opacity", schemaPath: "#/properties/opacity/maximum", keyword: "maximum", params: { comparison: "<=", limit: 1 }, message: "must be <= 1" };
471
+ if (vErrors === null) {
472
+ vErrors = [err16];
473
+ } else {
474
+ vErrors.push(err16);
475
+ }
476
+ errors++;
477
+ }
478
+ if (data5 < 0 || isNaN(data5)) {
479
+ const err17 = { instancePath: instancePath + "/opacity", schemaPath: "#/properties/opacity/minimum", keyword: "minimum", params: { comparison: ">=", limit: 0 }, message: "must be >= 0" };
480
+ if (vErrors === null) {
481
+ vErrors = [err17];
482
+ } else {
483
+ vErrors.push(err17);
484
+ }
485
+ errors++;
486
+ }
487
+ } else {
488
+ const err18 = { instancePath: instancePath + "/opacity", schemaPath: "#/properties/opacity/type", keyword: "type", params: { type: "number" }, message: "must be number" };
489
+ if (vErrors === null) {
490
+ vErrors = [err18];
491
+ } else {
492
+ vErrors.push(err18);
493
+ }
494
+ errors++;
495
+ }
496
+ }
497
+ if (data.order !== void 0) {
498
+ let data6 = data.order;
499
+ if (!(typeof data6 == "number" && (!(data6 % 1) && !isNaN(data6)) && isFinite(data6))) {
500
+ const err19 = { instancePath: instancePath + "/order", schemaPath: "#/properties/order/type", keyword: "type", params: { type: "integer" }, message: "must be integer" };
501
+ if (vErrors === null) {
502
+ vErrors = [err19];
503
+ } else {
504
+ vErrors.push(err19);
505
+ }
506
+ errors++;
507
+ }
508
+ }
509
+ if (data.date !== void 0) {
510
+ let data7 = data.date;
511
+ if (data7 && typeof data7 == "object" && !Array.isArray(data7)) {
512
+ if (data7.text === void 0) {
513
+ const err20 = { instancePath: instancePath + "/date", schemaPath: "#/$defs/storyDate/required", keyword: "required", params: { missingProperty: "text" }, message: "must have required property 'text'" };
514
+ if (vErrors === null) {
515
+ vErrors = [err20];
516
+ } else {
517
+ vErrors.push(err20);
518
+ }
519
+ errors++;
520
+ }
521
+ if (data7.text !== void 0) {
522
+ let data8 = data7.text;
523
+ if (typeof data8 === "string") {
524
+ if (!pattern5.test(data8)) {
525
+ const err21 = { instancePath: instancePath + "/date/text", schemaPath: "#/$defs/storyDate/properties/text/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
526
+ if (vErrors === null) {
527
+ vErrors = [err21];
528
+ } else {
529
+ vErrors.push(err21);
530
+ }
531
+ errors++;
532
+ }
533
+ } else {
534
+ const err22 = { instancePath: instancePath + "/date/text", schemaPath: "#/$defs/storyDate/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" };
535
+ if (vErrors === null) {
536
+ vErrors = [err22];
537
+ } else {
538
+ vErrors.push(err22);
539
+ }
540
+ errors++;
541
+ }
542
+ }
543
+ if (data7.edtf !== void 0) {
544
+ let data9 = data7.edtf;
545
+ if (typeof data9 === "string") {
546
+ if (!pattern5.test(data9)) {
547
+ const err23 = { instancePath: instancePath + "/date/edtf", schemaPath: "#/$defs/storyDate/properties/edtf/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
548
+ if (vErrors === null) {
549
+ vErrors = [err23];
550
+ } else {
551
+ vErrors.push(err23);
552
+ }
553
+ errors++;
554
+ }
555
+ } else {
556
+ const err24 = { instancePath: instancePath + "/date/edtf", schemaPath: "#/$defs/storyDate/properties/edtf/type", keyword: "type", params: { type: "string" }, message: "must be string" };
557
+ if (vErrors === null) {
558
+ vErrors = [err24];
559
+ } else {
560
+ vErrors.push(err24);
561
+ }
562
+ errors++;
563
+ }
564
+ }
565
+ if (data7.sortYear !== void 0) {
566
+ let data10 = data7.sortYear;
567
+ if (!(typeof data10 == "number" && (!(data10 % 1) && !isNaN(data10)) && isFinite(data10))) {
568
+ const err25 = { instancePath: instancePath + "/date/sortYear", schemaPath: "#/$defs/storyDate/properties/sortYear/type", keyword: "type", params: { type: "integer" }, message: "must be integer" };
569
+ if (vErrors === null) {
570
+ vErrors = [err25];
571
+ } else {
572
+ vErrors.push(err25);
573
+ }
574
+ errors++;
575
+ }
576
+ }
577
+ } else {
578
+ const err26 = { instancePath: instancePath + "/date", schemaPath: "#/$defs/storyDate/type", keyword: "type", params: { type: "object" }, message: "must be object" };
579
+ if (vErrors === null) {
580
+ vErrors = [err26];
581
+ } else {
582
+ vErrors.push(err26);
583
+ }
584
+ errors++;
585
+ }
586
+ }
587
+ } else {
588
+ const err27 = { instancePath, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" };
589
+ if (vErrors === null) {
590
+ vErrors = [err27];
591
+ } else {
592
+ vErrors.push(err27);
593
+ }
594
+ errors++;
595
+ }
596
+ validate23.errors = vErrors;
597
+ return errors === 0;
598
+ }
599
+ validate23.evaluated = { "props": { "media": true, "corners": true, "opacity": true, "order": true, "date": true }, "dynamicProps": false, "dynamicItems": false };
600
+ function validate22(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
601
+ let vErrors = null;
602
+ let errors = 0;
603
+ const evaluated0 = validate22.evaluated;
604
+ if (evaluated0.dynamicProps) {
605
+ evaluated0.props = void 0;
606
+ }
607
+ if (evaluated0.dynamicItems) {
608
+ evaluated0.items = void 0;
609
+ }
610
+ if (data && typeof data == "object" && !Array.isArray(data)) {
611
+ if (data.name !== void 0) {
612
+ if (typeof data.name !== "string") {
613
+ const err0 = { instancePath: instancePath + "/name", schemaPath: "#/properties/name/type", keyword: "type", params: { type: "string" }, message: "must be string" };
614
+ if (vErrors === null) {
615
+ vErrors = [err0];
616
+ } else {
617
+ vErrors.push(err0);
618
+ }
619
+ errors++;
620
+ }
621
+ }
622
+ if (data.lat !== void 0) {
623
+ let data1 = data.lat;
624
+ if (typeof data1 == "number" && isFinite(data1)) {
625
+ if (data1 > 90 || isNaN(data1)) {
626
+ const err1 = { instancePath: instancePath + "/lat", schemaPath: "#/properties/lat/maximum", keyword: "maximum", params: { comparison: "<=", limit: 90 }, message: "must be <= 90" };
627
+ if (vErrors === null) {
628
+ vErrors = [err1];
629
+ } else {
630
+ vErrors.push(err1);
631
+ }
632
+ errors++;
633
+ }
634
+ if (data1 < -90 || isNaN(data1)) {
635
+ const err2 = { instancePath: instancePath + "/lat", schemaPath: "#/properties/lat/minimum", keyword: "minimum", params: { comparison: ">=", limit: -90 }, message: "must be >= -90" };
636
+ if (vErrors === null) {
637
+ vErrors = [err2];
638
+ } else {
639
+ vErrors.push(err2);
640
+ }
641
+ errors++;
642
+ }
643
+ } else {
644
+ const err3 = { instancePath: instancePath + "/lat", schemaPath: "#/properties/lat/type", keyword: "type", params: { type: "number" }, message: "must be number" };
645
+ if (vErrors === null) {
646
+ vErrors = [err3];
647
+ } else {
648
+ vErrors.push(err3);
649
+ }
650
+ errors++;
651
+ }
652
+ }
653
+ if (data.lon !== void 0) {
654
+ let data2 = data.lon;
655
+ if (typeof data2 == "number" && isFinite(data2)) {
656
+ if (data2 > 180 || isNaN(data2)) {
657
+ const err4 = { instancePath: instancePath + "/lon", schemaPath: "#/properties/lon/maximum", keyword: "maximum", params: { comparison: "<=", limit: 180 }, message: "must be <= 180" };
658
+ if (vErrors === null) {
659
+ vErrors = [err4];
660
+ } else {
661
+ vErrors.push(err4);
662
+ }
663
+ errors++;
664
+ }
665
+ if (data2 < -180 || isNaN(data2)) {
666
+ const err5 = { instancePath: instancePath + "/lon", schemaPath: "#/properties/lon/minimum", keyword: "minimum", params: { comparison: ">=", limit: -180 }, message: "must be >= -180" };
667
+ if (vErrors === null) {
668
+ vErrors = [err5];
669
+ } else {
670
+ vErrors.push(err5);
671
+ }
672
+ errors++;
673
+ }
674
+ } else {
675
+ const err6 = { instancePath: instancePath + "/lon", schemaPath: "#/properties/lon/type", keyword: "type", params: { type: "number" }, message: "must be number" };
676
+ if (vErrors === null) {
677
+ vErrors = [err6];
678
+ } else {
679
+ vErrors.push(err6);
680
+ }
681
+ errors++;
682
+ }
683
+ }
684
+ if (data.geometry !== void 0) {
685
+ let data3 = data.geometry;
686
+ if (data3 && typeof data3 == "object" && !Array.isArray(data3)) {
687
+ if (data3.type === void 0) {
688
+ const err7 = { instancePath: instancePath + "/geometry", schemaPath: "#/properties/geometry/required", keyword: "required", params: { missingProperty: "type" }, message: "must have required property 'type'" };
689
+ if (vErrors === null) {
690
+ vErrors = [err7];
691
+ } else {
692
+ vErrors.push(err7);
693
+ }
694
+ errors++;
695
+ }
696
+ if (data3.type !== void 0) {
697
+ if (typeof data3.type !== "string") {
698
+ const err8 = { instancePath: instancePath + "/geometry/type", schemaPath: "#/properties/geometry/properties/type/type", keyword: "type", params: { type: "string" }, message: "must be string" };
699
+ if (vErrors === null) {
700
+ vErrors = [err8];
701
+ } else {
702
+ vErrors.push(err8);
703
+ }
704
+ errors++;
705
+ }
706
+ }
707
+ } else {
708
+ const err9 = { instancePath: instancePath + "/geometry", schemaPath: "#/properties/geometry/type", keyword: "type", params: { type: "object" }, message: "must be object" };
709
+ if (vErrors === null) {
710
+ vErrors = [err9];
711
+ } else {
712
+ vErrors.push(err9);
713
+ }
714
+ errors++;
715
+ }
716
+ }
717
+ if (data.overlays !== void 0) {
718
+ let data5 = data.overlays;
719
+ if (Array.isArray(data5)) {
720
+ const len0 = data5.length;
721
+ for (let i0 = 0; i0 < len0; i0++) {
722
+ if (!validate23(data5[i0], { instancePath: instancePath + "/overlays/" + i0, parentData: data5, parentDataProperty: i0, rootData, dynamicAnchors })) {
723
+ vErrors = vErrors === null ? validate23.errors : vErrors.concat(validate23.errors);
724
+ errors = vErrors.length;
725
+ }
726
+ }
727
+ } else {
728
+ const err10 = { instancePath: instancePath + "/overlays", schemaPath: "#/properties/overlays/type", keyword: "type", params: { type: "array" }, message: "must be array" };
729
+ if (vErrors === null) {
730
+ vErrors = [err10];
731
+ } else {
732
+ vErrors.push(err10);
733
+ }
734
+ errors++;
735
+ }
736
+ }
737
+ if (data.lat !== void 0) {
738
+ if (data.lon === void 0) {
739
+ const err11 = { instancePath, schemaPath: "#/dependentRequired", keyword: "dependentRequired", params: {
740
+ property: "lat",
741
+ missingProperty: "lon",
742
+ depsCount: 1,
743
+ deps: "lon"
744
+ }, message: "must have property lon when property lat is present" };
745
+ if (vErrors === null) {
746
+ vErrors = [err11];
747
+ } else {
748
+ vErrors.push(err11);
749
+ }
750
+ errors++;
751
+ }
752
+ }
753
+ if (data.lon !== void 0) {
754
+ if (data.lat === void 0) {
755
+ const err12 = { instancePath, schemaPath: "#/dependentRequired", keyword: "dependentRequired", params: {
756
+ property: "lon",
757
+ missingProperty: "lat",
758
+ depsCount: 1,
759
+ deps: "lat"
760
+ }, message: "must have property lat when property lon is present" };
761
+ if (vErrors === null) {
762
+ vErrors = [err12];
763
+ } else {
764
+ vErrors.push(err12);
765
+ }
766
+ errors++;
767
+ }
768
+ }
769
+ } else {
770
+ const err13 = { instancePath, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" };
771
+ if (vErrors === null) {
772
+ vErrors = [err13];
773
+ } else {
774
+ vErrors.push(err13);
775
+ }
776
+ errors++;
777
+ }
778
+ validate22.errors = vErrors;
779
+ return errors === 0;
780
+ }
781
+ validate22.evaluated = { "props": { "name": true, "lat": true, "lon": true, "geometry": true, "overlays": true }, "dynamicProps": false, "dynamicItems": false };
782
+ function validate26(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
783
+ let vErrors = null;
784
+ let errors = 0;
785
+ const evaluated0 = validate26.evaluated;
786
+ if (evaluated0.dynamicProps) {
787
+ evaluated0.props = void 0;
788
+ }
789
+ if (evaluated0.dynamicItems) {
790
+ evaluated0.items = void 0;
791
+ }
792
+ if (data && typeof data == "object" && !Array.isArray(data)) {
793
+ if (data.ref === void 0) {
794
+ const err0 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "ref" }, message: "must have required property 'ref'" };
795
+ if (vErrors === null) {
796
+ vErrors = [err0];
797
+ } else {
798
+ vErrors.push(err0);
799
+ }
800
+ errors++;
801
+ }
802
+ if (data.ref !== void 0) {
803
+ let data0 = data.ref;
804
+ if (typeof data0 === "string") {
805
+ if (!pattern10.test(data0)) {
806
+ const err1 = { instancePath: instancePath + "/ref", schemaPath: "#/$defs/id/pattern", keyword: "pattern", params: { pattern: "^[A-Za-z0-9_-]{1,64}$" }, message: 'must match pattern "^[A-Za-z0-9_-]{1,64}$"' };
807
+ if (vErrors === null) {
808
+ vErrors = [err1];
809
+ } else {
810
+ vErrors.push(err1);
811
+ }
812
+ errors++;
813
+ }
814
+ } else {
815
+ const err2 = { instancePath: instancePath + "/ref", schemaPath: "#/$defs/id/type", keyword: "type", params: { type: "string" }, message: "must be string" };
816
+ if (vErrors === null) {
817
+ vErrors = [err2];
818
+ } else {
819
+ vErrors.push(err2);
820
+ }
821
+ errors++;
822
+ }
823
+ }
824
+ if (data.caption !== void 0) {
825
+ if (typeof data.caption !== "string") {
826
+ const err3 = { instancePath: instancePath + "/caption", schemaPath: "#/properties/caption/type", keyword: "type", params: { type: "string" }, message: "must be string" };
827
+ if (vErrors === null) {
828
+ vErrors = [err3];
829
+ } else {
830
+ vErrors.push(err3);
831
+ }
832
+ errors++;
833
+ }
834
+ }
835
+ if (data.credit !== void 0) {
836
+ if (typeof data.credit !== "string") {
837
+ const err4 = { instancePath: instancePath + "/credit", schemaPath: "#/properties/credit/type", keyword: "type", params: { type: "string" }, message: "must be string" };
838
+ if (vErrors === null) {
839
+ vErrors = [err4];
840
+ } else {
841
+ vErrors.push(err4);
842
+ }
843
+ errors++;
844
+ }
845
+ }
846
+ } else {
847
+ const err5 = { instancePath, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" };
848
+ if (vErrors === null) {
849
+ vErrors = [err5];
850
+ } else {
851
+ vErrors.push(err5);
852
+ }
853
+ errors++;
854
+ }
855
+ validate26.errors = vErrors;
856
+ return errors === 0;
857
+ }
858
+ validate26.evaluated = { "props": { "ref": true, "caption": true, "credit": true }, "dynamicProps": false, "dynamicItems": false };
859
+ function validate21(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
860
+ let vErrors = null;
861
+ let errors = 0;
862
+ const evaluated0 = validate21.evaluated;
863
+ if (evaluated0.dynamicProps) {
864
+ evaluated0.props = void 0;
865
+ }
866
+ if (evaluated0.dynamicItems) {
867
+ evaluated0.items = void 0;
868
+ }
869
+ if (data && typeof data == "object" && !Array.isArray(data)) {
870
+ if (data.id === void 0) {
871
+ const err0 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "id" }, message: "must have required property 'id'" };
872
+ if (vErrors === null) {
873
+ vErrors = [err0];
874
+ } else {
875
+ vErrors.push(err0);
876
+ }
877
+ errors++;
878
+ }
879
+ if (data.id !== void 0) {
880
+ let data0 = data.id;
881
+ if (typeof data0 === "string") {
882
+ if (!pattern10.test(data0)) {
883
+ const err1 = { instancePath: instancePath + "/id", schemaPath: "#/$defs/id/pattern", keyword: "pattern", params: { pattern: "^[A-Za-z0-9_-]{1,64}$" }, message: 'must match pattern "^[A-Za-z0-9_-]{1,64}$"' };
884
+ if (vErrors === null) {
885
+ vErrors = [err1];
886
+ } else {
887
+ vErrors.push(err1);
888
+ }
889
+ errors++;
890
+ }
891
+ } else {
892
+ const err2 = { instancePath: instancePath + "/id", schemaPath: "#/$defs/id/type", keyword: "type", params: { type: "string" }, message: "must be string" };
893
+ if (vErrors === null) {
894
+ vErrors = [err2];
895
+ } else {
896
+ vErrors.push(err2);
897
+ }
898
+ errors++;
899
+ }
900
+ }
901
+ if (data.title !== void 0) {
902
+ if (typeof data.title !== "string") {
903
+ const err3 = { instancePath: instancePath + "/title", schemaPath: "#/properties/title/type", keyword: "type", params: { type: "string" }, message: "must be string" };
904
+ if (vErrors === null) {
905
+ vErrors = [err3];
906
+ } else {
907
+ vErrors.push(err3);
908
+ }
909
+ errors++;
910
+ }
911
+ }
912
+ if (data.text !== void 0) {
913
+ if (typeof data.text !== "string") {
914
+ const err4 = { instancePath: instancePath + "/text", schemaPath: "#/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" };
915
+ if (vErrors === null) {
916
+ vErrors = [err4];
917
+ } else {
918
+ vErrors.push(err4);
919
+ }
920
+ errors++;
921
+ }
922
+ }
923
+ if (data.date !== void 0) {
924
+ let data3 = data.date;
925
+ if (data3 && typeof data3 == "object" && !Array.isArray(data3)) {
926
+ if (data3.text === void 0) {
927
+ const err5 = { instancePath: instancePath + "/date", schemaPath: "#/$defs/storyDate/required", keyword: "required", params: { missingProperty: "text" }, message: "must have required property 'text'" };
928
+ if (vErrors === null) {
929
+ vErrors = [err5];
930
+ } else {
931
+ vErrors.push(err5);
932
+ }
933
+ errors++;
934
+ }
935
+ if (data3.text !== void 0) {
936
+ let data4 = data3.text;
937
+ if (typeof data4 === "string") {
938
+ if (!pattern5.test(data4)) {
939
+ const err6 = { instancePath: instancePath + "/date/text", schemaPath: "#/$defs/storyDate/properties/text/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
940
+ if (vErrors === null) {
941
+ vErrors = [err6];
942
+ } else {
943
+ vErrors.push(err6);
944
+ }
945
+ errors++;
946
+ }
947
+ } else {
948
+ const err7 = { instancePath: instancePath + "/date/text", schemaPath: "#/$defs/storyDate/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" };
949
+ if (vErrors === null) {
950
+ vErrors = [err7];
951
+ } else {
952
+ vErrors.push(err7);
953
+ }
954
+ errors++;
955
+ }
956
+ }
957
+ if (data3.edtf !== void 0) {
958
+ let data5 = data3.edtf;
959
+ if (typeof data5 === "string") {
960
+ if (!pattern5.test(data5)) {
961
+ const err8 = { instancePath: instancePath + "/date/edtf", schemaPath: "#/$defs/storyDate/properties/edtf/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
962
+ if (vErrors === null) {
963
+ vErrors = [err8];
964
+ } else {
965
+ vErrors.push(err8);
966
+ }
967
+ errors++;
968
+ }
969
+ } else {
970
+ const err9 = { instancePath: instancePath + "/date/edtf", schemaPath: "#/$defs/storyDate/properties/edtf/type", keyword: "type", params: { type: "string" }, message: "must be string" };
971
+ if (vErrors === null) {
972
+ vErrors = [err9];
973
+ } else {
974
+ vErrors.push(err9);
975
+ }
976
+ errors++;
977
+ }
978
+ }
979
+ if (data3.sortYear !== void 0) {
980
+ let data6 = data3.sortYear;
981
+ if (!(typeof data6 == "number" && (!(data6 % 1) && !isNaN(data6)) && isFinite(data6))) {
982
+ const err10 = { instancePath: instancePath + "/date/sortYear", schemaPath: "#/$defs/storyDate/properties/sortYear/type", keyword: "type", params: { type: "integer" }, message: "must be integer" };
983
+ if (vErrors === null) {
984
+ vErrors = [err10];
985
+ } else {
986
+ vErrors.push(err10);
987
+ }
988
+ errors++;
989
+ }
990
+ }
991
+ } else {
992
+ const err11 = { instancePath: instancePath + "/date", schemaPath: "#/$defs/storyDate/type", keyword: "type", params: { type: "object" }, message: "must be object" };
993
+ if (vErrors === null) {
994
+ vErrors = [err11];
995
+ } else {
996
+ vErrors.push(err11);
997
+ }
998
+ errors++;
999
+ }
1000
+ }
1001
+ if (data.place !== void 0) {
1002
+ if (!validate22(data.place, { instancePath: instancePath + "/place", parentData: data, parentDataProperty: "place", rootData, dynamicAnchors })) {
1003
+ vErrors = vErrors === null ? validate22.errors : vErrors.concat(validate22.errors);
1004
+ errors = vErrors.length;
1005
+ }
1006
+ }
1007
+ if (data.media !== void 0) {
1008
+ let data8 = data.media;
1009
+ if (Array.isArray(data8)) {
1010
+ const len0 = data8.length;
1011
+ for (let i0 = 0; i0 < len0; i0++) {
1012
+ if (!validate26(data8[i0], { instancePath: instancePath + "/media/" + i0, parentData: data8, parentDataProperty: i0, rootData, dynamicAnchors })) {
1013
+ vErrors = vErrors === null ? validate26.errors : vErrors.concat(validate26.errors);
1014
+ errors = vErrors.length;
1015
+ }
1016
+ }
1017
+ } else {
1018
+ const err12 = { instancePath: instancePath + "/media", schemaPath: "#/properties/media/type", keyword: "type", params: { type: "array" }, message: "must be array" };
1019
+ if (vErrors === null) {
1020
+ vErrors = [err12];
1021
+ } else {
1022
+ vErrors.push(err12);
1023
+ }
1024
+ errors++;
1025
+ }
1026
+ }
1027
+ if (data.people !== void 0) {
1028
+ let data10 = data.people;
1029
+ if (Array.isArray(data10)) {
1030
+ const len1 = data10.length;
1031
+ for (let i1 = 0; i1 < len1; i1++) {
1032
+ let data11 = data10[i1];
1033
+ if (data11 && typeof data11 == "object" && !Array.isArray(data11)) {
1034
+ if (data11.name === void 0) {
1035
+ const err13 = { instancePath: instancePath + "/people/" + i1, schemaPath: "#/properties/people/items/required", keyword: "required", params: { missingProperty: "name" }, message: "must have required property 'name'" };
1036
+ if (vErrors === null) {
1037
+ vErrors = [err13];
1038
+ } else {
1039
+ vErrors.push(err13);
1040
+ }
1041
+ errors++;
1042
+ }
1043
+ if (data11.name !== void 0) {
1044
+ let data12 = data11.name;
1045
+ if (typeof data12 === "string") {
1046
+ if (!pattern5.test(data12)) {
1047
+ const err14 = { instancePath: instancePath + "/people/" + i1 + "/name", schemaPath: "#/properties/people/items/properties/name/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
1048
+ if (vErrors === null) {
1049
+ vErrors = [err14];
1050
+ } else {
1051
+ vErrors.push(err14);
1052
+ }
1053
+ errors++;
1054
+ }
1055
+ } else {
1056
+ const err15 = { instancePath: instancePath + "/people/" + i1 + "/name", schemaPath: "#/properties/people/items/properties/name/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1057
+ if (vErrors === null) {
1058
+ vErrors = [err15];
1059
+ } else {
1060
+ vErrors.push(err15);
1061
+ }
1062
+ errors++;
1063
+ }
1064
+ }
1065
+ if (data11.role !== void 0) {
1066
+ if (typeof data11.role !== "string") {
1067
+ const err16 = { instancePath: instancePath + "/people/" + i1 + "/role", schemaPath: "#/properties/people/items/properties/role/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1068
+ if (vErrors === null) {
1069
+ vErrors = [err16];
1070
+ } else {
1071
+ vErrors.push(err16);
1072
+ }
1073
+ errors++;
1074
+ }
1075
+ }
1076
+ } else {
1077
+ const err17 = { instancePath: instancePath + "/people/" + i1, schemaPath: "#/properties/people/items/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1078
+ if (vErrors === null) {
1079
+ vErrors = [err17];
1080
+ } else {
1081
+ vErrors.push(err17);
1082
+ }
1083
+ errors++;
1084
+ }
1085
+ }
1086
+ } else {
1087
+ const err18 = { instancePath: instancePath + "/people", schemaPath: "#/properties/people/type", keyword: "type", params: { type: "array" }, message: "must be array" };
1088
+ if (vErrors === null) {
1089
+ vErrors = [err18];
1090
+ } else {
1091
+ vErrors.push(err18);
1092
+ }
1093
+ errors++;
1094
+ }
1095
+ }
1096
+ } else {
1097
+ const err19 = { instancePath, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1098
+ if (vErrors === null) {
1099
+ vErrors = [err19];
1100
+ } else {
1101
+ vErrors.push(err19);
1102
+ }
1103
+ errors++;
1104
+ }
1105
+ validate21.errors = vErrors;
1106
+ return errors === 0;
1107
+ }
1108
+ validate21.evaluated = { "props": { "id": true, "title": true, "text": true, "date": true, "place": true, "media": true, "people": true }, "dynamicProps": false, "dynamicItems": false };
1109
+ function validate20(data, { instancePath = "", parentData, parentDataProperty, rootData = data, dynamicAnchors = {} } = {}) {
1110
+ ;
1111
+ let vErrors = null;
1112
+ let errors = 0;
1113
+ const evaluated0 = validate20.evaluated;
1114
+ if (evaluated0.dynamicProps) {
1115
+ evaluated0.props = void 0;
1116
+ }
1117
+ if (evaluated0.dynamicItems) {
1118
+ evaluated0.items = void 0;
1119
+ }
1120
+ if (data && typeof data == "object" && !Array.isArray(data)) {
1121
+ if (data.format === void 0) {
1122
+ const err0 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "format" }, message: "must have required property 'format'" };
1123
+ if (vErrors === null) {
1124
+ vErrors = [err0];
1125
+ } else {
1126
+ vErrors.push(err0);
1127
+ }
1128
+ errors++;
1129
+ }
1130
+ if (data.version === void 0) {
1131
+ const err1 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "version" }, message: "must have required property 'version'" };
1132
+ if (vErrors === null) {
1133
+ vErrors = [err1];
1134
+ } else {
1135
+ vErrors.push(err1);
1136
+ }
1137
+ errors++;
1138
+ }
1139
+ if (data.title === void 0) {
1140
+ const err2 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "title" }, message: "must have required property 'title'" };
1141
+ if (vErrors === null) {
1142
+ vErrors = [err2];
1143
+ } else {
1144
+ vErrors.push(err2);
1145
+ }
1146
+ errors++;
1147
+ }
1148
+ if (data.slides === void 0) {
1149
+ const err3 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "slides" }, message: "must have required property 'slides'" };
1150
+ if (vErrors === null) {
1151
+ vErrors = [err3];
1152
+ } else {
1153
+ vErrors.push(err3);
1154
+ }
1155
+ errors++;
1156
+ }
1157
+ if (data.media === void 0) {
1158
+ const err4 = { instancePath, schemaPath: "#/required", keyword: "required", params: { missingProperty: "media" }, message: "must have required property 'media'" };
1159
+ if (vErrors === null) {
1160
+ vErrors = [err4];
1161
+ } else {
1162
+ vErrors.push(err4);
1163
+ }
1164
+ errors++;
1165
+ }
1166
+ if (data.$schema !== void 0) {
1167
+ if (typeof data.$schema !== "string") {
1168
+ const err5 = { instancePath: instancePath + "/$schema", schemaPath: "#/properties/%24schema/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1169
+ if (vErrors === null) {
1170
+ vErrors = [err5];
1171
+ } else {
1172
+ vErrors.push(err5);
1173
+ }
1174
+ errors++;
1175
+ }
1176
+ }
1177
+ if (data.format !== void 0) {
1178
+ if ("gramps-connect-story" !== data.format) {
1179
+ const err6 = { instancePath: instancePath + "/format", schemaPath: "#/properties/format/const", keyword: "const", params: { allowedValue: "gramps-connect-story" }, message: "must be equal to constant" };
1180
+ if (vErrors === null) {
1181
+ vErrors = [err6];
1182
+ } else {
1183
+ vErrors.push(err6);
1184
+ }
1185
+ errors++;
1186
+ }
1187
+ }
1188
+ if (data.version !== void 0) {
1189
+ let data2 = data.version;
1190
+ if (typeof data2 === "string") {
1191
+ if (!pattern4.test(data2)) {
1192
+ const err7 = { instancePath: instancePath + "/version", schemaPath: "#/properties/version/pattern", keyword: "pattern", params: { pattern: "^1\\.(0|[1-9][0-9]*)$" }, message: 'must match pattern "^1\\.(0|[1-9][0-9]*)$"' };
1193
+ if (vErrors === null) {
1194
+ vErrors = [err7];
1195
+ } else {
1196
+ vErrors.push(err7);
1197
+ }
1198
+ errors++;
1199
+ }
1200
+ } else {
1201
+ const err8 = { instancePath: instancePath + "/version", schemaPath: "#/properties/version/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1202
+ if (vErrors === null) {
1203
+ vErrors = [err8];
1204
+ } else {
1205
+ vErrors.push(err8);
1206
+ }
1207
+ errors++;
1208
+ }
1209
+ }
1210
+ if (data.title !== void 0) {
1211
+ let data3 = data.title;
1212
+ if (typeof data3 === "string") {
1213
+ if (!pattern5.test(data3)) {
1214
+ const err9 = { instancePath: instancePath + "/title", schemaPath: "#/properties/title/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
1215
+ if (vErrors === null) {
1216
+ vErrors = [err9];
1217
+ } else {
1218
+ vErrors.push(err9);
1219
+ }
1220
+ errors++;
1221
+ }
1222
+ } else {
1223
+ const err10 = { instancePath: instancePath + "/title", schemaPath: "#/properties/title/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1224
+ if (vErrors === null) {
1225
+ vErrors = [err10];
1226
+ } else {
1227
+ vErrors.push(err10);
1228
+ }
1229
+ errors++;
1230
+ }
1231
+ }
1232
+ if (data.language !== void 0) {
1233
+ let data4 = data.language;
1234
+ if (typeof data4 === "string") {
1235
+ if (!pattern6.test(data4)) {
1236
+ const err11 = { instancePath: instancePath + "/language", schemaPath: "#/properties/language/pattern", keyword: "pattern", params: { pattern: "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$" }, message: 'must match pattern "^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$"' };
1237
+ if (vErrors === null) {
1238
+ vErrors = [err11];
1239
+ } else {
1240
+ vErrors.push(err11);
1241
+ }
1242
+ errors++;
1243
+ }
1244
+ } else {
1245
+ const err12 = { instancePath: instancePath + "/language", schemaPath: "#/properties/language/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1246
+ if (vErrors === null) {
1247
+ vErrors = [err12];
1248
+ } else {
1249
+ vErrors.push(err12);
1250
+ }
1251
+ errors++;
1252
+ }
1253
+ }
1254
+ if (data.created !== void 0) {
1255
+ let data5 = data.created;
1256
+ if (typeof data5 === "string") {
1257
+ if (!pattern7.test(data5)) {
1258
+ const err13 = { instancePath: instancePath + "/created", schemaPath: "#/properties/created/pattern", keyword: "pattern", params: { pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$" }, message: 'must match pattern "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$"' };
1259
+ if (vErrors === null) {
1260
+ vErrors = [err13];
1261
+ } else {
1262
+ vErrors.push(err13);
1263
+ }
1264
+ errors++;
1265
+ }
1266
+ } else {
1267
+ const err14 = { instancePath: instancePath + "/created", schemaPath: "#/properties/created/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1268
+ if (vErrors === null) {
1269
+ vErrors = [err14];
1270
+ } else {
1271
+ vErrors.push(err14);
1272
+ }
1273
+ errors++;
1274
+ }
1275
+ }
1276
+ if (data.generator !== void 0) {
1277
+ let data6 = data.generator;
1278
+ if (data6 && typeof data6 == "object" && !Array.isArray(data6)) {
1279
+ if (data6.name === void 0) {
1280
+ const err15 = { instancePath: instancePath + "/generator", schemaPath: "#/properties/generator/required", keyword: "required", params: { missingProperty: "name" }, message: "must have required property 'name'" };
1281
+ if (vErrors === null) {
1282
+ vErrors = [err15];
1283
+ } else {
1284
+ vErrors.push(err15);
1285
+ }
1286
+ errors++;
1287
+ }
1288
+ if (data6.name !== void 0) {
1289
+ let data7 = data6.name;
1290
+ if (typeof data7 === "string") {
1291
+ if (!pattern5.test(data7)) {
1292
+ const err16 = { instancePath: instancePath + "/generator/name", schemaPath: "#/properties/generator/properties/name/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
1293
+ if (vErrors === null) {
1294
+ vErrors = [err16];
1295
+ } else {
1296
+ vErrors.push(err16);
1297
+ }
1298
+ errors++;
1299
+ }
1300
+ } else {
1301
+ const err17 = { instancePath: instancePath + "/generator/name", schemaPath: "#/properties/generator/properties/name/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1302
+ if (vErrors === null) {
1303
+ vErrors = [err17];
1304
+ } else {
1305
+ vErrors.push(err17);
1306
+ }
1307
+ errors++;
1308
+ }
1309
+ }
1310
+ if (data6.version !== void 0) {
1311
+ if (typeof data6.version !== "string") {
1312
+ const err18 = { instancePath: instancePath + "/generator/version", schemaPath: "#/properties/generator/properties/version/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1313
+ if (vErrors === null) {
1314
+ vErrors = [err18];
1315
+ } else {
1316
+ vErrors.push(err18);
1317
+ }
1318
+ errors++;
1319
+ }
1320
+ }
1321
+ if (data6.url !== void 0) {
1322
+ if (typeof data6.url !== "string") {
1323
+ const err19 = { instancePath: instancePath + "/generator/url", schemaPath: "#/properties/generator/properties/url/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1324
+ if (vErrors === null) {
1325
+ vErrors = [err19];
1326
+ } else {
1327
+ vErrors.push(err19);
1328
+ }
1329
+ errors++;
1330
+ }
1331
+ }
1332
+ } else {
1333
+ const err20 = { instancePath: instancePath + "/generator", schemaPath: "#/properties/generator/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1334
+ if (vErrors === null) {
1335
+ vErrors = [err20];
1336
+ } else {
1337
+ vErrors.push(err20);
1338
+ }
1339
+ errors++;
1340
+ }
1341
+ }
1342
+ if (data.authors !== void 0) {
1343
+ let data10 = data.authors;
1344
+ if (Array.isArray(data10)) {
1345
+ const len0 = data10.length;
1346
+ for (let i0 = 0; i0 < len0; i0++) {
1347
+ let data11 = data10[i0];
1348
+ if (data11 && typeof data11 == "object" && !Array.isArray(data11)) {
1349
+ if (data11.name === void 0) {
1350
+ const err21 = { instancePath: instancePath + "/authors/" + i0, schemaPath: "#/properties/authors/items/required", keyword: "required", params: { missingProperty: "name" }, message: "must have required property 'name'" };
1351
+ if (vErrors === null) {
1352
+ vErrors = [err21];
1353
+ } else {
1354
+ vErrors.push(err21);
1355
+ }
1356
+ errors++;
1357
+ }
1358
+ if (data11.name !== void 0) {
1359
+ let data12 = data11.name;
1360
+ if (typeof data12 === "string") {
1361
+ if (!pattern5.test(data12)) {
1362
+ const err22 = { instancePath: instancePath + "/authors/" + i0 + "/name", schemaPath: "#/properties/authors/items/properties/name/pattern", keyword: "pattern", params: { pattern: "\\S" }, message: 'must match pattern "\\S"' };
1363
+ if (vErrors === null) {
1364
+ vErrors = [err22];
1365
+ } else {
1366
+ vErrors.push(err22);
1367
+ }
1368
+ errors++;
1369
+ }
1370
+ } else {
1371
+ const err23 = { instancePath: instancePath + "/authors/" + i0 + "/name", schemaPath: "#/properties/authors/items/properties/name/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1372
+ if (vErrors === null) {
1373
+ vErrors = [err23];
1374
+ } else {
1375
+ vErrors.push(err23);
1376
+ }
1377
+ errors++;
1378
+ }
1379
+ }
1380
+ if (data11.url !== void 0) {
1381
+ if (typeof data11.url !== "string") {
1382
+ const err24 = { instancePath: instancePath + "/authors/" + i0 + "/url", schemaPath: "#/properties/authors/items/properties/url/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1383
+ if (vErrors === null) {
1384
+ vErrors = [err24];
1385
+ } else {
1386
+ vErrors.push(err24);
1387
+ }
1388
+ errors++;
1389
+ }
1390
+ }
1391
+ } else {
1392
+ const err25 = { instancePath: instancePath + "/authors/" + i0, schemaPath: "#/properties/authors/items/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1393
+ if (vErrors === null) {
1394
+ vErrors = [err25];
1395
+ } else {
1396
+ vErrors.push(err25);
1397
+ }
1398
+ errors++;
1399
+ }
1400
+ }
1401
+ } else {
1402
+ const err26 = { instancePath: instancePath + "/authors", schemaPath: "#/properties/authors/type", keyword: "type", params: { type: "array" }, message: "must be array" };
1403
+ if (vErrors === null) {
1404
+ vErrors = [err26];
1405
+ } else {
1406
+ vErrors.push(err26);
1407
+ }
1408
+ errors++;
1409
+ }
1410
+ }
1411
+ if (data.license !== void 0) {
1412
+ if (typeof data.license !== "string") {
1413
+ const err27 = { instancePath: instancePath + "/license", schemaPath: "#/properties/license/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1414
+ if (vErrors === null) {
1415
+ vErrors = [err27];
1416
+ } else {
1417
+ vErrors.push(err27);
1418
+ }
1419
+ errors++;
1420
+ }
1421
+ }
1422
+ if (data.presentation !== void 0) {
1423
+ let data15 = data.presentation;
1424
+ if (data15 && typeof data15 == "object" && !Array.isArray(data15)) {
1425
+ if (data15.basemap !== void 0) {
1426
+ let data16 = data15.basemap;
1427
+ if (!(data16 === "standard" || data16 === "historical" || data16 === "auto" || data16 === "none")) {
1428
+ const err28 = { instancePath: instancePath + "/presentation/basemap", schemaPath: "#/properties/presentation/properties/basemap/enum", keyword: "enum", params: { allowedValues: schema31.properties.presentation.properties.basemap.enum }, message: "must be equal to one of the allowed values" };
1429
+ if (vErrors === null) {
1430
+ vErrors = [err28];
1431
+ } else {
1432
+ vErrors.push(err28);
1433
+ }
1434
+ errors++;
1435
+ }
1436
+ }
1437
+ } else {
1438
+ const err29 = { instancePath: instancePath + "/presentation", schemaPath: "#/properties/presentation/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1439
+ if (vErrors === null) {
1440
+ vErrors = [err29];
1441
+ } else {
1442
+ vErrors.push(err29);
1443
+ }
1444
+ errors++;
1445
+ }
1446
+ }
1447
+ if (data.slides !== void 0) {
1448
+ let data17 = data.slides;
1449
+ if (Array.isArray(data17)) {
1450
+ if (data17.length < 1) {
1451
+ const err30 = { instancePath: instancePath + "/slides", schemaPath: "#/properties/slides/minItems", keyword: "minItems", params: { limit: 1 }, message: "must NOT have fewer than 1 items" };
1452
+ if (vErrors === null) {
1453
+ vErrors = [err30];
1454
+ } else {
1455
+ vErrors.push(err30);
1456
+ }
1457
+ errors++;
1458
+ }
1459
+ const len1 = data17.length;
1460
+ for (let i1 = 0; i1 < len1; i1++) {
1461
+ if (!validate21(data17[i1], { instancePath: instancePath + "/slides/" + i1, parentData: data17, parentDataProperty: i1, rootData, dynamicAnchors })) {
1462
+ vErrors = vErrors === null ? validate21.errors : vErrors.concat(validate21.errors);
1463
+ errors = vErrors.length;
1464
+ }
1465
+ }
1466
+ } else {
1467
+ const err31 = { instancePath: instancePath + "/slides", schemaPath: "#/properties/slides/type", keyword: "type", params: { type: "array" }, message: "must be array" };
1468
+ if (vErrors === null) {
1469
+ vErrors = [err31];
1470
+ } else {
1471
+ vErrors.push(err31);
1472
+ }
1473
+ errors++;
1474
+ }
1475
+ }
1476
+ if (data.media !== void 0) {
1477
+ let data19 = data.media;
1478
+ if (data19 && typeof data19 == "object" && !Array.isArray(data19)) {
1479
+ for (const key0 in data19) {
1480
+ const _errs38 = errors;
1481
+ if (typeof key0 === "string") {
1482
+ if (!pattern10.test(key0)) {
1483
+ const err32 = { instancePath: instancePath + "/media", schemaPath: "#/$defs/id/pattern", keyword: "pattern", params: { pattern: "^[A-Za-z0-9_-]{1,64}$" }, message: 'must match pattern "^[A-Za-z0-9_-]{1,64}$"', propertyName: key0 };
1484
+ if (vErrors === null) {
1485
+ vErrors = [err32];
1486
+ } else {
1487
+ vErrors.push(err32);
1488
+ }
1489
+ errors++;
1490
+ }
1491
+ } else {
1492
+ const err33 = { instancePath: instancePath + "/media", schemaPath: "#/$defs/id/type", keyword: "type", params: { type: "string" }, message: "must be string", propertyName: key0 };
1493
+ if (vErrors === null) {
1494
+ vErrors = [err33];
1495
+ } else {
1496
+ vErrors.push(err33);
1497
+ }
1498
+ errors++;
1499
+ }
1500
+ var valid8 = _errs38 === errors;
1501
+ if (!valid8) {
1502
+ const err34 = { instancePath: instancePath + "/media", schemaPath: "#/properties/media/propertyNames", keyword: "propertyNames", params: { propertyName: key0 }, message: "property name must be valid" };
1503
+ if (vErrors === null) {
1504
+ vErrors = [err34];
1505
+ } else {
1506
+ vErrors.push(err34);
1507
+ }
1508
+ errors++;
1509
+ }
1510
+ }
1511
+ for (const key1 in data19) {
1512
+ let data20 = data19[key1];
1513
+ if (data20 && typeof data20 == "object" && !Array.isArray(data20)) {
1514
+ if (data20.src === void 0) {
1515
+ const err35 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"), schemaPath: "#/$defs/mediaItem/required", keyword: "required", params: { missingProperty: "src" }, message: "must have required property 'src'" };
1516
+ if (vErrors === null) {
1517
+ vErrors = [err35];
1518
+ } else {
1519
+ vErrors.push(err35);
1520
+ }
1521
+ errors++;
1522
+ }
1523
+ if (data20.mime === void 0) {
1524
+ const err36 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"), schemaPath: "#/$defs/mediaItem/required", keyword: "required", params: { missingProperty: "mime" }, message: "must have required property 'mime'" };
1525
+ if (vErrors === null) {
1526
+ vErrors = [err36];
1527
+ } else {
1528
+ vErrors.push(err36);
1529
+ }
1530
+ errors++;
1531
+ }
1532
+ if (data20.src !== void 0) {
1533
+ let data21 = data20.src;
1534
+ if (typeof data21 === "string") {
1535
+ if (!pattern19.test(data21)) {
1536
+ const err37 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/src", schemaPath: "#/$defs/mediaItem/properties/src/pattern", keyword: "pattern", params: { pattern: "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+$" }, message: 'must match pattern "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[^\\\\]+$"' };
1537
+ if (vErrors === null) {
1538
+ vErrors = [err37];
1539
+ } else {
1540
+ vErrors.push(err37);
1541
+ }
1542
+ errors++;
1543
+ }
1544
+ } else {
1545
+ const err38 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/src", schemaPath: "#/$defs/mediaItem/properties/src/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1546
+ if (vErrors === null) {
1547
+ vErrors = [err38];
1548
+ } else {
1549
+ vErrors.push(err38);
1550
+ }
1551
+ errors++;
1552
+ }
1553
+ }
1554
+ if (data20.mime !== void 0) {
1555
+ let data22 = data20.mime;
1556
+ if (typeof data22 === "string") {
1557
+ if (!pattern20.test(data22)) {
1558
+ const err39 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/mime", schemaPath: "#/$defs/mediaItem/properties/mime/pattern", keyword: "pattern", params: { pattern: "^[a-z]+/[A-Za-z0-9.+-]+$" }, message: 'must match pattern "^[a-z]+/[A-Za-z0-9.+-]+$"' };
1559
+ if (vErrors === null) {
1560
+ vErrors = [err39];
1561
+ } else {
1562
+ vErrors.push(err39);
1563
+ }
1564
+ errors++;
1565
+ }
1566
+ } else {
1567
+ const err40 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/mime", schemaPath: "#/$defs/mediaItem/properties/mime/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1568
+ if (vErrors === null) {
1569
+ vErrors = [err40];
1570
+ } else {
1571
+ vErrors.push(err40);
1572
+ }
1573
+ errors++;
1574
+ }
1575
+ }
1576
+ if (data20.width !== void 0) {
1577
+ let data23 = data20.width;
1578
+ if (!(typeof data23 == "number" && (!(data23 % 1) && !isNaN(data23)) && isFinite(data23))) {
1579
+ const err41 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/width", schemaPath: "#/$defs/mediaItem/properties/width/type", keyword: "type", params: { type: "integer" }, message: "must be integer" };
1580
+ if (vErrors === null) {
1581
+ vErrors = [err41];
1582
+ } else {
1583
+ vErrors.push(err41);
1584
+ }
1585
+ errors++;
1586
+ }
1587
+ if (typeof data23 == "number" && isFinite(data23)) {
1588
+ if (data23 < 1 || isNaN(data23)) {
1589
+ const err42 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/width", schemaPath: "#/$defs/mediaItem/properties/width/minimum", keyword: "minimum", params: { comparison: ">=", limit: 1 }, message: "must be >= 1" };
1590
+ if (vErrors === null) {
1591
+ vErrors = [err42];
1592
+ } else {
1593
+ vErrors.push(err42);
1594
+ }
1595
+ errors++;
1596
+ }
1597
+ }
1598
+ }
1599
+ if (data20.height !== void 0) {
1600
+ let data24 = data20.height;
1601
+ if (!(typeof data24 == "number" && (!(data24 % 1) && !isNaN(data24)) && isFinite(data24))) {
1602
+ const err43 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/height", schemaPath: "#/$defs/mediaItem/properties/height/type", keyword: "type", params: { type: "integer" }, message: "must be integer" };
1603
+ if (vErrors === null) {
1604
+ vErrors = [err43];
1605
+ } else {
1606
+ vErrors.push(err43);
1607
+ }
1608
+ errors++;
1609
+ }
1610
+ if (typeof data24 == "number" && isFinite(data24)) {
1611
+ if (data24 < 1 || isNaN(data24)) {
1612
+ const err44 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/height", schemaPath: "#/$defs/mediaItem/properties/height/minimum", keyword: "minimum", params: { comparison: ">=", limit: 1 }, message: "must be >= 1" };
1613
+ if (vErrors === null) {
1614
+ vErrors = [err44];
1615
+ } else {
1616
+ vErrors.push(err44);
1617
+ }
1618
+ errors++;
1619
+ }
1620
+ }
1621
+ }
1622
+ if (data20.alt !== void 0) {
1623
+ if (typeof data20.alt !== "string") {
1624
+ const err45 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1") + "/alt", schemaPath: "#/$defs/mediaItem/properties/alt/type", keyword: "type", params: { type: "string" }, message: "must be string" };
1625
+ if (vErrors === null) {
1626
+ vErrors = [err45];
1627
+ } else {
1628
+ vErrors.push(err45);
1629
+ }
1630
+ errors++;
1631
+ }
1632
+ }
1633
+ } else {
1634
+ const err46 = { instancePath: instancePath + "/media/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"), schemaPath: "#/$defs/mediaItem/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1635
+ if (vErrors === null) {
1636
+ vErrors = [err46];
1637
+ } else {
1638
+ vErrors.push(err46);
1639
+ }
1640
+ errors++;
1641
+ }
1642
+ }
1643
+ } else {
1644
+ const err47 = { instancePath: instancePath + "/media", schemaPath: "#/properties/media/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1645
+ if (vErrors === null) {
1646
+ vErrors = [err47];
1647
+ } else {
1648
+ vErrors.push(err47);
1649
+ }
1650
+ errors++;
1651
+ }
1652
+ }
1653
+ if (data.extensions !== void 0) {
1654
+ let data26 = data.extensions;
1655
+ if (data26 && typeof data26 == "object" && !Array.isArray(data26)) {
1656
+ for (const key2 in data26) {
1657
+ let data27 = data26[key2];
1658
+ if (!(data27 && typeof data27 == "object" && !Array.isArray(data27))) {
1659
+ const err48 = { instancePath: instancePath + "/extensions/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"), schemaPath: "#/properties/extensions/additionalProperties/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1660
+ if (vErrors === null) {
1661
+ vErrors = [err48];
1662
+ } else {
1663
+ vErrors.push(err48);
1664
+ }
1665
+ errors++;
1666
+ }
1667
+ }
1668
+ } else {
1669
+ const err49 = { instancePath: instancePath + "/extensions", schemaPath: "#/properties/extensions/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1670
+ if (vErrors === null) {
1671
+ vErrors = [err49];
1672
+ } else {
1673
+ vErrors.push(err49);
1674
+ }
1675
+ errors++;
1676
+ }
1677
+ }
1678
+ } else {
1679
+ const err50 = { instancePath, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" };
1680
+ if (vErrors === null) {
1681
+ vErrors = [err50];
1682
+ } else {
1683
+ vErrors.push(err50);
1684
+ }
1685
+ errors++;
1686
+ }
1687
+ validate20.errors = vErrors;
1688
+ return errors === 0;
1689
+ }
1690
+ validate20.evaluated = { "props": { "$schema": true, "format": true, "version": true, "title": true, "language": true, "created": true, "generator": true, "authors": true, "license": true, "presentation": true, "slides": true, "media": true, "extensions": true }, "dynamicProps": false, "dynamicItems": false };
1691
+
1692
+ // src/version.ts
1693
+ var FORMAT_ID = "gramps-connect-story";
1694
+ var FORMAT_VERSION = "1.0";
1695
+ var CURRENT_MAJOR = 1;
1696
+ var UPGRADERS = {};
1697
+ var SUPPORTED_FORMAT_MAJORS = Object.freeze(
1698
+ Array.from({ length: CURRENT_MAJOR }, (_, i) => i + 1).filter(
1699
+ (major) => major === CURRENT_MAJOR || UPGRADERS[major] !== void 0
1700
+ )
1701
+ );
1702
+ function parseVersion(version) {
1703
+ if (typeof version !== "string") return null;
1704
+ const match = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(version);
1705
+ if (!match) return null;
1706
+ return { major: Number(match[1]), minor: Number(match[2]) };
1707
+ }
1708
+
1709
+ // src/validate.ts
1710
+ var SCHEMA_MINOR = 0;
1711
+ function validate2(doc, files) {
1712
+ const errors = [];
1713
+ const warnings = [];
1714
+ if (!validate(doc)) {
1715
+ for (const err of validate.errors ?? []) {
1716
+ errors.push({ path: err.instancePath, message: describeSchemaError(err) });
1717
+ }
1718
+ return { valid: false, errors, warnings };
1719
+ }
1720
+ const story = doc;
1721
+ const slideIds = /* @__PURE__ */ new Set();
1722
+ const usedMedia2 = /* @__PURE__ */ new Set();
1723
+ const checkRef = (key, path) => {
1724
+ usedMedia2.add(key);
1725
+ if (!Object.prototype.hasOwnProperty.call(story.media, key)) {
1726
+ errors.push({ path, message: `refers to media "${key}", which isn't in "media".` });
1727
+ }
1728
+ };
1729
+ story.slides.forEach((slide, i) => {
1730
+ if (slideIds.has(slide.id)) {
1731
+ errors.push({ path: `/slides/${i}/id`, message: `duplicate slide id "${slide.id}".` });
1732
+ }
1733
+ slideIds.add(slide.id);
1734
+ slide.media?.forEach((m, j) => checkRef(m.ref, `/slides/${i}/media/${j}/ref`));
1735
+ slide.place?.overlays?.forEach((o, j) => checkRef(o.media, `/slides/${i}/place/overlays/${j}/media`));
1736
+ });
1737
+ for (const [key, item] of Object.entries(story.media)) {
1738
+ const path = `/media/${escapePointer(key)}`;
1739
+ if (files && !files.has(item.src)) {
1740
+ errors.push({ path: `${path}/src`, message: `file "${item.src}" is not in the zip.` });
1741
+ }
1742
+ if (!usedMedia2.has(key)) {
1743
+ warnings.push({ path, message: `media "${key}" isn't used by any slide or overlay.` });
1744
+ }
1745
+ }
1746
+ const version = parseVersion(story.version);
1747
+ if (version && version.major === CURRENT_MAJOR && version.minor <= SCHEMA_MINOR) {
1748
+ findUnknownFields(story, story_schema_default, "", warnings);
1749
+ }
1750
+ return { valid: errors.length === 0, errors, warnings };
1751
+ }
1752
+ function describeSchemaError(err) {
1753
+ if (err.keyword === "pattern" && err.params.pattern === "\\S") return "must not be empty.";
1754
+ if (err.keyword === "required") return `is missing required field "${String(err.params.missingProperty)}".`;
1755
+ if (err.keyword === "const") return `must be ${JSON.stringify(err.params.allowedValue)}.`;
1756
+ if (err.keyword === "enum") return `must be one of ${err.params.allowedValues.map((v) => JSON.stringify(v)).join(", ")}.`;
1757
+ return `${err.message ?? "is invalid"}.`;
1758
+ }
1759
+ function escapePointer(key) {
1760
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
1761
+ }
1762
+ function resolveRef(node, root) {
1763
+ if (!node.$ref) return node;
1764
+ const name = node.$ref.replace(/^#\/\$defs\//, "");
1765
+ return root.$defs[name];
1766
+ }
1767
+ function findUnknownFields(value, node, path, warnings, root = story_schema_default) {
1768
+ node = resolveRef(node, root);
1769
+ if (Array.isArray(value)) {
1770
+ value.forEach((item, i) => {
1771
+ const itemNode = node.prefixItems?.[i] ?? node.items;
1772
+ if (itemNode) findUnknownFields(item, itemNode, `${path}/${i}`, warnings, root);
1773
+ });
1774
+ return;
1775
+ }
1776
+ if (typeof value !== "object" || value === null) return;
1777
+ for (const [key, child] of Object.entries(value)) {
1778
+ const childPath = `${path}/${escapePointer(key)}`;
1779
+ const known = node.properties?.[key];
1780
+ if (known) {
1781
+ findUnknownFields(child, known, childPath, warnings, root);
1782
+ } else if (typeof node.additionalProperties === "object") {
1783
+ findUnknownFields(child, node.additionalProperties, childPath, warnings, root);
1784
+ } else if (node.properties && node.additionalProperties !== true) {
1785
+ warnings.push({ path: childPath, message: `unknown field "${key}" (ignored).` });
1786
+ }
1787
+ }
1788
+ }
1789
+
1790
+ // src/player/StoryPlayer.tsx
1791
+ import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
1792
+
1793
+ // src/player/dates.ts
1794
+ var DATE = /^(-?\d{4})(?:-(\d{2}|XX))?(?:-(\d{2}|XX))?[~?%]?$/;
1795
+ function yearOf(part) {
1796
+ const match = DATE.exec(part.trim());
1797
+ return match ? Number(match[1]) : null;
1798
+ }
1799
+ function edtfYearRange(edtf) {
1800
+ if (!edtf) return null;
1801
+ const value = edtf.trim();
1802
+ const set = /^\[(.+)\.\.(.+)\]$/.exec(value);
1803
+ if (set) {
1804
+ const min = yearOf(set[1]);
1805
+ const max = yearOf(set[2]);
1806
+ return min !== null && max !== null ? { min: Math.min(min, max), max: Math.max(min, max) } : null;
1807
+ }
1808
+ if (value.includes("/")) {
1809
+ const [startText, endText] = value.split("/");
1810
+ const openStart = startText === ".." || startText === "";
1811
+ const openEnd = endText === ".." || endText === "";
1812
+ const start = openStart ? void 0 : yearOf(startText);
1813
+ const end = openEnd ? void 0 : yearOf(endText);
1814
+ if (start === null || end === null) return null;
1815
+ if (start === void 0 && end === void 0) return null;
1816
+ if (start !== void 0 && end !== void 0) return { min: Math.min(start, end), max: Math.max(start, end) };
1817
+ return { min: start, max: end };
1818
+ }
1819
+ const year = yearOf(value);
1820
+ return year === null ? null : { min: year, max: year };
1821
+ }
1822
+ function dateCoversYear(date, year) {
1823
+ if (!date || year === null) return true;
1824
+ const range = edtfYearRange(date.edtf);
1825
+ if (!range) return true;
1826
+ if (range.min !== void 0 && year < range.min) return false;
1827
+ if (range.max !== void 0 && year > range.max) return false;
1828
+ return true;
1829
+ }
1830
+ function timelineYear(date) {
1831
+ if (!date) return null;
1832
+ const match = date.edtf ? DATE.exec(date.edtf.trim()) : null;
1833
+ const sortYear = date.sortYear ?? (match ? Number(match[1]) : void 0);
1834
+ if (sortYear === void 0) return null;
1835
+ if (!match || Number(match[1]) !== sortYear) return sortYear;
1836
+ const month = match[2] && match[2] !== "XX" ? Number(match[2]) : 0;
1837
+ const day = match[3] && match[3] !== "XX" ? Number(match[3]) : 0;
1838
+ if (!month) return sortYear;
1839
+ return sortYear + (month - 1) / 12 + (day ? (day - 1) / 365 : 0);
1840
+ }
1841
+
1842
+ // src/player/geo.ts
1843
+ function visit(coords, bounds) {
1844
+ if (!Array.isArray(coords)) return;
1845
+ if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
1846
+ const [lon, lat] = coords;
1847
+ bounds[0] = Math.min(bounds[0], lon);
1848
+ bounds[1] = Math.min(bounds[1], lat);
1849
+ bounds[2] = Math.max(bounds[2], lon);
1850
+ bounds[3] = Math.max(bounds[3], lat);
1851
+ return;
1852
+ }
1853
+ for (const child of coords) visit(child, bounds);
1854
+ }
1855
+ function visitGeoJson(node, bounds) {
1856
+ if (!node || typeof node !== "object") return;
1857
+ const obj = node;
1858
+ if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
1859
+ if (obj.geometry) visitGeoJson(obj.geometry, bounds);
1860
+ for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
1861
+ for (const f of obj.features ?? []) visitGeoJson(f, bounds);
1862
+ }
1863
+ function placeBounds(place) {
1864
+ if (!place) return null;
1865
+ const bounds = [Infinity, Infinity, -Infinity, -Infinity];
1866
+ visitGeoJson(place.geometry, bounds);
1867
+ for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
1868
+ return Number.isFinite(bounds[0]) ? bounds : null;
1869
+ }
1870
+ function placeCenter(place) {
1871
+ if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
1872
+ const bounds = placeBounds(place);
1873
+ return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
1874
+ }
1875
+ function storyPlaces(places) {
1876
+ const bounds = [Infinity, Infinity, -Infinity, -Infinity];
1877
+ const points = /* @__PURE__ */ new Map();
1878
+ for (const place of places) {
1879
+ const center = placeCenter(place);
1880
+ if (!center) continue;
1881
+ points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
1882
+ visit(center, bounds);
1883
+ const box = placeBounds(place);
1884
+ if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
1885
+ }
1886
+ return points.size ? { points: [...points.values()], bounds } : null;
1887
+ }
1888
+
1889
+ // src/player/MapBackground.tsx
1890
+ import { useEffect, useRef, useState } from "react";
1891
+ import * as maplibregl from "maplibre-gl";
1892
+ import "maplibre-gl/dist/maplibre-gl.css";
1893
+
1894
+ // src/player/mapStyles.ts
1895
+ import { filterByDate } from "@openhistoricalmap/maplibre-gl-dates";
1896
+ var STYLE_LIGHT = "https://tiles.openfreemap.org/styles/liberty";
1897
+ var STYLE_DARK = "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json";
1898
+ var OHM_STYLE = "https://www.openhistoricalmap.org/map-styles/main/main.json";
1899
+ function mapStyleKey(dark, ohmYear) {
1900
+ return ohmYear != null ? "ohm" : dark ? "dark" : "light";
1901
+ }
1902
+ function mapStyleUrl(dark, ohmYear) {
1903
+ return ohmYear != null ? OHM_STYLE : dark ? STYLE_DARK : STYLE_LIGHT;
1904
+ }
1905
+ function crossfadeStyleSwap(map, styleUrl, beforeSwap, duration = 300) {
1906
+ const container = map.getContainer();
1907
+ if (!container.style.zIndex) container.style.zIndex = "0";
1908
+ let snapshotUrl = null;
1909
+ try {
1910
+ snapshotUrl = map.getCanvas().toDataURL("image/png");
1911
+ } catch {
1912
+ }
1913
+ beforeSwap();
1914
+ map.setStyle(styleUrl);
1915
+ if (!snapshotUrl) return;
1916
+ const overlay = document.createElement("img");
1917
+ overlay.src = snapshotUrl;
1918
+ overlay.alt = "";
1919
+ Object.assign(overlay.style, {
1920
+ position: "absolute",
1921
+ inset: "0",
1922
+ width: "100%",
1923
+ height: "100%",
1924
+ zIndex: "5",
1925
+ pointerEvents: "none",
1926
+ transition: `opacity ${duration}ms ease`
1927
+ });
1928
+ container.appendChild(overlay);
1929
+ map.once("idle", () => {
1930
+ requestAnimationFrame(() => {
1931
+ overlay.style.opacity = "0";
1932
+ });
1933
+ window.setTimeout(() => overlay.remove(), duration + 50);
1934
+ });
1935
+ }
1936
+ function applyOhmYear(map, year) {
1937
+ filterByDate(map, String(Math.round(year)));
1938
+ }
1939
+
1940
+ // src/player/theme.ts
1941
+ function defaultColors(dark) {
1942
+ return dark ? { surface: "#242424", text: "#c9c9c9", muted: "#828282", border: "#424242", mark: "#3987e5", panel: "rgb(20,20,20)" } : { surface: "#ffffff", text: "#000000", muted: "#868e96", border: "#dee2e6", mark: "#2a78d6", panel: "rgb(255,255,255)" };
1943
+ }
1944
+ var PIN_PATH_D = "M12 0C7.03 0 3 4.03 3 9c0 6.25 9 15 9 15s9-8.75 9-15c0-4.97-4.03-9-9-9z";
1945
+
1946
+ // src/player/MapBackground.tsx
1947
+ import { jsx } from "react/jsx-runtime";
1948
+ var SHAPE_SOURCE = "story-shape";
1949
+ var FILL_LAYER = "story-shape-fill";
1950
+ var LINE_LAYER = "story-shape-line";
1951
+ var POINT_LAYER = "story-shape-points";
1952
+ var LABEL_LAYER = "story-shape-labels";
1953
+ var OVERLAY_PREFIX = "story-overlay-";
1954
+ var PLACES_SOURCE = "story-places";
1955
+ var PLACES_LAYER = "story-places-dots";
1956
+ var OVERVIEW_PADDING = 60;
1957
+ var EMPTY = { type: "FeatureCollection", features: [] };
1958
+ function moveCamera(map, slide, duration) {
1959
+ if (slide.overview) {
1960
+ const [west, south, east, north] = slide.overview.bounds;
1961
+ map.fitBounds([[west, south], [east, north]], { maxZoom: 9, padding: OVERVIEW_PADDING, duration });
1962
+ return;
1963
+ }
1964
+ const place = slide.place;
1965
+ const bounds = placeBounds(place);
1966
+ if (bounds) {
1967
+ map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { maxZoom: 17, duration });
1968
+ return;
1969
+ }
1970
+ const center = placeCenter(place);
1971
+ if (center) map.flyTo({ center, zoom: 9, essential: true, duration });
1972
+ }
1973
+ function placeMarker(marker, slide) {
1974
+ if (!marker) return;
1975
+ const center = placeCenter(slide.place);
1976
+ if (center) marker.setLngLat(center);
1977
+ marker.setOpacity(slide.overview ? 0 : center ? 1 : 0.4);
1978
+ }
1979
+ var placeDots = (overview) => ({
1980
+ type: "FeatureCollection",
1981
+ features: (overview?.points ?? []).map((coordinates) => ({
1982
+ type: "Feature",
1983
+ properties: {},
1984
+ geometry: { type: "Point", coordinates }
1985
+ }))
1986
+ });
1987
+ function MapBackground({ initialCenter, current, dark, colors, panelFraction, ohmYear, resolveMedia }) {
1988
+ const [containerEl, setContainerEl] = useState(null);
1989
+ const mapRef = useRef(null);
1990
+ const markerRef = useRef(null);
1991
+ const [ready, setReady] = useState(false);
1992
+ const darkRef = useRef(dark);
1993
+ darkRef.current = dark;
1994
+ const ohmYearRef = useRef(ohmYear);
1995
+ ohmYearRef.current = ohmYear;
1996
+ const colorsRef = useRef(colors);
1997
+ colorsRef.current = colors;
1998
+ const overlayDatesRef = useRef(/* @__PURE__ */ new Map());
1999
+ const appliedStyleKeyRef = useRef(mapStyleKey(dark, ohmYear));
2000
+ const currentRef = useRef(current);
2001
+ currentRef.current = current;
2002
+ useEffect(() => {
2003
+ if (!containerEl) return;
2004
+ setReady(false);
2005
+ appliedStyleKeyRef.current = mapStyleKey(darkRef.current, ohmYearRef.current);
2006
+ const map = new maplibregl.Map({
2007
+ container: containerEl,
2008
+ style: mapStyleUrl(darkRef.current, ohmYearRef.current),
2009
+ zoom: 5,
2010
+ attributionControl: { compact: true },
2011
+ // So crossfadeStyleSwap's snapshot has the last frame in it.
2012
+ canvasContextAttributes: { preserveDrawingBuffer: true }
2013
+ });
2014
+ mapRef.current = map;
2015
+ map.on("style.load", () => setReady(true));
2016
+ map.setPadding({ top: 0, bottom: 0, left: 0, right: containerEl.clientWidth * panelFraction });
2017
+ const start = currentRef.current;
2018
+ map.jumpTo({ center: placeCenter(start.place) ?? initialCenter });
2019
+ moveCamera(map, start, 0);
2020
+ map.on("load", () => {
2021
+ const c = colorsRef.current;
2022
+ const el = document.createElement("div");
2023
+ el.style.width = "26px";
2024
+ el.style.height = "26px";
2025
+ el.style.transition = "opacity 300ms";
2026
+ el.innerHTML = `<svg viewBox="0 0 24 24" width="26" height="26"><path d="${PIN_PATH_D}" fill="${c.mark}" stroke="${c.surface}" stroke-width="1.5" /></svg>`;
2027
+ markerRef.current = new maplibregl.Marker({ element: el, anchor: "bottom" }).setLngLat(initialCenter).addTo(map);
2028
+ placeMarker(markerRef.current, currentRef.current);
2029
+ });
2030
+ return () => {
2031
+ markerRef.current?.remove();
2032
+ markerRef.current = null;
2033
+ map.remove();
2034
+ mapRef.current = null;
2035
+ };
2036
+ }, [containerEl]);
2037
+ useEffect(() => {
2038
+ const map = mapRef.current;
2039
+ const key = mapStyleKey(dark, ohmYear);
2040
+ if (!map || appliedStyleKeyRef.current === key) return;
2041
+ appliedStyleKeyRef.current = key;
2042
+ crossfadeStyleSwap(map, mapStyleUrl(dark, ohmYear), () => setReady(false));
2043
+ }, [dark, ohmYear]);
2044
+ useEffect(() => {
2045
+ const map = mapRef.current;
2046
+ if (!map || !ready || ohmYear == null) return;
2047
+ applyOhmYear(map, ohmYear);
2048
+ }, [ready, ohmYear]);
2049
+ useEffect(() => {
2050
+ const map = mapRef.current;
2051
+ if (!map || !ready || map.getSource(SHAPE_SOURCE)) return;
2052
+ const c = colorsRef.current;
2053
+ map.addSource(SHAPE_SOURCE, { type: "geojson", data: EMPTY });
2054
+ const color = ["coalesce", ["get", "color"], c.mark];
2055
+ map.addLayer({
2056
+ id: FILL_LAYER,
2057
+ type: "fill",
2058
+ source: SHAPE_SOURCE,
2059
+ filter: ["==", ["geometry-type"], "Polygon"],
2060
+ paint: { "fill-color": color, "fill-opacity": 0.25 }
2061
+ });
2062
+ map.addLayer({
2063
+ id: LINE_LAYER,
2064
+ type: "line",
2065
+ source: SHAPE_SOURCE,
2066
+ filter: ["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "LineString"]],
2067
+ paint: { "line-color": color, "line-width": 2 }
2068
+ });
2069
+ map.addLayer({
2070
+ id: POINT_LAYER,
2071
+ type: "circle",
2072
+ source: SHAPE_SOURCE,
2073
+ // A named point is a label (next layer), drawn as text only.
2074
+ filter: ["all", ["==", ["geometry-type"], "Point"], ["!", ["has", "name"]]],
2075
+ paint: { "circle-radius": 5, "circle-color": color, "circle-stroke-width": 1, "circle-stroke-color": c.surface }
2076
+ });
2077
+ map.addLayer({
2078
+ id: LABEL_LAYER,
2079
+ type: "symbol",
2080
+ source: SHAPE_SOURCE,
2081
+ filter: ["all", ["==", ["geometry-type"], "Point"], ["has", "name"]],
2082
+ layout: {
2083
+ "text-field": ["get", "name"],
2084
+ "text-size": 12,
2085
+ "text-font": ["Noto Sans Regular"],
2086
+ "text-anchor": "left",
2087
+ "text-offset": [0.6, 0]
2088
+ },
2089
+ paint: {
2090
+ "text-color": ["coalesce", ["get", "color"], c.text],
2091
+ "text-halo-color": c.surface,
2092
+ "text-halo-width": 1.5
2093
+ }
2094
+ });
2095
+ map.addSource(PLACES_SOURCE, { type: "geojson", data: EMPTY });
2096
+ map.addLayer({
2097
+ id: PLACES_LAYER,
2098
+ type: "circle",
2099
+ source: PLACES_SOURCE,
2100
+ paint: { "circle-radius": 6, "circle-color": c.mark, "circle-stroke-width": 2, "circle-stroke-color": c.surface }
2101
+ });
2102
+ }, [ready]);
2103
+ const place = current.place;
2104
+ useEffect(() => {
2105
+ const map = mapRef.current;
2106
+ if (!map || !ready) return;
2107
+ const source = map.getSource(SHAPE_SOURCE);
2108
+ source?.setData(place?.geometry ?? EMPTY);
2109
+ const dots = map.getSource(PLACES_SOURCE);
2110
+ dots?.setData(placeDots(current.overview));
2111
+ }, [ready, current.key, place, current.overview]);
2112
+ useEffect(() => {
2113
+ const map = mapRef.current;
2114
+ if (!map) return;
2115
+ placeMarker(markerRef.current, current);
2116
+ moveCamera(map, current, 900);
2117
+ }, [current.key]);
2118
+ const overlayKey = JSON.stringify(place?.overlays ?? []);
2119
+ useEffect(() => {
2120
+ const map = mapRef.current;
2121
+ const overlays = place?.overlays ?? [];
2122
+ if (!map || !ready || overlays.length === 0) return;
2123
+ let cancelled = false;
2124
+ const added = [];
2125
+ (async () => {
2126
+ const sorted = overlays.map((overlay, i) => ({ overlay, i })).sort((a, b) => (a.overlay.order ?? a.i) - (b.overlay.order ?? b.i));
2127
+ for (const { overlay, i } of sorted) {
2128
+ const url = await resolveMedia(overlay.media);
2129
+ if (cancelled) return;
2130
+ if (!url) continue;
2131
+ const id = `${OVERLAY_PREFIX}${i}`;
2132
+ if (map.getSource(id)) continue;
2133
+ map.addSource(id, { type: "image", url, coordinates: overlay.corners });
2134
+ map.addLayer({ id, type: "raster", source: id }, FILL_LAYER);
2135
+ map.setPaintProperty(id, "raster-opacity", overlay.opacity ?? 1);
2136
+ overlayDatesRef.current.set(id, overlay.date);
2137
+ map.setLayoutProperty(id, "visibility", dateCoversYear(overlay.date, ohmYearRef.current) ? "visible" : "none");
2138
+ added.push(id);
2139
+ }
2140
+ })();
2141
+ return () => {
2142
+ cancelled = true;
2143
+ for (const id of added) {
2144
+ overlayDatesRef.current.delete(id);
2145
+ if (map.getLayer(id)) map.removeLayer(id);
2146
+ if (map.getSource(id)) map.removeSource(id);
2147
+ }
2148
+ };
2149
+ }, [overlayKey, ready]);
2150
+ useEffect(() => {
2151
+ const map = mapRef.current;
2152
+ if (!map || !ready) return;
2153
+ for (const [id, date] of overlayDatesRef.current) {
2154
+ if (!map.getLayer(id)) continue;
2155
+ map.setLayoutProperty(id, "visibility", dateCoversYear(date, ohmYear) ? "visible" : "none");
2156
+ }
2157
+ }, [ohmYear, ready]);
2158
+ return /* @__PURE__ */ jsx("div", { ref: setContainerEl, style: { position: "absolute", inset: 0 } });
2159
+ }
2160
+
2161
+ // src/player/MapModeControl.tsx
2162
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
2163
+ function MapModeControl({ mode, onModeChange, autoYear, year, onYearChange, yearBounds, strings, colors }) {
2164
+ const modes = [["auto", strings.auto], ["standard", strings.standard], ["historical", strings.historical]];
2165
+ const font = { fontSize: 12, fontFamily: "inherit" };
2166
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, whiteSpace: "nowrap" }, children: [
2167
+ /* @__PURE__ */ jsx2("div", { role: "radiogroup", "aria-label": strings.mapMode, style: { display: "flex", border: `1px solid ${colors.border}`, borderRadius: 6, overflow: "hidden", background: colors.surface }, children: modes.map(([value, label]) => /* @__PURE__ */ jsx2(
2168
+ "button",
2169
+ {
2170
+ type: "button",
2171
+ role: "radio",
2172
+ "aria-checked": mode === value,
2173
+ onClick: () => onModeChange(value),
2174
+ style: {
2175
+ ...font,
2176
+ padding: "4px 10px",
2177
+ border: "none",
2178
+ cursor: "pointer",
2179
+ background: mode === value ? colors.mark : "transparent",
2180
+ color: mode === value ? "#ffffff" : colors.text
2181
+ },
2182
+ children: label
2183
+ },
2184
+ value
2185
+ )) }),
2186
+ mode === "auto" && /* @__PURE__ */ jsx2("span", { style: { ...font, color: colors.muted }, children: autoYear != null ? strings.historicalMapYear(autoYear) : strings.noDateStandardMap }),
2187
+ mode === "historical" && /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2188
+ /* @__PURE__ */ jsx2(
2189
+ "input",
2190
+ {
2191
+ type: "range",
2192
+ min: yearBounds[0],
2193
+ max: yearBounds[1],
2194
+ step: 1,
2195
+ value: year,
2196
+ onChange: (e) => onYearChange(Number(e.currentTarget.value)),
2197
+ "aria-label": strings.historicalYear,
2198
+ style: { width: 150, accentColor: colors.mark }
2199
+ }
2200
+ ),
2201
+ /* @__PURE__ */ jsx2(
2202
+ "input",
2203
+ {
2204
+ type: "number",
2205
+ min: yearBounds[0],
2206
+ max: yearBounds[1],
2207
+ step: 1,
2208
+ value: year,
2209
+ onChange: (e) => {
2210
+ const value = Number(e.currentTarget.value);
2211
+ if (Number.isFinite(value) && e.currentTarget.value !== "") onYearChange(Math.round(value));
2212
+ },
2213
+ "aria-label": strings.historicalYear,
2214
+ style: { ...font, width: 70, padding: "3px 4px", border: `1px solid ${colors.border}`, borderRadius: 4, background: colors.surface, color: colors.text }
2215
+ }
2216
+ )
2217
+ ] })
2218
+ ] });
2219
+ }
2220
+
2221
+ // src/player/text.ts
2222
+ var MARKERS = /\*\*([^\n]+?)\*\*|\*([^\n]+?)\*/g;
2223
+ function parseLine(line) {
2224
+ const spans = [];
2225
+ let last = 0;
2226
+ for (const match of line.matchAll(MARKERS)) {
2227
+ const index = match.index ?? 0;
2228
+ if (index > last) spans.push({ text: line.slice(last, index) });
2229
+ if (match[1] !== void 0) spans.push({ text: match[1], bold: true });
2230
+ else spans.push({ text: match[2], italic: true });
2231
+ last = index + match[0].length;
2232
+ }
2233
+ if (last < line.length) spans.push({ text: line.slice(last) });
2234
+ return spans;
2235
+ }
2236
+ function parseStoryText(text) {
2237
+ return text.replace(/\r\n?/g, "\n").split(/\n[ \t]*\n+/).map((paragraph) => paragraph.replace(/^\n+|\n+$/g, "")).filter((paragraph) => paragraph.length > 0).map((paragraph) => paragraph.split("\n").map(parseLine));
2238
+ }
2239
+
2240
+ // src/player/strings.ts
2241
+ var ENGLISH = {
2242
+ previous: "Previous",
2243
+ next: "Next",
2244
+ close: "Close",
2245
+ mapMode: "Map",
2246
+ auto: "Auto",
2247
+ standard: "Standard",
2248
+ historical: "Historical",
2249
+ historicalMapYear: (year) => `Historical map, ${year}`,
2250
+ noDateStandardMap: "No date here \u2014 standard map",
2251
+ historicalYear: "Historical map year",
2252
+ photoOf: (n, total) => `Photo ${n} of ${total}`
2253
+ };
2254
+ var TABLE = {
2255
+ en: ENGLISH,
2256
+ de: {
2257
+ previous: "Zur\xFCck",
2258
+ next: "Weiter",
2259
+ close: "Schlie\xDFen",
2260
+ mapMode: "Karte",
2261
+ auto: "Automatisch",
2262
+ standard: "Standard",
2263
+ historical: "Historisch",
2264
+ historicalMapYear: (year) => `Historische Karte, ${year}`,
2265
+ noDateStandardMap: "Kein Datum hier \u2014 Standardkarte",
2266
+ historicalYear: "Jahr der historischen Karte",
2267
+ photoOf: (n, total) => `Foto ${n} von ${total}`
2268
+ },
2269
+ es: {
2270
+ previous: "Anterior",
2271
+ next: "Siguiente",
2272
+ close: "Cerrar",
2273
+ mapMode: "Mapa",
2274
+ auto: "Autom\xE1tico",
2275
+ standard: "Est\xE1ndar",
2276
+ historical: "Hist\xF3rico",
2277
+ historicalMapYear: (year) => `Mapa hist\xF3rico, ${year}`,
2278
+ noDateStandardMap: "Sin fecha aqu\xED \u2014 mapa est\xE1ndar",
2279
+ historicalYear: "A\xF1o del mapa hist\xF3rico",
2280
+ photoOf: (n, total) => `Foto ${n} de ${total}`
2281
+ },
2282
+ fr: {
2283
+ previous: "Pr\xE9c\xE9dent",
2284
+ next: "Suivant",
2285
+ close: "Fermer",
2286
+ mapMode: "Carte",
2287
+ auto: "Auto",
2288
+ standard: "Standard",
2289
+ historical: "Historique",
2290
+ historicalMapYear: (year) => `Carte historique, ${year}`,
2291
+ noDateStandardMap: "Pas de date ici \u2014 carte standard",
2292
+ historicalYear: "Ann\xE9e de la carte historique",
2293
+ photoOf: (n, total) => `Photo ${n} sur ${total}`
2294
+ },
2295
+ nl: {
2296
+ previous: "Vorige",
2297
+ next: "Volgende",
2298
+ close: "Sluiten",
2299
+ mapMode: "Kaart",
2300
+ auto: "Automatisch",
2301
+ standard: "Standaard",
2302
+ historical: "Historisch",
2303
+ historicalMapYear: (year) => `Historische kaart, ${year}`,
2304
+ noDateStandardMap: "Geen datum hier \u2014 standaardkaart",
2305
+ historicalYear: "Jaar van de historische kaart",
2306
+ photoOf: (n, total) => `Foto ${n} van ${total}`
2307
+ }
2308
+ };
2309
+ function stringsFor(...languages) {
2310
+ for (const tag of languages) {
2311
+ const base = tag?.toLowerCase().split(/[-_]/)[0];
2312
+ if (base && TABLE[base]) return TABLE[base];
2313
+ }
2314
+ return ENGLISH;
2315
+ }
2316
+ var PLAYER_LANGUAGES = Object.keys(TABLE);
2317
+
2318
+ // src/player/TimelineStrip.tsx
2319
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
2320
+
2321
+ // src/player/timelineLayout.ts
2322
+ function storyDomain(years) {
2323
+ const min = Math.min(...years);
2324
+ const max = Math.max(...years);
2325
+ if (max - min < 1) return [min - 5, min + 5];
2326
+ const padding = (max - min) * 0.05;
2327
+ return [min - padding, max + padding];
2328
+ }
2329
+ function xForYear(year, domain, width) {
2330
+ const [start, end] = domain;
2331
+ const span = end - start;
2332
+ if (!(span > 0)) return width / 2;
2333
+ return (year - start) / span * width;
2334
+ }
2335
+ var TICK_STEPS = [1e3, 500, 250, 100, 50, 25, 10, 5, 2, 1];
2336
+ var TARGET_TICKS = 8;
2337
+ function axisTicks(domain, width) {
2338
+ const [start, end] = domain;
2339
+ const span = end - start;
2340
+ if (!(span > 0) || width <= 0) return [];
2341
+ const step = TICK_STEPS.find((candidate) => span / candidate >= TARGET_TICKS) ?? 1;
2342
+ const ticks = [];
2343
+ for (let year = Math.ceil(start / step) * step; year < end; year += step) {
2344
+ ticks.push({ x: (year - start) / span * width, label: tickLabel(year) });
2345
+ }
2346
+ return ticks;
2347
+ }
2348
+ function tickLabel(year) {
2349
+ const whole = Math.floor(year);
2350
+ return whole < 0 ? `${-whole} BC` : String(whole);
2351
+ }
2352
+
2353
+ // src/player/TimelineStrip.tsx
2354
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2355
+ var TIMELINE_HEIGHT = 70;
2356
+ var MARGIN = 28;
2357
+ var AXIS_Y = 42;
2358
+ var LABEL_Y = 62;
2359
+ var CURRENT_SCALE = 0.95;
2360
+ function TimelineStrip({ points, currentIndex, onSelect, colors }) {
2361
+ const containerRef = useRef2(null);
2362
+ const [width, setWidth] = useState2(0);
2363
+ useEffect2(() => {
2364
+ const el = containerRef.current;
2365
+ if (!el) return;
2366
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
2367
+ observer.observe(el);
2368
+ return () => observer.disconnect();
2369
+ }, []);
2370
+ const plotWidth = Math.max(0, width - MARGIN * 2);
2371
+ const domain = storyDomain(points.map((p) => p.year));
2372
+ const ticks = plotWidth > 0 ? axisTicks(domain, plotWidth) : [];
2373
+ return /* @__PURE__ */ jsx3(
2374
+ "div",
2375
+ {
2376
+ ref: containerRef,
2377
+ style: {
2378
+ position: "absolute",
2379
+ left: 0,
2380
+ right: 0,
2381
+ bottom: 0,
2382
+ height: TIMELINE_HEIGHT,
2383
+ background: colors.surface,
2384
+ // Above the slide panel, whose fading background would otherwise
2385
+ // wash out any dot beneath it.
2386
+ zIndex: 2
2387
+ },
2388
+ children: width > 0 && /* @__PURE__ */ jsxs2("svg", { width, height: TIMELINE_HEIGHT, style: { display: "block", overflow: "visible" }, children: [
2389
+ /* @__PURE__ */ jsx3("line", { x1: MARGIN, y1: AXIS_Y, x2: width - MARGIN, y2: AXIS_Y, stroke: colors.border, strokeWidth: 1 }),
2390
+ ticks.map((tick) => /* @__PURE__ */ jsx3("text", { x: MARGIN + tick.x, y: LABEL_Y, fontSize: 15, fill: colors.muted, textAnchor: "middle", children: tick.label }, tick.x)),
2391
+ points.map((point) => {
2392
+ const cx = MARGIN + xForYear(point.year, domain, plotWidth);
2393
+ const current = point.index === currentIndex;
2394
+ return current ? /* @__PURE__ */ jsx3(
2395
+ "path",
2396
+ {
2397
+ d: PIN_PATH_D,
2398
+ transform: `translate(${cx - 12 * CURRENT_SCALE} ${AXIS_Y - 24 * CURRENT_SCALE}) scale(${CURRENT_SCALE})`,
2399
+ fill: colors.mark,
2400
+ stroke: colors.text,
2401
+ strokeWidth: 1.5,
2402
+ style: { cursor: "pointer" },
2403
+ onClick: () => onSelect(point.index),
2404
+ children: /* @__PURE__ */ jsx3("title", { children: tickLabel(point.year) })
2405
+ },
2406
+ point.index
2407
+ ) : /* @__PURE__ */ jsx3(
2408
+ "circle",
2409
+ {
2410
+ cx,
2411
+ cy: AXIS_Y,
2412
+ r: 3.5,
2413
+ fill: colors.mark,
2414
+ stroke: colors.surface,
2415
+ strokeWidth: 1.5,
2416
+ style: { cursor: "pointer" },
2417
+ onClick: () => onSelect(point.index),
2418
+ children: /* @__PURE__ */ jsx3("title", { children: tickLabel(point.year) })
2419
+ },
2420
+ point.index
2421
+ );
2422
+ })
2423
+ ] })
2424
+ }
2425
+ );
2426
+ }
2427
+
2428
+ // src/player/StoryPlayer.tsx
2429
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2430
+ var PANEL_FRACTION = 0.62;
2431
+ var BAR_HEIGHT = 56;
2432
+ function initialMapMode(story) {
2433
+ return story.presentation?.basemap === "historical" ? "auto" : "standard";
2434
+ }
2435
+ function SlideText({ text, colors }) {
2436
+ return /* @__PURE__ */ jsx4(Fragment, { children: parseStoryText(text).map((lines, p) => /* @__PURE__ */ jsx4("p", { dir: "auto", style: { margin: "0 0 0.6em", color: colors.text, lineHeight: 1.55 }, children: lines.map((spans, l) => /* @__PURE__ */ jsxs3("span", { children: [
2437
+ l > 0 && /* @__PURE__ */ jsx4("br", {}),
2438
+ spans.map((span, s) => {
2439
+ if (span.bold) return /* @__PURE__ */ jsx4("b", { children: span.text }, s);
2440
+ if (span.italic) return /* @__PURE__ */ jsx4("i", { children: span.text }, s);
2441
+ return /* @__PURE__ */ jsx4("span", { children: span.text }, s);
2442
+ })
2443
+ ] }, l)) }, p)) });
2444
+ }
2445
+ function SlidePhoto({ mediaKey, item, caption, credit, resolveMedia, colors }) {
2446
+ const [src, setSrc] = useState3(null);
2447
+ useEffect3(() => {
2448
+ setSrc(null);
2449
+ let cancelled = false;
2450
+ resolveMedia(mediaKey, item).then((url) => {
2451
+ if (!cancelled) setSrc(url);
2452
+ }, () => {
2453
+ });
2454
+ return () => {
2455
+ cancelled = true;
2456
+ };
2457
+ }, [mediaKey, item, resolveMedia]);
2458
+ if (!src) return null;
2459
+ return /* @__PURE__ */ jsxs3("figure", { style: { margin: 0, display: "flex", flexDirection: "column", alignItems: "center", gap: 4 }, children: [
2460
+ /* @__PURE__ */ jsx4(
2461
+ "img",
2462
+ {
2463
+ src,
2464
+ alt: item.alt ?? "",
2465
+ width: item.width,
2466
+ height: item.height,
2467
+ style: { maxHeight: "42vh", maxWidth: "100%", width: "auto", height: "auto", objectFit: "contain", borderRadius: 4 }
2468
+ }
2469
+ ),
2470
+ (caption || credit) && /* @__PURE__ */ jsx4("figcaption", { style: { fontSize: 13, color: colors.muted }, children: [caption, credit && `\xA9 ${credit}`].filter(Boolean).join(" \xB7 ") })
2471
+ ] });
2472
+ }
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)];
2512
+ const eyebrow = [slide?.date?.text, slide?.place?.name].filter(Boolean).join(" \xB7 ");
2513
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
2514
+ photo && /* @__PURE__ */ jsx4(
2515
+ SlidePhoto,
2516
+ {
2517
+ mediaKey: photo.ref,
2518
+ item: story.media[photo.ref],
2519
+ caption: photo.caption,
2520
+ credit: photo.credit,
2521
+ resolveMedia,
2522
+ colors
2523
+ }
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
+ )) }),
2538
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", flexDirection: "column", gap: 4, alignItems: "center", maxWidth: 640, textAlign: "center" }, children: [
2539
+ eyebrow && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 14, color: colors.muted }, children: eyebrow }),
2540
+ slide?.title && /* @__PURE__ */ jsx4("div", { dir: "auto", style: { fontSize: 20, fontWeight: 700, color: colors.text }, children: slide.title }),
2541
+ slide?.text && /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(SlideText, { text: slide.text, colors }) })
2542
+ ] })
2543
+ ] });
2544
+ }
2545
+ function RoundButton({ label, onClick, disabled, size, children, style }) {
2546
+ return /* @__PURE__ */ jsx4(
2547
+ "button",
2548
+ {
2549
+ type: "button",
2550
+ "aria-label": label,
2551
+ title: label,
2552
+ onClick,
2553
+ disabled,
2554
+ style: {
2555
+ width: size,
2556
+ height: size,
2557
+ borderRadius: "50%",
2558
+ border: "none",
2559
+ background: "#25262b",
2560
+ color: "#ffffff",
2561
+ fontSize: size >= 48 ? 22 : 18,
2562
+ cursor: disabled ? "default" : "pointer",
2563
+ opacity: disabled ? 0.35 : 0.8,
2564
+ display: "flex",
2565
+ alignItems: "center",
2566
+ justifyContent: "center",
2567
+ padding: 0,
2568
+ fontFamily: "inherit",
2569
+ ...style
2570
+ },
2571
+ children
2572
+ }
2573
+ );
2574
+ }
2575
+ function NavArrows({ index, total, onPrev, onNext, variant, strings, colors }) {
2576
+ if (variant === "edge") {
2577
+ const edge = { position: "absolute", top: "50%", transform: "translateY(-50%)", zIndex: 2 };
2578
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
2579
+ /* @__PURE__ */ jsx4(RoundButton, { label: strings.previous, size: 56, disabled: index === 0, onClick: onPrev, style: { ...edge, left: 24 }, children: "\u2190" }),
2580
+ /* @__PURE__ */ jsx4(RoundButton, { label: strings.next, size: 56, disabled: index === total - 1, onClick: onNext, style: { ...edge, right: 24 }, children: "\u2192" })
2581
+ ] });
2582
+ }
2583
+ return /* @__PURE__ */ jsxs3("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: 16, marginTop: 16 }, children: [
2584
+ /* @__PURE__ */ jsx4(RoundButton, { label: strings.previous, size: 34, disabled: index === 0, onClick: onPrev, children: "\u2190" }),
2585
+ /* @__PURE__ */ jsxs3("span", { style: { fontSize: 14, color: colors.muted }, children: [
2586
+ index + 1,
2587
+ " / ",
2588
+ total
2589
+ ] }),
2590
+ /* @__PURE__ */ jsx4(RoundButton, { label: strings.next, size: 34, disabled: index === total - 1, onClick: onNext, children: "\u2192" })
2591
+ ] });
2592
+ }
2593
+ function StoryPlayer({
2594
+ story,
2595
+ resolveMedia,
2596
+ dark = false,
2597
+ colors: colorOverrides,
2598
+ strings: stringOverrides,
2599
+ onClose,
2600
+ keyboard = true,
2601
+ onSlideChange,
2602
+ renderMapModeControl,
2603
+ brand,
2604
+ actions,
2605
+ initialSlide = 0
2606
+ }) {
2607
+ const colors = useMemo(() => ({ ...defaultColors(dark), ...colorOverrides }), [dark, colorOverrides]);
2608
+ const strings = useMemo(
2609
+ () => ({
2610
+ ...stringsFor(story.language, typeof navigator === "undefined" ? void 0 : navigator.language),
2611
+ ...stringOverrides
2612
+ }),
2613
+ [story.language, stringOverrides]
2614
+ );
2615
+ const rootRef = useRef3(null);
2616
+ const slides = story.slides;
2617
+ const [index, setIndex] = useState3(initialSlide);
2618
+ const slide = slides[index];
2619
+ useEffect3(() => {
2620
+ setIndex(Math.min(Math.max(initialSlide, 0), Math.max(slides.length - 1, 0)));
2621
+ }, [story, initialSlide, slides.length]);
2622
+ const datedSlides = useMemo(() => {
2623
+ const points = [];
2624
+ slides.forEach((s, i) => {
2625
+ const year = timelineYear(s.date);
2626
+ if (year !== null) points.push({ index: i, year });
2627
+ });
2628
+ return points;
2629
+ }, [slides]);
2630
+ const showTimeline = datedSlides.length >= 2;
2631
+ const [mapMode, setMapMode] = useState3(() => initialMapMode(story));
2632
+ const [historicalYear, setHistoricalYear] = useState3(() => (/* @__PURE__ */ new Date()).getFullYear());
2633
+ const currentYear = timelineYear(slide?.date);
2634
+ const autoYear = currentYear !== null ? Math.floor(currentYear) : null;
2635
+ const yearBounds = useMemo(() => {
2636
+ if (datedSlides.length === 0) {
2637
+ const thisYear = (/* @__PURE__ */ new Date()).getFullYear();
2638
+ return [thisYear - 200, thisYear];
2639
+ }
2640
+ const years = datedSlides.map((d) => d.year);
2641
+ return [Math.floor(Math.min(...years)), Math.ceil(Math.max(...years))];
2642
+ }, [datedSlides]);
2643
+ const wasHistoricalRef = useRef3(false);
2644
+ useEffect3(() => {
2645
+ if (mapMode === "historical" && !wasHistoricalRef.current) setHistoricalYear(autoYear ?? yearBounds[1]);
2646
+ wasHistoricalRef.current = mapMode === "historical";
2647
+ }, [mapMode, autoYear, yearBounds]);
2648
+ const ohmYear = mapMode === "standard" ? null : mapMode === "auto" ? autoYear : historicalYear;
2649
+ const goPrev = () => setIndex((i) => Math.max(i - 1, 0));
2650
+ const goNext = () => setIndex((i) => Math.min(i + 1, slides.length - 1));
2651
+ const onSlideChangeRef = useRef3(onSlideChange);
2652
+ onSlideChangeRef.current = onSlideChange;
2653
+ useEffect3(() => {
2654
+ onSlideChangeRef.current?.(index);
2655
+ }, [index]);
2656
+ useEffect3(() => {
2657
+ if (!keyboard) return;
2658
+ const target = keyboard === "focus" ? rootRef.current : window;
2659
+ if (!target) return;
2660
+ function onKey(e) {
2661
+ const key = e.key;
2662
+ const from = e.target;
2663
+ if (from && (from.tagName === "INPUT" || from.tagName === "TEXTAREA" || from.isContentEditable)) return;
2664
+ if (key === "ArrowRight") setIndex((i) => Math.min(i + 1, slides.length - 1));
2665
+ if (key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
2666
+ }
2667
+ target.addEventListener("keydown", onKey);
2668
+ return () => target.removeEventListener("keydown", onKey);
2669
+ }, [keyboard, slides.length]);
2670
+ const swipeStart = useRef3(null);
2671
+ const swipeHandlers = {
2672
+ onTouchStart: (e) => {
2673
+ const t = e.touches[0];
2674
+ swipeStart.current = e.touches.length === 1 ? { x: t.clientX, y: t.clientY } : null;
2675
+ },
2676
+ onTouchEnd: (e) => {
2677
+ const start = swipeStart.current;
2678
+ swipeStart.current = null;
2679
+ const t = e.changedTouches[0];
2680
+ if (!start || !t) return;
2681
+ const dx = t.clientX - start.x;
2682
+ const dy = t.clientY - start.y;
2683
+ if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
2684
+ if (dx < 0) goNext();
2685
+ else goPrev();
2686
+ }
2687
+ };
2688
+ const places = useMemo(() => storyPlaces(slides.map((s) => s.place)), [slides]);
2689
+ const mapSlide = useMemo(() => ({
2690
+ key: slide?.id ?? String(index),
2691
+ place: slide?.place,
2692
+ // The opening card, with no place of its own, shows where the story goes.
2693
+ overview: index === 0 && !placeCenter(slide?.place) ? places : null
2694
+ }), [slide, index, places]);
2695
+ const firstCenter = useMemo(() => {
2696
+ if (story.presentation?.basemap === "none") return null;
2697
+ for (const s of slides) {
2698
+ const center = placeCenter(s.place);
2699
+ if (center) return center;
2700
+ }
2701
+ return null;
2702
+ }, [story, slides]);
2703
+ const modeControlProps = {
2704
+ mode: mapMode,
2705
+ onModeChange: setMapMode,
2706
+ autoYear,
2707
+ year: historicalYear,
2708
+ onYearChange: setHistoricalYear,
2709
+ yearBounds
2710
+ };
2711
+ const mediaResolver = useMemo(() => (key) => {
2712
+ const item = Object.prototype.hasOwnProperty.call(story.media, key) ? story.media[key] : void 0;
2713
+ return item ? resolveMedia(key, item) : Promise.resolve(null);
2714
+ }, [story, resolveMedia]);
2715
+ const content = /* @__PURE__ */ jsx4(SlideContent, { slide, story, resolveMedia, colors, strings }, slide?.id ?? index);
2716
+ const timeline = showTimeline && /* @__PURE__ */ jsx4(TimelineStrip, { points: datedSlides, currentIndex: index, onSelect: setIndex, colors });
2717
+ return /* @__PURE__ */ jsxs3(
2718
+ "div",
2719
+ {
2720
+ ref: rootRef,
2721
+ lang: story.language,
2722
+ tabIndex: keyboard === "focus" ? 0 : void 0,
2723
+ style: {
2724
+ position: "relative",
2725
+ width: "100%",
2726
+ height: "100%",
2727
+ overflow: "hidden",
2728
+ background: colors.surface,
2729
+ color: colors.text
2730
+ },
2731
+ children: [
2732
+ /* @__PURE__ */ jsx4(
2733
+ "div",
2734
+ {
2735
+ style: {
2736
+ position: "absolute",
2737
+ top: 0,
2738
+ left: 0,
2739
+ right: 0,
2740
+ height: BAR_HEIGHT,
2741
+ zIndex: 3,
2742
+ background: dark ? "rgba(20,20,20,0.55)" : "rgba(255,255,255,0.55)"
2743
+ }
2744
+ }
2745
+ ),
2746
+ /* @__PURE__ */ jsxs3(
2747
+ "div",
2748
+ {
2749
+ dir: "auto",
2750
+ style: {
2751
+ position: "absolute",
2752
+ top: 0,
2753
+ left: 20,
2754
+ height: BAR_HEIGHT,
2755
+ maxWidth: "26%",
2756
+ zIndex: 3,
2757
+ display: "flex",
2758
+ alignItems: "center",
2759
+ gap: 10,
2760
+ fontWeight: 600
2761
+ },
2762
+ children: [
2763
+ brand && /* @__PURE__ */ jsx4("span", { style: { flex: "none", display: "flex" }, children: brand }),
2764
+ /* @__PURE__ */ jsx4("span", { style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: story.title })
2765
+ ]
2766
+ }
2767
+ ),
2768
+ firstCenter && /* @__PURE__ */ jsx4(
2769
+ "div",
2770
+ {
2771
+ style: {
2772
+ position: "absolute",
2773
+ top: 0,
2774
+ left: 0,
2775
+ right: 0,
2776
+ height: BAR_HEIGHT,
2777
+ zIndex: 3,
2778
+ display: "flex",
2779
+ alignItems: "center",
2780
+ justifyContent: "center",
2781
+ pointerEvents: "none"
2782
+ },
2783
+ children: /* @__PURE__ */ jsx4("div", { style: { pointerEvents: "auto" }, children: renderMapModeControl ? renderMapModeControl(modeControlProps) : /* @__PURE__ */ jsx4(MapModeControl, { ...modeControlProps, strings, colors }) })
2784
+ }
2785
+ ),
2786
+ actions && /* @__PURE__ */ jsx4(
2787
+ "div",
2788
+ {
2789
+ style: {
2790
+ position: "absolute",
2791
+ top: 0,
2792
+ right: onClose ? 60 : 12,
2793
+ height: BAR_HEIGHT,
2794
+ zIndex: 3,
2795
+ display: "flex",
2796
+ alignItems: "center",
2797
+ gap: 8
2798
+ },
2799
+ children: actions
2800
+ }
2801
+ ),
2802
+ onClose && /* @__PURE__ */ jsx4(
2803
+ RoundButton,
2804
+ {
2805
+ label: strings.close,
2806
+ size: 36,
2807
+ onClick: onClose,
2808
+ style: { position: "absolute", top: 10, right: 12, zIndex: 3 },
2809
+ children: "\xD7"
2810
+ }
2811
+ ),
2812
+ firstCenter ? /* @__PURE__ */ jsxs3("div", { style: { position: "absolute", inset: 0 }, children: [
2813
+ /* @__PURE__ */ jsx4(
2814
+ MapBackground,
2815
+ {
2816
+ initialCenter: firstCenter,
2817
+ current: mapSlide,
2818
+ dark,
2819
+ colors,
2820
+ panelFraction: PANEL_FRACTION,
2821
+ ohmYear,
2822
+ resolveMedia: mediaResolver
2823
+ }
2824
+ ),
2825
+ /* @__PURE__ */ jsx4(
2826
+ "div",
2827
+ {
2828
+ style: {
2829
+ position: "absolute",
2830
+ top: 0,
2831
+ bottom: 0,
2832
+ right: 0,
2833
+ width: `${PANEL_FRACTION * 100}%`,
2834
+ zIndex: 1,
2835
+ padding: `${BAR_HEIGHT}px 32px ${showTimeline ? TIMELINE_HEIGHT : 32}px`,
2836
+ boxSizing: "border-box",
2837
+ display: "flex",
2838
+ flexDirection: "column",
2839
+ justifyContent: "center",
2840
+ alignItems: "center",
2841
+ gap: 16,
2842
+ overflowY: "auto",
2843
+ // Transparent where the panel starts, mid-screen, and solid
2844
+ // well before the centered content, so text never sits on map.
2845
+ background: `linear-gradient(to right, transparent 0%, ${colors.panel} 22%)`
2846
+ },
2847
+ ...swipeHandlers,
2848
+ children: content
2849
+ }
2850
+ ),
2851
+ /* @__PURE__ */ jsx4(NavArrows, { index, total: slides.length, onPrev: goPrev, onNext: goNext, variant: "edge", strings, colors }),
2852
+ timeline
2853
+ ] }) : /* @__PURE__ */ jsxs3(
2854
+ "div",
2855
+ {
2856
+ style: {
2857
+ position: "absolute",
2858
+ inset: 0,
2859
+ overflowY: "auto",
2860
+ boxSizing: "border-box",
2861
+ padding: `${BAR_HEIGHT + 16}px 24px ${showTimeline ? TIMELINE_HEIGHT + 16 : 24}px`,
2862
+ display: "flex",
2863
+ flexDirection: "column",
2864
+ alignItems: "center",
2865
+ justifyContent: "center",
2866
+ gap: 16
2867
+ },
2868
+ ...swipeHandlers,
2869
+ children: [
2870
+ content,
2871
+ /* @__PURE__ */ jsx4(NavArrows, { index, total: slides.length, onPrev: goPrev, onNext: goNext, variant: "inline", strings, colors }),
2872
+ timeline
2873
+ ]
2874
+ }
2875
+ )
2876
+ ]
2877
+ }
2878
+ );
2879
+ }
2880
+
2881
+ // src/editor/dates.ts
2882
+ import { loadLocale, parseDate, resolveLocaleCode, toEdtf } from "@gramps-connect/gramps-date";
2883
+ var suggestEdtf = async (text, language) => {
2884
+ if (!text.trim()) return void 0;
2885
+ const code = language && resolveLocaleCode(language.replace(/-/g, "_")) || "en";
2886
+ if (code !== "en" && !await loadLocale(code)) return void 0;
2887
+ try {
2888
+ return toEdtf(parseDate(text, { locale: code }));
2889
+ } catch {
2890
+ return void 0;
2891
+ }
2892
+ };
2893
+
2894
+ // src/editor/DateField.tsx
2895
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2896
+ import { Anchor, Group, Stack, Text, TextInput } from "@mantine/core";
2897
+
2898
+ // src/editor/model.ts
2899
+ function blankStory(title, language) {
2900
+ return {
2901
+ format: FORMAT_ID,
2902
+ version: FORMAT_VERSION,
2903
+ title,
2904
+ ...language ? { language } : {},
2905
+ slides: [{ id: "s0", title }],
2906
+ media: {}
2907
+ };
2908
+ }
2909
+ function nextSlideId(doc) {
2910
+ let max = -1;
2911
+ for (const slide of doc.slides) {
2912
+ const match = /^s(\d+)$/.exec(slide.id);
2913
+ if (match) max = Math.max(max, Number(match[1]));
2914
+ }
2915
+ let n = max + 1;
2916
+ const ids = new Set(doc.slides.map((s) => s.id));
2917
+ while (ids.has(`s${n}`)) n += 1;
2918
+ return `s${n}`;
2919
+ }
2920
+ function withSlides(doc, slides) {
2921
+ return { ...doc, slides };
2922
+ }
2923
+ function addSlide(doc, index) {
2924
+ const slides = [...doc.slides];
2925
+ const at = Math.min(Math.max(index + 1, 0), slides.length);
2926
+ slides.splice(at, 0, { id: nextSlideId(doc) });
2927
+ return { doc: withSlides(doc, slides), index: at };
2928
+ }
2929
+ function duplicateSlide(doc, index) {
2930
+ const slides = [...doc.slides];
2931
+ const copy = structuredClone(slides[index]);
2932
+ copy.id = nextSlideId(doc);
2933
+ slides.splice(index + 1, 0, copy);
2934
+ return { doc: withSlides(doc, slides), index: index + 1 };
2935
+ }
2936
+ function deleteSlide(doc, index) {
2937
+ if (doc.slides.length <= 1) return { doc, index };
2938
+ const slides = doc.slides.filter((_, i) => i !== index);
2939
+ return { doc: withSlides(doc, slides), index: Math.min(index, slides.length - 1) };
2940
+ }
2941
+ function moveSlide(doc, from, to) {
2942
+ if (from === to || to < 0 || to >= doc.slides.length) return doc;
2943
+ const slides = [...doc.slides];
2944
+ const [slide] = slides.splice(from, 1);
2945
+ slides.splice(to, 0, slide);
2946
+ return withSlides(doc, slides);
2947
+ }
2948
+ function patched(base, patch) {
2949
+ const next = { ...base };
2950
+ for (const [key, value] of Object.entries(patch)) {
2951
+ if (value === void 0) delete next[key];
2952
+ else next[key] = value;
2953
+ }
2954
+ return next;
2955
+ }
2956
+ function updateSlide(doc, index, patch) {
2957
+ const slides = [...doc.slides];
2958
+ slides[index] = patched(slides[index], patch);
2959
+ return withSlides(doc, slides);
2960
+ }
2961
+ function updateStory(doc, patch) {
2962
+ return patched(doc, patch);
2963
+ }
2964
+ function slideDate(previous, text, edtf) {
2965
+ if (!text.trim()) return void 0;
2966
+ const next = patched(previous ?? { text }, { text, edtf: edtf || void 0 });
2967
+ if (next.edtf !== previous?.edtf) delete next.sortYear;
2968
+ return next;
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
+ }
3008
+ function usedMedia(doc) {
3009
+ const used = /* @__PURE__ */ new Set();
3010
+ for (const slide of doc.slides) {
3011
+ for (const m of slide.media ?? []) used.add(m.ref);
3012
+ for (const o of slide.place?.overlays ?? []) used.add(o.media);
3013
+ }
3014
+ return used;
3015
+ }
3016
+ function pruneMedia(doc, files) {
3017
+ const used = usedMedia(doc);
3018
+ const media = Object.fromEntries(Object.entries(doc.media).filter(([key]) => used.has(key)));
3019
+ const srcs = new Set(Object.values(media).map((m) => m.src));
3020
+ return {
3021
+ doc: { ...doc, media },
3022
+ files: new Map([...files].filter(([path]) => srcs.has(path)))
3023
+ };
3024
+ }
3025
+ function storyFileName(title) {
3026
+ const safe = title.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
3027
+ return `${safe || "story"}.story.zip`;
3028
+ }
3029
+ var COALESCE_MS = 1500;
3030
+ var MAX_STEPS = 200;
3031
+ function startHistory(doc) {
3032
+ return { past: [], present: doc, future: [] };
3033
+ }
3034
+ function record(history, next, key, now = Date.now()) {
3035
+ if (next === history.present) return history;
3036
+ const coalesce = key !== void 0 && key === history.lastKey && history.lastAt !== void 0 && now - history.lastAt < COALESCE_MS;
3037
+ const past = coalesce ? history.past : [...history.past, history.present].slice(-MAX_STEPS);
3038
+ return { past, present: next, future: [], lastKey: key, lastAt: now };
3039
+ }
3040
+ function undo(history) {
3041
+ const previous = history.past[history.past.length - 1];
3042
+ if (!previous) return history;
3043
+ return { past: history.past.slice(0, -1), present: previous, future: [history.present, ...history.future] };
3044
+ }
3045
+ function redo(history) {
3046
+ const [next, ...future] = history.future;
3047
+ if (!next) return history;
3048
+ return { past: [...history.past, history.present], present: next, future };
3049
+ }
3050
+
3051
+ // src/editor/DateField.tsx
3052
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3053
+ function DateField({ value, language, parse, strings, onChange }) {
3054
+ const [following, setFollowing] = useState4(value?.edtf ? null : true);
3055
+ const latest = useRef4(value);
3056
+ latest.current = value;
3057
+ const request = useRef4(0);
3058
+ useEffect4(() => {
3059
+ if (following !== null) return;
3060
+ let live = true;
3061
+ void parse(value?.text ?? "", language).then((edtf2) => {
3062
+ if (live) setFollowing(edtf2 === value?.edtf);
3063
+ });
3064
+ return () => {
3065
+ live = false;
3066
+ };
3067
+ }, []);
3068
+ const follow = (text) => {
3069
+ const id = ++request.current;
3070
+ void parse(text, language).then((edtf2) => {
3071
+ if (id !== request.current || latest.current?.text !== text) return;
3072
+ onChange(slideDate(latest.current, text, edtf2));
3073
+ });
3074
+ };
3075
+ const onText = (text) => {
3076
+ onChange(slideDate(latest.current, text, latest.current?.edtf));
3077
+ if (following !== false) follow(text);
3078
+ };
3079
+ const edtf = value?.edtf ?? "";
3080
+ const understood = !edtf || edtfYearRange(edtf) !== null;
3081
+ let note;
3082
+ if (edtf && !understood) note = strings.preciseDateNotUnderstood;
3083
+ else if (!edtf) note = strings.noPreciseDate;
3084
+ else if (following) note = strings.preciseDateFollows;
3085
+ else note = strings.preciseDateHint;
3086
+ return /* @__PURE__ */ jsxs4(Stack, { gap: 6, children: [
3087
+ /* @__PURE__ */ jsx5(
3088
+ TextInput,
3089
+ {
3090
+ label: strings.date,
3091
+ description: strings.dateHint,
3092
+ value: value?.text ?? "",
3093
+ onChange: (e) => onText(e.currentTarget.value)
3094
+ }
3095
+ ),
3096
+ value?.text && /* @__PURE__ */ jsxs4(Group, { gap: "sm", align: "flex-end", wrap: "nowrap", children: [
3097
+ /* @__PURE__ */ jsx5(
3098
+ TextInput,
3099
+ {
3100
+ label: strings.preciseDate,
3101
+ size: "xs",
3102
+ w: 200,
3103
+ value: edtf,
3104
+ error: !understood,
3105
+ styles: { input: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" } },
3106
+ onChange: (e) => {
3107
+ setFollowing(false);
3108
+ onChange(slideDate(latest.current, latest.current?.text ?? "", e.currentTarget.value.trim() || void 0));
3109
+ }
3110
+ }
3111
+ ),
3112
+ /* @__PURE__ */ jsxs4(Stack, { gap: 0, pb: 4, children: [
3113
+ /* @__PURE__ */ jsx5(Text, { size: "xs", c: understood ? "dimmed" : "red", children: note }),
3114
+ following === false && /* @__PURE__ */ jsx5(Anchor, { component: "button", type: "button", size: "xs", onClick: () => {
3115
+ setFollowing(true);
3116
+ follow(value.text);
3117
+ }, children: strings.matchDateAsWritten })
3118
+ ] })
3119
+ ] })
3120
+ ] });
3121
+ }
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
+
3252
+ // src/editor/SlideList.tsx
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";
3256
+ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
3257
+ const [dragging, setDragging] = useState6(null);
3258
+ const [over, setOver] = useState6(null);
3259
+ const end = () => {
3260
+ setDragging(null);
3261
+ setOver(null);
3262
+ };
3263
+ return /* @__PURE__ */ jsx7(Stack3, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
3264
+ const isSelected = i === selected;
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)" } : {};
3266
+ return /* @__PURE__ */ jsx7(
3267
+ UnstyledButton,
3268
+ {
3269
+ role: "option",
3270
+ "aria-selected": isSelected,
3271
+ draggable: true,
3272
+ onClick: () => onSelect(i),
3273
+ onDragStart: (e) => {
3274
+ setDragging(i);
3275
+ e.dataTransfer.effectAllowed = "move";
3276
+ e.dataTransfer.setData("text/plain", String(i));
3277
+ },
3278
+ onDragOver: (e) => {
3279
+ if (dragging === null) return;
3280
+ e.preventDefault();
3281
+ setOver(i);
3282
+ },
3283
+ onDrop: (e) => {
3284
+ e.preventDefault();
3285
+ if (dragging !== null && dragging !== i) onMove(dragging, i);
3286
+ end();
3287
+ },
3288
+ onDragEnd: end,
3289
+ style: {
3290
+ display: "block",
3291
+ width: "100%",
3292
+ padding: "6px 8px",
3293
+ borderRadius: 6,
3294
+ background: isSelected ? "var(--mantine-primary-color-light)" : void 0,
3295
+ opacity: dragging === i ? 0.5 : 1,
3296
+ ...dropLine
3297
+ },
3298
+ children: renderItem(item, i)
3299
+ },
3300
+ getKey(item, i)
3301
+ );
3302
+ }) });
3303
+ }
3304
+
3305
+ // src/editor/StoryTextField.tsx
3306
+ import { useRef as useRef6 } from "react";
3307
+ import { Button as Button2, Group as Group3, Stack as Stack4, Text as Text3, Textarea } from "@mantine/core";
3308
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
3309
+ function applyMarker(el, marker, value, onChange) {
3310
+ const start = el.selectionStart ?? value.length;
3311
+ const end = el.selectionEnd ?? value.length;
3312
+ if (start === end) {
3313
+ onChange(value.slice(0, start) + marker + marker + value.slice(end));
3314
+ requestAnimationFrame(() => {
3315
+ el.focus();
3316
+ el.setSelectionRange(start + marker.length, start + marker.length);
3317
+ });
3318
+ return;
3319
+ }
3320
+ const selected = value.slice(start, end);
3321
+ const alreadyWrapped = selected.length > marker.length * 2 && selected.startsWith(marker) && selected.endsWith(marker);
3322
+ const replacement = alreadyWrapped ? selected.slice(marker.length, selected.length - marker.length) : `${marker}${selected}${marker}`;
3323
+ onChange(value.slice(0, start) + replacement + value.slice(end));
3324
+ }
3325
+ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
3326
+ const ref = useRef6(null);
3327
+ return /* @__PURE__ */ jsxs6(Stack4, { gap: 4, children: [
3328
+ /* @__PURE__ */ jsxs6(Group3, { gap: "xs", children: [
3329
+ /* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: label }),
3330
+ /* @__PURE__ */ jsx8(
3331
+ Button2,
3332
+ {
3333
+ variant: "default",
3334
+ size: "compact-xs",
3335
+ fw: 700,
3336
+ "aria-label": boldLabel,
3337
+ title: boldLabel,
3338
+ onClick: () => ref.current && applyMarker(ref.current, "**", value, onChange),
3339
+ children: "B"
3340
+ }
3341
+ ),
3342
+ /* @__PURE__ */ jsx8(
3343
+ Button2,
3344
+ {
3345
+ variant: "default",
3346
+ size: "compact-xs",
3347
+ fs: "italic",
3348
+ "aria-label": italicLabel,
3349
+ title: italicLabel,
3350
+ onClick: () => ref.current && applyMarker(ref.current, "*", value, onChange),
3351
+ children: "i"
3352
+ }
3353
+ )
3354
+ ] }),
3355
+ /* @__PURE__ */ jsx8(
3356
+ Textarea,
3357
+ {
3358
+ ref,
3359
+ "aria-label": label,
3360
+ autosize: true,
3361
+ minRows,
3362
+ maxRows: 14,
3363
+ value,
3364
+ onChange: (e) => onChange(e.currentTarget.value)
3365
+ }
3366
+ )
3367
+ ] });
3368
+ }
3369
+
3370
+ // src/editor/strings.ts
3371
+ var EDITOR_ENGLISH = {
3372
+ story: "Story",
3373
+ storySettings: "Title, language and map",
3374
+ title: "Title",
3375
+ language: "Language",
3376
+ languageHint: "The language the story is written in, e.g. en, de or fr-CA. Dates are read in it.",
3377
+ map: "Map",
3378
+ mapStandard: "Today's map",
3379
+ mapHistorical: "Historical maps for each slide's year",
3380
+ mapAuto: "Viewer's choice",
3381
+ mapNone: "No map",
3382
+ slides: "Slides",
3383
+ untitledSlide: "Untitled slide",
3384
+ addSlide: "Add slide",
3385
+ duplicateSlide: "Duplicate",
3386
+ deleteSlide: "Delete",
3387
+ moveUp: "Move up",
3388
+ moveDown: "Move down",
3389
+ slideTitle: "Title",
3390
+ text: "Text",
3391
+ bold: "Bold",
3392
+ italic: "Italic",
3393
+ date: "Date",
3394
+ dateHint: "As readers should see it, e.g. about 1850, 3 May 1850, between 1850 and 1860.",
3395
+ preciseDate: "Precise date (EDTF)",
3396
+ preciseDateHint: "Places the slide on the timeline and picks historical maps.",
3397
+ preciseDateFollows: "Worked out from the date as written.",
3398
+ preciseDateNotUnderstood: "Not a date the timeline can use.",
3399
+ matchDateAsWritten: "Work it out from the date",
3400
+ noPreciseDate: "None: this slide stays off the timeline.",
3401
+ place: "Place",
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.`,
3415
+ undo: "Undo",
3416
+ redo: "Redo",
3417
+ present: "Present",
3418
+ preview: "Preview",
3419
+ problems: (count) => count === 1 ? "1 problem to fix before saving" : `${count} problems to fix before saving`,
3420
+ openingSlide: "Opening slide"
3421
+ };
3422
+
3423
+ // src/editor/StoryEditor.tsx
3424
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3425
+ var BAR_HEIGHT2 = 56;
3426
+ var STORY = -1;
3427
+ function issueSlide(issue) {
3428
+ const match = /^\/slides\/(\d+)/.exec(issue.path);
3429
+ return match ? Number(match[1]) : STORY;
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
+ }
3435
+ function Thumb({ doc, slide, resolveMedia }) {
3436
+ const ref = slide.media?.[0]?.ref;
3437
+ const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
3438
+ const [url, setUrl] = useState7(null);
3439
+ useEffect6(() => {
3440
+ let live = true;
3441
+ setUrl(null);
3442
+ if (ref && item) void resolveMedia(ref, item).then((u) => {
3443
+ if (live) setUrl(u);
3444
+ });
3445
+ return () => {
3446
+ live = false;
3447
+ };
3448
+ }, [ref, item, resolveMedia]);
3449
+ return /* @__PURE__ */ jsx9(
3450
+ Box2,
3451
+ {
3452
+ w: 40,
3453
+ h: 40,
3454
+ style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
3455
+ children: url && /* @__PURE__ */ jsx9("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
3456
+ }
3457
+ );
3458
+ }
3459
+ function StoryEditor({
3460
+ initialStory,
3461
+ initialFiles,
3462
+ onChange,
3463
+ prepareImage: prepare = prepareImage,
3464
+ toolbar,
3465
+ brand,
3466
+ dark = false,
3467
+ strings: stringOverrides,
3468
+ playerStrings,
3469
+ parseDateText = suggestEdtf
3470
+ }) {
3471
+ const strings = useMemo2(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
3472
+ const [history, setHistory] = useState7(() => startHistory(initialStory));
3473
+ const [selected, setSelected] = useState7(0);
3474
+ const [presenting, setPresenting] = useState7(false);
3475
+ const [problemsOpen, setProblemsOpen] = useState7(false);
3476
+ const wide = useMediaQuery("(min-width: 1000px)", true);
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]);
3502
+ const apply = useCallback((next, key) => {
3503
+ setHistory((h) => record(h, next, key));
3504
+ }, []);
3505
+ useEffect6(() => {
3506
+ onChange?.(doc, files);
3507
+ }, [doc, files, onChange]);
3508
+ const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
3509
+ const slide = index === STORY ? void 0 : doc.slides[index];
3510
+ const setSlide = (patch, key) => {
3511
+ if (index !== STORY) apply(updateSlide(history.present, index, patch), `${slide.id}:${key}`);
3512
+ };
3513
+ const issues = useMemo2(() => validate2(doc, files).errors, [doc, files]);
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(() => {
3563
+ const onKey = (e) => {
3564
+ if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
3565
+ const target = e.target;
3566
+ if (target?.closest("input, textarea, [contenteditable=true]")) return;
3567
+ const key = e.key.toLowerCase();
3568
+ if (key === "z" && !e.shiftKey) {
3569
+ e.preventDefault();
3570
+ setHistory(undo);
3571
+ } else if (key === "z" && e.shiftKey || key === "y") {
3572
+ e.preventDefault();
3573
+ setHistory(redo);
3574
+ }
3575
+ };
3576
+ window.addEventListener("keydown", onKey);
3577
+ return () => window.removeEventListener("keydown", onKey);
3578
+ }, []);
3579
+ const slideActions = slide && /* @__PURE__ */ jsxs7(Group4, { gap: 6, children: [
3580
+ /* @__PURE__ */ jsx9(Tooltip2, { label: strings.moveUp, children: /* @__PURE__ */ jsx9(
3581
+ ActionIcon2,
3582
+ {
3583
+ variant: "default",
3584
+ "aria-label": strings.moveUp,
3585
+ disabled: index === 0,
3586
+ onClick: () => {
3587
+ apply(moveSlide(doc, index, index - 1));
3588
+ setSelected(index - 1);
3589
+ },
3590
+ children: "\u2191"
3591
+ }
3592
+ ) }),
3593
+ /* @__PURE__ */ jsx9(Tooltip2, { label: strings.moveDown, children: /* @__PURE__ */ jsx9(
3594
+ ActionIcon2,
3595
+ {
3596
+ variant: "default",
3597
+ "aria-label": strings.moveDown,
3598
+ disabled: index === doc.slides.length - 1,
3599
+ onClick: () => {
3600
+ apply(moveSlide(doc, index, index + 1));
3601
+ setSelected(index + 1);
3602
+ },
3603
+ children: "\u2193"
3604
+ }
3605
+ ) }),
3606
+ /* @__PURE__ */ jsx9(Button3, { variant: "default", size: "xs", onClick: () => {
3607
+ const r = duplicateSlide(doc, index);
3608
+ apply(r.doc);
3609
+ setSelected(r.index);
3610
+ }, children: strings.duplicateSlide }),
3611
+ /* @__PURE__ */ jsx9(
3612
+ Button3,
3613
+ {
3614
+ variant: "default",
3615
+ size: "xs",
3616
+ color: "red",
3617
+ disabled: doc.slides.length <= 1,
3618
+ onClick: () => {
3619
+ const r = deleteSlide(doc, index);
3620
+ apply(r.doc);
3621
+ setSelected(r.index);
3622
+ },
3623
+ children: strings.deleteSlide
3624
+ }
3625
+ )
3626
+ ] });
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,
3631
+ {
3632
+ label: strings.title,
3633
+ required: true,
3634
+ value: doc.title,
3635
+ onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
3636
+ }
3637
+ ),
3638
+ /* @__PURE__ */ jsx9(
3639
+ TextInput3,
3640
+ {
3641
+ label: strings.language,
3642
+ description: strings.languageHint,
3643
+ w: 260,
3644
+ value: doc.language ?? "",
3645
+ onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
3646
+ }
3647
+ ),
3648
+ /* @__PURE__ */ jsx9(
3649
+ Select,
3650
+ {
3651
+ label: strings.map,
3652
+ w: 320,
3653
+ allowDeselect: false,
3654
+ data: [
3655
+ { value: "auto", label: strings.mapAuto },
3656
+ { value: "standard", label: strings.mapStandard },
3657
+ { value: "historical", label: strings.mapHistorical },
3658
+ { value: "none", label: strings.mapNone }
3659
+ ],
3660
+ value: doc.presentation?.basemap ?? "auto",
3661
+ onChange: (value) => value && apply(updateStory(doc, {
3662
+ presentation: { ...doc.presentation, basemap: value }
3663
+ }))
3664
+ }
3665
+ )
3666
+ ] });
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}` }),
3670
+ slideActions
3671
+ ] }),
3672
+ /* @__PURE__ */ jsx9(
3673
+ TextInput3,
3674
+ {
3675
+ label: strings.slideTitle,
3676
+ value: slide.title ?? "",
3677
+ onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
3678
+ }
3679
+ ),
3680
+ /* @__PURE__ */ jsx9(
3681
+ DateField,
3682
+ {
3683
+ value: slide.date,
3684
+ language: doc.language,
3685
+ parse: parseDateText,
3686
+ strings,
3687
+ onChange: (date) => setSlide({ date }, "date")
3688
+ }
3689
+ ),
3690
+ /* @__PURE__ */ jsx9(
3691
+ StoryTextField,
3692
+ {
3693
+ label: strings.text,
3694
+ value: slide.text ?? "",
3695
+ boldLabel: strings.bold,
3696
+ italicLabel: strings.italic,
3697
+ minRows: 5,
3698
+ onChange: (text) => setSlide({ text: text || void 0 }, "text")
3699
+ }
3700
+ ),
3701
+ slide.place && /* @__PURE__ */ jsxs7(Stack5, { gap: 2, children: [
3702
+ /* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, children: strings.place }),
3703
+ /* @__PURE__ */ jsxs7(Text4, { size: "sm", children: [
3704
+ slide.place.name ?? "",
3705
+ slide.place.lat !== void 0 && slide.place.lon !== void 0 && /* @__PURE__ */ jsxs7(Text4, { span: true, c: "dimmed", size: "sm", children: [
3706
+ " ",
3707
+ slide.place.lat.toFixed(4),
3708
+ ", ",
3709
+ slide.place.lon.toFixed(4)
3710
+ ] })
3711
+ ] })
3712
+ ] }),
3713
+ /* @__PURE__ */ jsx9(
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 })
3727
+ ] }, slide.id);
3728
+ const list = /* @__PURE__ */ jsxs7(Stack5, { gap: "xs", children: [
3729
+ /* @__PURE__ */ jsxs7(
3730
+ UnstyledButton2,
3731
+ {
3732
+ onClick: () => setSelected(STORY),
3733
+ style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
3734
+ children: [
3735
+ /* @__PURE__ */ jsx9(Text4, { fw: 600, size: "sm", children: strings.story }),
3736
+ /* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", children: strings.storySettings })
3737
+ ]
3738
+ }
3739
+ ),
3740
+ /* @__PURE__ */ jsx9(Divider, { label: strings.slides, labelPosition: "left" }),
3741
+ /* @__PURE__ */ jsx9(
3742
+ SlideList,
3743
+ {
3744
+ label: strings.slides,
3745
+ items: doc.slides,
3746
+ selected: index,
3747
+ onSelect: setSelected,
3748
+ onMove: (from, to) => {
3749
+ apply(moveSlide(doc, from, to));
3750
+ setSelected(to);
3751
+ },
3752
+ getKey: (s) => s.id,
3753
+ renderItem: (s, i) => /* @__PURE__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", children: [
3754
+ /* @__PURE__ */ jsx9(Thumb, { doc, slide: s, resolveMedia }),
3755
+ /* @__PURE__ */ jsxs7(Box2, { style: { minWidth: 0 }, children: [
3756
+ /* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
3757
+ /* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
3758
+ ] })
3759
+ ] })
3760
+ }
3761
+ ),
3762
+ /* @__PURE__ */ jsxs7(
3763
+ Button3,
3764
+ {
3765
+ variant: "light",
3766
+ size: "xs",
3767
+ mt: 4,
3768
+ onClick: () => {
3769
+ const r = addSlide(doc, index === STORY ? doc.slides.length - 1 : index);
3770
+ apply(r.doc);
3771
+ setSelected(r.index);
3772
+ },
3773
+ children: [
3774
+ "+ ",
3775
+ strings.addSlide
3776
+ ]
3777
+ }
3778
+ )
3779
+ ] });
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(
3781
+ StoryPlayer,
3782
+ {
3783
+ story: doc,
3784
+ resolveMedia,
3785
+ dark,
3786
+ strings: playerStrings,
3787
+ keyboard: false,
3788
+ initialSlide: index === STORY ? 0 : index,
3789
+ onSlideChange: (i) => {
3790
+ if (i !== index && index !== STORY) setSelected(i);
3791
+ }
3792
+ }
3793
+ ) });
3794
+ const column = (children, width) => /* @__PURE__ */ jsx9(
3795
+ Box2,
3796
+ {
3797
+ style: {
3798
+ width,
3799
+ flex: width ? "none" : 1,
3800
+ minWidth: 0,
3801
+ padding: 16,
3802
+ overflowY: wide ? "auto" : void 0,
3803
+ height: wide ? "100%" : void 0
3804
+ },
3805
+ children
3806
+ }
3807
+ );
3808
+ return /* @__PURE__ */ jsxs7(Box2, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
3809
+ /* @__PURE__ */ jsxs7(
3810
+ Group4,
3811
+ {
3812
+ h: BAR_HEIGHT2,
3813
+ px: "md",
3814
+ gap: "sm",
3815
+ wrap: "nowrap",
3816
+ justify: "space-between",
3817
+ style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
3818
+ children: [
3819
+ /* @__PURE__ */ jsxs7(Group4, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
3820
+ brand,
3821
+ /* @__PURE__ */ jsx9(Text4, { fw: 600, truncate: true, dir: "auto", children: doc.title })
3822
+ ] }),
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) => {
3827
+ const at = issueSlide(issue);
3828
+ return /* @__PURE__ */ jsxs7(UnstyledButton2, { onClick: () => {
3829
+ setSelected(at);
3830
+ setProblemsOpen(false);
3831
+ }, 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: [
3834
+ issue.path,
3835
+ " ",
3836
+ issue.message
3837
+ ] })
3838
+ ] }, i);
3839
+ }) }) })
3840
+ ] }),
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 }),
3844
+ toolbar
3845
+ ] })
3846
+ ]
3847
+ }
3848
+ ),
3849
+ /* @__PURE__ */ jsxs7(
3850
+ Box2,
3851
+ {
3852
+ style: {
3853
+ flex: 1,
3854
+ minHeight: 0,
3855
+ display: "flex",
3856
+ flexDirection: wide ? "row" : "column",
3857
+ overflowY: wide ? void 0 : "auto"
3858
+ },
3859
+ children: [
3860
+ column(list, wide ? 280 : void 0),
3861
+ column(index === STORY ? storyForm : slideForm, wide ? "min(520px, 36%)" : void 0),
3862
+ column(preview)
3863
+ ]
3864
+ }
3865
+ ),
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(
3867
+ StoryPlayer,
3868
+ {
3869
+ story: doc,
3870
+ resolveMedia,
3871
+ dark,
3872
+ strings: playerStrings,
3873
+ initialSlide: index === STORY ? 0 : index,
3874
+ onClose: () => setPresenting(false)
3875
+ }
3876
+ ) }) })
3877
+ ] });
3878
+ }
3879
+ export {
3880
+ COALESCE_MS,
3881
+ DateField,
3882
+ EDITOR_ENGLISH,
3883
+ PhotosField,
3884
+ SlideList,
3885
+ StoryEditor,
3886
+ StoryTextField,
3887
+ addImage,
3888
+ addSlide,
3889
+ blankStory,
3890
+ deleteSlide,
3891
+ duplicateSlide,
3892
+ findMedia,
3893
+ moveSlide,
3894
+ nextSlideId,
3895
+ prepareImage,
3896
+ pruneMedia,
3897
+ record,
3898
+ redo,
3899
+ setSlidePhotos,
3900
+ slideDate,
3901
+ startHistory,
3902
+ storyFileName,
3903
+ suggestEdtf,
3904
+ undo,
3905
+ updateMedia,
3906
+ updateSlide,
3907
+ updateStory,
3908
+ usedMedia
3909
+ };
3910
+ //# sourceMappingURL=editor.js.map