@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.d.mts +174 -0
- package/dist/index.d.ts +174 -0
- package/dist/index.js +732 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +689 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
- package/src/series-player.css +384 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/SeriesPlayer.tsx","../src/useSeriesPlayer.ts","../src/SeriesStepList.tsx","../src/AnimatorControls.tsx","../src/AnimatedEventMap.tsx","../src/EventAnimator.ts","../src/SeriesEventPanel.tsx"],"sourcesContent":["import React, { useCallback, useRef, useState } from 'react';\nimport type { HistoricalEvent } from '@neobyzantine/types';\nimport type { AnimatorEvents } from './types';\nimport { useSeriesPlayer } from './useSeriesPlayer';\nimport { SeriesStepList } from './SeriesStepList';\nimport { AnimatorControls } from './AnimatorControls';\nimport { AnimatedEventMap } from './AnimatedEventMap';\nimport { SeriesEventPanel } from './SeriesEventPanel';\nimport type { SeriesEntry } from './types';\nimport type { EventAnimator } from './EventAnimator';\n\nexport interface SeriesPlayerProps {\n entries: SeriesEntry[];\n onEventLoad: (slug: string) => Promise<HistoricalEvent>;\n apiBase?: string;\n /** Color for map animation pulses. Defaults to active entry color or gold. */\n accentColor?: string;\n ttsEnabled?: boolean;\n autoPlayInterval?: number;\n resolvePortrait?: (path: string) => string;\n sanitizeHtml?: (html: string) => string;\n onEventChange?: (index: number, event: HistoricalEvent) => void;\n height?: string;\n className?: string;\n}\n\nexport function SeriesPlayer({\n entries,\n onEventLoad,\n accentColor,\n ttsEnabled = false,\n autoPlayInterval = 12000,\n resolvePortrait,\n sanitizeHtml,\n onEventChange,\n height = '600px',\n className,\n}: SeriesPlayerProps) {\n const [animatorState, setAnimatorState] = useState<{\n playing: boolean;\n current: number;\n currentProps: import('./types').AnimationFeatureProperties | null;\n }>({ playing: false, current: -1, currentProps: null });\n\n const animatorRef = useRef<EventAnimator | null>(null);\n\n const [state, actions] = useSeriesPlayer(entries, onEventLoad, {\n onEventChange,\n autoPlayInterval,\n });\n\n const currentEntry = entries[state.currentIndex];\n const resolvedColor = accentColor ?? currentEntry?.color ?? '#CFB53B';\n\n const geojson = state.event?.locations\n ? {\n type: 'FeatureCollection' as const,\n features: state.event.locations.map((loc) => ({\n type: 'Feature' as const,\n geometry: { type: 'Point' as const, coordinates: [loc.lng, loc.lat] as [number, number] },\n properties: {\n sort_order: loc.sort_order,\n label: loc.label,\n narration: loc.narration_text,\n zoom: undefined,\n animation_pause_ms: undefined,\n },\n })),\n }\n : null;\n\n const handleAnimatorReady = useCallback((animator: EventAnimator) => {\n animatorRef.current = animator;\n setAnimatorState({ playing: false, current: -1, currentProps: null });\n }, []);\n\n const handleStep = useCallback((data: AnimatorEvents['step']) => {\n setAnimatorState((prev) => ({ ...prev, current: data.index, currentProps: data.props }));\n }, []);\n\n const handleComplete = useCallback(() => {\n setAnimatorState((prev) => ({ ...prev, playing: false }));\n }, []);\n\n const handlePlay = useCallback(() => {\n animatorRef.current?.play();\n setAnimatorState((prev) => ({ ...prev, playing: true }));\n }, []);\n\n const handlePause = useCallback(() => {\n animatorRef.current?.pause();\n setAnimatorState((prev) => ({ ...prev, playing: false }));\n }, []);\n\n const handleReset = useCallback(() => {\n animatorRef.current?.reset();\n setAnimatorState({ playing: false, current: -1, currentProps: null });\n }, []);\n\n const handleStepForward = useCallback(() => {\n animatorRef.current?.stepForward();\n }, []);\n\n const handleStepBack = useCallback(() => {\n animatorRef.current?.stepBack();\n }, []);\n\n return (\n <div\n className={`nb-sp-root${className ? ` ${className}` : ''}`}\n style={{ '--nb-sp-accent': resolvedColor } as React.CSSProperties}\n >\n {/* Left: step list */}\n <aside className=\"nb-sp-sidebar\">\n <SeriesStepList\n entries={entries}\n currentIndex={state.currentIndex}\n jumpHistory={state.jumpHistory}\n onSelect={actions.goTo}\n />\n </aside>\n\n {/* Center: map + controls */}\n <main className=\"nb-sp-main\" style={{ height }}>\n <AnimatedEventMap\n geojson={geojson}\n color={resolvedColor}\n ttsEnabled={ttsEnabled}\n onAnimatorReady={handleAnimatorReady}\n onStep={handleStep}\n onComplete={handleComplete}\n height=\"100%\"\n className=\"nb-sp-map\"\n />\n <AnimatorControls\n playing={animatorState.playing}\n current={animatorState.current}\n total={geojson?.features.length ?? 0}\n currentStep={animatorState.currentProps}\n accentColor={resolvedColor}\n onPlay={handlePlay}\n onPause={handlePause}\n onReset={handleReset}\n onStepForward={handleStepForward}\n onStepBack={handleStepBack}\n />\n <div className=\"nb-sp-nav-row\">\n <button\n type=\"button\"\n className=\"nb-sp-nav-btn\"\n onClick={actions.prev}\n disabled={state.currentIndex === 0 && state.jumpHistory.length === 0}\n >\n ← Previous\n </button>\n <span className=\"nb-sp-nav-label\">\n {state.currentIndex + 1} / {entries.length}\n </span>\n <button\n type=\"button\"\n className=\"nb-sp-nav-btn\"\n onClick={actions.next}\n disabled={state.currentIndex >= entries.length - 1}\n >\n Next →\n </button>\n </div>\n </main>\n\n {/* Right: event detail */}\n <aside className=\"nb-sp-detail\">\n <SeriesEventPanel\n event={state.event}\n loading={state.loading}\n error={state.error}\n accentColor={resolvedColor}\n resolvePortrait={resolvePortrait}\n sanitizeHtml={sanitizeHtml}\n onRelatedClick={actions.jumpToRelated}\n />\n </aside>\n </div>\n );\n}\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport type { HistoricalEvent } from '@neobyzantine/types';\nimport type { SeriesEntry } from './types';\n\nexport interface SeriesPlayerState {\n currentIndex: number;\n event: HistoricalEvent | null;\n loading: boolean;\n error: string | null;\n jumpHistory: number[];\n autoPlaying: boolean;\n}\n\nexport interface SeriesPlayerActions {\n next(): void;\n prev(): void;\n goTo(index: number): void;\n jumpToRelated(slug: string): void;\n backFromJump(): void;\n toggleAutoPlay(): void;\n}\n\nexport function useSeriesPlayer(\n entries: SeriesEntry[],\n onEventLoad: (slug: string) => Promise<HistoricalEvent>,\n opts?: {\n onEventChange?: (index: number, event: HistoricalEvent) => void;\n autoPlayInterval?: number;\n },\n): [SeriesPlayerState, SeriesPlayerActions] {\n const [currentIndex, setCurrentIndex] = useState(0);\n const [event, setEvent] = useState<HistoricalEvent | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [jumpHistory, setJumpHistory] = useState<number[]>([]);\n const [autoPlaying, setAutoPlaying] = useState(false);\n\n const autoTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const loadingSlugRef = useRef<string | null>(null);\n const onEventChangeRef = useRef(opts?.onEventChange);\n onEventChangeRef.current = opts?.onEventChange;\n const autoPlayIntervalRef = useRef(opts?.autoPlayInterval ?? 12000);\n autoPlayIntervalRef.current = opts?.autoPlayInterval ?? 12000;\n\n const clearAutoTimer = useCallback(() => {\n if (autoTimerRef.current) {\n clearTimeout(autoTimerRef.current);\n autoTimerRef.current = null;\n }\n }, []);\n\n const loadSlug = useCallback(\n async (slug: string, index: number) => {\n loadingSlugRef.current = slug;\n clearAutoTimer();\n setLoading(true);\n setError(null);\n try {\n const ev = await onEventLoad(slug);\n if (loadingSlugRef.current !== slug) return; // superseded\n setEvent(ev);\n if (index >= 0) onEventChangeRef.current?.(index, ev);\n } catch (err) {\n if (loadingSlugRef.current === slug) {\n setError(err instanceof Error ? err.message : 'Failed to load event');\n }\n } finally {\n if (loadingSlugRef.current === slug) setLoading(false);\n }\n },\n [onEventLoad, clearAutoTimer],\n );\n\n // Load initial event on mount\n useEffect(() => {\n const entry = entries[0];\n if (entry) void loadSlug(entry.slug, 0);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Load event when currentIndex changes (driven by nav actions)\n const loadAtIndex = useCallback(\n (index: number) => {\n const entry = entries[index];\n if (entry) void loadSlug(entry.slug, index);\n },\n [entries, loadSlug],\n );\n\n const next = useCallback(() => {\n setCurrentIndex((prev) => {\n if (prev >= entries.length - 1) {\n setAutoPlaying(false);\n clearAutoTimer();\n return prev;\n }\n const next = prev + 1;\n setJumpHistory([]);\n loadAtIndex(next);\n return next;\n });\n }, [entries.length, loadAtIndex, clearAutoTimer]);\n\n const prev = useCallback(() => {\n setJumpHistory((hist) => {\n if (hist.length > 0) {\n // back from jump\n const restored = hist[hist.length - 1];\n const newHist = hist.slice(0, -1);\n setCurrentIndex(restored);\n loadAtIndex(restored);\n return newHist;\n }\n setCurrentIndex((idx) => {\n if (idx <= 0) return idx;\n const newIdx = idx - 1;\n loadAtIndex(newIdx);\n return newIdx;\n });\n return hist;\n });\n }, [loadAtIndex]);\n\n const goTo = useCallback(\n (index: number) => {\n if (index < 0 || index >= entries.length) return;\n setJumpHistory([]);\n setAutoPlaying(false);\n clearAutoTimer();\n setCurrentIndex(index);\n loadAtIndex(index);\n },\n [entries.length, loadAtIndex, clearAutoTimer],\n );\n\n const jumpToRelated = useCallback(\n (slug: string) => {\n setJumpHistory((hist) => [...hist, currentIndex]);\n loadingSlugRef.current = slug;\n clearAutoTimer();\n setLoading(true);\n setError(null);\n onEventLoad(slug)\n .then((ev) => {\n if (loadingSlugRef.current !== slug) return;\n setEvent(ev);\n setLoading(false);\n })\n .catch((err) => {\n if (loadingSlugRef.current === slug) {\n setError(err instanceof Error ? err.message : 'Failed to load event');\n setLoading(false);\n }\n });\n },\n [currentIndex, onEventLoad, clearAutoTimer],\n );\n\n const backFromJump = useCallback(() => {\n setJumpHistory((hist) => {\n if (!hist.length) return hist;\n const restored = hist[hist.length - 1];\n const newHist = hist.slice(0, -1);\n setCurrentIndex(restored);\n loadAtIndex(restored);\n return newHist;\n });\n }, [loadAtIndex]);\n\n const toggleAutoPlay = useCallback(() => {\n setAutoPlaying((prev) => {\n if (prev) {\n clearAutoTimer();\n return false;\n }\n // Start auto-play timer\n autoTimerRef.current = setTimeout(() => {\n next();\n }, autoPlayIntervalRef.current);\n return true;\n });\n }, [clearAutoTimer, next]);\n\n return [\n { currentIndex, event, loading, error, jumpHistory, autoPlaying },\n { next, prev, goTo, jumpToRelated, backFromJump, toggleAutoPlay },\n ];\n}\n","import React from 'react';\nimport type { SeriesEntry } from './types';\n\nexport interface SeriesStepListProps {\n entries: SeriesEntry[];\n currentIndex: number;\n jumpHistory: number[];\n onSelect: (index: number) => void;\n}\n\nexport function SeriesStepList({ entries, currentIndex, jumpHistory, onSelect }: SeriesStepListProps) {\n const isJumped = jumpHistory.length > 0;\n\n return (\n <ol className=\"nb-sp-steps\">\n {entries.map((entry, i) => {\n const isActive = !isJumped && i === currentIndex;\n const isDone = !isJumped && i < currentIndex;\n return (\n <li\n key={entry.slug}\n className={`nb-sp-step${isActive ? ' nb-sp-step--active' : ''}${isDone ? ' nb-sp-step--done' : ''}`}\n >\n <button\n type=\"button\"\n className=\"nb-sp-step-btn\"\n onClick={() => onSelect(i)}\n aria-current={isActive ? 'step' : undefined}\n >\n <span className=\"nb-sp-step-num\">{i + 1}</span>\n <span className=\"nb-sp-step-body\">\n <span className=\"nb-sp-step-title\">{entry.title}</span>\n {entry.date_display && (\n <span className=\"nb-sp-step-date\">{entry.date_display}</span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </ol>\n );\n}\n","import React from 'react';\nimport type { AnimationFeatureProperties } from './types';\n\nexport interface AnimatorControlsProps {\n playing: boolean;\n current: number;\n total: number;\n currentStep: AnimationFeatureProperties | null;\n accentColor?: string;\n onPlay: () => void;\n onPause: () => void;\n onReset: () => void;\n onStepForward: () => void;\n onStepBack: () => void;\n}\n\nexport function AnimatorControls({\n playing,\n current,\n total,\n currentStep,\n accentColor = '#CFB53B',\n onPlay,\n onPause,\n onReset,\n onStepForward,\n onStepBack,\n}: AnimatorControlsProps) {\n if (total === 0) return null;\n\n return (\n <div className=\"nb-sp-anim-controls\">\n <div className=\"nb-sp-anim-btns\">\n <button\n type=\"button\"\n className=\"nb-sp-anim-btn\"\n onClick={onReset}\n title=\"Reset animation\"\n disabled={current < 0}\n >⏮</button>\n <button\n type=\"button\"\n className=\"nb-sp-anim-btn\"\n onClick={onStepBack}\n title=\"Step back\"\n disabled={current <= 0}\n >⏪</button>\n <button\n type=\"button\"\n className={`nb-sp-anim-btn nb-sp-anim-btn--play${playing ? ' nb-sp-anim-btn--active' : ''}`}\n onClick={playing ? onPause : onPlay}\n style={{ borderColor: `${accentColor}66`, color: accentColor }}\n >\n {playing ? '⏸' : '▶'}\n </button>\n <button\n type=\"button\"\n className=\"nb-sp-anim-btn\"\n onClick={onStepForward}\n title=\"Step forward\"\n disabled={current >= total - 1}\n >⏩</button>\n <span className=\"nb-sp-anim-counter\">\n {current < 0 ? `0 / ${total}` : `${current + 1} / ${total}`}\n </span>\n </div>\n\n {currentStep && (currentStep.label || currentStep.narration) && (\n <div className=\"nb-sp-anim-narration\">\n {currentStep.label && (\n <span className=\"nb-sp-anim-label\" style={{ color: accentColor }}>\n {currentStep.label}\n </span>\n )}\n {currentStep.narration && (\n <span className=\"nb-sp-anim-text\">{currentStep.narration}</span>\n )}\n </div>\n )}\n </div>\n );\n}\n","import React, { useEffect, useRef } from 'react';\nimport { MapContainer, TileLayer, useMap } from 'react-leaflet';\nimport type { Map as LMap } from 'leaflet';\nimport { EventAnimator } from './EventAnimator';\nimport type { AnimationFeatureCollection, AnimatorEvents } from './types';\n\nexport interface AnimatedEventMapProps {\n geojson: AnimationFeatureCollection | null;\n color?: string;\n stepMs?: number;\n ttsEnabled?: boolean;\n onAnimatorReady?: (animator: EventAnimator) => void;\n onStep?: (data: AnimatorEvents['step']) => void;\n onComplete?: () => void;\n center?: [number, number];\n zoom?: number;\n height?: string;\n className?: string;\n}\n\n// Zero-render inner component: lives inside MapContainer, bridges useMap() to EventAnimator\ninterface DriverProps {\n geojson: AnimationFeatureCollection;\n color: string;\n stepMs: number;\n ttsEnabled: boolean;\n onAnimatorReady?: (animator: EventAnimator) => void;\n onStep?: (data: AnimatorEvents['step']) => void;\n onComplete?: () => void;\n}\n\nfunction AnimatorDriver({ geojson, color, stepMs, ttsEnabled, onAnimatorReady, onStep, onComplete }: DriverProps) {\n const map = useMap() as LMap;\n const animatorRef = useRef<EventAnimator | null>(null);\n\n useEffect(() => {\n const animator = new EventAnimator(map, geojson, color, { stepMs, ttsEnabled });\n if (onStep) animator.on('step', onStep);\n if (onComplete) animator.on('complete', onComplete);\n animatorRef.current = animator;\n onAnimatorReady?.(animator);\n\n return () => {\n animator.reset();\n animatorRef.current = null;\n };\n // Rebuild animator when geojson or key visual settings change.\n // Stable refs used for callbacks — they don't need to be deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [geojson, color, stepMs, ttsEnabled]);\n\n return null;\n}\n\nexport function AnimatedEventMap({\n geojson,\n color = '#CFB53B',\n stepMs = 7500,\n ttsEnabled = false,\n onAnimatorReady,\n onStep,\n onComplete,\n center = [41.0, 29.0], // Constantinople\n zoom = 5,\n height = '400px',\n className,\n}: AnimatedEventMapProps) {\n return (\n <div className={`nb-sp-map-wrap${className ? ` ${className}` : ''}`} style={{ height }}>\n <MapContainer\n center={center}\n zoom={zoom}\n style={{ height: '100%', width: '100%' }}\n zoomControl\n scrollWheelZoom={false}\n >\n <TileLayer\n url=\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\"\n attribution='© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a>'\n maxZoom={18}\n />\n {geojson && geojson.features.length > 0 && (\n <AnimatorDriver\n geojson={geojson}\n color={color}\n stepMs={stepMs}\n ttsEnabled={ttsEnabled}\n onAnimatorReady={onAnimatorReady}\n onStep={onStep}\n onComplete={onComplete}\n />\n )}\n </MapContainer>\n </div>\n );\n}\n","import L from 'leaflet';\nimport type {\n AnimationFeature,\n AnimationFeatureCollection,\n AnimationFeatureProperties,\n AnimatorEvents,\n AnimatorEventName,\n} from './types';\n\ntype Listener<K extends AnimatorEventName> = (data: AnimatorEvents[K]) => void;\n\nexport interface EventAnimatorOptions {\n stepMs?: number;\n ttsEnabled?: boolean;\n}\n\n/**\n * Step-by-step playback of a GeoJSON feature collection on a Leaflet map.\n * Each Point feature represents one animation step: the map flies to it,\n * a pulsing marker is shown, and optional TTS narration is spoken.\n *\n * Pure TypeScript class — no React. Wire into React via AnimatorDriver (uses useMap()).\n */\nexport class EventAnimator {\n private readonly map: L.Map;\n private readonly color: string;\n private readonly stepMs: number;\n private ttsEnabled: boolean;\n\n current = -1;\n playing = false;\n\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pulseMarker: L.Marker | null = null;\n private listeners = new Map<AnimatorEventName, Array<Listener<AnimatorEventName>>>();\n\n readonly steps: AnimationFeature[];\n\n constructor(\n map: L.Map,\n geojson: AnimationFeatureCollection,\n color: string,\n options?: EventAnimatorOptions,\n ) {\n this.map = map;\n this.color = color;\n this.stepMs = options?.stepMs ?? 7500;\n this.ttsEnabled = options?.ttsEnabled ?? false;\n\n this.steps = (geojson.features ?? [])\n .filter((f) => f.properties.sort_order != null && f.geometry?.type === 'Point')\n .sort((a, b) => a.properties.sort_order - b.properties.sort_order);\n }\n\n get total(): number { return this.steps.length; }\n\n on<K extends AnimatorEventName>(event: K, fn: Listener<K>): this {\n if (!this.listeners.has(event)) this.listeners.set(event, []);\n this.listeners.get(event)!.push(fn as Listener<AnimatorEventName>);\n return this;\n }\n\n private emit<K extends AnimatorEventName>(event: K, data?: AnimatorEvents[K]): void {\n this.listeners.get(event)?.forEach((fn) => (fn as Listener<K>)(data as AnimatorEvents[K]));\n }\n\n play(): void {\n if (this.playing || this.total === 0) return;\n this.playing = true;\n this.emit('play');\n if (this.current < 0) {\n this.goto(0);\n } else {\n this.schedule();\n }\n }\n\n pause(): void {\n if (!this.playing) return;\n this.playing = false;\n if (this.timer) { clearTimeout(this.timer); this.timer = null; }\n window.speechSynthesis?.cancel();\n this.emit('pause');\n }\n\n reset(): void {\n this.pause();\n this.current = -1;\n this.clearPulse();\n window.speechSynthesis?.cancel();\n this.emit('reset');\n }\n\n stepForward(): void {\n this.pause();\n if (this.current + 1 < this.total) this.goto(this.current + 1);\n }\n\n stepBack(): void {\n this.pause();\n if (this.current - 1 >= 0) this.goto(this.current - 1);\n }\n\n setTTS(enabled: boolean): void {\n this.ttsEnabled = enabled;\n if (!enabled) window.speechSynthesis?.cancel();\n }\n\n private goto(index: number): void {\n if (index < 0 || index >= this.total) return;\n this.current = index;\n\n const feat = this.steps[index];\n const props = feat.properties;\n const [lng, lat] = feat.geometry.coordinates;\n\n this.map.flyTo([lat, lng], props.zoom ?? 8, { duration: 1.4, easeLinearity: 0.4 });\n\n this.clearPulse();\n this.pulseMarker = L.marker([lat, lng], {\n icon: L.divIcon({\n className: '',\n html: `<div class=\"nb-anim-pulse\" style=\"--anim-color:${this.color}\"></div>`,\n iconSize: [48, 48],\n iconAnchor: [24, 24],\n }),\n zIndexOffset: 1000,\n interactive: false,\n }).addTo(this.map);\n\n if (this.ttsEnabled) this.speak(props.narration ?? props.label ?? '');\n\n this.emit('step', { index, total: this.total, props });\n\n if (this.playing) this.schedule();\n }\n\n private schedule(): void {\n const props = this.steps[this.current]?.properties;\n const delay = props?.animation_pause_ms ?? this.stepMs;\n this.timer = setTimeout(() => {\n if (this.current + 1 >= this.total) {\n this.playing = false;\n window.speechSynthesis?.cancel();\n this.emit('complete');\n } else {\n this.goto(this.current + 1);\n }\n }, delay);\n }\n\n private clearPulse(): void {\n if (this.pulseMarker) {\n this.map.removeLayer(this.pulseMarker);\n this.pulseMarker = null;\n }\n }\n\n private speak(text: string): void {\n const ss = window.speechSynthesis;\n if (!ss || !text) return;\n ss.cancel();\n setTimeout(() => {\n if (ss.paused) ss.resume();\n const u = new SpeechSynthesisUtterance(text);\n u.lang = 'en-GB';\n u.rate = 0.70;\n u.pitch = 0.95;\n u.volume = 1;\n ss.speak(u);\n }, 80);\n }\n}\n","import React from 'react';\nimport type { HistoricalEvent } from '@neobyzantine/types';\nimport { ActorGrid } from '@neobyzantine/actor';\n\nexport interface SeriesEventPanelProps {\n event: HistoricalEvent | null;\n loading?: boolean;\n error?: string | null;\n accentColor?: string;\n /** Convert storage path to URL — consumer knows the URL scheme. */\n resolvePortrait?: (path: string) => string;\n /** Sanitize HTML before dangerouslySetInnerHTML — consumer provides sanitizer. */\n sanitizeHtml?: (html: string) => string;\n onActorClick?: (actor: import('@neobyzantine/types').Actor) => void;\n onRelatedClick?: (slug: string) => void;\n className?: string;\n}\n\n/** Default strips all tags — safe fallback. Pass DOMPurify.sanitize for full HTML rendering. */\nfunction defaultSanitize(html: string) { return html.replace(/<[^>]*>/g, ''); }\n\nexport function SeriesEventPanel({\n event,\n loading,\n error,\n accentColor = '#CFB53B',\n resolvePortrait,\n sanitizeHtml = defaultSanitize,\n onActorClick,\n onRelatedClick,\n className,\n}: SeriesEventPanelProps) {\n if (loading) {\n return (\n <div className={`nb-sp-panel nb-sp-panel--loading${className ? ` ${className}` : ''}`}>\n <div className=\"nb-sp-panel-spinner\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className={`nb-sp-panel nb-sp-panel--error${className ? ` ${className}` : ''}`}>\n <p className=\"nb-sp-panel-error\">{error}</p>\n </div>\n );\n }\n\n if (!event) return null;\n\n const hasActors = (event.actors?.length ?? 0) > 0;\n const hasGallery = (event.gallery_images?.length ?? 0) > 0;\n const hasRelations = (event.relations?.length ?? 0) > 0;\n\n return (\n <article className={`nb-sp-panel${className ? ` ${className}` : ''}`}>\n <header className=\"nb-sp-panel-header\">\n <h2 className=\"nb-sp-panel-title\" style={{ color: accentColor }}>{event.title}</h2>\n {event.date_display && (\n <p className=\"nb-sp-panel-date\">{event.date_display}</p>\n )}\n </header>\n\n {event.description && (\n <div\n className=\"nb-sp-panel-body\"\n dangerouslySetInnerHTML={{ __html: sanitizeHtml(event.description) }}\n />\n )}\n\n {hasActors && (\n <section className=\"nb-sp-panel-section\">\n <h3 className=\"nb-sp-panel-section-title\">Key Figures</h3>\n <ActorGrid\n actors={event.actors!}\n resolvePortrait={resolvePortrait}\n onActorClick={onActorClick}\n columns={2}\n compact\n />\n </section>\n )}\n\n {hasGallery && (\n <section className=\"nb-sp-panel-section\">\n <h3 className=\"nb-sp-panel-section-title\">Gallery</h3>\n <div className=\"nb-sp-panel-gallery\">\n {event.gallery_images!.map((src, i) => (\n <img\n key={i}\n src={src}\n alt=\"\"\n className=\"nb-sp-panel-gallery-img\"\n loading=\"lazy\"\n />\n ))}\n </div>\n </section>\n )}\n\n {hasRelations && (\n <section className=\"nb-sp-panel-section\">\n <h3 className=\"nb-sp-panel-section-title\">Related Events</h3>\n <ul className=\"nb-sp-panel-relations\">\n {event.relations!.map((rel) => (\n <li key={rel.slug} className={`nb-sp-panel-rel nb-sp-panel-rel--${rel.type}`}>\n <button\n type=\"button\"\n className=\"nb-sp-panel-rel-btn\"\n onClick={() => onRelatedClick?.(rel.slug)}\n >\n {rel.title}\n {rel.date && <span className=\"nb-sp-panel-rel-date\"> ({rel.date})</span>}\n </button>\n </li>\n ))}\n </ul>\n </section>\n )}\n </article>\n );\n}\n"],"mappings":"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","names":["useCallback","useRef","useState","prev","next","jsx","jsxs","useEffect","useRef","jsx","jsxs","useRef","useEffect","jsx","jsxs","jsx","jsxs","useState","useRef","useCallback"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neobyzantine/series-player",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Animated multi-event series player with step-by-step map flythrough and optional TTS narration",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./series-player.css": "./src/series-player.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src/series-player.css"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@neobyzantine/tokens": "0.2.0",
|
|
23
|
+
"@neobyzantine/types": "0.2.0",
|
|
24
|
+
"@neobyzantine/actor": "0.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
28
|
+
"@testing-library/react": "^16.3.0",
|
|
29
|
+
"@testing-library/user-event": "^14.5.2",
|
|
30
|
+
"@types/leaflet": "^1.9.18",
|
|
31
|
+
"@types/react": "^18.3.23",
|
|
32
|
+
"jsdom": "^26.1.0",
|
|
33
|
+
"leaflet": "^1.9.4",
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"react-dom": "^18.3.1",
|
|
36
|
+
"react-leaflet": "^4.2.1",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "^5.8.0",
|
|
39
|
+
"vitest": "^3.2.3"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"leaflet": ">=1.9",
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"react-dom": ">=18",
|
|
45
|
+
"react-leaflet": ">=4"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"lint": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/* ── Series Player ─────────────────────────────────────────────────────────── */
|
|
2
|
+
.nb-sp-root {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: 240px 1fr 320px;
|
|
5
|
+
gap: 0;
|
|
6
|
+
background: #0d1220;
|
|
7
|
+
color: #e8dfc0;
|
|
8
|
+
font-family: 'Crimson Text', Georgia, serif;
|
|
9
|
+
border: 1px solid #1e2d45;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* ── Sidebar (step list) ───────────────────────────────────────────────────── */
|
|
15
|
+
.nb-sp-sidebar {
|
|
16
|
+
background: #080e1a;
|
|
17
|
+
border-right: 1px solid #1e2d45;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
max-height: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.nb-sp-steps {
|
|
23
|
+
list-style: none;
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.nb-sp-step {
|
|
29
|
+
border-bottom: 1px solid #1a2235;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.nb-sp-step-btn {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: flex-start;
|
|
35
|
+
gap: 10px;
|
|
36
|
+
width: 100%;
|
|
37
|
+
padding: 10px 12px;
|
|
38
|
+
background: transparent;
|
|
39
|
+
border: none;
|
|
40
|
+
color: #8899aa;
|
|
41
|
+
text-align: left;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: background 0.15s, color 0.15s;
|
|
44
|
+
font-family: inherit;
|
|
45
|
+
font-size: 0.85rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.nb-sp-step-btn:hover {
|
|
49
|
+
background: #111928;
|
|
50
|
+
color: #c8bfa0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.nb-sp-step--active .nb-sp-step-btn {
|
|
54
|
+
background: #111928;
|
|
55
|
+
color: var(--nb-sp-accent, #cfb53b);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.nb-sp-step--done .nb-sp-step-btn {
|
|
59
|
+
color: #556677;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.nb-sp-step-num {
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
width: 20px;
|
|
65
|
+
height: 20px;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
background: #1a2235;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
font-size: 0.7rem;
|
|
72
|
+
font-weight: bold;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.nb-sp-step--active .nb-sp-step-num {
|
|
76
|
+
background: var(--nb-sp-accent, #cfb53b);
|
|
77
|
+
color: #080e1a;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.nb-sp-step--done .nb-sp-step-num {
|
|
81
|
+
background: #222d3a;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.nb-sp-step-body {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: 2px;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.nb-sp-step-title {
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
text-overflow: ellipsis;
|
|
95
|
+
font-size: 0.83rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.nb-sp-step-date {
|
|
99
|
+
font-size: 0.72rem;
|
|
100
|
+
color: #556677;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ── Main (map + controls) ─────────────────────────────────────────────────── */
|
|
104
|
+
.nb-sp-main {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.nb-sp-map-wrap {
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-height: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.nb-sp-map-wrap .leaflet-container {
|
|
116
|
+
background: #0a1020;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Pulsing animation marker */
|
|
120
|
+
.nb-anim-pulse {
|
|
121
|
+
width: 48px;
|
|
122
|
+
height: 48px;
|
|
123
|
+
border-radius: 50%;
|
|
124
|
+
background: color-mix(in srgb, var(--anim-color, #cfb53b) 35%, transparent);
|
|
125
|
+
border: 2px solid var(--anim-color, #cfb53b);
|
|
126
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--anim-color, #cfb53b) 60%, transparent);
|
|
127
|
+
animation: nb-anim-pulse 1.6s ease-out infinite;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@keyframes nb-anim-pulse {
|
|
131
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--anim-color, #cfb53b) 60%, transparent); }
|
|
132
|
+
70% { box-shadow: 0 0 0 18px transparent; }
|
|
133
|
+
100% { box-shadow: 0 0 0 0 transparent; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── Animator controls ─────────────────────────────────────────────────────── */
|
|
137
|
+
.nb-sp-anim-controls {
|
|
138
|
+
background: #080e1acc;
|
|
139
|
+
border-top: 1px solid #1e2d45;
|
|
140
|
+
padding: 8px 12px;
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
gap: 6px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.nb-sp-anim-btns {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 6px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.nb-sp-anim-btn {
|
|
153
|
+
background: #111928;
|
|
154
|
+
border: 1px solid #1e2d45;
|
|
155
|
+
color: #8899aa;
|
|
156
|
+
border-radius: 3px;
|
|
157
|
+
padding: 4px 8px;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
font-size: 0.9rem;
|
|
160
|
+
transition: background 0.12s, color 0.12s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nb-sp-anim-btn:hover:not(:disabled) {
|
|
164
|
+
background: #1a2235;
|
|
165
|
+
color: #c8bfa0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.nb-sp-anim-btn:disabled {
|
|
169
|
+
opacity: 0.35;
|
|
170
|
+
cursor: default;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.nb-sp-anim-btn--play {
|
|
174
|
+
border-color: transparent;
|
|
175
|
+
padding: 4px 12px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.nb-sp-anim-btn--active {
|
|
179
|
+
background: #1a2235;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.nb-sp-anim-counter {
|
|
183
|
+
margin-left: auto;
|
|
184
|
+
font-size: 0.78rem;
|
|
185
|
+
color: #556677;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.nb-sp-anim-narration {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
gap: 2px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.nb-sp-anim-label {
|
|
195
|
+
font-size: 0.8rem;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.nb-sp-anim-text {
|
|
200
|
+
font-size: 0.82rem;
|
|
201
|
+
color: #8899aa;
|
|
202
|
+
font-style: italic;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ── Series nav row ────────────────────────────────────────────────────────── */
|
|
206
|
+
.nb-sp-nav-row {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 12px;
|
|
210
|
+
background: #080e1acc;
|
|
211
|
+
border-top: 1px solid #1e2d45;
|
|
212
|
+
padding: 6px 12px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.nb-sp-nav-btn {
|
|
216
|
+
background: transparent;
|
|
217
|
+
border: 1px solid #1e2d45;
|
|
218
|
+
color: #8899aa;
|
|
219
|
+
border-radius: 3px;
|
|
220
|
+
padding: 4px 10px;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
font-family: inherit;
|
|
223
|
+
font-size: 0.82rem;
|
|
224
|
+
transition: background 0.12s, color 0.12s;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.nb-sp-nav-btn:hover:not(:disabled) {
|
|
228
|
+
background: #111928;
|
|
229
|
+
color: #c8bfa0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.nb-sp-nav-btn:disabled {
|
|
233
|
+
opacity: 0.35;
|
|
234
|
+
cursor: default;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.nb-sp-nav-label {
|
|
238
|
+
flex: 1;
|
|
239
|
+
text-align: center;
|
|
240
|
+
font-size: 0.78rem;
|
|
241
|
+
color: #556677;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ── Event detail panel ────────────────────────────────────────────────────── */
|
|
245
|
+
.nb-sp-detail {
|
|
246
|
+
background: #080e1a;
|
|
247
|
+
border-left: 1px solid #1e2d45;
|
|
248
|
+
overflow-y: auto;
|
|
249
|
+
max-height: 100%;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.nb-sp-panel {
|
|
253
|
+
padding: 16px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.nb-sp-panel--loading,
|
|
257
|
+
.nb-sp-panel--error {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: center;
|
|
261
|
+
min-height: 200px;
|
|
262
|
+
padding: 16px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.nb-sp-panel-spinner {
|
|
266
|
+
width: 28px;
|
|
267
|
+
height: 28px;
|
|
268
|
+
border: 3px solid #1e2d45;
|
|
269
|
+
border-top-color: var(--nb-sp-accent, #cfb53b);
|
|
270
|
+
border-radius: 50%;
|
|
271
|
+
animation: nb-sp-spin 0.7s linear infinite;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@keyframes nb-sp-spin {
|
|
275
|
+
to { transform: rotate(360deg); }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.nb-sp-panel-error {
|
|
279
|
+
color: #cc4444;
|
|
280
|
+
font-size: 0.85rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.nb-sp-panel-header {
|
|
284
|
+
margin-bottom: 12px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.nb-sp-panel-title {
|
|
288
|
+
font-family: 'Old Standard TT', Georgia, serif;
|
|
289
|
+
font-size: 1.1rem;
|
|
290
|
+
margin: 0 0 4px;
|
|
291
|
+
line-height: 1.3;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.nb-sp-panel-date {
|
|
295
|
+
font-size: 0.78rem;
|
|
296
|
+
color: #556677;
|
|
297
|
+
margin: 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.nb-sp-panel-body {
|
|
301
|
+
font-size: 0.88rem;
|
|
302
|
+
line-height: 1.65;
|
|
303
|
+
color: #c8bfa0;
|
|
304
|
+
margin-bottom: 16px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.nb-sp-panel-section {
|
|
308
|
+
margin-top: 16px;
|
|
309
|
+
border-top: 1px solid #1a2235;
|
|
310
|
+
padding-top: 12px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.nb-sp-panel-section-title {
|
|
314
|
+
font-family: 'Old Standard TT', Georgia, serif;
|
|
315
|
+
font-size: 0.8rem;
|
|
316
|
+
color: #556677;
|
|
317
|
+
text-transform: uppercase;
|
|
318
|
+
letter-spacing: 0.08em;
|
|
319
|
+
margin: 0 0 10px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.nb-sp-panel-gallery {
|
|
323
|
+
display: grid;
|
|
324
|
+
grid-template-columns: repeat(2, 1fr);
|
|
325
|
+
gap: 6px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.nb-sp-panel-gallery-img {
|
|
329
|
+
width: 100%;
|
|
330
|
+
aspect-ratio: 4/3;
|
|
331
|
+
object-fit: cover;
|
|
332
|
+
border-radius: 2px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.nb-sp-panel-relations {
|
|
336
|
+
list-style: none;
|
|
337
|
+
margin: 0;
|
|
338
|
+
padding: 0;
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
gap: 4px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.nb-sp-panel-rel-btn {
|
|
345
|
+
background: transparent;
|
|
346
|
+
border: 1px solid #1e2d45;
|
|
347
|
+
color: #6699ff;
|
|
348
|
+
border-radius: 3px;
|
|
349
|
+
padding: 4px 10px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
font-family: inherit;
|
|
352
|
+
font-size: 0.82rem;
|
|
353
|
+
width: 100%;
|
|
354
|
+
text-align: left;
|
|
355
|
+
transition: background 0.12s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.nb-sp-panel-rel-btn:hover {
|
|
359
|
+
background: #111928;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.nb-sp-panel-rel-date {
|
|
363
|
+
color: #556677;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* ── Responsive: stack on narrow viewports ─────────────────────────────────── */
|
|
367
|
+
@media (max-width: 900px) {
|
|
368
|
+
.nb-sp-root {
|
|
369
|
+
grid-template-columns: 1fr;
|
|
370
|
+
grid-template-rows: auto auto auto;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.nb-sp-sidebar {
|
|
374
|
+
border-right: none;
|
|
375
|
+
border-bottom: 1px solid #1e2d45;
|
|
376
|
+
max-height: 180px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.nb-sp-detail {
|
|
380
|
+
border-left: none;
|
|
381
|
+
border-top: 1px solid #1e2d45;
|
|
382
|
+
max-height: none;
|
|
383
|
+
}
|
|
384
|
+
}
|