@neobyzantine/series-player 0.2.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/index.mjs ADDED
@@ -0,0 +1,689 @@
1
+ // src/SeriesPlayer.tsx
2
+ import { useCallback as useCallback2, useRef as useRef3, useState as useState2 } from "react";
3
+
4
+ // src/useSeriesPlayer.ts
5
+ import { useState, useCallback, useRef, useEffect } from "react";
6
+ function useSeriesPlayer(entries, onEventLoad, opts) {
7
+ const [currentIndex, setCurrentIndex] = useState(0);
8
+ const [event, setEvent] = useState(null);
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState(null);
11
+ const [jumpHistory, setJumpHistory] = useState([]);
12
+ const [autoPlaying, setAutoPlaying] = useState(false);
13
+ const autoTimerRef = useRef(null);
14
+ const loadingSlugRef = useRef(null);
15
+ const onEventChangeRef = useRef(opts?.onEventChange);
16
+ onEventChangeRef.current = opts?.onEventChange;
17
+ const autoPlayIntervalRef = useRef(opts?.autoPlayInterval ?? 12e3);
18
+ autoPlayIntervalRef.current = opts?.autoPlayInterval ?? 12e3;
19
+ const clearAutoTimer = useCallback(() => {
20
+ if (autoTimerRef.current) {
21
+ clearTimeout(autoTimerRef.current);
22
+ autoTimerRef.current = null;
23
+ }
24
+ }, []);
25
+ const loadSlug = useCallback(
26
+ async (slug, index) => {
27
+ loadingSlugRef.current = slug;
28
+ clearAutoTimer();
29
+ setLoading(true);
30
+ setError(null);
31
+ try {
32
+ const ev = await onEventLoad(slug);
33
+ if (loadingSlugRef.current !== slug) return;
34
+ setEvent(ev);
35
+ if (index >= 0) onEventChangeRef.current?.(index, ev);
36
+ } catch (err) {
37
+ if (loadingSlugRef.current === slug) {
38
+ setError(err instanceof Error ? err.message : "Failed to load event");
39
+ }
40
+ } finally {
41
+ if (loadingSlugRef.current === slug) setLoading(false);
42
+ }
43
+ },
44
+ [onEventLoad, clearAutoTimer]
45
+ );
46
+ useEffect(() => {
47
+ const entry = entries[0];
48
+ if (entry) void loadSlug(entry.slug, 0);
49
+ }, []);
50
+ const loadAtIndex = useCallback(
51
+ (index) => {
52
+ const entry = entries[index];
53
+ if (entry) void loadSlug(entry.slug, index);
54
+ },
55
+ [entries, loadSlug]
56
+ );
57
+ const next = useCallback(() => {
58
+ setCurrentIndex((prev2) => {
59
+ if (prev2 >= entries.length - 1) {
60
+ setAutoPlaying(false);
61
+ clearAutoTimer();
62
+ return prev2;
63
+ }
64
+ const next2 = prev2 + 1;
65
+ setJumpHistory([]);
66
+ loadAtIndex(next2);
67
+ return next2;
68
+ });
69
+ }, [entries.length, loadAtIndex, clearAutoTimer]);
70
+ const prev = useCallback(() => {
71
+ setJumpHistory((hist) => {
72
+ if (hist.length > 0) {
73
+ const restored = hist[hist.length - 1];
74
+ const newHist = hist.slice(0, -1);
75
+ setCurrentIndex(restored);
76
+ loadAtIndex(restored);
77
+ return newHist;
78
+ }
79
+ setCurrentIndex((idx) => {
80
+ if (idx <= 0) return idx;
81
+ const newIdx = idx - 1;
82
+ loadAtIndex(newIdx);
83
+ return newIdx;
84
+ });
85
+ return hist;
86
+ });
87
+ }, [loadAtIndex]);
88
+ const goTo = useCallback(
89
+ (index) => {
90
+ if (index < 0 || index >= entries.length) return;
91
+ setJumpHistory([]);
92
+ setAutoPlaying(false);
93
+ clearAutoTimer();
94
+ setCurrentIndex(index);
95
+ loadAtIndex(index);
96
+ },
97
+ [entries.length, loadAtIndex, clearAutoTimer]
98
+ );
99
+ const jumpToRelated = useCallback(
100
+ (slug) => {
101
+ setJumpHistory((hist) => [...hist, currentIndex]);
102
+ loadingSlugRef.current = slug;
103
+ clearAutoTimer();
104
+ setLoading(true);
105
+ setError(null);
106
+ onEventLoad(slug).then((ev) => {
107
+ if (loadingSlugRef.current !== slug) return;
108
+ setEvent(ev);
109
+ setLoading(false);
110
+ }).catch((err) => {
111
+ if (loadingSlugRef.current === slug) {
112
+ setError(err instanceof Error ? err.message : "Failed to load event");
113
+ setLoading(false);
114
+ }
115
+ });
116
+ },
117
+ [currentIndex, onEventLoad, clearAutoTimer]
118
+ );
119
+ const backFromJump = useCallback(() => {
120
+ setJumpHistory((hist) => {
121
+ if (!hist.length) return hist;
122
+ const restored = hist[hist.length - 1];
123
+ const newHist = hist.slice(0, -1);
124
+ setCurrentIndex(restored);
125
+ loadAtIndex(restored);
126
+ return newHist;
127
+ });
128
+ }, [loadAtIndex]);
129
+ const toggleAutoPlay = useCallback(() => {
130
+ setAutoPlaying((prev2) => {
131
+ if (prev2) {
132
+ clearAutoTimer();
133
+ return false;
134
+ }
135
+ autoTimerRef.current = setTimeout(() => {
136
+ next();
137
+ }, autoPlayIntervalRef.current);
138
+ return true;
139
+ });
140
+ }, [clearAutoTimer, next]);
141
+ return [
142
+ { currentIndex, event, loading, error, jumpHistory, autoPlaying },
143
+ { next, prev, goTo, jumpToRelated, backFromJump, toggleAutoPlay }
144
+ ];
145
+ }
146
+
147
+ // src/SeriesStepList.tsx
148
+ import { jsx, jsxs } from "react/jsx-runtime";
149
+ function SeriesStepList({ entries, currentIndex, jumpHistory, onSelect }) {
150
+ const isJumped = jumpHistory.length > 0;
151
+ return /* @__PURE__ */ jsx("ol", { className: "nb-sp-steps", children: entries.map((entry, i) => {
152
+ const isActive = !isJumped && i === currentIndex;
153
+ const isDone = !isJumped && i < currentIndex;
154
+ return /* @__PURE__ */ jsx(
155
+ "li",
156
+ {
157
+ className: `nb-sp-step${isActive ? " nb-sp-step--active" : ""}${isDone ? " nb-sp-step--done" : ""}`,
158
+ children: /* @__PURE__ */ jsxs(
159
+ "button",
160
+ {
161
+ type: "button",
162
+ className: "nb-sp-step-btn",
163
+ onClick: () => onSelect(i),
164
+ "aria-current": isActive ? "step" : void 0,
165
+ children: [
166
+ /* @__PURE__ */ jsx("span", { className: "nb-sp-step-num", children: i + 1 }),
167
+ /* @__PURE__ */ jsxs("span", { className: "nb-sp-step-body", children: [
168
+ /* @__PURE__ */ jsx("span", { className: "nb-sp-step-title", children: entry.title }),
169
+ entry.date_display && /* @__PURE__ */ jsx("span", { className: "nb-sp-step-date", children: entry.date_display })
170
+ ] })
171
+ ]
172
+ }
173
+ )
174
+ },
175
+ entry.slug
176
+ );
177
+ }) });
178
+ }
179
+
180
+ // src/AnimatorControls.tsx
181
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
182
+ function AnimatorControls({
183
+ playing,
184
+ current,
185
+ total,
186
+ currentStep,
187
+ accentColor = "#CFB53B",
188
+ onPlay,
189
+ onPause,
190
+ onReset,
191
+ onStepForward,
192
+ onStepBack
193
+ }) {
194
+ if (total === 0) return null;
195
+ return /* @__PURE__ */ jsxs2("div", { className: "nb-sp-anim-controls", children: [
196
+ /* @__PURE__ */ jsxs2("div", { className: "nb-sp-anim-btns", children: [
197
+ /* @__PURE__ */ jsx2(
198
+ "button",
199
+ {
200
+ type: "button",
201
+ className: "nb-sp-anim-btn",
202
+ onClick: onReset,
203
+ title: "Reset animation",
204
+ disabled: current < 0,
205
+ children: "\u23EE"
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsx2(
209
+ "button",
210
+ {
211
+ type: "button",
212
+ className: "nb-sp-anim-btn",
213
+ onClick: onStepBack,
214
+ title: "Step back",
215
+ disabled: current <= 0,
216
+ children: "\u23EA"
217
+ }
218
+ ),
219
+ /* @__PURE__ */ jsx2(
220
+ "button",
221
+ {
222
+ type: "button",
223
+ className: `nb-sp-anim-btn nb-sp-anim-btn--play${playing ? " nb-sp-anim-btn--active" : ""}`,
224
+ onClick: playing ? onPause : onPlay,
225
+ style: { borderColor: `${accentColor}66`, color: accentColor },
226
+ children: playing ? "\u23F8" : "\u25B6"
227
+ }
228
+ ),
229
+ /* @__PURE__ */ jsx2(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ className: "nb-sp-anim-btn",
234
+ onClick: onStepForward,
235
+ title: "Step forward",
236
+ disabled: current >= total - 1,
237
+ children: "\u23E9"
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsx2("span", { className: "nb-sp-anim-counter", children: current < 0 ? `0 / ${total}` : `${current + 1} / ${total}` })
241
+ ] }),
242
+ currentStep && (currentStep.label || currentStep.narration) && /* @__PURE__ */ jsxs2("div", { className: "nb-sp-anim-narration", children: [
243
+ currentStep.label && /* @__PURE__ */ jsx2("span", { className: "nb-sp-anim-label", style: { color: accentColor }, children: currentStep.label }),
244
+ currentStep.narration && /* @__PURE__ */ jsx2("span", { className: "nb-sp-anim-text", children: currentStep.narration })
245
+ ] })
246
+ ] });
247
+ }
248
+
249
+ // src/AnimatedEventMap.tsx
250
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
251
+ import { MapContainer, TileLayer, useMap } from "react-leaflet";
252
+
253
+ // src/EventAnimator.ts
254
+ import L from "leaflet";
255
+ var EventAnimator = class {
256
+ constructor(map, geojson, color, options) {
257
+ this.current = -1;
258
+ this.playing = false;
259
+ this.timer = null;
260
+ this.pulseMarker = null;
261
+ this.listeners = /* @__PURE__ */ new Map();
262
+ this.map = map;
263
+ this.color = color;
264
+ this.stepMs = options?.stepMs ?? 7500;
265
+ this.ttsEnabled = options?.ttsEnabled ?? false;
266
+ this.steps = (geojson.features ?? []).filter((f) => f.properties.sort_order != null && f.geometry?.type === "Point").sort((a, b) => a.properties.sort_order - b.properties.sort_order);
267
+ }
268
+ get total() {
269
+ return this.steps.length;
270
+ }
271
+ on(event, fn) {
272
+ if (!this.listeners.has(event)) this.listeners.set(event, []);
273
+ this.listeners.get(event).push(fn);
274
+ return this;
275
+ }
276
+ emit(event, data) {
277
+ this.listeners.get(event)?.forEach((fn) => fn(data));
278
+ }
279
+ play() {
280
+ if (this.playing || this.total === 0) return;
281
+ this.playing = true;
282
+ this.emit("play");
283
+ if (this.current < 0) {
284
+ this.goto(0);
285
+ } else {
286
+ this.schedule();
287
+ }
288
+ }
289
+ pause() {
290
+ if (!this.playing) return;
291
+ this.playing = false;
292
+ if (this.timer) {
293
+ clearTimeout(this.timer);
294
+ this.timer = null;
295
+ }
296
+ window.speechSynthesis?.cancel();
297
+ this.emit("pause");
298
+ }
299
+ reset() {
300
+ this.pause();
301
+ this.current = -1;
302
+ this.clearPulse();
303
+ window.speechSynthesis?.cancel();
304
+ this.emit("reset");
305
+ }
306
+ stepForward() {
307
+ this.pause();
308
+ if (this.current + 1 < this.total) this.goto(this.current + 1);
309
+ }
310
+ stepBack() {
311
+ this.pause();
312
+ if (this.current - 1 >= 0) this.goto(this.current - 1);
313
+ }
314
+ setTTS(enabled) {
315
+ this.ttsEnabled = enabled;
316
+ if (!enabled) window.speechSynthesis?.cancel();
317
+ }
318
+ goto(index) {
319
+ if (index < 0 || index >= this.total) return;
320
+ this.current = index;
321
+ const feat = this.steps[index];
322
+ const props = feat.properties;
323
+ const [lng, lat] = feat.geometry.coordinates;
324
+ this.map.flyTo([lat, lng], props.zoom ?? 8, { duration: 1.4, easeLinearity: 0.4 });
325
+ this.clearPulse();
326
+ this.pulseMarker = L.marker([lat, lng], {
327
+ icon: L.divIcon({
328
+ className: "",
329
+ html: `<div class="nb-anim-pulse" style="--anim-color:${this.color}"></div>`,
330
+ iconSize: [48, 48],
331
+ iconAnchor: [24, 24]
332
+ }),
333
+ zIndexOffset: 1e3,
334
+ interactive: false
335
+ }).addTo(this.map);
336
+ if (this.ttsEnabled) this.speak(props.narration ?? props.label ?? "");
337
+ this.emit("step", { index, total: this.total, props });
338
+ if (this.playing) this.schedule();
339
+ }
340
+ schedule() {
341
+ const props = this.steps[this.current]?.properties;
342
+ const delay = props?.animation_pause_ms ?? this.stepMs;
343
+ this.timer = setTimeout(() => {
344
+ if (this.current + 1 >= this.total) {
345
+ this.playing = false;
346
+ window.speechSynthesis?.cancel();
347
+ this.emit("complete");
348
+ } else {
349
+ this.goto(this.current + 1);
350
+ }
351
+ }, delay);
352
+ }
353
+ clearPulse() {
354
+ if (this.pulseMarker) {
355
+ this.map.removeLayer(this.pulseMarker);
356
+ this.pulseMarker = null;
357
+ }
358
+ }
359
+ speak(text) {
360
+ const ss = window.speechSynthesis;
361
+ if (!ss || !text) return;
362
+ ss.cancel();
363
+ setTimeout(() => {
364
+ if (ss.paused) ss.resume();
365
+ const u = new SpeechSynthesisUtterance(text);
366
+ u.lang = "en-GB";
367
+ u.rate = 0.7;
368
+ u.pitch = 0.95;
369
+ u.volume = 1;
370
+ ss.speak(u);
371
+ }, 80);
372
+ }
373
+ };
374
+
375
+ // src/AnimatedEventMap.tsx
376
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
377
+ function AnimatorDriver({ geojson, color, stepMs, ttsEnabled, onAnimatorReady, onStep, onComplete }) {
378
+ const map = useMap();
379
+ const animatorRef = useRef2(null);
380
+ useEffect2(() => {
381
+ const animator = new EventAnimator(map, geojson, color, { stepMs, ttsEnabled });
382
+ if (onStep) animator.on("step", onStep);
383
+ if (onComplete) animator.on("complete", onComplete);
384
+ animatorRef.current = animator;
385
+ onAnimatorReady?.(animator);
386
+ return () => {
387
+ animator.reset();
388
+ animatorRef.current = null;
389
+ };
390
+ }, [geojson, color, stepMs, ttsEnabled]);
391
+ return null;
392
+ }
393
+ function AnimatedEventMap({
394
+ geojson,
395
+ color = "#CFB53B",
396
+ stepMs = 7500,
397
+ ttsEnabled = false,
398
+ onAnimatorReady,
399
+ onStep,
400
+ onComplete,
401
+ center = [41, 29],
402
+ // Constantinople
403
+ zoom = 5,
404
+ height = "400px",
405
+ className
406
+ }) {
407
+ return /* @__PURE__ */ jsx3("div", { className: `nb-sp-map-wrap${className ? ` ${className}` : ""}`, style: { height }, children: /* @__PURE__ */ jsxs3(
408
+ MapContainer,
409
+ {
410
+ center,
411
+ zoom,
412
+ style: { height: "100%", width: "100%" },
413
+ zoomControl: true,
414
+ scrollWheelZoom: false,
415
+ children: [
416
+ /* @__PURE__ */ jsx3(
417
+ TileLayer,
418
+ {
419
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
420
+ attribution: '\xA9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
421
+ maxZoom: 18
422
+ }
423
+ ),
424
+ geojson && geojson.features.length > 0 && /* @__PURE__ */ jsx3(
425
+ AnimatorDriver,
426
+ {
427
+ geojson,
428
+ color,
429
+ stepMs,
430
+ ttsEnabled,
431
+ onAnimatorReady,
432
+ onStep,
433
+ onComplete
434
+ }
435
+ )
436
+ ]
437
+ }
438
+ ) });
439
+ }
440
+
441
+ // src/SeriesEventPanel.tsx
442
+ import { ActorGrid } from "@neobyzantine/actor";
443
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
444
+ function defaultSanitize(html) {
445
+ return html.replace(/<[^>]*>/g, "");
446
+ }
447
+ function SeriesEventPanel({
448
+ event,
449
+ loading,
450
+ error,
451
+ accentColor = "#CFB53B",
452
+ resolvePortrait,
453
+ sanitizeHtml = defaultSanitize,
454
+ onActorClick,
455
+ onRelatedClick,
456
+ className
457
+ }) {
458
+ if (loading) {
459
+ return /* @__PURE__ */ jsx4("div", { className: `nb-sp-panel nb-sp-panel--loading${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx4("div", { className: "nb-sp-panel-spinner" }) });
460
+ }
461
+ if (error) {
462
+ return /* @__PURE__ */ jsx4("div", { className: `nb-sp-panel nb-sp-panel--error${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx4("p", { className: "nb-sp-panel-error", children: error }) });
463
+ }
464
+ if (!event) return null;
465
+ const hasActors = (event.actors?.length ?? 0) > 0;
466
+ const hasGallery = (event.gallery_images?.length ?? 0) > 0;
467
+ const hasRelations = (event.relations?.length ?? 0) > 0;
468
+ return /* @__PURE__ */ jsxs4("article", { className: `nb-sp-panel${className ? ` ${className}` : ""}`, children: [
469
+ /* @__PURE__ */ jsxs4("header", { className: "nb-sp-panel-header", children: [
470
+ /* @__PURE__ */ jsx4("h2", { className: "nb-sp-panel-title", style: { color: accentColor }, children: event.title }),
471
+ event.date_display && /* @__PURE__ */ jsx4("p", { className: "nb-sp-panel-date", children: event.date_display })
472
+ ] }),
473
+ event.description && /* @__PURE__ */ jsx4(
474
+ "div",
475
+ {
476
+ className: "nb-sp-panel-body",
477
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(event.description) }
478
+ }
479
+ ),
480
+ hasActors && /* @__PURE__ */ jsxs4("section", { className: "nb-sp-panel-section", children: [
481
+ /* @__PURE__ */ jsx4("h3", { className: "nb-sp-panel-section-title", children: "Key Figures" }),
482
+ /* @__PURE__ */ jsx4(
483
+ ActorGrid,
484
+ {
485
+ actors: event.actors,
486
+ resolvePortrait,
487
+ onActorClick,
488
+ columns: 2,
489
+ compact: true
490
+ }
491
+ )
492
+ ] }),
493
+ hasGallery && /* @__PURE__ */ jsxs4("section", { className: "nb-sp-panel-section", children: [
494
+ /* @__PURE__ */ jsx4("h3", { className: "nb-sp-panel-section-title", children: "Gallery" }),
495
+ /* @__PURE__ */ jsx4("div", { className: "nb-sp-panel-gallery", children: event.gallery_images.map((src, i) => /* @__PURE__ */ jsx4(
496
+ "img",
497
+ {
498
+ src,
499
+ alt: "",
500
+ className: "nb-sp-panel-gallery-img",
501
+ loading: "lazy"
502
+ },
503
+ i
504
+ )) })
505
+ ] }),
506
+ hasRelations && /* @__PURE__ */ jsxs4("section", { className: "nb-sp-panel-section", children: [
507
+ /* @__PURE__ */ jsx4("h3", { className: "nb-sp-panel-section-title", children: "Related Events" }),
508
+ /* @__PURE__ */ jsx4("ul", { className: "nb-sp-panel-relations", children: event.relations.map((rel) => /* @__PURE__ */ jsx4("li", { className: `nb-sp-panel-rel nb-sp-panel-rel--${rel.type}`, children: /* @__PURE__ */ jsxs4(
509
+ "button",
510
+ {
511
+ type: "button",
512
+ className: "nb-sp-panel-rel-btn",
513
+ onClick: () => onRelatedClick?.(rel.slug),
514
+ children: [
515
+ rel.title,
516
+ rel.date && /* @__PURE__ */ jsxs4("span", { className: "nb-sp-panel-rel-date", children: [
517
+ " (",
518
+ rel.date,
519
+ ")"
520
+ ] })
521
+ ]
522
+ }
523
+ ) }, rel.slug)) })
524
+ ] })
525
+ ] });
526
+ }
527
+
528
+ // src/SeriesPlayer.tsx
529
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
530
+ function SeriesPlayer({
531
+ entries,
532
+ onEventLoad,
533
+ accentColor,
534
+ ttsEnabled = false,
535
+ autoPlayInterval = 12e3,
536
+ resolvePortrait,
537
+ sanitizeHtml,
538
+ onEventChange,
539
+ height = "600px",
540
+ className
541
+ }) {
542
+ const [animatorState, setAnimatorState] = useState2({ playing: false, current: -1, currentProps: null });
543
+ const animatorRef = useRef3(null);
544
+ const [state, actions] = useSeriesPlayer(entries, onEventLoad, {
545
+ onEventChange,
546
+ autoPlayInterval
547
+ });
548
+ const currentEntry = entries[state.currentIndex];
549
+ const resolvedColor = accentColor ?? currentEntry?.color ?? "#CFB53B";
550
+ const geojson = state.event?.locations ? {
551
+ type: "FeatureCollection",
552
+ features: state.event.locations.map((loc) => ({
553
+ type: "Feature",
554
+ geometry: { type: "Point", coordinates: [loc.lng, loc.lat] },
555
+ properties: {
556
+ sort_order: loc.sort_order,
557
+ label: loc.label,
558
+ narration: loc.narration_text,
559
+ zoom: void 0,
560
+ animation_pause_ms: void 0
561
+ }
562
+ }))
563
+ } : null;
564
+ const handleAnimatorReady = useCallback2((animator) => {
565
+ animatorRef.current = animator;
566
+ setAnimatorState({ playing: false, current: -1, currentProps: null });
567
+ }, []);
568
+ const handleStep = useCallback2((data) => {
569
+ setAnimatorState((prev) => ({ ...prev, current: data.index, currentProps: data.props }));
570
+ }, []);
571
+ const handleComplete = useCallback2(() => {
572
+ setAnimatorState((prev) => ({ ...prev, playing: false }));
573
+ }, []);
574
+ const handlePlay = useCallback2(() => {
575
+ animatorRef.current?.play();
576
+ setAnimatorState((prev) => ({ ...prev, playing: true }));
577
+ }, []);
578
+ const handlePause = useCallback2(() => {
579
+ animatorRef.current?.pause();
580
+ setAnimatorState((prev) => ({ ...prev, playing: false }));
581
+ }, []);
582
+ const handleReset = useCallback2(() => {
583
+ animatorRef.current?.reset();
584
+ setAnimatorState({ playing: false, current: -1, currentProps: null });
585
+ }, []);
586
+ const handleStepForward = useCallback2(() => {
587
+ animatorRef.current?.stepForward();
588
+ }, []);
589
+ const handleStepBack = useCallback2(() => {
590
+ animatorRef.current?.stepBack();
591
+ }, []);
592
+ return /* @__PURE__ */ jsxs5(
593
+ "div",
594
+ {
595
+ className: `nb-sp-root${className ? ` ${className}` : ""}`,
596
+ style: { "--nb-sp-accent": resolvedColor },
597
+ children: [
598
+ /* @__PURE__ */ jsx5("aside", { className: "nb-sp-sidebar", children: /* @__PURE__ */ jsx5(
599
+ SeriesStepList,
600
+ {
601
+ entries,
602
+ currentIndex: state.currentIndex,
603
+ jumpHistory: state.jumpHistory,
604
+ onSelect: actions.goTo
605
+ }
606
+ ) }),
607
+ /* @__PURE__ */ jsxs5("main", { className: "nb-sp-main", style: { height }, children: [
608
+ /* @__PURE__ */ jsx5(
609
+ AnimatedEventMap,
610
+ {
611
+ geojson,
612
+ color: resolvedColor,
613
+ ttsEnabled,
614
+ onAnimatorReady: handleAnimatorReady,
615
+ onStep: handleStep,
616
+ onComplete: handleComplete,
617
+ height: "100%",
618
+ className: "nb-sp-map"
619
+ }
620
+ ),
621
+ /* @__PURE__ */ jsx5(
622
+ AnimatorControls,
623
+ {
624
+ playing: animatorState.playing,
625
+ current: animatorState.current,
626
+ total: geojson?.features.length ?? 0,
627
+ currentStep: animatorState.currentProps,
628
+ accentColor: resolvedColor,
629
+ onPlay: handlePlay,
630
+ onPause: handlePause,
631
+ onReset: handleReset,
632
+ onStepForward: handleStepForward,
633
+ onStepBack: handleStepBack
634
+ }
635
+ ),
636
+ /* @__PURE__ */ jsxs5("div", { className: "nb-sp-nav-row", children: [
637
+ /* @__PURE__ */ jsx5(
638
+ "button",
639
+ {
640
+ type: "button",
641
+ className: "nb-sp-nav-btn",
642
+ onClick: actions.prev,
643
+ disabled: state.currentIndex === 0 && state.jumpHistory.length === 0,
644
+ children: "\u2190 Previous"
645
+ }
646
+ ),
647
+ /* @__PURE__ */ jsxs5("span", { className: "nb-sp-nav-label", children: [
648
+ state.currentIndex + 1,
649
+ " / ",
650
+ entries.length
651
+ ] }),
652
+ /* @__PURE__ */ jsx5(
653
+ "button",
654
+ {
655
+ type: "button",
656
+ className: "nb-sp-nav-btn",
657
+ onClick: actions.next,
658
+ disabled: state.currentIndex >= entries.length - 1,
659
+ children: "Next \u2192"
660
+ }
661
+ )
662
+ ] })
663
+ ] }),
664
+ /* @__PURE__ */ jsx5("aside", { className: "nb-sp-detail", children: /* @__PURE__ */ jsx5(
665
+ SeriesEventPanel,
666
+ {
667
+ event: state.event,
668
+ loading: state.loading,
669
+ error: state.error,
670
+ accentColor: resolvedColor,
671
+ resolvePortrait,
672
+ sanitizeHtml,
673
+ onRelatedClick: actions.jumpToRelated
674
+ }
675
+ ) })
676
+ ]
677
+ }
678
+ );
679
+ }
680
+ export {
681
+ AnimatedEventMap,
682
+ AnimatorControls,
683
+ EventAnimator,
684
+ SeriesEventPanel,
685
+ SeriesPlayer,
686
+ SeriesStepList,
687
+ useSeriesPlayer
688
+ };
689
+ //# sourceMappingURL=index.mjs.map