@hyperframes/player 0.7.6 → 0.7.8
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/slideshow/hyperframes-slideshow.cjs +22 -22
- package/dist/slideshow/hyperframes-slideshow.cjs.map +1 -1
- package/dist/slideshow/hyperframes-slideshow.d.cts +42 -0
- package/dist/slideshow/hyperframes-slideshow.d.ts +42 -0
- package/dist/slideshow/hyperframes-slideshow.global.js +22 -22
- package/dist/slideshow/hyperframes-slideshow.global.js.map +1 -1
- package/dist/slideshow/hyperframes-slideshow.js +27 -27
- package/dist/slideshow/hyperframes-slideshow.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../core/src/slideshow/parseSlideshow.ts","../../src/slideshow/SlideshowController.ts","../../src/slideshow/slideshowPresenter.ts","../../src/slideshow/hyperframes-slideshow.ts"],"sourcesContent":["// packages/core/src/slideshow/parseSlideshow.ts\nimport type {\n SlideshowManifest,\n SlideRef,\n ResolvedSlide,\n ResolvedSlideshow,\n ResolvedSlideSequence,\n} from \"./slideshow.types\";\n\nexport const SLIDESHOW_ISLAND_TYPE = \"application/hyperframes-slideshow+json\";\n\n/**\n * Builds the island <script> matcher. Capture group 1 = inner JSON.\n *\n * Factory (fresh RegExp per call) on purpose: a RegExp with the `g` flag carries\n * a mutable `lastIndex`, so callers that need `g` must call this each time rather\n * than caching a shared instance.\n */\nexport function slideshowIslandRegex(flags = \"i\"): RegExp {\n // Escape ALL regex metacharacters in the constant (CodeQL flags the incomplete\n // `\\` escape). The constant has none today, but a complete escape is correct.\n const escaped = SLIDESHOW_ISLAND_TYPE.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`<script[^>]*type=[\"']${escaped}[\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/script>`, flags);\n}\n\ninterface SceneRange {\n id: string;\n start: number;\n duration: number;\n}\n\n/** Extract the JSON island from composition HTML. Returns null if absent. */\nexport function parseSlideshowManifest(html: string): SlideshowManifest | null {\n // Match <script type=\"application/hyperframes-slideshow+json\"> ... </script>\n const re = slideshowIslandRegex(\"i\");\n const match = re.exec(html);\n if (!match || match[1] === undefined) return null;\n const raw = match[1].trim();\n if (raw.length === 0) return null;\n const parsed: unknown = JSON.parse(raw);\n if (!isManifest(parsed)) {\n throw new Error(\"slideshow island is not a valid SlideshowManifest\");\n }\n return parsed;\n}\n\nfunction isSlideRef(v: unknown): v is SlideRef {\n if (typeof v !== \"object\" || v === null) return false;\n const r = v as Record<string, unknown>;\n if (typeof r[\"sceneId\"] !== \"string\") return false;\n if (\n r[\"fragments\"] !== undefined &&\n !(Array.isArray(r[\"fragments\"]) && r[\"fragments\"].every((n) => typeof n === \"number\"))\n )\n return false;\n if (r[\"hotspots\"] !== undefined && !Array.isArray(r[\"hotspots\"])) return false;\n return true;\n}\n\nfunction isSlideSequence(v: unknown): boolean {\n if (typeof v !== \"object\" || v === null) return false;\n const s = v as Record<string, unknown>;\n return (\n typeof s[\"id\"] === \"string\" &&\n typeof s[\"label\"] === \"string\" &&\n Array.isArray(s[\"slides\"]) &&\n s[\"slides\"].every(isSlideRef)\n );\n}\n\nfunction isManifest(v: unknown): v is SlideshowManifest {\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) return false;\n const o = v as Record<string, unknown>;\n if (!Array.isArray(o[\"slides\"]) || !o[\"slides\"].every(isSlideRef)) return false;\n if (o[\"slideSequences\"] !== undefined) {\n if (!Array.isArray(o[\"slideSequences\"]) || !o[\"slideSequences\"].every(isSlideSequence))\n return false;\n }\n return true;\n}\n\nfunction missingBoundError(sceneId: string, missing: \"startTime\" | \"endTime\"): string {\n const present = missing === \"startTime\" ? \"endTime\" : \"startTime\";\n return `slide \"${sceneId}\" sets ${present} but ${missing} cannot be resolved (no scene \"${sceneId}\")`;\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTimeRange(\n ref: SlideRef,\n scene: SceneRange | undefined,\n errors: string[],\n): { start: number; end: number } {\n const { startTime, endTime, sceneId } = ref;\n\n // Both explicit — use them directly, no scene needed.\n if (startTime !== undefined && endTime !== undefined) {\n return { start: startTime, end: endTime };\n }\n\n // Neither explicit — resolve both from scene.\n if (startTime === undefined && endTime === undefined) {\n if (!scene) {\n errors.push(`slide references unresolved sceneId \"${sceneId}\"`);\n return { start: 0, end: 0 };\n }\n return { start: scene.start, end: scene.start + scene.duration };\n }\n\n // Exactly one bound explicit — fill from scene, or report a clear error.\n if (!scene) {\n const missing = startTime === undefined ? \"startTime\" : \"endTime\";\n errors.push(missingBoundError(sceneId, missing));\n const bound = startTime ?? endTime ?? 0;\n return { start: bound, end: bound };\n }\n\n return {\n start: startTime ?? scene.start,\n end: endTime ?? scene.start + scene.duration,\n };\n}\n\nfunction validateFragments(\n sceneId: string,\n fragments: number[],\n start: number,\n end: number,\n errors: string[],\n): void {\n for (const f of fragments) {\n if (f < start || f > end) {\n errors.push(`slide \"${sceneId}\" fragment ${f} is outside range [${start}, ${end}]`);\n }\n }\n}\n\nfunction resolveSlide(\n ref: SlideRef,\n sceneById: Map<string, SceneRange>,\n errors: string[],\n): ResolvedSlide {\n const scene = sceneById.get(ref.sceneId);\n const { start, end } = resolveTimeRange(ref, scene, errors);\n if (ref.startTime !== undefined && ref.endTime !== undefined && end <= start) {\n errors.push(`slide \"${ref.sceneId}\" has endTime (${end}) <= startTime (${start})`);\n }\n const fragments = [...new Set(ref.fragments ?? [])].sort((a, b) => a - b);\n validateFragments(ref.sceneId, fragments, start, end, errors);\n return { ...ref, start, end, fragments, hotspots: ref.hotspots ?? [] };\n}\n\nexport function resolveSlideshow(\n manifest: SlideshowManifest,\n scenes: SceneRange[],\n): { resolved: ResolvedSlideshow; errors: string[] } {\n const errors: string[] = [];\n const sceneById = new Map(scenes.map((s) => [s.id, s]));\n\n const sequences: Record<string, ResolvedSlideSequence> = {};\n for (const seq of manifest.slideSequences ?? []) {\n // Flag duplicate sequence ids rather than silently overwriting the earlier one.\n if (Object.prototype.hasOwnProperty.call(sequences, seq.id)) {\n errors.push(`duplicate slideSequence id \"${seq.id}\" — only the last definition is kept`);\n }\n sequences[seq.id] = {\n id: seq.id,\n label: seq.label,\n slides: seq.slides.map((s) => resolveSlide(s, sceneById, errors)),\n };\n }\n\n const slides = manifest.slides.map((s) => resolveSlide(s, sceneById, errors));\n\n // Validate hotspot targets.\n const allSlides = [...slides, ...Object.values(sequences).flatMap((s) => s.slides)];\n for (const slide of allSlides) {\n for (const h of slide.hotspots) {\n const seq = sequences[h.target];\n if (!seq) {\n errors.push(`hotspot \"${h.id}\" targets unknown sequence \"${h.target}\"`);\n } else if (seq.slides.length === 0) {\n errors.push(`hotspot \"${h.id}\" targets empty sequence \"${h.target}\"`);\n }\n }\n }\n\n // Validate no main-line overlap (sorted by start; adjacent compare).\n const ordered = [...slides].sort((a, b) => a.start - b.start);\n for (let i = 1; i < ordered.length; i++) {\n const prev = ordered[i - 1];\n const curr = ordered[i];\n if (prev !== undefined && curr !== undefined && curr.start < prev.end) {\n errors.push(`main-line slides \"${prev.sceneId}\" and \"${curr.sceneId}\" overlap`);\n }\n }\n\n return { resolved: { slides, sequences }, errors };\n}\n","import type { ResolvedSlideshow, ResolvedSlide } from \"@hyperframes/core/slideshow\";\n\nexport interface PlayerPort {\n seek(t: number): void;\n play(): void;\n pause(): void;\n stopMedia?(): void;\n readonly currentTime: number;\n onTimeUpdate(cb: (t: number) => void): () => void;\n}\n\ninterface StackFrame {\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number; // -1 = before first fragment / at slide start\n}\n\nconst MAIN = \"main\";\n\nexport class SlideshowController {\n private stack: StackFrame[] = [{ sequenceId: MAIN, slideIndex: 0, fragmentIndex: -1 }];\n private changeCbs = new Set<() => void>();\n\n constructor(\n private player: PlayerPort,\n private show: ResolvedSlideshow,\n ) {\n this.enterSlide(0);\n }\n\n // fallow-ignore-next-line unused-class-member\n dispose(): void {\n // No subscriptions to tear down — navigation is seek-driven (see playTo).\n }\n\n private slidesOf(sequenceId: string): ResolvedSlide[] {\n if (sequenceId === MAIN) return this.show.slides;\n return this.show.sequences[sequenceId]?.slides ?? [];\n }\n\n private get frame(): StackFrame {\n return this.stack[this.stack.length - 1];\n }\n\n get currentSlide(): ResolvedSlide | undefined {\n return this.slidesOf(this.frame.sequenceId)[this.frame.slideIndex];\n }\n\n get nextSlide(): ResolvedSlide | null {\n const slides = this.slidesOf(this.frame.sequenceId);\n const next = slides[this.frame.slideIndex + 1];\n return next ?? null;\n }\n\n get position(): { sequenceId: string; slideIndex: number; fragmentIndex: number } {\n return { ...this.frame };\n }\n\n get counter(): { index: number; total: number } {\n return {\n index: this.frame.slideIndex + 1,\n total: this.slidesOf(this.frame.sequenceId).length,\n };\n }\n\n get canPrev(): boolean {\n // prev has a destination: an earlier slide in this sequence, OR (in a branch) the parent.\n return this.frame.slideIndex > 0 || this.stack.length > 1;\n }\n\n get canNext(): boolean {\n // next has a destination: a later slide in this sequence, OR (in a branch) the parent.\n const slides = this.slidesOf(this.frame.sequenceId);\n return this.frame.slideIndex + 1 < slides.length || this.stack.length > 1;\n }\n\n get breadcrumb(): { id: string; label: string }[] {\n return this.stack.map((f) =>\n f.sequenceId === MAIN\n ? { id: MAIN, label: \"Main deck\" }\n : { id: f.sequenceId, label: this.show.sequences[f.sequenceId]?.label ?? f.sequenceId },\n );\n }\n\n // fallow-ignore-next-line unused-class-member\n onChange(cb: () => void): () => void {\n this.changeCbs.add(cb);\n return () => this.changeCbs.delete(cb);\n }\n\n private emitChange(): void {\n for (const cb of this.changeCbs) cb();\n }\n\n private stopSlideMedia(): void {\n this.player.stopMedia?.();\n }\n\n private enterSlide(index: number): void {\n if (index !== this.frame.slideIndex) this.stopSlideMedia();\n this.frame.slideIndex = index;\n const slide = this.currentSlide;\n if (!slide) {\n this.frame.fragmentIndex = -1;\n return;\n }\n // Jump to the slide's first hold and stay there (no auto-progress). With\n // fragments that's the first fragment (fragmentIndex 0); without, a settled\n // frame INSIDE the slide (its midpoint) — NOT slide.end, which is the boundary\n // where the next scene begins (else slide 1 would render slide 2's content).\n if (slide.fragments.length > 0) {\n this.frame.fragmentIndex = 0;\n this.playTo(slide.fragments[0] ?? slide.end);\n } else {\n this.frame.fragmentIndex = -1;\n this.playTo(this.restFrame(slide));\n }\n this.emitChange();\n }\n\n /** A representative, non-boundary frame for a slide with no fragments. */\n private restFrame(slide: ResolvedSlide): number {\n return slide.start + (slide.end - slide.start) * 0.5;\n }\n\n /**\n * Resumes a slide at a saved fragmentIndex without resetting to slide start.\n * Used by back()/backToMain()/syncTo() to restore an exact position.\n */\n private resumeSlide(index: number, fragmentIndex: number): void {\n this.frame.slideIndex = index;\n this.frame.fragmentIndex = fragmentIndex;\n const slide = this.currentSlide;\n if (!slide) return;\n // Resume position, mirroring enterSlide so going back to a slide lands where\n // entering it forward does:\n // - at a saved fragment → that fragment's hold time\n // - fragmented, pre-first → slide.start (before the first reveal)\n // - no fragments → restFrame (midpoint), NOT slide.start, so the\n // slide is visible at rest instead of frozen at its frame-0 (pre-entrance).\n const seekTime =\n fragmentIndex >= 0 && fragmentIndex < slide.fragments.length\n ? (slide.fragments[fragmentIndex] ?? slide.start)\n : slide.fragments.length > 0\n ? slide.start\n : this.restFrame(slide);\n this.playTo(seekTime);\n this.emitChange();\n }\n\n /**\n * Jump to hold time `t` and hold there — a pure, synchronous seek with NO\n * sustained playback, so a slide can never auto-progress.\n *\n * `player.seek(t)` drives the composition's GSAP timeline directly (the player\n * reaches the same-origin iframe's `__timelines`), and GSAP `.seek()` renders\n * that frame synchronously AND leaves the timeline paused. So one seek both\n * repaints and holds — deterministically, in every window including a\n * backgrounded one. (The previous play-a-frame-then-pause-on-a-timeupdate\n * \"render nudge\" left an unfocused audience window playing while it waited for\n * a throttled tick to pause it — that was the auto-progress / one-side-frozen\n * flakiness. fragmentIndex is now set by the caller, not on a played tick.)\n */\n private playTo(t: number): void {\n this.player.seek(t);\n }\n\n next(): void {\n const slide = this.currentSlide;\n if (!slide) return;\n const hasMoreFragments = this.frame.fragmentIndex + 1 < slide.fragments.length;\n if (hasMoreFragments) {\n // Reveal the next fragment — advance the index and seek to its hold time.\n this.frame.fragmentIndex += 1;\n const target = slide.fragments[this.frame.fragmentIndex] ?? slide.end;\n this.playTo(target);\n this.emitChange();\n return;\n }\n // No more fragments to reveal — advance to the next slide immediately instead of\n // playing the current slide out to its end.\n const slides = this.slidesOf(this.frame.sequenceId);\n if (this.frame.slideIndex + 1 < slides.length) {\n this.enterSlide(this.frame.slideIndex + 1);\n } else if (this.stack.length > 1) {\n // End of a branch → return to the parent timeline.\n this.back();\n }\n }\n\n prev(): void {\n if (this.frame.slideIndex > 0) {\n this.enterSlide(this.frame.slideIndex - 1);\n return;\n }\n if (this.stack.length > 1) {\n // First slide of a branch → return to the parent timeline.\n this.back();\n }\n }\n\n goToSlide(index: number): void {\n const slides = this.slidesOf(this.frame.sequenceId);\n if (index >= 0 && index < slides.length) this.enterSlide(index);\n }\n\n enterBranch(sequenceId: string): void {\n const seq = this.show.sequences[sequenceId];\n if (!seq || seq.slides.length === 0) return;\n this.stopSlideMedia();\n this.stack.push({ sequenceId, slideIndex: 0, fragmentIndex: -1 });\n this.enterSlide(0);\n }\n\n back(): void {\n if (this.stack.length <= 1) return;\n this.stopSlideMedia();\n this.stack.pop();\n // Restore the saved fragmentIndex from the parent frame rather than\n // resetting to -1 (which enterSlide would do). This preserves the exact\n // position the presenter was at before entering the branch.\n this.resumeSlide(this.frame.slideIndex, this.frame.fragmentIndex);\n }\n\n backToMain(): void {\n if (this.stack.length <= 1) return;\n this.stopSlideMedia();\n this.stack = [this.stack[0]];\n this.resumeSlide(this.frame.slideIndex, this.frame.fragmentIndex);\n }\n\n /**\n * Jump to an absolute position without animation (audience mirroring).\n * Re-roots the stack to the target sequence, then restores slide+fragment\n * statically via resumeSlide.\n */\n syncTo(sequenceId: string, slideIndex: number, fragmentIndex: number): void {\n if (!this.isValidSyncTarget(sequenceId, slideIndex)) return;\n if (this.isCrossSlide(sequenceId, slideIndex)) this.stopSlideMedia();\n if (!this.rerootStackTo(sequenceId)) return;\n this.resumeSlide(slideIndex, fragmentIndex);\n }\n\n /** True if the target sequence + slide index resolves to a real slide. */\n private isValidSyncTarget(sequenceId: string, slideIndex: number): boolean {\n if (!this.stack[0]) return false;\n const targetSlides =\n sequenceId === MAIN ? this.show.slides : (this.show.sequences[sequenceId]?.slides ?? null);\n if (!targetSlides) return false;\n return slideIndex >= 0 && slideIndex < targetSlides.length;\n }\n\n /** True if the sync target lands on a different slide than current. */\n private isCrossSlide(sequenceId: string, slideIndex: number): boolean {\n return this.frame.sequenceId !== sequenceId || this.frame.slideIndex !== slideIndex;\n }\n\n /**\n * Re-root the navigation stack to `sequenceId` if we're not already there.\n * Returns false only when the target branch sequence is empty (no slides),\n * mirroring the early-return guard in the previous inline form.\n */\n private rerootStackTo(sequenceId: string): boolean {\n if (this.frame.sequenceId === sequenceId) return true;\n const base = this.stack[0];\n if (!base) return false;\n this.stack = [base];\n if (sequenceId === MAIN) return true;\n const seq = this.show.sequences[sequenceId];\n if (!seq || seq.slides.length === 0) return false;\n this.stack.push({ sequenceId, slideIndex: 0, fragmentIndex: -1 });\n return true;\n }\n}\n","export interface PresenterPosition {\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\nconst COUNTER_FONT_FAMILY =\n \"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\n\nexport type PresenterMediaAction =\n | \"play\"\n | \"pause\"\n | \"seeking\"\n | \"seeked\"\n | \"ratechange\"\n | \"volumechange\"\n | \"ended\"\n | \"timeupdate\";\n\nconst MEDIA_ACTIONS = new Set<unknown>([\n \"play\",\n \"pause\",\n \"seeking\",\n \"seeked\",\n \"ratechange\",\n \"volumechange\",\n \"ended\",\n \"timeupdate\",\n]);\nconst MEDIA_NUMBER_FIELDS = [\"currentTime\", \"volume\", \"playbackRate\"] as const;\nconst MEDIA_BOOLEAN_FIELDS = [\"paused\", \"ended\", \"muted\"] as const;\n\ninterface GotoMessage {\n type: \"goto\";\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\nexport interface PresenterMediaMessage {\n type: \"media\";\n sender: \"presenter\" | \"audience\";\n key: string;\n action: PresenterMediaAction;\n currentTime: number;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n volume: number;\n playbackRate: number;\n}\n\nfunction isRecord(data: unknown): data is Record<string, unknown> {\n if (typeof data !== \"object\" || data === null) return false;\n return true;\n}\n\nfunction isGotoMessage(data: unknown): data is GotoMessage {\n if (!isRecord(data)) return false;\n const d = data as Record<string, unknown>;\n return (\n d[\"type\"] === \"goto\" &&\n typeof d[\"sequenceId\"] === \"string\" &&\n typeof d[\"slideIndex\"] === \"number\" &&\n typeof d[\"fragmentIndex\"] === \"number\"\n );\n}\n\nfunction isMediaAction(value: unknown): value is PresenterMediaAction {\n return MEDIA_ACTIONS.has(value);\n}\n\nfunction isMediaSender(value: unknown): value is PresenterMediaMessage[\"sender\"] {\n return value === \"presenter\" || value === \"audience\";\n}\n\nfunction hasMediaNumberFields(data: Record<string, unknown>): boolean {\n return MEDIA_NUMBER_FIELDS.every((field) => typeof data[field] === \"number\");\n}\n\nfunction hasMediaBooleanFields(data: Record<string, unknown>): boolean {\n return MEDIA_BOOLEAN_FIELDS.every((field) => typeof data[field] === \"boolean\");\n}\n\nfunction isMediaMessage(data: unknown): data is PresenterMediaMessage {\n if (!isRecord(data)) return false;\n const d = data;\n return (\n d[\"type\"] === \"media\" &&\n isMediaSender(d[\"sender\"]) &&\n typeof d[\"key\"] === \"string\" &&\n isMediaAction(d[\"action\"]) &&\n hasMediaNumberFields(d) &&\n hasMediaBooleanFields(d)\n );\n}\n\n/**\n * Manages the BroadcastChannel connection for a single slideshow element.\n * Presenter (default) mode: posts position updates to the channel.\n * Audience mode: listens for goto messages and calls the provided handler.\n */\n/**\n * Per-deck channel name. The presenter and its audience window load the same URL\n * (path), so keying on pathname keeps them paired while isolating other decks\n * presenting on the same origin (which would otherwise cross-talk on a fixed name).\n */\nexport function slideshowChannelName(): string {\n const path = typeof location !== \"undefined\" ? location.pathname : \"\";\n return `hf-slideshow:${path}`;\n}\n\nexport class SlideshowChannel {\n private channel: BroadcastChannel | null = null;\n\n constructor(\n private readonly mode: \"presenter\" | \"audience\",\n private readonly onGoto: (msg: GotoMessage) => void,\n private readonly onMedia: (msg: PresenterMediaMessage) => void = () => {},\n ) {\n try {\n this.channel = new BroadcastChannel(slideshowChannelName());\n } catch {\n // BroadcastChannel unavailable (e.g. unsupported env); degrade silently.\n return;\n }\n\n this.channel.onmessage = (e: MessageEvent) => {\n if (isGotoMessage(e.data)) {\n if (mode === \"audience\") {\n this.onGoto(e.data);\n }\n return;\n }\n if (isMediaMessage(e.data) && e.data.sender !== mode) {\n this.onMedia(e.data);\n }\n };\n }\n\n postPosition(pos: PresenterPosition): void {\n if (this.mode !== \"presenter\" || !this.channel) return;\n const msg: GotoMessage = { type: \"goto\", ...pos };\n this.channel.postMessage(msg);\n }\n\n postMedia(msg: Omit<PresenterMediaMessage, \"type\" | \"sender\">): void {\n if (!this.channel) return;\n this.channel.postMessage({ type: \"media\", sender: this.mode, ...msg });\n }\n\n destroy(): void {\n if (this.channel) {\n this.channel.onmessage = null;\n this.channel.close();\n this.channel = null;\n }\n }\n}\n\n/**\n * Builds the presenter-mode bottom panel: speaker notes + up-next + counter +\n * elapsed. The live slide is shown ABOVE this panel (the component confines the\n * player to the top region). Returns the panel HTML only — the component appends\n * the nav controls separately.\n */\nexport function buildPresenterLayout(opts: {\n notes: string;\n notesStorageKey: string | null;\n nextText: string;\n counterText: string;\n elapsedText: string;\n hotspots: { id: string; label: string; target: string }[];\n}): string {\n const esc = (s: string) => s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n const escAttr = (s: string) => esc(s).replace(/\"/g, \""\");\n const notes = esc(opts.notes);\n // Branch entries for the current slide — the presenter clicks these to enter a\n // branch (the audience follows). The component wires [data-hotspot-id] to\n // enterBranch(); positioned pills don't align with the letterboxed slide, so\n // they live in the console as a list.\n const branches = opts.hotspots.length\n ? `<div style=\"display:flex;flex-direction:column;gap:6px;\">\n <div style=\"font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;\">Branches</div>\n ${opts.hotspots\n .map(\n (h) =>\n `<button data-hotspot-id=\"${escAttr(h.id)}\" data-hotspot-target=\"${escAttr(h.target)}\" type=\"button\" title=\"${escAttr(h.label)}\" style=\"text-align:left;background:rgba(244,183,64,0.14);color:#f4b740;border:1px solid rgba(244,183,64,0.4);border-radius:8px;padding:8px 12px;font-size:15px;cursor:pointer;pointer-events:auto;font-family:inherit;\">↳ ${esc(h.label)}</button>`,\n )\n .join(\"\")}\n </div>`\n : \"\";\n return `\n<div data-hf-presenter style=\"position:absolute;left:0;right:0;bottom:0;height:32%;display:flex;background:#11151f;color:#fff;border-top:2px solid rgba(255,255,255,0.12);box-sizing:border-box;font-family:sans-serif;pointer-events:auto;\">\n <textarea data-hf-presenter-notes data-hf-presenter-notes-key=\"${escAttr(opts.notesStorageKey ?? \"\")}\" aria-label=\"Speaker notes\" placeholder=\"No notes for this slide\" spellcheck=\"true\" style=\"flex:1;min-width:0;padding:24px 36px;overflow:auto;font:inherit;font-size:21px;line-height:1.55;color:#fff;background:transparent;border:0;outline:none;resize:none;white-space:pre-wrap;pointer-events:auto;\">${notes}</textarea>\n <div style=\"width:380px;flex-shrink:0;border-left:1px solid rgba(255,255,255,0.12);padding:24px 28px;display:flex;flex-direction:column;gap:10px;\">\n <div style=\"font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;\">Up next</div>\n <div data-hf-presenter-next style=\"font-size:17px;opacity:.9;line-height:1.4;\">${esc(opts.nextText)}</div>\n ${branches}\n <div style=\"display:flex;gap:34px;margin-top:auto;\">\n <div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:.1em;opacity:.5;margin-bottom:3px;\">Slide</div><div data-hf-presenter-counter style=\"font-family:${COUNTER_FONT_FAMILY};font-size:23px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;\">${esc(opts.counterText)}</div></div>\n <div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:.1em;opacity:.5;margin-bottom:3px;\">Elapsed</div><div data-hf-presenter-elapsed style=\"font-family:${COUNTER_FONT_FAMILY};font-size:23px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;\">${esc(opts.elapsedText)}</div></div>\n </div>\n </div>\n</div>`.trim();\n}\n\n/** Format elapsed seconds as mm:ss */\nexport function formatElapsed(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.floor(seconds % 60);\n return `${String(m).padStart(2, \"0\")}:${String(s).padStart(2, \"0\")}`;\n}\n","import {\n parseSlideshowManifest,\n resolveSlideshow,\n type ResolvedSlideshow,\n} from \"@hyperframes/core/slideshow\";\nimport { SlideshowController, type PlayerPort } from \"./SlideshowController\";\nimport {\n SlideshowChannel,\n buildPresenterLayout,\n formatElapsed,\n type PresenterMediaAction,\n type PresenterMediaMessage,\n} from \"./slideshowPresenter\";\n\ninterface Hotspot {\n id: string;\n label: string;\n target: string;\n region?: { x: number; y: number; w: number; h: number };\n}\n\ninterface ControllerLike {\n next(): void;\n prev(): void;\n onChange(cb: () => void): () => void;\n readonly counter: { index: number; total: number };\n readonly breadcrumb: { id: string; label: string }[];\n readonly currentSlide: { hotspots: Hotspot[]; notes?: string; sceneId?: string } | undefined;\n readonly nextSlide: { sceneId: string; notes?: string } | null;\n readonly position: { sequenceId: string; slideIndex: number; fragmentIndex: number };\n readonly canPrev?: boolean;\n readonly canNext?: boolean;\n goToSlide?(index: number): void;\n syncTo?(sequenceId: string, slideIndex: number, fragmentIndex: number): void;\n enterBranch?(id: string): void;\n back?(): void;\n backToMain?(): void;\n dispose?(): void;\n}\n\ninterface SlideNotesTarget {\n sceneId?: string;\n}\n\ntype SlideshowManifest = NonNullable<ReturnType<typeof parseSlideshowManifest>>;\n\ntype PlayerElement = HTMLElement & {\n seek(t: number): void;\n play(): void;\n pause(): void;\n stopMedia?(): void;\n muted?: boolean;\n readonly iframeElement?: HTMLIFrameElement;\n readonly currentTime: number;\n readonly ready: boolean;\n};\n\ntype SlideshowMediaElement = HTMLMediaElement & {\n dataset: DOMStringMap;\n};\n\nfunction isPlayerElement(el: HTMLElement): el is PlayerElement {\n return (\n typeof (el as PlayerElement).seek === \"function\" &&\n typeof (el as PlayerElement).play === \"function\" &&\n typeof (el as PlayerElement).pause === \"function\"\n );\n}\n\nconst PRESENTER_NOTES_STORAGE_PREFIX = \"hf-slideshow:presenter-notes:v1:\";\n\n// Injected once per document to avoid duplicating @keyframes across multiple elements.\nlet _keyframesInjected = false;\nfunction injectKeyframesOnce(): void {\n if (_keyframesInjected) return;\n _keyframesInjected = true;\n const style = document.createElement(\"style\");\n style.textContent = `\n @keyframes hf-hotspot-pulse {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0.35), 0 4px 16px rgba(0,0,0,0.35); }\n 50% { box-shadow: 0 0 0 8px rgba(255,255,255,0), 0 4px 20px rgba(0,0,0,0.45); }\n }\n @keyframes hf-nav-spin {\n to { transform: rotate(360deg); }\n }\n @media (prefers-reduced-motion: reduce) {\n .hf-hotspot-pill,\n .hf-nav-spinner { animation: none !important; }\n }\n /* Nav-button hover (replaces inline onmouseover/onmouseout — CSP-safe).\n !important beats the inline base color set on each button. */\n [data-hf-nav-cluster] button:hover {\n background: rgba(255,255,255,0.12) !important;\n color: #fff !important;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip] {\n position: relative;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]::after {\n position: absolute;\n left: 50%;\n opacity: 0;\n pointer-events: none;\n transform: translateX(-50%) translateY(3px);\n transition: opacity 0.12s ease, transform 0.12s ease;\n z-index: 20;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::before {\n content: \"\";\n bottom: calc(100% + 4px);\n border: 5px solid transparent;\n border-top-color: rgba(12,12,14,0.95);\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::after {\n content: attr(data-hf-tooltip);\n bottom: calc(100% + 14px);\n padding: 6px 8px;\n border-radius: 6px;\n background: rgba(12,12,14,0.95);\n color: #fff;\n box-shadow: 0 6px 20px rgba(0,0,0,0.35);\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0;\n line-height: 1;\n white-space: nowrap;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]:hover::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]:hover::after,\n [data-hf-nav-cluster] button[data-hf-tooltip]:focus-visible::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]:focus-visible::after {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n /* When muted, the speaker button stays dimmed on hover so the mute-state\n affordance isn't erased (higher specificity than the rule above). */\n [data-hf-muted] [data-hf-mute]:hover {\n color: rgba(255,255,255,0.6) !important;\n }\n `;\n document.head.appendChild(style);\n}\n\n// Fullscreen glyphs (enter = expand corners, exit = collapse corners). Module-level\n// so onFsChange can swap just this glyph without re-rendering the whole chrome.\nconst ENTER_FS_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3\"/></svg>`;\nconst EXIT_FS_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3\"/></svg>`;\nconst PRESENT_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"13\" rx=\"2\"/><path d=\"M8 21h8\"/><path d=\"M12 17v4\"/><path d=\"M10 8.5v4l4-2-4-2z\"/></svg>`;\nconst COUNTER_FONT_FAMILY =\n \"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\n\nexport class HyperframesSlideshow extends HTMLElement {\n private controller: ControllerLike | null = null;\n private offChange: (() => void) | null = null;\n private chrome: HTMLDivElement | null = null;\n private touchStartX = 0;\n private touchStartY = 0;\n private channel: SlideshowChannel | null = null;\n private presenterStartMs: number | null = null;\n private presenterInterval: ReturnType<typeof setInterval> | null = null;\n private presenterPositionTimers: ReturnType<typeof setTimeout>[] = [];\n private disconnected = false;\n private initTimer: ReturnType<typeof setTimeout> | null = null;\n private initInFlight = false;\n private initGeneration = 0;\n private _muted = false;\n private mediaWireInterval: ReturnType<typeof setInterval> | null = null;\n private applyingRemoteMedia = false;\n private lastMediaTimeBroadcastMs = 0;\n private audienceMutedPlaybackKeys = new Set<string>();\n private blockedAudienceMedia = new Map<string, PresenterMediaMessage>();\n private audienceMediaUnlockButton: HTMLButtonElement | null = null;\n\n /** Whether audio is currently muted. Reflects `data-hf-muted` attribute. */\n get muted(): boolean {\n return this._muted;\n }\n\n /** Mode resolves from the `mode` attribute, falling back to the URL query\n * (?mode=audience) so the audience window opened by present() is detected. */\n private resolveMode(): string | null {\n const attr = this.getAttribute(\"mode\");\n if (attr) return attr;\n try {\n return new URLSearchParams(location.search).get(\"mode\");\n } catch {\n return null;\n }\n }\n\n // Observe the attributes the component reads so runtime toggles take effect.\n static get observedAttributes(): string[] {\n return [\"sound\", \"mode\"];\n }\n\n attributeChangedCallback(): void {\n // Re-render once bound so a flipped `sound`/`mode` is reflected (mute button,\n // audience-vs-presenter chrome). No-op before the controller binds.\n if (this.controller) this.render();\n }\n\n connectedCallback(): void {\n this.disconnected = false;\n this.initInFlight = false;\n this.initGeneration += 1;\n this.tabIndex = 0;\n // note: if the inner player iframe has keyboard focus, window keydown in the\n // top document won't fire — that edge remains; this listener fixes the dominant\n // case where the page loads and arrows should work without clicking the element.\n window.addEventListener(\"keydown\", this.onKey);\n this.addEventListener(\"touchstart\", this.onTouchStart, { passive: true });\n this.addEventListener(\"touchend\", this.onTouchEnd);\n window.addEventListener(\"message\", this.onMessage);\n document.addEventListener(\"fullscreenchange\", this.onFsChange);\n this.initChannel();\n // Defer player-dependent init to a macrotask so that child elements are\n // parsed before we query for <hyperframes-player>. This matters when the\n // bundle is loaded synchronously (e.g. <script src> in <head>), where\n // connectedCallback fires while the parser is still inside the\n // <hyperframes-slideshow> open tag — before its children exist. A microtask\n // is NOT sufficient: during streamed parsing the children are appended in a\n // later task, so a queued microtask still observes an empty subtree. A\n // setTimeout(0) macrotask yields to the parser so the children land first.\n this.initTimer = setTimeout(() => {\n this.initTimer = null;\n if (this.isConnected && !this.disconnected) void this.init();\n }, 0);\n }\n\n disconnectedCallback(): void {\n this.disconnected = true;\n this.initGeneration += 1;\n if (this.initTimer !== null) {\n clearTimeout(this.initTimer);\n this.initTimer = null;\n }\n window.removeEventListener(\"keydown\", this.onKey);\n this.removeEventListener(\"touchstart\", this.onTouchStart);\n this.removeEventListener(\"touchend\", this.onTouchEnd);\n window.removeEventListener(\"message\", this.onMessage);\n document.removeEventListener(\"fullscreenchange\", this.onFsChange);\n this.offChange?.();\n this.offChange = null;\n this.controller?.dispose?.();\n this.controller = null;\n this.chrome = null;\n this.channel?.destroy();\n this.channel = null;\n if (this.mediaWireInterval !== null) {\n clearInterval(this.mediaWireInterval);\n this.mediaWireInterval = null;\n }\n this.audienceMediaUnlockButton?.remove();\n this.audienceMediaUnlockButton = null;\n this.audienceMutedPlaybackKeys.clear();\n this.blockedAudienceMedia.clear();\n if (this.presenterInterval !== null) {\n clearInterval(this.presenterInterval);\n this.presenterInterval = null;\n }\n this.clearPresenterPositionTimers();\n }\n\n /** Test seam: inject a controller without a live player. */\n __setControllerForTest(c: ControllerLike): void {\n this.bindController(c);\n }\n\n /**\n * Opens an audience window and switches this element to presenter layout.\n * Audience window URL: current page URL with `mode=audience` query param.\n */\n present(): void {\n if (this.resolveMode() === \"audience\" || this.getAttribute(\"data-hf-presenting\") === \"true\") {\n return;\n }\n const sep = location.search ? \"&\" : \"?\";\n // noopener,noreferrer: the audience window must not get a reference back to\n // this window (it syncs over BroadcastChannel, not window.opener).\n window.open(location.href + sep + \"mode=audience\", \"_blank\", \"noopener,noreferrer\");\n this.setAttribute(\"data-hf-presenting\", \"true\");\n this.postCurrentPresenterPositionBurst();\n this.presenterStartMs = Date.now();\n if (this.presenterInterval === null) {\n this.presenterInterval = setInterval(() => this.updateElapsed(), 1000);\n }\n this.render();\n }\n\n /**\n * Update only the elapsed readout. Re-rendering the whole chrome every second\n * (the old behavior) rebuilt the nav buttons' DOM on each tick — they\n * flickered and clicks landing mid-rebuild were dropped.\n */\n private updateElapsed(): void {\n if (this.presenterStartMs === null) return;\n const el = this.chrome?.querySelector(\"[data-hf-presenter-elapsed]\");\n if (el) {\n el.textContent = formatElapsed(Math.floor((Date.now() - this.presenterStartMs) / 1000));\n }\n }\n\n private initChannel(): void {\n const mode = this.resolveMode();\n if (mode === \"audience\") {\n this.channel = new SlideshowChannel(\n \"audience\",\n (msg) => {\n if (!this.controller) return;\n this.controller.syncTo?.(msg.sequenceId, msg.slideIndex, msg.fragmentIndex);\n },\n this.onRemoteMedia,\n );\n } else {\n this.channel = new SlideshowChannel(\n \"presenter\",\n () => {\n // presenter channel does not receive goto messages; posting happens in bindController.\n },\n this.onRemoteMedia,\n );\n }\n }\n\n // fallow-ignore-next-line complexity\n private async init(): Promise<void> {\n if (this.initInFlight) return;\n this.initInFlight = true;\n const gen = this.initGeneration;\n\n try {\n const playerEl = this.querySelector(\"hyperframes-player\");\n if (!playerEl || !(playerEl instanceof HTMLElement)) return;\n\n const html = this.innerHTML;\n let manifest: ReturnType<typeof parseSlideshowManifest>;\n try {\n manifest = parseSlideshowManifest(html);\n } catch {\n // Malformed island (e.g. bad JSON) — fail gracefully, no chrome.\n return;\n }\n if (!manifest) return;\n\n this.renderInitialChrome(manifest);\n\n if (!isPlayerElement(playerEl)) return;\n\n await waitForReady(playerEl);\n\n // Guard: if a disconnect or reconnect happened while waiting, bail out.\n if (gen !== this.initGeneration) return;\n\n // Wait for scenes to be populated (the runtime \"timeline\" postMessage\n // arrives ~1000ms after waitForReady resolves). Graceful fallback to []\n // on timeout so explicit startTime/endTime slides still work.\n const scenes = await waitForScenes(playerEl, 2500, () => gen !== this.initGeneration);\n\n // Guard again in case we were disconnected or reconnected during the scenes wait.\n if (gen !== this.initGeneration) return;\n\n const { resolved, errors } = resolveSlideshow(manifest, scenes);\n if (errors.length > 0) {\n console.warn(\"[hyperframes-slideshow] manifest errors:\", errors);\n }\n const cleaned = dropInvalidSlides(resolved);\n if (cleaned.slides.length === 0 && manifest.slides.length > 0) {\n console.error(\n \"[hyperframes-slideshow] no main-line slides resolved — the scene timeline may not have loaded in time, or sceneIds/timing are invalid:\",\n errors,\n );\n }\n\n const port: PlayerPort = {\n seek: (t) => playerEl.seek(t),\n play: () => playerEl.play(),\n pause: () => playerEl.pause(),\n stopMedia: () => {\n playerEl.stopMedia?.();\n this.stopDocumentMedia();\n },\n get currentTime() {\n return playerEl.currentTime;\n },\n onTimeUpdate: (cb) => {\n const handler = (e: Event) => {\n const detail = (e as CustomEvent<{ currentTime: number }>).detail;\n cb(detail.currentTime);\n };\n playerEl.addEventListener(\"timeupdate\", handler);\n return () => playerEl.removeEventListener(\"timeupdate\", handler);\n },\n };\n\n this.bindController(new SlideshowController(port, cleaned));\n\n // Slow-iframe recovery: if the scene timeline hadn't posted yet (empty\n // scenes → sceneId-based slides were dropped), re-init once when it finally\n // arrives so those slides resolve instead of being permanently lost.\n if (scenes.length === 0 && manifest.slides.length > 0) {\n playerEl.addEventListener(\n \"scenes\",\n () => {\n if (gen === this.initGeneration) void this.init();\n },\n { once: true },\n );\n }\n } finally {\n this.initInFlight = false;\n }\n }\n\n private renderInitialChrome(manifest: SlideshowManifest): void {\n if (this.controller || manifest.slides.length === 0) return;\n const counter = { index: 1, total: manifest.slides.length };\n if (this.resolveMode() === \"audience\") {\n this.paintChrome(this.buildNavCluster(counter, \"28px\", \"fs-only\"));\n return;\n }\n this.paintChrome(\n this.buildNavCluster(counter, \"28px\", \"full\", {\n canPrev: false,\n canNext: manifest.slides.length > 1,\n loading: manifest.slides.length > 1,\n }),\n );\n }\n\n private bindController(c: ControllerLike): void {\n this.offChange?.();\n this.controller?.dispose?.();\n this.controller = c;\n this.startMediaSync();\n this.offChange = c.onChange(() => {\n // Presenter posts position to channel on every change\n if (this.resolveMode() !== \"audience\" && this.channel) {\n this.channel.postPosition(c.position);\n }\n this.render();\n });\n // Post initial position if presenter\n if (this.resolveMode() !== \"audience\" && this.channel) {\n this.channel.postPosition(c.position);\n }\n this.render();\n }\n\n private postCurrentPresenterPosition(): void {\n if (this.resolveMode() === \"audience\" || !this.channel || !this.controller) return;\n this.channel.postPosition(this.controller.position);\n }\n\n private postCurrentPresenterPositionBurst(): void {\n this.clearPresenterPositionTimers();\n this.postCurrentPresenterPosition();\n for (const delay of [250, 750, 1500, 3000, 5000]) {\n const timer = setTimeout(() => {\n this.presenterPositionTimers = this.presenterPositionTimers.filter(\n (item) => item !== timer,\n );\n this.postCurrentPresenterPosition();\n }, delay);\n this.presenterPositionTimers.push(timer);\n }\n }\n\n private clearPresenterPositionTimers(): void {\n for (const timer of this.presenterPositionTimers) {\n clearTimeout(timer);\n }\n this.presenterPositionTimers = [];\n }\n\n private startMediaSync(): void {\n this.wireSlideshowMedia();\n if (this.mediaWireInterval === null) {\n // Same-origin player iframes can hydrate media after the slideshow binds.\n // The dataset guard prevents duplicate listeners, and removed iframe nodes\n // are collectable because this component keeps no media element references.\n this.mediaWireInterval = setInterval(() => this.wireSlideshowMedia(), 1000);\n }\n }\n\n private mediaPlayerElements(): (Partial<PlayerElement> & HTMLElement)[] {\n return Array.from(this.querySelectorAll(\"hyperframes-player\")).filter(\n (player): player is Partial<PlayerElement> & HTMLElement => player instanceof HTMLElement,\n );\n }\n\n private playerFrameDocument(player: Partial<PlayerElement> & HTMLElement): Document | null {\n const frame = player.iframeElement;\n if (!(frame instanceof HTMLIFrameElement)) return null;\n try {\n return frame.contentDocument;\n } catch {\n return null;\n }\n }\n\n private mediaKey(\n player: HTMLElement,\n playerIndex: number,\n media: SlideshowMediaElement,\n mediaIndex: number,\n ): string {\n const playerKey = player.id ? `player-id:${player.id}` : `player:${playerIndex}`;\n const mediaKey = media.id ? `id:${media.id}` : `${media.tagName.toLowerCase()}:${mediaIndex}`;\n return `${playerKey}|${mediaKey}`;\n }\n\n private mediaEntries(): { key: string; el: SlideshowMediaElement }[] {\n const entries: { key: string; el: SlideshowMediaElement }[] = [];\n this.mediaPlayerElements().forEach((player, playerIndex) => {\n const doc = this.playerFrameDocument(player);\n if (!doc) return;\n Array.from(doc.querySelectorAll(\"video,audio\"))\n .filter(isSlideshowMediaElement)\n .forEach((el, mediaIndex) => {\n entries.push({ key: this.mediaKey(player, playerIndex, el, mediaIndex), el });\n });\n });\n return entries;\n }\n\n private wireSlideshowMedia(): void {\n const actions: PresenterMediaAction[] = [\n \"play\",\n \"pause\",\n \"seeking\",\n \"seeked\",\n \"ratechange\",\n \"volumechange\",\n \"ended\",\n \"timeupdate\",\n ];\n for (const { key, el } of this.mediaEntries()) {\n if (el.dataset.hfSlideshowMediaSync === \"1\") continue;\n el.dataset.hfSlideshowMediaSync = \"1\";\n for (const action of actions) {\n el.addEventListener(action, () => this.publishMediaState(el, key, action));\n }\n }\n }\n\n private publishMediaState(\n el: SlideshowMediaElement,\n key: string,\n action: PresenterMediaAction,\n ): void {\n if (this.applyingRemoteMedia || this.resolveMode() === \"audience\" || !this.channel) return;\n if (action === \"timeupdate\") {\n const now = performance.now();\n if (now - this.lastMediaTimeBroadcastMs < 450 && !el.paused) return;\n this.lastMediaTimeBroadcastMs = now;\n }\n this.channel.postMedia({\n key,\n action,\n currentTime: finiteMediaNumber(el.currentTime, 0),\n paused: el.paused,\n ended: el.ended,\n muted: el.muted,\n volume: finiteMediaNumber(el.volume, 1),\n playbackRate: finiteMediaNumber(el.playbackRate, 1),\n });\n }\n\n private onRemoteMedia = (msg: PresenterMediaMessage): void => {\n if (this.resolveMode() !== \"audience\") return;\n if (this.blockedAudienceMedia.has(msg.key) && msg.action === \"timeupdate\") {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n return;\n }\n this.wireSlideshowMedia();\n const entry = this.mediaEntries().find((candidate) => candidate.key === msg.key);\n if (!entry) return;\n this.applyingRemoteMedia = true;\n try {\n this.applyRemoteMedia(entry.el, msg);\n } finally {\n setTimeout(() => {\n this.applyingRemoteMedia = false;\n }, 300);\n }\n };\n\n private applyRemoteMedia(el: SlideshowMediaElement, msg: PresenterMediaMessage): void {\n if (msg.action === \"pause\" || msg.action === \"ended\") {\n this.audienceMutedPlaybackKeys.delete(msg.key);\n this.blockedAudienceMedia.delete(msg.key);\n this.syncRemoteMediaState(el, msg, true);\n el.pause();\n this.hideAudienceMediaUnlockIfClear();\n return;\n }\n\n const remoteWantsPlayback =\n msg.action === \"play\" || (msg.action === \"timeupdate\" && msg.paused === false && el.paused);\n if (remoteWantsPlayback) {\n this.playAudienceMediaMuted(el, msg);\n return;\n }\n\n this.syncRemoteMediaState(el, msg, true);\n }\n\n private syncRemoteMediaState(\n el: SlideshowMediaElement,\n msg: PresenterMediaMessage,\n allowTimeSync: boolean,\n ): void {\n el.playbackRate = finiteMediaNumber(msg.playbackRate, 1);\n el.volume = Math.max(0, Math.min(1, finiteMediaNumber(msg.volume, 1)));\n el.muted = this.audienceMutedPlaybackKeys.has(msg.key) ? true : msg.muted;\n if (\n allowTimeSync &&\n Number.isFinite(msg.currentTime) &&\n Math.abs((el.currentTime || 0) - msg.currentTime) > 0.35\n ) {\n el.currentTime = Math.max(0, msg.currentTime);\n }\n }\n\n private playAudienceMediaMuted(el: SlideshowMediaElement, msg: PresenterMediaMessage): void {\n this.audienceMutedPlaybackKeys.add(msg.key);\n this.syncRemoteMediaState(el, msg, true);\n el.muted = true;\n try {\n void el\n .play()\n .then(() => {\n this.blockedAudienceMedia.delete(msg.key);\n this.hideAudienceMediaUnlockIfClear();\n })\n .catch(() => {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n });\n } catch {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n }\n }\n\n private retryBlockedAudienceMedia = (): void => {\n this.wireSlideshowMedia();\n const entries = new Map(this.mediaEntries().map((entry) => [entry.key, entry.el]));\n for (const [key, msg] of this.blockedAudienceMedia) {\n const el = entries.get(key);\n if (el) this.playAudienceMediaMuted(el, msg);\n }\n };\n\n private showAudienceMediaUnlock(): void {\n if (this.resolveMode() !== \"audience\" || this.audienceMediaUnlockButton) return;\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.textContent = \"Play audience media muted\";\n button.style.cssText =\n \"position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:100000;border:0;border-radius:999px;padding:12px 18px;background:#fff;color:#111827;box-shadow:0 10px 32px rgba(0,0,0,.28);font:700 14px/1 system-ui,sans-serif;cursor:pointer;pointer-events:auto;\";\n button.addEventListener(\"click\", this.retryBlockedAudienceMedia);\n this.appendChild(button);\n this.audienceMediaUnlockButton = button;\n }\n\n private hideAudienceMediaUnlockIfClear(): void {\n if (this.blockedAudienceMedia.size > 0 || !this.audienceMediaUnlockButton) return;\n this.audienceMediaUnlockButton.remove();\n this.audienceMediaUnlockButton = null;\n }\n\n // fallow-ignore-next-line complexity\n private onKey = (e: KeyboardEvent): void => {\n if (!this.controller) return;\n const target = e.target;\n if (\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable)\n ) {\n return;\n }\n const active = document.activeElement;\n const focused = active === this || this.contains(active);\n // Arrows act even when nothing is focused (active === body/null) so a freshly\n // loaded deck responds without a click; Space/Backspace have strong page-level\n // defaults (scroll / history) so they only act when the deck actually has focus.\n // When several decks share a page, drop the unfocused-convenience so a key\n // doesn't drive every instance at once — only the focused deck responds.\n const multiInstance = document.querySelectorAll(\"hyperframes-slideshow\").length > 1;\n const ambient = focused || (!multiInstance && (active === document.body || active === null));\n if (e.key === \"ArrowRight\") {\n if (!ambient) return;\n this.controller.next();\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n if (!ambient) return;\n this.controller.prev();\n e.preventDefault();\n } else if (e.key === \" \") {\n if (!focused) return;\n this.controller.next();\n e.preventDefault();\n } else if (e.key === \"Backspace\") {\n if (!focused) return;\n this.controller.prev();\n e.preventDefault();\n } else if ((e.key === \"f\" || e.key === \"F\") && !e.metaKey && !e.ctrlKey && !e.altKey) {\n if (!focused) return;\n this.toggleFullscreen();\n e.preventDefault();\n } else if ((e.key === \"p\" || e.key === \"P\") && !e.metaKey && !e.ctrlKey && !e.altKey) {\n if (!ambient || !this.shouldShowPresentControl()) return;\n this.present();\n e.preventDefault();\n }\n };\n\n // fallow-ignore-next-line complexity\n private onMessage = (e: MessageEvent): void => {\n // Audience mode is driven by BroadcastChannel; ignore embed postMessage nav.\n if (this.resolveMode() === \"audience\") return;\n const data = e.data as { type?: unknown; slideIndex?: unknown } | null;\n if (!data || !this.controller) return;\n if (data.type === \"next\") {\n this.controller.next();\n } else if (data.type === \"prev\") {\n this.controller.prev();\n } else if (data.type === \"goto\" && typeof data.slideIndex === \"number\") {\n this.controller.goToSlide?.(data.slideIndex);\n } else if (data.type === \"back\") {\n this.controller.back?.();\n }\n };\n\n private onTouchStart = (e: TouchEvent): void => {\n const touch = e.touches[0];\n if (touch) {\n this.touchStartX = touch.clientX;\n this.touchStartY = touch.clientY;\n }\n };\n\n private onTouchEnd = (e: TouchEvent): void => {\n if (!this.controller) return;\n const touch = e.changedTouches[0];\n if (!touch) return;\n const deltaX = touch.clientX - this.touchStartX;\n const deltaY = touch.clientY - this.touchStartY;\n // Require a dominant horizontal gesture: |deltaX| > 40 AND |deltaX| > |deltaY|\n // so that diagonal page-scrolls do not accidentally trigger slide navigation.\n if (Math.abs(deltaX) <= 40 || Math.abs(deltaX) <= Math.abs(deltaY)) return;\n if (deltaX < 0) {\n this.controller.next();\n } else {\n this.controller.prev();\n }\n };\n\n // fallow-ignore-next-line complexity\n private render(): void {\n if (!this.controller) return;\n\n if (this.resolveMode() === \"audience\") {\n // Audience (viewer) window: no nav controls — but keep a fullscreen toggle\n // so the presentation can fill the display.\n const { counter } = this.controller;\n this.paintChrome(this.buildNavCluster(counter, \"28px\", \"fs-only\"));\n return;\n }\n\n if (this.getAttribute(\"data-hf-presenting\") === \"true\") {\n this.renderPresenter();\n return;\n }\n\n const { counter, currentSlide } = this.controller;\n if (!currentSlide) return;\n\n // Hotspot pills: compact floating buttons anchored to the region's top-left,\n // sized to content (not filling the region). The region x/y positions the pill;\n // w/h are ignored for sizing (pill is content-sized). XSS: escHtml guards all\n // user-supplied strings.\n const hotspotsHtml = currentSlide.hotspots\n .map((h) => {\n const posStyle = h.region\n ? `left:${h.region.x}%;top:${h.region.y}%;`\n : \"right:5%;bottom:18%;\";\n return `<button\n class=\"hf-hotspot-pill\"\n data-hotspot-id=\"${escHtml(h.id)}\"\n data-hotspot-target=\"${escHtml(h.target)}\"\n type=\"button\"\n style=\"position:absolute;${posStyle}display:inline-flex;align-items:center;gap:6px;padding:8px 14px;background:var(--hf-slideshow-accent,rgba(255,255,255,0.92));color:#111;border:none;border-radius:999px;font-size:13px;font-weight:600;letter-spacing:0.01em;cursor:pointer;pointer-events:auto;box-shadow:0 4px 16px rgba(0,0,0,0.35);animation:hf-hotspot-pulse 1.8s ease-in-out infinite;white-space:nowrap;\"\n aria-label=\"${escHtml(h.label)}\"\n ><span aria-hidden=\"true\" style=\"font-size:14px;line-height:1;\">⊕</span>${escHtml(h.label)}</button>`;\n })\n .join(\"\");\n\n this.paintChrome(hotspotsHtml + this.buildNavCluster(counter, \"28px\"));\n }\n\n /** Ensure the overlay chrome layer exists, set its content, and wire its buttons. */\n private paintChrome(html: string): void {\n injectKeyframesOnce(); // nav-button :hover + hotspot keyframes (CSP-safe, once per doc)\n if (!this.chrome) {\n this.chrome = document.createElement(\"div\");\n this.chrome.setAttribute(\"data-hf-chrome\", \"\");\n this.appendChild(this.chrome);\n }\n this.chrome.style.cssText = \"position:absolute;inset:0;pointer-events:none;z-index:10;\";\n this.chrome.innerHTML = html;\n this.wireChromeButtons();\n }\n\n // Builds the nav cluster ([mute?] [prev] counter [next] | [present?] [fullscreen]) as a\n // floating capsule. `bottomCss` positions it (normal view: \"28px\"; presenter\n // view: above the notes panel). Reused by render() and renderPresenter().\n // fallow-ignore-next-line complexity\n private buildNavCluster(\n counter: { index: number; total: number },\n bottomCss: string,\n variant: \"full\" | \"fs-only\" = \"full\",\n options: { canPrev?: boolean; canNext?: boolean; loading?: boolean } = {},\n ): string {\n const c = this.controller;\n const showPrev = options.canPrev ?? c?.canPrev ?? true;\n const showNext = options.canNext ?? c?.canNext ?? true;\n const showLoading = options.loading === true && showNext;\n const showSound = this.hasAttribute(\"sound\");\n const btnStyle =\n \"display:flex;align-items:center;justify-content:center;width:34px;height:34px;background:transparent;border:none;border-radius:999px;color:rgba(255,255,255,0.85);font-size:16px;cursor:pointer;transition:background 0.15s,color 0.15s;padding:0;\";\n const speakerSvg = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><path d=\"M15.54 8.46a5 5 0 0 1 0 7.07\"/><path d=\"M19.07 4.93a10 10 0 0 1 0 14.14\"/></svg>`;\n const speakerMutedSvg = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><line x1=\"23\" y1=\"9\" x2=\"17\" y2=\"15\"/><line x1=\"17\" y1=\"9\" x2=\"23\" y2=\"15\"/></svg>`;\n const muteBtnHtml = showSound\n ? `<button\n data-hf-mute\n type=\"button\"\n aria-label=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n title=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n data-hf-tooltip=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n aria-pressed=\"${this._muted ? \"true\" : \"false\"}\"\n style=\"${btnStyle}${this._muted ? \"color:rgba(255,255,255,0.45);\" : \"\"}\"\n >${this._muted ? speakerMutedSvg : speakerSvg}</button>`\n : \"\";\n const prevBtnHtml = showPrev\n ? `<button\n data-hf-prev\n type=\"button\"\n aria-label=\"Previous slide\"\n title=\"Previous slide\"\n data-hf-tooltip=\"Previous slide\"\n style=\"${btnStyle}\" >‹</button>`\n : \"\";\n const loadingHtml = showLoading\n ? `<span\n data-hf-nav-loading\n role=\"status\"\n aria-label=\"Loading slides\"\n title=\"Loading slides\"\n style=\"${btnStyle}cursor:progress;color:rgba(255,255,255,0.72);\"\n ><span class=\"hf-nav-spinner\" aria-hidden=\"true\" style=\"width:14px;height:14px;border:2px solid rgba(255,255,255,0.32);border-top-color:rgba(255,255,255,0.92);border-radius:999px;animation:hf-nav-spin 0.8s linear infinite;\"></span></span>`\n : \"\";\n const nextBtnHtml = showLoading\n ? loadingHtml\n : showNext\n ? `<button\n data-hf-next\n type=\"button\"\n aria-label=\"Next slide\"\n title=\"Next slide\"\n data-hf-tooltip=\"Next slide\"\n style=\"${btnStyle}\" >›</button>`\n : \"\";\n const presentBtnHtml = this.shouldShowPresentControl()\n ? `<button\n data-hf-present\n type=\"button\"\n aria-label=\"Present\"\n title=\"Present\"\n data-hf-tooltip=\"Present\"\n style=\"${btnStyle}\" >${PRESENT_SVG}</button>`\n : \"\";\n const isFs = document.fullscreenElement === this;\n const fsLabel = isFs ? \"Exit full screen\" : \"Full screen\";\n const fsBtnHtml = `<button\n data-hf-fullscreen\n type=\"button\"\n aria-label=\"${fsLabel}\"\n title=\"${fsLabel}\"\n data-hf-tooltip=\"${fsLabel}\"\n aria-pressed=\"${isFs ? \"true\" : \"false\"}\"\n style=\"${btnStyle}\" >${isFs ? EXIT_FS_SVG : ENTER_FS_SVG}</button>`;\n // Audience/viewer: only the fullscreen control (no navigation).\n if (variant === \"fs-only\") {\n return `\n <div\n data-hf-nav-cluster\n style=\"position:absolute;bottom:${bottomCss};right:32px;display:inline-flex;align-items:center;background:rgba(20,20,22,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.12);border-radius:999px;box-shadow:0 4px 24px rgba(0,0,0,0.45);padding:4px;pointer-events:auto;\"\n >${fsBtnHtml}</div>`;\n }\n const counterPadLeft = showPrev ? \"4px\" : \"10px\";\n const counterPadRight = showNext ? \"4px\" : \"10px\";\n return `\n <div\n data-hf-nav-cluster\n style=\"position:absolute;bottom:${bottomCss};right:32px;display:inline-flex;align-items:center;gap:2px;background:rgba(20,20,22,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.12);border-radius:999px;box-shadow:0 4px 24px rgba(0,0,0,0.45);padding:4px;pointer-events:auto;\"\n >\n ${muteBtnHtml}\n ${showSound ? `<span aria-hidden=\"true\" style=\"width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;\"></span>` : \"\"}\n ${prevBtnHtml}\n <span\n data-hf-counter\n aria-label=\"Slide ${counter.index} of ${counter.total}\"\n style=\"min-width:46px;text-align:center;color:rgba(255,255,255,0.9);font-family:${COUNTER_FONT_FAMILY};font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;padding:0 ${counterPadRight} 0 ${counterPadLeft};user-select:none;\"\n >${counter.index} / ${counter.total}</span>\n ${nextBtnHtml}\n <span aria-hidden=\"true\" style=\"width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;\"></span>\n ${presentBtnHtml}\n ${fsBtnHtml}\n </div>`;\n }\n\n private wireChromeButtons(): void {\n const chrome = this.chrome;\n if (!chrome) return;\n this.wireChromeClick(chrome, \"[data-hf-mute]\", () => this.toggleMute());\n this.wireChromeClick(chrome, \"[data-hf-prev]\", () => this.controller?.prev());\n this.wireChromeClick(chrome, \"[data-hf-next]\", () => this.controller?.next());\n this.wireChromeClick(chrome, \"[data-hf-present]\", () => this.present());\n this.wireChromeClick(chrome, \"[data-hf-fullscreen]\", () => this.toggleFullscreen());\n this.wirePresenterNotes(chrome);\n this.wireHotspots(chrome);\n }\n\n private wireChromeClick(chrome: HTMLDivElement, selector: string, handler: () => void): void {\n const btn = chrome.querySelector(selector);\n if (btn) btn.addEventListener(\"click\", handler);\n }\n\n private wirePresenterNotes(chrome: HTMLDivElement): void {\n const notesInput = chrome.querySelector(\"[data-hf-presenter-notes]\");\n if (notesInput instanceof HTMLTextAreaElement) {\n const key = notesInput.getAttribute(\"data-hf-presenter-notes-key\");\n notesInput.addEventListener(\"input\", () => this.writePresenterNotes(key, notesInput.value));\n }\n }\n\n private wireHotspots(chrome: HTMLDivElement): void {\n for (const btn of chrome.querySelectorAll(\"[data-hotspot-id]\")) {\n const target = btn.getAttribute(\"data-hotspot-target\") ?? \"\";\n btn.addEventListener(\"click\", () => this.controller?.enterBranch?.(target));\n }\n }\n\n private onFsChange = (): void => {\n // Swap only the fullscreen glyph + label — re-rendering the whole chrome here\n // would rebuild every nav button on each fullscreen toggle.\n const btn = this.chrome?.querySelector(\"[data-hf-fullscreen]\");\n if (!btn) return;\n const isFs = document.fullscreenElement === this;\n btn.innerHTML = isFs ? EXIT_FS_SVG : ENTER_FS_SVG;\n const label = isFs ? \"Exit full screen\" : \"Full screen\";\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"title\", label);\n btn.setAttribute(\"data-hf-tooltip\", label);\n btn.setAttribute(\"aria-pressed\", isFs ? \"true\" : \"false\");\n };\n\n private toggleFullscreen(): void {\n if (document.fullscreenElement === this) {\n void document.exitFullscreen().catch(() => {});\n } else {\n void this.requestFullscreen().catch(() => {});\n }\n }\n\n private shouldShowPresentControl(): boolean {\n return this.resolveMode() !== \"audience\" && this.getAttribute(\"data-hf-presenting\") !== \"true\";\n }\n\n private toggleMute(): void {\n this._muted = !this._muted;\n if (this._muted) {\n this.setAttribute(\"data-hf-muted\", \"\");\n } else {\n this.removeAttribute(\"data-hf-muted\");\n }\n this.applyGlobalMute(this._muted);\n this.dispatchEvent(\n new CustomEvent(\"hf-sound\", {\n detail: { muted: this._muted },\n bubbles: true,\n composed: true,\n }),\n );\n // Re-render to flip the glyph.\n this.render();\n }\n\n private applyGlobalMute(muted: boolean): void {\n for (const player of this.querySelectorAll(\"hyperframes-player\")) {\n if (!(player instanceof HTMLElement)) continue;\n const playerEl = player as Partial<PlayerElement> & HTMLElement;\n if (\"muted\" in playerEl) {\n playerEl.muted = muted;\n } else if (muted) {\n playerEl.setAttribute(\"muted\", \"\");\n } else {\n playerEl.removeAttribute(\"muted\");\n }\n }\n\n const doc = this.ownerDocument;\n for (const el of doc.querySelectorAll(\"video, audio\")) {\n if (el instanceof HTMLMediaElement) el.muted = muted || el.defaultMuted;\n }\n }\n\n private stopDocumentMedia(): void {\n const doc = this.ownerDocument;\n for (const el of doc.querySelectorAll(\"video, audio\")) {\n if (el instanceof HTMLMediaElement) el.pause();\n }\n }\n\n private presenterNotesDeckKey(): string {\n const explicit = this.getAttribute(\"notes-storage-key\")?.trim();\n if (explicit) return explicit;\n\n const playerSrc = this.querySelector(\"hyperframes-player\")?.getAttribute(\"src\") ?? \"\";\n let resolvedPlayerSrc = playerSrc;\n try {\n const baseHref = typeof location !== \"undefined\" ? location.href : \"http://localhost/\";\n resolvedPlayerSrc = new URL(playerSrc, baseHref).href;\n } catch {\n // Keep the raw src when URL construction is unavailable.\n }\n\n const locationKey =\n typeof location !== \"undefined\" ? `${location.origin}${location.pathname}` : \"\";\n const title = this.ownerDocument.title;\n return `${locationKey}|${title}|${resolvedPlayerSrc}`;\n }\n\n private presenterNotesStorageKey(slide: SlideNotesTarget): string | null {\n const pos = this.controller?.position;\n if (!pos) return null;\n return `${PRESENTER_NOTES_STORAGE_PREFIX}${JSON.stringify([\n this.presenterNotesDeckKey(),\n pos.sequenceId,\n pos.slideIndex,\n slide.sceneId ?? \"\",\n ])}`;\n }\n\n private readPresenterNotes(key: string | null): string | null {\n if (!key) return null;\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n }\n\n private writePresenterNotes(key: string | null, notes: string): void {\n if (!key) return;\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, notes);\n } catch {\n // localStorage may be disabled or quota-limited; editing still works for\n // the current render even when persistence is unavailable.\n }\n }\n\n private renderPresenter(): void {\n if (!this.controller) return;\n const { counter, currentSlide, nextSlide } = this.controller;\n if (!currentSlide) return;\n\n const elapsedSec =\n this.presenterStartMs !== null ? Math.floor((Date.now() - this.presenterStartMs) / 1000) : 0;\n\n // Pin the live slide to the TOP and reserve the bottom 32% for the notes\n // panel. The player contains the composition, so the FULL slide stays visible\n // (letterboxed) at any width — its bottom is never hidden behind the panel —\n // and it re-fits to the top region on window resize.\n const playerEl = this.querySelector(\"hyperframes-player\");\n if (playerEl instanceof HTMLElement) {\n playerEl.style.top = \"0\";\n playerEl.style.bottom = \"32%\";\n playerEl.style.height = \"auto\";\n }\n\n // Full-overlay chrome (pointer-events:none); the notes panel and nav cluster\n // are the only interactive children.\n const notesStorageKey = this.presenterNotesStorageKey(currentSlide);\n const notes = this.readPresenterNotes(notesStorageKey) ?? currentSlide.notes ?? \"\";\n this.paintChrome(\n buildPresenterLayout({\n notes,\n notesStorageKey,\n nextText: nextPanelText(nextSlide),\n counterText: `${counter.index} / ${counter.total}`,\n elapsedText: formatElapsed(elapsedSec),\n hotspots: currentSlide.hotspots,\n }) + this.buildNavCluster(counter, \"calc(32% + 18px)\"),\n );\n }\n}\n\nfunction nextPanelText(slide: { sceneId: string; notes?: string } | null): string {\n if (slide === null) return \"End of sequence\";\n const firstLine = slide.notes != null ? (slide.notes.split(\"\\n\")[0] ?? \"\") : \"\";\n return firstLine.length > 0\n ? `${escHtml(slide.sceneId)}: ${escHtml(firstLine)}`\n : escHtml(slide.sceneId);\n}\n\nfunction isSlideshowMediaElement(el: Element): el is SlideshowMediaElement {\n const win = el.ownerDocument.defaultView;\n if (!win || typeof win.HTMLMediaElement !== \"function\") return false;\n return el instanceof win.HTMLMediaElement;\n}\n\nfunction finiteMediaNumber(value: number, fallback: number): number {\n return Number.isFinite(value) ? value : fallback;\n}\n\nfunction readScenes(player: HTMLElement): { id: string; start: number; duration: number }[] {\n if (\"scenes\" in player && Array.isArray((player as { scenes: unknown }).scenes)) {\n return (player as { scenes: { id: string; start: number; duration: number }[] }).scenes;\n }\n return [];\n}\n\nconst WAIT_FOR_READY_TIMEOUT_MS = 5000;\n\nfunction waitForReady(player: HTMLElement & { ready?: boolean }): Promise<void> {\n if (player.ready === true) return Promise.resolve();\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const handler = (): void => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n resolve();\n };\n player.addEventListener(\"ready\", handler, { once: true });\n timer = setTimeout(() => {\n player.removeEventListener(\"ready\", handler);\n resolve();\n }, WAIT_FOR_READY_TIMEOUT_MS);\n });\n}\n\n/**\n * Polls `player.scenes` until at least one scene is present, then resolves\n * with the scenes array. Resolves with `[]` if no scenes appear within\n * `timeoutMs` (graceful: explicit startTime/endTime slides still work).\n *\n * Avoids Date.now(): counts poll iterations instead (100ms per iteration).\n *\n * `isCancelled` is checked before each poll iteration; if it returns true\n * the promise resolves with `[]` immediately so the caller can bail out.\n */\nfunction waitForScenes(\n player: HTMLElement,\n timeoutMs: number,\n isCancelled: () => boolean = () => false,\n): Promise<{ id: string; start: number; duration: number }[]> {\n const initial = readScenes(player);\n if (initial.length > 0) return Promise.resolve(initial);\n\n const maxIterations = Math.ceil(timeoutMs / 100);\n\n return new Promise((resolve) => {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let iterations = 0;\n\n const finish = (val: { id: string; start: number; duration: number }[]): void => {\n if (done) return;\n done = true;\n if (timer !== null) clearTimeout(timer);\n player.removeEventListener(\"scenes\", onScenes);\n resolve(val);\n };\n const onScenes = (): void => {\n if (isCancelled()) return finish([]);\n const s = readScenes(player);\n if (s.length > 0) finish(s);\n };\n const poll = (): void => {\n if (done) return;\n if (isCancelled()) return finish([]);\n const cur = readScenes(player);\n if (cur.length > 0) return finish(cur);\n iterations += 1;\n if (iterations >= maxIterations) return finish([]);\n timer = setTimeout(poll, 100);\n };\n\n player.addEventListener(\"scenes\", onScenes);\n timer = setTimeout(poll, 100);\n });\n}\n\n/**\n * Returns a new ResolvedSlideshow with zero-duration (end <= start) slides\n * removed from the main slide list and every sequence's slide list.\n *\n * Valid manifests never produce zero-duration slides — this only drops\n * phantom slides created from partially-specified refs whose scene is absent.\n *\n * Exported as a seam for unit testing.\n */\nexport function dropInvalidSlides(show: ResolvedSlideshow): ResolvedSlideshow {\n const validSlide = (s: { start: number; end: number }): boolean => s.end > s.start;\n\n const slides = show.slides.filter(validSlide);\n\n const sequences: ResolvedSlideshow[\"sequences\"] = {};\n for (const [id, seq] of Object.entries(show.sequences)) {\n sequences[id] = { ...seq, slides: seq.slides.filter(validSlide) };\n }\n\n return { slides, sequences };\n}\n\nfunction escHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nif (!customElements.get(\"hyperframes-slideshow\")) {\n customElements.define(\"hyperframes-slideshow\", HyperframesSlideshow);\n}\n"],"mappings":"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","names":["SLIDESHOW_ISLAND_TYPE","slideshowIslandRegex","flags","escaped","parseSlideshowManifest","html","match","raw","parsed","isManifest","isSlideRef","v","r","n","isSlideSequence","s","o","missingBoundError","sceneId","missing","resolveTimeRange","ref","scene","errors","startTime","endTime","bound","validateFragments","fragments","start","end","f","resolveSlide","sceneById","a","b","resolveSlideshow","manifest","scenes","sequences","seq","slides","allSlides","slide","h","ordered","i","prev","curr","MAIN","SlideshowController","player","show","sequenceId","slides","f","cb","index","slide","fragmentIndex","seekTime","t","target","seq","slideIndex","targetSlides","base","COUNTER_FONT_FAMILY","MEDIA_ACTIONS","MEDIA_NUMBER_FIELDS","MEDIA_BOOLEAN_FIELDS","isRecord","data","isGotoMessage","d","isMediaAction","value","isMediaSender","hasMediaNumberFields","field","hasMediaBooleanFields","isMediaMessage","slideshowChannelName","SlideshowChannel","mode","onGoto","onMedia","e","pos","msg","buildPresenterLayout","opts","esc","escAttr","notes","branches","h","formatElapsed","seconds","m","s","isPlayerElement","el","PRESENTER_NOTES_STORAGE_PREFIX","_keyframesInjected","injectKeyframesOnce","style","ENTER_FS_SVG","EXIT_FS_SVG","PRESENT_SVG","COUNTER_FONT_FAMILY","HyperframesSlideshow","attr","c","sep","formatElapsed","SlideshowChannel","msg","gen","playerEl","html","manifest","parseSlideshowManifest","waitForReady","scenes","waitForScenes","resolved","errors","resolveSlideshow","cleaned","dropInvalidSlides","port","t","cb","handler","e","detail","SlideshowController","counter","delay","timer","item","player","frame","playerIndex","media","mediaIndex","playerKey","mediaKey","entries","doc","isSlideshowMediaElement","actions","key","action","now","finiteMediaNumber","entry","candidate","allowTimeSync","button","target","active","focused","multiInstance","ambient","data","touch","deltaX","deltaY","currentSlide","hotspotsHtml","h","posStyle","escHtml","bottomCss","variant","options","showPrev","showNext","showLoading","showSound","btnStyle","muteBtnHtml","prevBtnHtml","loadingHtml","nextBtnHtml","presentBtnHtml","isFs","fsLabel","fsBtnHtml","counterPadLeft","counterPadRight","chrome","selector","btn","notesInput","label","muted","explicit","playerSrc","resolvedPlayerSrc","baseHref","locationKey","title","slide","pos","notes","nextSlide","elapsedSec","notesStorageKey","buildPresenterLayout","nextPanelText","firstLine","win","value","fallback","readScenes","WAIT_FOR_READY_TIMEOUT_MS","resolve","timeoutMs","isCancelled","initial","maxIterations","done","iterations","finish","val","onScenes","s","poll","cur","show","validSlide","slides","sequences","id","seq"]}
|
|
1
|
+
{"version":3,"sources":["../../../core/src/slideshow/parseSlideshow.ts","../../src/slideshow/SlideshowController.ts","../../src/slideshow/slideshowPresenter.ts","../../src/slideshow/hyperframes-slideshow.ts"],"sourcesContent":["// packages/core/src/slideshow/parseSlideshow.ts\nimport type {\n SlideshowManifest,\n SlideRef,\n ResolvedSlide,\n ResolvedSlideshow,\n ResolvedSlideSequence,\n} from \"./slideshow.types\";\n\nexport const SLIDESHOW_ISLAND_TYPE = \"application/hyperframes-slideshow+json\";\n\n/**\n * Builds the island <script> matcher. Capture group 1 = inner JSON.\n *\n * Factory (fresh RegExp per call) on purpose: a RegExp with the `g` flag carries\n * a mutable `lastIndex`, so callers that need `g` must call this each time rather\n * than caching a shared instance.\n */\nexport function slideshowIslandRegex(flags = \"i\"): RegExp {\n // Escape ALL regex metacharacters in the constant (CodeQL flags the incomplete\n // `\\` escape). The constant has none today, but a complete escape is correct.\n const escaped = SLIDESHOW_ISLAND_TYPE.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`<script[^>]*type=[\"']${escaped}[\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/script>`, flags);\n}\n\ninterface SceneRange {\n id: string;\n start: number;\n duration: number;\n}\n\n/** Extract the JSON island from composition HTML. Returns null if absent. */\nexport function parseSlideshowManifest(html: string): SlideshowManifest | null {\n // Match <script type=\"application/hyperframes-slideshow+json\"> ... </script>\n const re = slideshowIslandRegex(\"i\");\n const match = re.exec(html);\n if (!match || match[1] === undefined) return null;\n const raw = match[1].trim();\n if (raw.length === 0) return null;\n const parsed: unknown = JSON.parse(raw);\n if (!isManifest(parsed)) {\n throw new Error(\"slideshow island is not a valid SlideshowManifest\");\n }\n return parsed;\n}\n\nfunction isOptionalNumberArray(v: unknown): boolean {\n return v === undefined || (Array.isArray(v) && v.every((n) => typeof n === \"number\"));\n}\n\nfunction isOptionalBoolean(v: unknown): v is boolean | undefined {\n return v === undefined || typeof v === \"boolean\";\n}\n\nfunction isSlideRef(v: unknown): v is SlideRef {\n if (typeof v !== \"object\" || v === null) return false;\n const r = v as Record<string, unknown>;\n if (typeof r[\"sceneId\"] !== \"string\") return false;\n if (!isOptionalNumberArray(r[\"fragments\"])) return false;\n if (r[\"hotspots\"] !== undefined && !Array.isArray(r[\"hotspots\"])) return false;\n if (!isOptionalBoolean(r[\"autoplay\"])) return false;\n return true;\n}\n\nfunction isSlideSequence(v: unknown): boolean {\n if (typeof v !== \"object\" || v === null) return false;\n const s = v as Record<string, unknown>;\n return (\n typeof s[\"id\"] === \"string\" &&\n typeof s[\"label\"] === \"string\" &&\n Array.isArray(s[\"slides\"]) &&\n s[\"slides\"].every(isSlideRef)\n );\n}\n\nfunction isManifest(v: unknown): v is SlideshowManifest {\n if (typeof v !== \"object\" || v === null || Array.isArray(v)) return false;\n const o = v as Record<string, unknown>;\n if (!Array.isArray(o[\"slides\"]) || !o[\"slides\"].every(isSlideRef)) return false;\n if (o[\"slideSequences\"] !== undefined) {\n if (!Array.isArray(o[\"slideSequences\"]) || !o[\"slideSequences\"].every(isSlideSequence))\n return false;\n }\n return true;\n}\n\nfunction missingBoundError(sceneId: string, missing: \"startTime\" | \"endTime\"): string {\n const present = missing === \"startTime\" ? \"endTime\" : \"startTime\";\n return `slide \"${sceneId}\" sets ${present} but ${missing} cannot be resolved (no scene \"${sceneId}\")`;\n}\n\n// fallow-ignore-next-line complexity\nfunction resolveTimeRange(\n ref: SlideRef,\n scene: SceneRange | undefined,\n errors: string[],\n): { start: number; end: number } {\n const { startTime, endTime, sceneId } = ref;\n\n // Both explicit — use them directly, no scene needed.\n if (startTime !== undefined && endTime !== undefined) {\n return { start: startTime, end: endTime };\n }\n\n // Neither explicit — resolve both from scene.\n if (startTime === undefined && endTime === undefined) {\n if (!scene) {\n errors.push(`slide references unresolved sceneId \"${sceneId}\"`);\n return { start: 0, end: 0 };\n }\n return { start: scene.start, end: scene.start + scene.duration };\n }\n\n // Exactly one bound explicit — fill from scene, or report a clear error.\n if (!scene) {\n const missing = startTime === undefined ? \"startTime\" : \"endTime\";\n errors.push(missingBoundError(sceneId, missing));\n const bound = startTime ?? endTime ?? 0;\n return { start: bound, end: bound };\n }\n\n return {\n start: startTime ?? scene.start,\n end: endTime ?? scene.start + scene.duration,\n };\n}\n\nfunction validateFragments(\n sceneId: string,\n fragments: number[],\n start: number,\n end: number,\n errors: string[],\n): void {\n for (const f of fragments) {\n if (f < start || f > end) {\n errors.push(`slide \"${sceneId}\" fragment ${f} is outside range [${start}, ${end}]`);\n }\n }\n}\n\nfunction resolveSlide(\n ref: SlideRef,\n sceneById: Map<string, SceneRange>,\n errors: string[],\n): ResolvedSlide {\n const scene = sceneById.get(ref.sceneId);\n const { start, end } = resolveTimeRange(ref, scene, errors);\n if (ref.startTime !== undefined && ref.endTime !== undefined && end <= start) {\n errors.push(`slide \"${ref.sceneId}\" has endTime (${end}) <= startTime (${start})`);\n }\n const fragments = [...new Set(ref.fragments ?? [])].sort((a, b) => a - b);\n validateFragments(ref.sceneId, fragments, start, end, errors);\n return { ...ref, start, end, fragments, hotspots: ref.hotspots ?? [] };\n}\n\nexport function resolveSlideshow(\n manifest: SlideshowManifest,\n scenes: SceneRange[],\n): { resolved: ResolvedSlideshow; errors: string[] } {\n const errors: string[] = [];\n const sceneById = new Map(scenes.map((s) => [s.id, s]));\n\n const sequences: Record<string, ResolvedSlideSequence> = {};\n for (const seq of manifest.slideSequences ?? []) {\n // Flag duplicate sequence ids rather than silently overwriting the earlier one.\n if (Object.prototype.hasOwnProperty.call(sequences, seq.id)) {\n errors.push(`duplicate slideSequence id \"${seq.id}\" — only the last definition is kept`);\n }\n sequences[seq.id] = {\n id: seq.id,\n label: seq.label,\n slides: seq.slides.map((s) => resolveSlide(s, sceneById, errors)),\n };\n }\n\n const slides = manifest.slides.map((s) => resolveSlide(s, sceneById, errors));\n\n // Validate hotspot targets.\n const allSlides = [...slides, ...Object.values(sequences).flatMap((s) => s.slides)];\n for (const slide of allSlides) {\n for (const h of slide.hotspots) {\n const seq = sequences[h.target];\n if (!seq) {\n errors.push(`hotspot \"${h.id}\" targets unknown sequence \"${h.target}\"`);\n } else if (seq.slides.length === 0) {\n errors.push(`hotspot \"${h.id}\" targets empty sequence \"${h.target}\"`);\n }\n }\n }\n\n // Validate no main-line overlap (sorted by start; adjacent compare).\n const ordered = [...slides].sort((a, b) => a.start - b.start);\n for (let i = 1; i < ordered.length; i++) {\n const prev = ordered[i - 1];\n const curr = ordered[i];\n if (prev !== undefined && curr !== undefined && curr.start < prev.end) {\n errors.push(`main-line slides \"${prev.sceneId}\" and \"${curr.sceneId}\" overlap`);\n }\n }\n\n return { resolved: { slides, sequences }, errors };\n}\n","import type { ResolvedSlideshow, ResolvedSlide } from \"@hyperframes/core/slideshow\";\n\nexport interface PlayerPort {\n seek(t: number): void;\n play(): void;\n pause(): void;\n stopMedia?(): void;\n /** Play the `<video>` inside the given scene (for `autoplay` slides). */\n playSceneMedia?(sceneId: string): void;\n readonly currentTime: number;\n onTimeUpdate(cb: (t: number) => void): () => void;\n}\n\ninterface StackFrame {\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number; // -1 = before first fragment / at slide start\n}\n\nconst MAIN = \"main\";\n\nexport class SlideshowController {\n private stack: StackFrame[] = [{ sequenceId: MAIN, slideIndex: 0, fragmentIndex: -1 }];\n private changeCbs = new Set<() => void>();\n\n constructor(\n private player: PlayerPort,\n private show: ResolvedSlideshow,\n ) {\n this.enterSlide(0);\n }\n\n // fallow-ignore-next-line unused-class-member\n dispose(): void {\n // No subscriptions to tear down — navigation is seek-driven (see playTo).\n }\n\n private slidesOf(sequenceId: string): ResolvedSlide[] {\n if (sequenceId === MAIN) return this.show.slides;\n return this.show.sequences[sequenceId]?.slides ?? [];\n }\n\n private get frame(): StackFrame {\n return this.stack[this.stack.length - 1];\n }\n\n get currentSlide(): ResolvedSlide | undefined {\n return this.slidesOf(this.frame.sequenceId)[this.frame.slideIndex];\n }\n\n get nextSlide(): ResolvedSlide | null {\n const slides = this.slidesOf(this.frame.sequenceId);\n const next = slides[this.frame.slideIndex + 1];\n return next ?? null;\n }\n\n get position(): { sequenceId: string; slideIndex: number; fragmentIndex: number } {\n return { ...this.frame };\n }\n\n get counter(): { index: number; total: number } {\n return {\n index: this.frame.slideIndex + 1,\n total: this.slidesOf(this.frame.sequenceId).length,\n };\n }\n\n get canPrev(): boolean {\n // prev has a destination: an earlier slide in this sequence, OR (in a branch) the parent.\n return this.frame.slideIndex > 0 || this.stack.length > 1;\n }\n\n get canNext(): boolean {\n // next has a destination: a later slide in this sequence, OR (in a branch) the parent.\n const slides = this.slidesOf(this.frame.sequenceId);\n return this.frame.slideIndex + 1 < slides.length || this.stack.length > 1;\n }\n\n get breadcrumb(): { id: string; label: string }[] {\n return this.stack.map((f) =>\n f.sequenceId === MAIN\n ? { id: MAIN, label: \"Main deck\" }\n : { id: f.sequenceId, label: this.show.sequences[f.sequenceId]?.label ?? f.sequenceId },\n );\n }\n\n // fallow-ignore-next-line unused-class-member\n onChange(cb: () => void): () => void {\n this.changeCbs.add(cb);\n return () => this.changeCbs.delete(cb);\n }\n\n private emitChange(): void {\n for (const cb of this.changeCbs) cb();\n }\n\n private stopSlideMedia(): void {\n this.player.stopMedia?.();\n }\n\n private enterSlide(index: number): void {\n if (index !== this.frame.slideIndex) this.stopSlideMedia();\n this.frame.slideIndex = index;\n const slide = this.currentSlide;\n if (!slide) {\n this.frame.fragmentIndex = -1;\n return;\n }\n // Jump to the slide's first hold and stay there (no auto-progress). With\n // fragments that's the first fragment (fragmentIndex 0); without, a settled\n // frame INSIDE the slide (its midpoint) — NOT slide.end, which is the boundary\n // where the next scene begins (else slide 1 would render slide 2's content).\n if (slide.fragments.length > 0) {\n this.frame.fragmentIndex = 0;\n this.playTo(slide.fragments[0] ?? slide.end);\n } else {\n this.frame.fragmentIndex = -1;\n this.playTo(this.restFrame(slide));\n }\n // Opt-in: play the slide's own clip on enter (saves a click into the\n // pointer-events:none composition). We never auto-advance — the presenter\n // still clicks Next. Fires from enterSlide (next / prev / goToSlide), NOT\n // from resumeSlide (back / backToMain / syncTo), which restores a saved\n // position; the component also skips it on the audience, which mirrors the\n // presenter's media events rather than driving its own.\n if (slide.autoplay) this.player.playSceneMedia?.(slide.sceneId);\n this.emitChange();\n }\n\n /** A representative, non-boundary frame for a slide with no fragments. */\n private restFrame(slide: ResolvedSlide): number {\n return slide.start + (slide.end - slide.start) * 0.5;\n }\n\n /**\n * Resumes a slide at a saved fragmentIndex without resetting to slide start.\n * Used by back()/backToMain()/syncTo() to restore an exact position.\n */\n private resumeSlide(index: number, fragmentIndex: number): void {\n this.frame.slideIndex = index;\n this.frame.fragmentIndex = fragmentIndex;\n const slide = this.currentSlide;\n if (!slide) return;\n // Resume position, mirroring enterSlide so going back to a slide lands where\n // entering it forward does:\n // - at a saved fragment → that fragment's hold time\n // - fragmented, pre-first → slide.start (before the first reveal)\n // - no fragments → restFrame (midpoint), NOT slide.start, so the\n // slide is visible at rest instead of frozen at its frame-0 (pre-entrance).\n const seekTime =\n fragmentIndex >= 0 && fragmentIndex < slide.fragments.length\n ? (slide.fragments[fragmentIndex] ?? slide.start)\n : slide.fragments.length > 0\n ? slide.start\n : this.restFrame(slide);\n this.playTo(seekTime);\n this.emitChange();\n }\n\n /**\n * Jump to hold time `t` and hold there — a pure, synchronous seek with NO\n * sustained playback, so a slide can never auto-progress.\n *\n * `player.seek(t)` drives the composition's GSAP timeline directly (the player\n * reaches the same-origin iframe's `__timelines`), and GSAP `.seek()` renders\n * that frame synchronously AND leaves the timeline paused. So one seek both\n * repaints and holds — deterministically, in every window including a\n * backgrounded one. (The previous play-a-frame-then-pause-on-a-timeupdate\n * \"render nudge\" left an unfocused audience window playing while it waited for\n * a throttled tick to pause it — that was the auto-progress / one-side-frozen\n * flakiness. fragmentIndex is now set by the caller, not on a played tick.)\n */\n private playTo(t: number): void {\n this.player.seek(t);\n }\n\n next(): void {\n const slide = this.currentSlide;\n if (!slide) return;\n const hasMoreFragments = this.frame.fragmentIndex + 1 < slide.fragments.length;\n if (hasMoreFragments) {\n // Reveal the next fragment — advance the index and seek to its hold time.\n this.frame.fragmentIndex += 1;\n const target = slide.fragments[this.frame.fragmentIndex] ?? slide.end;\n this.playTo(target);\n this.emitChange();\n return;\n }\n // No more fragments to reveal — advance to the next slide immediately instead of\n // playing the current slide out to its end.\n const slides = this.slidesOf(this.frame.sequenceId);\n if (this.frame.slideIndex + 1 < slides.length) {\n this.enterSlide(this.frame.slideIndex + 1);\n } else if (this.stack.length > 1) {\n // End of a branch → return to the parent timeline.\n this.back();\n }\n }\n\n prev(): void {\n if (this.frame.slideIndex > 0) {\n this.enterSlide(this.frame.slideIndex - 1);\n return;\n }\n if (this.stack.length > 1) {\n // First slide of a branch → return to the parent timeline.\n this.back();\n }\n }\n\n goToSlide(index: number): void {\n const slides = this.slidesOf(this.frame.sequenceId);\n if (index >= 0 && index < slides.length) this.enterSlide(index);\n }\n\n enterBranch(sequenceId: string): void {\n const seq = this.show.sequences[sequenceId];\n if (!seq || seq.slides.length === 0) return;\n this.stopSlideMedia();\n this.stack.push({ sequenceId, slideIndex: 0, fragmentIndex: -1 });\n this.enterSlide(0);\n }\n\n back(): void {\n if (this.stack.length <= 1) return;\n this.stopSlideMedia();\n this.stack.pop();\n // Restore the saved fragmentIndex from the parent frame rather than\n // resetting to -1 (which enterSlide would do). This preserves the exact\n // position the presenter was at before entering the branch.\n this.resumeSlide(this.frame.slideIndex, this.frame.fragmentIndex);\n }\n\n backToMain(): void {\n if (this.stack.length <= 1) return;\n this.stopSlideMedia();\n this.stack = [this.stack[0]];\n this.resumeSlide(this.frame.slideIndex, this.frame.fragmentIndex);\n }\n\n /**\n * Jump to an absolute position without animation (audience mirroring).\n * Re-roots the stack to the target sequence, then restores slide+fragment\n * statically via resumeSlide.\n */\n syncTo(sequenceId: string, slideIndex: number, fragmentIndex: number): void {\n if (!this.isValidSyncTarget(sequenceId, slideIndex)) return;\n if (this.isCrossSlide(sequenceId, slideIndex)) this.stopSlideMedia();\n if (!this.rerootStackTo(sequenceId)) return;\n this.resumeSlide(slideIndex, fragmentIndex);\n }\n\n /** True if the target sequence + slide index resolves to a real slide. */\n private isValidSyncTarget(sequenceId: string, slideIndex: number): boolean {\n if (!this.stack[0]) return false;\n const targetSlides =\n sequenceId === MAIN ? this.show.slides : (this.show.sequences[sequenceId]?.slides ?? null);\n if (!targetSlides) return false;\n return slideIndex >= 0 && slideIndex < targetSlides.length;\n }\n\n /** True if the sync target lands on a different slide than current. */\n private isCrossSlide(sequenceId: string, slideIndex: number): boolean {\n return this.frame.sequenceId !== sequenceId || this.frame.slideIndex !== slideIndex;\n }\n\n /**\n * Re-root the navigation stack to `sequenceId` if we're not already there.\n * Returns false only when the target branch sequence is empty (no slides),\n * mirroring the early-return guard in the previous inline form.\n */\n private rerootStackTo(sequenceId: string): boolean {\n if (this.frame.sequenceId === sequenceId) return true;\n const base = this.stack[0];\n if (!base) return false;\n this.stack = [base];\n if (sequenceId === MAIN) return true;\n const seq = this.show.sequences[sequenceId];\n if (!seq || seq.slides.length === 0) return false;\n this.stack.push({ sequenceId, slideIndex: 0, fragmentIndex: -1 });\n return true;\n }\n}\n","export interface PresenterPosition {\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\nconst COUNTER_FONT_FAMILY =\n \"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\n\nexport type PresenterMediaAction =\n | \"play\"\n | \"pause\"\n | \"seeking\"\n | \"seeked\"\n | \"ratechange\"\n | \"volumechange\"\n | \"ended\"\n | \"timeupdate\";\n\nconst MEDIA_ACTIONS = new Set<unknown>([\n \"play\",\n \"pause\",\n \"seeking\",\n \"seeked\",\n \"ratechange\",\n \"volumechange\",\n \"ended\",\n \"timeupdate\",\n]);\nconst MEDIA_NUMBER_FIELDS = [\"currentTime\", \"volume\", \"playbackRate\"] as const;\nconst MEDIA_BOOLEAN_FIELDS = [\"paused\", \"ended\", \"muted\"] as const;\n\ninterface GotoMessage {\n type: \"goto\";\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\nexport interface PresenterMediaMessage {\n type: \"media\";\n sender: \"presenter\" | \"audience\";\n key: string;\n action: PresenterMediaAction;\n currentTime: number;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n volume: number;\n playbackRate: number;\n}\n\nfunction isRecord(data: unknown): data is Record<string, unknown> {\n if (typeof data !== \"object\" || data === null) return false;\n return true;\n}\n\nfunction isGotoMessage(data: unknown): data is GotoMessage {\n if (!isRecord(data)) return false;\n const d = data as Record<string, unknown>;\n return (\n d[\"type\"] === \"goto\" &&\n typeof d[\"sequenceId\"] === \"string\" &&\n typeof d[\"slideIndex\"] === \"number\" &&\n typeof d[\"fragmentIndex\"] === \"number\"\n );\n}\n\nfunction isMediaAction(value: unknown): value is PresenterMediaAction {\n return MEDIA_ACTIONS.has(value);\n}\n\nfunction isMediaSender(value: unknown): value is PresenterMediaMessage[\"sender\"] {\n return value === \"presenter\" || value === \"audience\";\n}\n\nfunction hasMediaNumberFields(data: Record<string, unknown>): boolean {\n return MEDIA_NUMBER_FIELDS.every((field) => typeof data[field] === \"number\");\n}\n\nfunction hasMediaBooleanFields(data: Record<string, unknown>): boolean {\n return MEDIA_BOOLEAN_FIELDS.every((field) => typeof data[field] === \"boolean\");\n}\n\nfunction isMediaMessage(data: unknown): data is PresenterMediaMessage {\n if (!isRecord(data)) return false;\n const d = data;\n return (\n d[\"type\"] === \"media\" &&\n isMediaSender(d[\"sender\"]) &&\n typeof d[\"key\"] === \"string\" &&\n isMediaAction(d[\"action\"]) &&\n hasMediaNumberFields(d) &&\n hasMediaBooleanFields(d)\n );\n}\n\n/**\n * Manages the BroadcastChannel connection for a single slideshow element.\n * Presenter (default) mode: posts position updates to the channel.\n * Audience mode: listens for goto messages and calls the provided handler.\n */\n/**\n * Per-deck channel name. The presenter and its audience window load the same URL\n * (path), so keying on pathname keeps them paired while isolating other decks\n * presenting on the same origin (which would otherwise cross-talk on a fixed name).\n */\nexport function slideshowChannelName(): string {\n const path = typeof location !== \"undefined\" ? location.pathname : \"\";\n return `hf-slideshow:${path}`;\n}\n\nexport class SlideshowChannel {\n private channel: BroadcastChannel | null = null;\n\n constructor(\n private readonly mode: \"presenter\" | \"audience\",\n private readonly onGoto: (msg: GotoMessage) => void,\n private readonly onMedia: (msg: PresenterMediaMessage) => void = () => {},\n ) {\n try {\n this.channel = new BroadcastChannel(slideshowChannelName());\n } catch {\n // BroadcastChannel unavailable (e.g. unsupported env); degrade silently.\n return;\n }\n\n this.channel.onmessage = (e: MessageEvent) => {\n if (isGotoMessage(e.data)) {\n if (mode === \"audience\") {\n this.onGoto(e.data);\n }\n return;\n }\n if (isMediaMessage(e.data) && e.data.sender !== mode) {\n this.onMedia(e.data);\n }\n };\n }\n\n postPosition(pos: PresenterPosition): void {\n if (this.mode !== \"presenter\" || !this.channel) return;\n const msg: GotoMessage = { type: \"goto\", ...pos };\n this.channel.postMessage(msg);\n }\n\n postMedia(msg: Omit<PresenterMediaMessage, \"type\" | \"sender\">): void {\n if (!this.channel) return;\n this.channel.postMessage({ type: \"media\", sender: this.mode, ...msg });\n }\n\n destroy(): void {\n if (this.channel) {\n this.channel.onmessage = null;\n this.channel.close();\n this.channel = null;\n }\n }\n}\n\n/**\n * Builds the presenter-mode bottom panel: speaker notes + up-next + counter +\n * elapsed. The live slide is shown ABOVE this panel (the component confines the\n * player to the top region). Returns the panel HTML only — the component appends\n * the nav controls separately.\n */\nexport function buildPresenterLayout(opts: {\n notes: string;\n notesStorageKey: string | null;\n nextText: string;\n counterText: string;\n elapsedText: string;\n hotspots: { id: string; label: string; target: string }[];\n}): string {\n const esc = (s: string) => s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n const escAttr = (s: string) => esc(s).replace(/\"/g, \""\");\n const notes = esc(opts.notes);\n // Branch entries for the current slide — the presenter clicks these to enter a\n // branch (the audience follows). The component wires [data-hotspot-id] to\n // enterBranch(); positioned pills don't align with the letterboxed slide, so\n // they live in the console as a list.\n const branches = opts.hotspots.length\n ? `<div style=\"display:flex;flex-direction:column;gap:6px;\">\n <div style=\"font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;\">Branches</div>\n ${opts.hotspots\n .map(\n (h) =>\n `<button data-hotspot-id=\"${escAttr(h.id)}\" data-hotspot-target=\"${escAttr(h.target)}\" type=\"button\" title=\"${escAttr(h.label)}\" style=\"text-align:left;background:rgba(244,183,64,0.14);color:#f4b740;border:1px solid rgba(244,183,64,0.4);border-radius:8px;padding:8px 12px;font-size:15px;cursor:pointer;pointer-events:auto;font-family:inherit;\">↳ ${esc(h.label)}</button>`,\n )\n .join(\"\")}\n </div>`\n : \"\";\n return `\n<div data-hf-presenter style=\"position:absolute;left:0;right:0;bottom:0;height:32%;display:flex;background:#11151f;color:#fff;border-top:2px solid rgba(255,255,255,0.12);box-sizing:border-box;font-family:sans-serif;pointer-events:auto;\">\n <textarea data-hf-presenter-notes data-hf-presenter-notes-key=\"${escAttr(opts.notesStorageKey ?? \"\")}\" aria-label=\"Speaker notes\" placeholder=\"No notes for this slide\" spellcheck=\"true\" style=\"flex:1;min-width:0;padding:24px 36px;overflow:auto;font:inherit;font-size:21px;line-height:1.55;color:#fff;background:transparent;border:0;outline:none;resize:none;white-space:pre-wrap;pointer-events:auto;\">${notes}</textarea>\n <div style=\"width:380px;flex-shrink:0;border-left:1px solid rgba(255,255,255,0.12);padding:24px 28px;display:flex;flex-direction:column;gap:10px;\">\n <div style=\"font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;\">Up next</div>\n <div data-hf-presenter-next style=\"font-size:17px;opacity:.9;line-height:1.4;\">${esc(opts.nextText)}</div>\n ${branches}\n <div style=\"display:flex;gap:34px;margin-top:auto;\">\n <div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:.1em;opacity:.5;margin-bottom:3px;\">Slide</div><div data-hf-presenter-counter style=\"font-family:${COUNTER_FONT_FAMILY};font-size:23px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;\">${esc(opts.counterText)}</div></div>\n <div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:.1em;opacity:.5;margin-bottom:3px;\">Elapsed</div><div data-hf-presenter-elapsed style=\"font-family:${COUNTER_FONT_FAMILY};font-size:23px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;\">${esc(opts.elapsedText)}</div></div>\n </div>\n </div>\n</div>`.trim();\n}\n\n/** Format elapsed seconds as mm:ss */\nexport function formatElapsed(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.floor(seconds % 60);\n return `${String(m).padStart(2, \"0\")}:${String(s).padStart(2, \"0\")}`;\n}\n","import {\n parseSlideshowManifest,\n resolveSlideshow,\n type ResolvedSlideshow,\n} from \"@hyperframes/core/slideshow\";\nimport { SlideshowController, type PlayerPort } from \"./SlideshowController\";\nimport {\n SlideshowChannel,\n buildPresenterLayout,\n formatElapsed,\n type PresenterMediaAction,\n type PresenterMediaMessage,\n} from \"./slideshowPresenter\";\n\ninterface Hotspot {\n id: string;\n label: string;\n target: string;\n region?: { x: number; y: number; w: number; h: number };\n}\n\ninterface ControllerLike {\n next(): void;\n prev(): void;\n onChange(cb: () => void): () => void;\n readonly counter: { index: number; total: number };\n readonly breadcrumb: { id: string; label: string }[];\n readonly currentSlide: { hotspots: Hotspot[]; notes?: string; sceneId?: string } | undefined;\n readonly nextSlide: { sceneId: string; notes?: string } | null;\n readonly position: { sequenceId: string; slideIndex: number; fragmentIndex: number };\n readonly canPrev?: boolean;\n readonly canNext?: boolean;\n goToSlide?(index: number): void;\n syncTo?(sequenceId: string, slideIndex: number, fragmentIndex: number): void;\n enterBranch?(id: string): void;\n back?(): void;\n backToMain?(): void;\n dispose?(): void;\n}\n\ninterface SlideNotesTarget {\n sceneId?: string;\n}\n\ntype SlideshowManifest = NonNullable<ReturnType<typeof parseSlideshowManifest>>;\n\n// Autoplay re-assert poll (see playSceneDocumentMedia): the player drives clips\n// during bootstrap and on enter, so a single play() loses the race; we poll\n// briefly until the clip is advancing.\nconst AUTOPLAY_STEP_MS = 150;\nconst AUTOPLAY_MAX_MS = 6000;\ninterface AutoplayPollState {\n started: boolean;\n lastTime: number;\n advancingTicks: number;\n waited: number;\n warned: boolean;\n}\n\ntype PlayerElement = HTMLElement & {\n seek(t: number): void;\n play(): void;\n pause(): void;\n stopMedia?(): void;\n muted?: boolean;\n readonly iframeElement?: HTMLIFrameElement;\n readonly currentTime: number;\n readonly ready: boolean;\n};\n\ntype SlideshowMediaElement = HTMLMediaElement & {\n dataset: DOMStringMap;\n};\n\nfunction isPlayerElement(el: HTMLElement): el is PlayerElement {\n return (\n typeof (el as PlayerElement).seek === \"function\" &&\n typeof (el as PlayerElement).play === \"function\" &&\n typeof (el as PlayerElement).pause === \"function\"\n );\n}\n\nconst PRESENTER_NOTES_STORAGE_PREFIX = \"hf-slideshow:presenter-notes:v1:\";\n\n// Injected once per document to avoid duplicating @keyframes across multiple elements.\nlet _keyframesInjected = false;\nfunction injectKeyframesOnce(): void {\n if (_keyframesInjected) return;\n _keyframesInjected = true;\n const style = document.createElement(\"style\");\n style.textContent = `\n @keyframes hf-hotspot-pulse {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0.35), 0 4px 16px rgba(0,0,0,0.35); }\n 50% { box-shadow: 0 0 0 8px rgba(255,255,255,0), 0 4px 20px rgba(0,0,0,0.45); }\n }\n @keyframes hf-nav-spin {\n to { transform: rotate(360deg); }\n }\n @media (prefers-reduced-motion: reduce) {\n .hf-hotspot-pill,\n .hf-nav-spinner { animation: none !important; }\n }\n /* Nav-button hover (replaces inline onmouseover/onmouseout — CSP-safe).\n !important beats the inline base color set on each button. */\n [data-hf-nav-cluster] button:hover {\n background: rgba(255,255,255,0.12) !important;\n color: #fff !important;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip] {\n position: relative;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]::after {\n position: absolute;\n left: 50%;\n opacity: 0;\n pointer-events: none;\n transform: translateX(-50%) translateY(3px);\n transition: opacity 0.12s ease, transform 0.12s ease;\n z-index: 20;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::before {\n content: \"\";\n bottom: calc(100% + 4px);\n border: 5px solid transparent;\n border-top-color: rgba(12,12,14,0.95);\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]::after {\n content: attr(data-hf-tooltip);\n bottom: calc(100% + 14px);\n padding: 6px 8px;\n border-radius: 6px;\n background: rgba(12,12,14,0.95);\n color: #fff;\n box-shadow: 0 6px 20px rgba(0,0,0,0.35);\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0;\n line-height: 1;\n white-space: nowrap;\n }\n [data-hf-nav-cluster] button[data-hf-tooltip]:hover::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]:hover::after,\n [data-hf-nav-cluster] button[data-hf-tooltip]:focus-visible::before,\n [data-hf-nav-cluster] button[data-hf-tooltip]:focus-visible::after {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n /* When muted, the speaker button stays dimmed on hover so the mute-state\n affordance isn't erased (higher specificity than the rule above). */\n [data-hf-muted] [data-hf-mute]:hover {\n color: rgba(255,255,255,0.6) !important;\n }\n `;\n document.head.appendChild(style);\n}\n\n// Fullscreen glyphs (enter = expand corners, exit = collapse corners). Module-level\n// so onFsChange can swap just this glyph without re-rendering the whole chrome.\nconst ENTER_FS_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3\"/></svg>`;\nconst EXIT_FS_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 3v3a2 2 0 0 1-2 2H3M21 8h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3M16 21v-3a2 2 0 0 1 2-2h3\"/></svg>`;\nconst PRESENT_SVG = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"13\" rx=\"2\"/><path d=\"M8 21h8\"/><path d=\"M12 17v4\"/><path d=\"M10 8.5v4l4-2-4-2z\"/></svg>`;\nconst COUNTER_FONT_FAMILY =\n \"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\n\nexport class HyperframesSlideshow extends HTMLElement {\n private controller: ControllerLike | null = null;\n private offChange: (() => void) | null = null;\n private chrome: HTMLDivElement | null = null;\n private touchStartX = 0;\n private touchStartY = 0;\n private channel: SlideshowChannel | null = null;\n private presenterStartMs: number | null = null;\n private presenterInterval: ReturnType<typeof setInterval> | null = null;\n private presenterPositionTimers: ReturnType<typeof setTimeout>[] = [];\n private disconnected = false;\n private initTimer: ReturnType<typeof setTimeout> | null = null;\n private initInFlight = false;\n private initGeneration = 0;\n private _muted = false;\n private mediaWireInterval: ReturnType<typeof setInterval> | null = null;\n private playerObserver: MutationObserver | null = null;\n private applyingRemoteMedia = false;\n private lastMediaTimeBroadcastMs = 0;\n private audienceMutedPlaybackKeys = new Set<string>();\n private blockedAudienceMedia = new Map<string, PresenterMediaMessage>();\n private audienceMediaUnlockButton: HTMLButtonElement | null = null;\n // Bumped whenever autoplay starts or media is stopped (slide change), so a\n // pending re-assert from a previous autoplay can't replay a clip we've left.\n private autoplayToken = 0;\n\n /** Whether audio is currently muted. Reflects `data-hf-muted` attribute. */\n get muted(): boolean {\n return this._muted;\n }\n\n /** Mode resolves from the `mode` attribute, falling back to the URL query\n * (?mode=audience) so the audience window opened by present() is detected. */\n private resolveMode(): string | null {\n const attr = this.getAttribute(\"mode\");\n if (attr) return attr;\n try {\n return new URLSearchParams(location.search).get(\"mode\");\n } catch {\n return null;\n }\n }\n\n // Observe the attributes the component reads so runtime toggles take effect.\n static get observedAttributes(): string[] {\n return [\"sound\", \"mode\"];\n }\n\n attributeChangedCallback(): void {\n // Re-render once bound so a flipped `sound`/`mode` is reflected (mute button,\n // audience-vs-presenter chrome). No-op before the controller binds.\n if (this.controller) this.render();\n }\n\n connectedCallback(): void {\n this.disconnected = false;\n this.initInFlight = false;\n this.initGeneration += 1;\n this.tabIndex = 0;\n // note: if the inner player iframe has keyboard focus, window keydown in the\n // top document won't fire — that edge remains; this listener fixes the dominant\n // case where the page loads and arrows should work without clicking the element.\n window.addEventListener(\"keydown\", this.onKey);\n this.addEventListener(\"touchstart\", this.onTouchStart, { passive: true });\n this.addEventListener(\"touchend\", this.onTouchEnd);\n window.addEventListener(\"message\", this.onMessage);\n document.addEventListener(\"fullscreenchange\", this.onFsChange);\n this.initChannel();\n this.observeInteractivePlayers();\n // Defer player-dependent init to a macrotask so that child elements are\n // parsed before we query for <hyperframes-player>. This matters when the\n // bundle is loaded synchronously (e.g. <script src> in <head>), where\n // connectedCallback fires while the parser is still inside the\n // <hyperframes-slideshow> open tag — before its children exist. A microtask\n // is NOT sufficient: during streamed parsing the children are appended in a\n // later task, so a queued microtask still observes an empty subtree. A\n // setTimeout(0) macrotask yields to the parser so the children land first.\n this.initTimer = setTimeout(() => {\n this.initTimer = null;\n if (this.isConnected && !this.disconnected) {\n this.ensureInteractivePlayers();\n void this.init();\n }\n }, 0);\n }\n\n disconnectedCallback(): void {\n this.disconnected = true;\n this.initGeneration += 1;\n this.autoplayToken++; // cancel any in-flight autoplay re-assert loop\n if (this.initTimer !== null) {\n clearTimeout(this.initTimer);\n this.initTimer = null;\n }\n window.removeEventListener(\"keydown\", this.onKey);\n this.removeEventListener(\"touchstart\", this.onTouchStart);\n this.removeEventListener(\"touchend\", this.onTouchEnd);\n window.removeEventListener(\"message\", this.onMessage);\n document.removeEventListener(\"fullscreenchange\", this.onFsChange);\n this.offChange?.();\n this.offChange = null;\n this.controller?.dispose?.();\n this.controller = null;\n this.chrome = null;\n this.channel?.destroy();\n this.channel = null;\n if (this.mediaWireInterval !== null) {\n clearInterval(this.mediaWireInterval);\n this.mediaWireInterval = null;\n }\n if (this.playerObserver !== null) {\n this.playerObserver.disconnect();\n this.playerObserver = null;\n }\n this.audienceMediaUnlockButton?.remove();\n this.audienceMediaUnlockButton = null;\n this.audienceMutedPlaybackKeys.clear();\n this.blockedAudienceMedia.clear();\n if (this.presenterInterval !== null) {\n clearInterval(this.presenterInterval);\n this.presenterInterval = null;\n }\n this.clearPresenterPositionTimers();\n }\n\n /** Test seam: inject a controller without a live player. */\n __setControllerForTest(c: ControllerLike): void {\n this.bindController(c);\n }\n\n /**\n * Opens an audience window and switches this element to presenter layout.\n * Audience window URL: current page URL with `mode=audience` query param.\n */\n present(): void {\n if (this.resolveMode() === \"audience\" || this.getAttribute(\"data-hf-presenting\") === \"true\") {\n return;\n }\n const sep = location.search ? \"&\" : \"?\";\n // noopener,noreferrer: the audience window must not get a reference back to\n // this window (it syncs over BroadcastChannel, not window.opener).\n window.open(location.href + sep + \"mode=audience\", \"_blank\", \"noopener,noreferrer\");\n this.setAttribute(\"data-hf-presenting\", \"true\");\n this.postCurrentPresenterPositionBurst();\n this.presenterStartMs = Date.now();\n if (this.presenterInterval === null) {\n this.presenterInterval = setInterval(() => this.updateElapsed(), 1000);\n }\n this.render();\n }\n\n /**\n * Update only the elapsed readout. Re-rendering the whole chrome every second\n * (the old behavior) rebuilt the nav buttons' DOM on each tick — they\n * flickered and clicks landing mid-rebuild were dropped.\n */\n private updateElapsed(): void {\n if (this.presenterStartMs === null) return;\n const el = this.chrome?.querySelector(\"[data-hf-presenter-elapsed]\");\n if (el) {\n el.textContent = formatElapsed(Math.floor((Date.now() - this.presenterStartMs) / 1000));\n }\n }\n\n private initChannel(): void {\n const mode = this.resolveMode();\n if (mode === \"audience\") {\n this.channel = new SlideshowChannel(\n \"audience\",\n (msg) => {\n if (!this.controller) return;\n this.controller.syncTo?.(msg.sequenceId, msg.slideIndex, msg.fragmentIndex);\n },\n this.onRemoteMedia,\n );\n } else {\n this.channel = new SlideshowChannel(\n \"presenter\",\n () => {\n // presenter channel does not receive goto messages; posting happens in bindController.\n },\n this.onRemoteMedia,\n );\n }\n }\n\n // fallow-ignore-next-line complexity\n private async init(): Promise<void> {\n if (this.initInFlight) return;\n this.initInFlight = true;\n const gen = this.initGeneration;\n\n try {\n const playerEl = this.querySelector(\"hyperframes-player\");\n if (!playerEl || !(playerEl instanceof HTMLElement)) return;\n\n const html = this.innerHTML;\n let manifest: ReturnType<typeof parseSlideshowManifest>;\n try {\n manifest = parseSlideshowManifest(html);\n } catch {\n // Malformed island (e.g. bad JSON) — fail gracefully, no chrome.\n return;\n }\n if (!manifest) return;\n\n this.renderInitialChrome(manifest);\n\n if (!isPlayerElement(playerEl)) return;\n\n await waitForReady(playerEl);\n\n // Guard: if a disconnect or reconnect happened while waiting, bail out.\n if (gen !== this.initGeneration) return;\n\n // Wait for scenes to be populated (the runtime \"timeline\" postMessage\n // arrives ~1000ms after waitForReady resolves). Graceful fallback to []\n // on timeout so explicit startTime/endTime slides still work.\n const scenes = await waitForScenes(playerEl, 2500, () => gen !== this.initGeneration);\n\n // Guard again in case we were disconnected or reconnected during the scenes wait.\n if (gen !== this.initGeneration) return;\n\n const { resolved, errors } = resolveSlideshow(manifest, scenes);\n if (errors.length > 0) {\n console.warn(\"[hyperframes-slideshow] manifest errors:\", errors);\n }\n const cleaned = dropInvalidSlides(resolved);\n if (cleaned.slides.length === 0 && manifest.slides.length > 0) {\n console.error(\n \"[hyperframes-slideshow] no main-line slides resolved — the scene timeline may not have loaded in time, or sceneIds/timing are invalid:\",\n errors,\n );\n }\n\n const port: PlayerPort = {\n seek: (t) => playerEl.seek(t),\n play: () => playerEl.play(),\n pause: () => playerEl.pause(),\n stopMedia: () => {\n playerEl.stopMedia?.();\n this.stopDocumentMedia();\n },\n playSceneMedia: (sceneId) => this.playSceneDocumentMedia(sceneId),\n get currentTime() {\n return playerEl.currentTime;\n },\n onTimeUpdate: (cb) => {\n const handler = (e: Event) => {\n const detail = (e as CustomEvent<{ currentTime: number }>).detail;\n cb(detail.currentTime);\n };\n playerEl.addEventListener(\"timeupdate\", handler);\n return () => playerEl.removeEventListener(\"timeupdate\", handler);\n },\n };\n\n this.bindController(new SlideshowController(port, cleaned));\n\n // Slow-iframe recovery: if the scene timeline hadn't posted yet (empty\n // scenes → sceneId-based slides were dropped), re-init once when it finally\n // arrives so those slides resolve instead of being permanently lost.\n if (scenes.length === 0 && manifest.slides.length > 0) {\n playerEl.addEventListener(\n \"scenes\",\n () => {\n if (gen === this.initGeneration) void this.init();\n },\n { once: true },\n );\n }\n } finally {\n this.initInFlight = false;\n }\n }\n\n private renderInitialChrome(manifest: SlideshowManifest): void {\n if (this.controller || manifest.slides.length === 0) return;\n const counter = { index: 1, total: manifest.slides.length };\n if (this.resolveMode() === \"audience\") {\n this.paintChrome(this.buildNavCluster(counter, \"28px\", \"fs-only\"));\n return;\n }\n this.paintChrome(\n this.buildNavCluster(counter, \"28px\", \"full\", {\n canPrev: false,\n canNext: manifest.slides.length > 1,\n loading: manifest.slides.length > 1,\n }),\n );\n }\n\n private bindController(c: ControllerLike): void {\n this.offChange?.();\n this.controller?.dispose?.();\n this.controller = c;\n this.startMediaSync();\n this.offChange = c.onChange(() => {\n // Presenter posts position to channel on every change\n if (this.resolveMode() !== \"audience\" && this.channel) {\n this.channel.postPosition(c.position);\n }\n this.render();\n });\n // Post initial position if presenter\n if (this.resolveMode() !== \"audience\" && this.channel) {\n this.channel.postPosition(c.position);\n }\n this.render();\n }\n\n private postCurrentPresenterPosition(): void {\n if (this.resolveMode() === \"audience\" || !this.channel || !this.controller) return;\n this.channel.postPosition(this.controller.position);\n }\n\n private postCurrentPresenterPositionBurst(): void {\n this.clearPresenterPositionTimers();\n this.postCurrentPresenterPosition();\n for (const delay of [250, 750, 1500, 3000, 5000]) {\n const timer = setTimeout(() => {\n this.presenterPositionTimers = this.presenterPositionTimers.filter(\n (item) => item !== timer,\n );\n this.postCurrentPresenterPosition();\n }, delay);\n this.presenterPositionTimers.push(timer);\n }\n }\n\n private clearPresenterPositionTimers(): void {\n for (const timer of this.presenterPositionTimers) {\n clearTimeout(timer);\n }\n this.presenterPositionTimers = [];\n }\n\n private startMediaSync(): void {\n this.wireSlideshowMedia();\n if (this.mediaWireInterval === null) {\n // Same-origin player iframes can hydrate media after the slideshow binds.\n // The dataset guard prevents duplicate listeners, and removed iframe nodes\n // are collectable because this component keeps no media element references.\n this.mediaWireInterval = setInterval(() => this.wireSlideshowMedia(), 1000);\n }\n }\n\n private mediaPlayerElements(): (Partial<PlayerElement> & HTMLElement)[] {\n return Array.from(this.querySelectorAll(\"hyperframes-player\")).filter(\n (player): player is Partial<PlayerElement> & HTMLElement => player instanceof HTMLElement,\n );\n }\n\n /**\n * Inner `<hyperframes-player>` instances inside a slideshow need the\n * `interactive` attribute so clickable controls, links, native media\n * controls, and custom players inside the composition iframe receive\n * pointer events (the player's default is `pointer-events: none`).\n *\n * Set it mechanically so authors / agents don't have to remember.\n * Idempotent: if the host already declared `interactive` (any value,\n * including `interactive=\"false\"`), it is preserved.\n */\n private ensureInteractivePlayers(): void {\n for (const player of this.querySelectorAll(\"hyperframes-player\")) {\n if (!player.hasAttribute(\"interactive\")) {\n player.setAttribute(\"interactive\", \"\");\n }\n }\n }\n\n /**\n * Watch for `<hyperframes-player>` children added after the initial mount\n * (dynamic templating, hydration, drag-drop authoring) and apply the\n * `interactive` attribute to those too.\n */\n private observeInteractivePlayers(): void {\n if (typeof MutationObserver === \"undefined\") return;\n if (this.playerObserver !== null) return;\n this.playerObserver = new MutationObserver(() => this.ensureInteractivePlayers());\n this.playerObserver.observe(this, { childList: true, subtree: true });\n }\n\n private playerFrameDocument(player: Partial<PlayerElement> & HTMLElement): Document | null {\n const frame = player.iframeElement;\n if (!(frame instanceof HTMLIFrameElement)) return null;\n try {\n return frame.contentDocument;\n } catch {\n return null;\n }\n }\n\n private mediaKey(\n player: HTMLElement,\n playerIndex: number,\n media: SlideshowMediaElement,\n mediaIndex: number,\n ): string {\n const playerKey = player.id ? `player-id:${player.id}` : `player:${playerIndex}`;\n const mediaKey = media.id ? `id:${media.id}` : `${media.tagName.toLowerCase()}:${mediaIndex}`;\n return `${playerKey}|${mediaKey}`;\n }\n\n private mediaEntries(): { key: string; el: SlideshowMediaElement }[] {\n const entries: { key: string; el: SlideshowMediaElement }[] = [];\n this.mediaPlayerElements().forEach((player, playerIndex) => {\n const doc = this.playerFrameDocument(player);\n if (!doc) return;\n Array.from(doc.querySelectorAll(\"video,audio\"))\n .filter(isSlideshowMediaElement)\n .forEach((el, mediaIndex) => {\n entries.push({ key: this.mediaKey(player, playerIndex, el, mediaIndex), el });\n });\n });\n return entries;\n }\n\n private wireSlideshowMedia(): void {\n const actions: PresenterMediaAction[] = [\n \"play\",\n \"pause\",\n \"seeking\",\n \"seeked\",\n \"ratechange\",\n \"volumechange\",\n \"ended\",\n \"timeupdate\",\n ];\n for (const { key, el } of this.mediaEntries()) {\n if (el.dataset.hfSlideshowMediaSync === \"1\") continue;\n el.dataset.hfSlideshowMediaSync = \"1\";\n for (const action of actions) {\n el.addEventListener(action, () => this.publishMediaState(el, key, action));\n }\n }\n }\n\n private publishMediaState(\n el: SlideshowMediaElement,\n key: string,\n action: PresenterMediaAction,\n ): void {\n if (this.applyingRemoteMedia || this.resolveMode() === \"audience\" || !this.channel) return;\n if (action === \"timeupdate\") {\n const now = performance.now();\n if (now - this.lastMediaTimeBroadcastMs < 450 && !el.paused) return;\n this.lastMediaTimeBroadcastMs = now;\n }\n this.channel.postMedia({\n key,\n action,\n currentTime: finiteMediaNumber(el.currentTime, 0),\n paused: el.paused,\n ended: el.ended,\n muted: el.muted,\n volume: finiteMediaNumber(el.volume, 1),\n playbackRate: finiteMediaNumber(el.playbackRate, 1),\n });\n }\n\n private onRemoteMedia = (msg: PresenterMediaMessage): void => {\n if (this.resolveMode() !== \"audience\") return;\n if (this.blockedAudienceMedia.has(msg.key) && msg.action === \"timeupdate\") {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n return;\n }\n this.wireSlideshowMedia();\n const entry = this.mediaEntries().find((candidate) => candidate.key === msg.key);\n if (!entry) return;\n this.applyingRemoteMedia = true;\n try {\n this.applyRemoteMedia(entry.el, msg);\n } finally {\n setTimeout(() => {\n this.applyingRemoteMedia = false;\n }, 300);\n }\n };\n\n private applyRemoteMedia(el: SlideshowMediaElement, msg: PresenterMediaMessage): void {\n if (msg.action === \"pause\" || msg.action === \"ended\") {\n this.audienceMutedPlaybackKeys.delete(msg.key);\n this.blockedAudienceMedia.delete(msg.key);\n this.syncRemoteMediaState(el, msg, true);\n el.pause();\n this.hideAudienceMediaUnlockIfClear();\n return;\n }\n\n const remoteWantsPlayback =\n msg.action === \"play\" || (msg.action === \"timeupdate\" && msg.paused === false && el.paused);\n if (remoteWantsPlayback) {\n this.playAudienceMediaMuted(el, msg);\n return;\n }\n\n this.syncRemoteMediaState(el, msg, true);\n }\n\n private syncRemoteMediaState(\n el: SlideshowMediaElement,\n msg: PresenterMediaMessage,\n allowTimeSync: boolean,\n ): void {\n el.playbackRate = finiteMediaNumber(msg.playbackRate, 1);\n el.volume = Math.max(0, Math.min(1, finiteMediaNumber(msg.volume, 1)));\n el.muted = this.audienceMutedPlaybackKeys.has(msg.key) ? true : msg.muted;\n if (\n allowTimeSync &&\n Number.isFinite(msg.currentTime) &&\n Math.abs((el.currentTime || 0) - msg.currentTime) > 0.35\n ) {\n el.currentTime = Math.max(0, msg.currentTime);\n }\n }\n\n private playAudienceMediaMuted(el: SlideshowMediaElement, msg: PresenterMediaMessage): void {\n this.audienceMutedPlaybackKeys.add(msg.key);\n this.syncRemoteMediaState(el, msg, true);\n el.muted = true;\n try {\n void el\n .play()\n .then(() => {\n this.blockedAudienceMedia.delete(msg.key);\n this.hideAudienceMediaUnlockIfClear();\n })\n .catch(() => {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n });\n } catch {\n this.blockedAudienceMedia.set(msg.key, msg);\n this.showAudienceMediaUnlock();\n }\n }\n\n private retryBlockedAudienceMedia = (): void => {\n this.wireSlideshowMedia();\n const entries = new Map(this.mediaEntries().map((entry) => [entry.key, entry.el]));\n for (const [key, msg] of this.blockedAudienceMedia) {\n const el = entries.get(key);\n if (el) this.playAudienceMediaMuted(el, msg);\n }\n };\n\n private showAudienceMediaUnlock(): void {\n if (this.resolveMode() !== \"audience\" || this.audienceMediaUnlockButton) return;\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.textContent = \"Play audience media muted\";\n button.style.cssText =\n \"position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:100000;border:0;border-radius:999px;padding:12px 18px;background:#fff;color:#111827;box-shadow:0 10px 32px rgba(0,0,0,.28);font:700 14px/1 system-ui,sans-serif;cursor:pointer;pointer-events:auto;\";\n button.addEventListener(\"click\", this.retryBlockedAudienceMedia);\n this.appendChild(button);\n this.audienceMediaUnlockButton = button;\n }\n\n private hideAudienceMediaUnlockIfClear(): void {\n if (this.blockedAudienceMedia.size > 0 || !this.audienceMediaUnlockButton) return;\n this.audienceMediaUnlockButton.remove();\n this.audienceMediaUnlockButton = null;\n }\n\n // fallow-ignore-next-line complexity\n private onKey = (e: KeyboardEvent): void => {\n if (!this.controller) return;\n const target = e.target;\n if (\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement && target.isContentEditable)\n ) {\n return;\n }\n const active = document.activeElement;\n const focused = active === this || this.contains(active);\n // Arrows act even when nothing is focused (active === body/null) so a freshly\n // loaded deck responds without a click; Space/Backspace have strong page-level\n // defaults (scroll / history) so they only act when the deck actually has focus.\n // When several decks share a page, drop the unfocused-convenience so a key\n // doesn't drive every instance at once — only the focused deck responds.\n const multiInstance = document.querySelectorAll(\"hyperframes-slideshow\").length > 1;\n const ambient = focused || (!multiInstance && (active === document.body || active === null));\n if (e.key === \"ArrowRight\") {\n if (!ambient) return;\n this.controller.next();\n e.preventDefault();\n } else if (e.key === \"ArrowLeft\") {\n if (!ambient) return;\n this.controller.prev();\n e.preventDefault();\n } else if (e.key === \" \") {\n if (!focused) return;\n this.controller.next();\n e.preventDefault();\n } else if (e.key === \"Backspace\") {\n if (!focused) return;\n this.controller.prev();\n e.preventDefault();\n } else if ((e.key === \"f\" || e.key === \"F\") && !e.metaKey && !e.ctrlKey && !e.altKey) {\n if (!focused) return;\n this.toggleFullscreen();\n e.preventDefault();\n } else if ((e.key === \"p\" || e.key === \"P\") && !e.metaKey && !e.ctrlKey && !e.altKey) {\n if (!ambient || !this.shouldShowPresentControl()) return;\n this.present();\n e.preventDefault();\n }\n };\n\n // fallow-ignore-next-line complexity\n private onMessage = (e: MessageEvent): void => {\n // Audience mode is driven by BroadcastChannel; ignore embed postMessage nav.\n if (this.resolveMode() === \"audience\") return;\n const data = e.data as { type?: unknown; slideIndex?: unknown } | null;\n if (!data || !this.controller) return;\n if (data.type === \"next\") {\n this.controller.next();\n } else if (data.type === \"prev\") {\n this.controller.prev();\n } else if (data.type === \"goto\" && typeof data.slideIndex === \"number\") {\n this.controller.goToSlide?.(data.slideIndex);\n } else if (data.type === \"back\") {\n this.controller.back?.();\n }\n };\n\n private onTouchStart = (e: TouchEvent): void => {\n const touch = e.touches[0];\n if (touch) {\n this.touchStartX = touch.clientX;\n this.touchStartY = touch.clientY;\n }\n };\n\n private onTouchEnd = (e: TouchEvent): void => {\n if (!this.controller) return;\n const touch = e.changedTouches[0];\n if (!touch) return;\n const deltaX = touch.clientX - this.touchStartX;\n const deltaY = touch.clientY - this.touchStartY;\n // Require a dominant horizontal gesture: |deltaX| > 40 AND |deltaX| > |deltaY|\n // so that diagonal page-scrolls do not accidentally trigger slide navigation.\n if (Math.abs(deltaX) <= 40 || Math.abs(deltaX) <= Math.abs(deltaY)) return;\n if (deltaX < 0) {\n this.controller.next();\n } else {\n this.controller.prev();\n }\n };\n\n // fallow-ignore-next-line complexity\n private render(): void {\n if (!this.controller) return;\n\n if (this.resolveMode() === \"audience\") {\n // Audience (viewer) window: no nav controls — but keep a fullscreen toggle\n // so the presentation can fill the display.\n const { counter } = this.controller;\n this.paintChrome(this.buildNavCluster(counter, \"28px\", \"fs-only\"));\n return;\n }\n\n if (this.getAttribute(\"data-hf-presenting\") === \"true\") {\n this.renderPresenter();\n return;\n }\n\n const { counter, currentSlide } = this.controller;\n if (!currentSlide) return;\n\n // Hotspot pills: compact floating buttons anchored to the region's top-left,\n // sized to content (not filling the region). The region x/y positions the pill;\n // w/h are ignored for sizing (pill is content-sized). XSS: escHtml guards all\n // user-supplied strings.\n const hotspotsHtml = currentSlide.hotspots\n .map((h) => {\n const posStyle = h.region\n ? `left:${h.region.x}%;top:${h.region.y}%;`\n : \"right:5%;bottom:18%;\";\n return `<button\n class=\"hf-hotspot-pill\"\n data-hotspot-id=\"${escHtml(h.id)}\"\n data-hotspot-target=\"${escHtml(h.target)}\"\n type=\"button\"\n style=\"position:absolute;${posStyle}display:inline-flex;align-items:center;gap:6px;padding:8px 14px;background:var(--hf-slideshow-accent,rgba(255,255,255,0.92));color:#111;border:none;border-radius:999px;font-size:13px;font-weight:600;letter-spacing:0.01em;cursor:pointer;pointer-events:auto;box-shadow:0 4px 16px rgba(0,0,0,0.35);animation:hf-hotspot-pulse 1.8s ease-in-out infinite;white-space:nowrap;\"\n aria-label=\"${escHtml(h.label)}\"\n ><span aria-hidden=\"true\" style=\"font-size:14px;line-height:1;\">⊕</span>${escHtml(h.label)}</button>`;\n })\n .join(\"\");\n\n this.paintChrome(hotspotsHtml + this.buildNavCluster(counter, \"28px\"));\n }\n\n /** Ensure the overlay chrome layer exists, set its content, and wire its buttons. */\n private paintChrome(html: string): void {\n injectKeyframesOnce(); // nav-button :hover + hotspot keyframes (CSP-safe, once per doc)\n if (!this.chrome) {\n this.chrome = document.createElement(\"div\");\n this.chrome.setAttribute(\"data-hf-chrome\", \"\");\n this.appendChild(this.chrome);\n }\n this.chrome.style.cssText = \"position:absolute;inset:0;pointer-events:none;z-index:10;\";\n this.chrome.innerHTML = html;\n this.wireChromeButtons();\n }\n\n // Builds the nav cluster ([mute?] [prev] counter [next] | [present?] [fullscreen]) as a\n // floating capsule. `bottomCss` positions it (normal view: \"28px\"; presenter\n // view: above the notes panel). Reused by render() and renderPresenter().\n // fallow-ignore-next-line complexity\n private buildNavCluster(\n counter: { index: number; total: number },\n bottomCss: string,\n variant: \"full\" | \"fs-only\" = \"full\",\n options: { canPrev?: boolean; canNext?: boolean; loading?: boolean } = {},\n ): string {\n const c = this.controller;\n const showPrev = options.canPrev ?? c?.canPrev ?? true;\n const showNext = options.canNext ?? c?.canNext ?? true;\n const showLoading = options.loading === true && showNext;\n const showSound = this.hasAttribute(\"sound\");\n const btnStyle =\n \"display:flex;align-items:center;justify-content:center;width:34px;height:34px;background:transparent;border:none;border-radius:999px;color:rgba(255,255,255,0.85);font-size:16px;cursor:pointer;transition:background 0.15s,color 0.15s;padding:0;\";\n const speakerSvg = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><path d=\"M15.54 8.46a5 5 0 0 1 0 7.07\"/><path d=\"M19.07 4.93a10 10 0 0 1 0 14.14\"/></svg>`;\n const speakerMutedSvg = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\"/><line x1=\"23\" y1=\"9\" x2=\"17\" y2=\"15\"/><line x1=\"17\" y1=\"9\" x2=\"23\" y2=\"15\"/></svg>`;\n const muteBtnHtml = showSound\n ? `<button\n data-hf-mute\n type=\"button\"\n aria-label=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n title=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n data-hf-tooltip=\"${this._muted ? \"Unmute\" : \"Mute\"}\"\n aria-pressed=\"${this._muted ? \"true\" : \"false\"}\"\n style=\"${btnStyle}${this._muted ? \"color:rgba(255,255,255,0.45);\" : \"\"}\"\n >${this._muted ? speakerMutedSvg : speakerSvg}</button>`\n : \"\";\n const prevBtnHtml = showPrev\n ? `<button\n data-hf-prev\n type=\"button\"\n aria-label=\"Previous slide\"\n title=\"Previous slide\"\n data-hf-tooltip=\"Previous slide\"\n style=\"${btnStyle}\" >‹</button>`\n : \"\";\n const loadingHtml = showLoading\n ? `<span\n data-hf-nav-loading\n role=\"status\"\n aria-label=\"Loading slides\"\n title=\"Loading slides\"\n style=\"${btnStyle}cursor:progress;color:rgba(255,255,255,0.72);\"\n ><span class=\"hf-nav-spinner\" aria-hidden=\"true\" style=\"width:14px;height:14px;border:2px solid rgba(255,255,255,0.32);border-top-color:rgba(255,255,255,0.92);border-radius:999px;animation:hf-nav-spin 0.8s linear infinite;\"></span></span>`\n : \"\";\n const nextBtnHtml = showLoading\n ? loadingHtml\n : showNext\n ? `<button\n data-hf-next\n type=\"button\"\n aria-label=\"Next slide\"\n title=\"Next slide\"\n data-hf-tooltip=\"Next slide\"\n style=\"${btnStyle}\" >›</button>`\n : \"\";\n const presentBtnHtml = this.shouldShowPresentControl()\n ? `<button\n data-hf-present\n type=\"button\"\n aria-label=\"Present\"\n title=\"Present\"\n data-hf-tooltip=\"Present\"\n style=\"${btnStyle}\" >${PRESENT_SVG}</button>`\n : \"\";\n const isFs = document.fullscreenElement === this;\n const fsLabel = isFs ? \"Exit full screen\" : \"Full screen\";\n const fsBtnHtml = `<button\n data-hf-fullscreen\n type=\"button\"\n aria-label=\"${fsLabel}\"\n title=\"${fsLabel}\"\n data-hf-tooltip=\"${fsLabel}\"\n aria-pressed=\"${isFs ? \"true\" : \"false\"}\"\n style=\"${btnStyle}\" >${isFs ? EXIT_FS_SVG : ENTER_FS_SVG}</button>`;\n // Audience/viewer: only the fullscreen control (no navigation).\n if (variant === \"fs-only\") {\n return `\n <div\n data-hf-nav-cluster\n style=\"position:absolute;bottom:${bottomCss};right:32px;display:inline-flex;align-items:center;background:rgba(20,20,22,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.12);border-radius:999px;box-shadow:0 4px 24px rgba(0,0,0,0.45);padding:4px;pointer-events:auto;\"\n >${fsBtnHtml}</div>`;\n }\n const counterPadLeft = showPrev ? \"4px\" : \"10px\";\n const counterPadRight = showNext ? \"4px\" : \"10px\";\n return `\n <div\n data-hf-nav-cluster\n style=\"position:absolute;bottom:${bottomCss};right:32px;display:inline-flex;align-items:center;gap:2px;background:rgba(20,20,22,0.55);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.12);border-radius:999px;box-shadow:0 4px 24px rgba(0,0,0,0.45);padding:4px;pointer-events:auto;\"\n >\n ${muteBtnHtml}\n ${showSound ? `<span aria-hidden=\"true\" style=\"width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;\"></span>` : \"\"}\n ${prevBtnHtml}\n <span\n data-hf-counter\n aria-label=\"Slide ${counter.index} of ${counter.total}\"\n style=\"min-width:46px;text-align:center;color:rgba(255,255,255,0.9);font-family:${COUNTER_FONT_FAMILY};font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:0;padding:0 ${counterPadRight} 0 ${counterPadLeft};user-select:none;\"\n >${counter.index} / ${counter.total}</span>\n ${nextBtnHtml}\n <span aria-hidden=\"true\" style=\"width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;\"></span>\n ${presentBtnHtml}\n ${fsBtnHtml}\n </div>`;\n }\n\n private wireChromeButtons(): void {\n const chrome = this.chrome;\n if (!chrome) return;\n this.wireChromeClick(chrome, \"[data-hf-mute]\", () => this.toggleMute());\n this.wireChromeClick(chrome, \"[data-hf-prev]\", () => this.controller?.prev());\n this.wireChromeClick(chrome, \"[data-hf-next]\", () => this.controller?.next());\n this.wireChromeClick(chrome, \"[data-hf-present]\", () => this.present());\n this.wireChromeClick(chrome, \"[data-hf-fullscreen]\", () => this.toggleFullscreen());\n this.wirePresenterNotes(chrome);\n this.wireHotspots(chrome);\n }\n\n private wireChromeClick(chrome: HTMLDivElement, selector: string, handler: () => void): void {\n const btn = chrome.querySelector(selector);\n if (btn) btn.addEventListener(\"click\", handler);\n }\n\n private wirePresenterNotes(chrome: HTMLDivElement): void {\n const notesInput = chrome.querySelector(\"[data-hf-presenter-notes]\");\n if (notesInput instanceof HTMLTextAreaElement) {\n const key = notesInput.getAttribute(\"data-hf-presenter-notes-key\");\n notesInput.addEventListener(\"input\", () => this.writePresenterNotes(key, notesInput.value));\n }\n }\n\n private wireHotspots(chrome: HTMLDivElement): void {\n for (const btn of chrome.querySelectorAll(\"[data-hotspot-id]\")) {\n const target = btn.getAttribute(\"data-hotspot-target\") ?? \"\";\n btn.addEventListener(\"click\", () => this.controller?.enterBranch?.(target));\n }\n }\n\n private onFsChange = (): void => {\n // Swap only the fullscreen glyph + label — re-rendering the whole chrome here\n // would rebuild every nav button on each fullscreen toggle.\n const btn = this.chrome?.querySelector(\"[data-hf-fullscreen]\");\n if (!btn) return;\n const isFs = document.fullscreenElement === this;\n btn.innerHTML = isFs ? EXIT_FS_SVG : ENTER_FS_SVG;\n const label = isFs ? \"Exit full screen\" : \"Full screen\";\n btn.setAttribute(\"aria-label\", label);\n btn.setAttribute(\"title\", label);\n btn.setAttribute(\"data-hf-tooltip\", label);\n btn.setAttribute(\"aria-pressed\", isFs ? \"true\" : \"false\");\n };\n\n private toggleFullscreen(): void {\n if (document.fullscreenElement === this) {\n void document.exitFullscreen().catch(() => {});\n } else {\n void this.requestFullscreen().catch(() => {});\n }\n }\n\n private shouldShowPresentControl(): boolean {\n return this.resolveMode() !== \"audience\" && this.getAttribute(\"data-hf-presenting\") !== \"true\";\n }\n\n private toggleMute(): void {\n this._muted = !this._muted;\n if (this._muted) {\n this.setAttribute(\"data-hf-muted\", \"\");\n } else {\n this.removeAttribute(\"data-hf-muted\");\n }\n this.applyGlobalMute(this._muted);\n this.dispatchEvent(\n new CustomEvent(\"hf-sound\", {\n detail: { muted: this._muted },\n bubbles: true,\n composed: true,\n }),\n );\n // Re-render to flip the glyph.\n this.render();\n }\n\n private applyGlobalMute(muted: boolean): void {\n for (const player of this.querySelectorAll(\"hyperframes-player\")) {\n if (!(player instanceof HTMLElement)) continue;\n const playerEl = player as Partial<PlayerElement> & HTMLElement;\n if (\"muted\" in playerEl) {\n playerEl.muted = muted;\n } else if (muted) {\n playerEl.setAttribute(\"muted\", \"\");\n } else {\n playerEl.removeAttribute(\"muted\");\n }\n }\n\n const doc = this.ownerDocument;\n for (const el of doc.querySelectorAll(\"video, audio\")) {\n if (el instanceof HTMLMediaElement) el.muted = muted || el.defaultMuted;\n }\n }\n\n private stopDocumentMedia(): void {\n // Invalidate any in-flight autoplay re-assert so leaving a slide can't be\n // undone by a pending timeout replaying the clip we just paused.\n this.autoplayToken++;\n const doc = this.ownerDocument;\n for (const el of doc.querySelectorAll(\"video, audio\")) {\n if (el instanceof HTMLMediaElement) el.pause();\n }\n }\n\n /**\n * Play the `<video>` inside a given scene from its start — the runtime side of\n * a slide's `autoplay`. Reaches into the same-origin composition iframe (which\n * is pointer-events:none, so its own controls can't be clicked). The play()\n * fires a \"play\" event that wireSlideshowMedia() mirrors to any audience\n * window, so this runs on the presenter only — the audience drives its copy\n * from those mirrored events, never on its own.\n *\n * Robust against two timing hazards: (1) the clip may not be in the iframe DOM\n * yet at construction (first slide), and (2) the player drives clips during\n * bootstrap and seeks the timeline on enter — both pause the clip (and reject\n * an in-flight play() with AbortError), so a single play() loses the race. So\n * we poll on a short timer: locate the clip, then assert play() until it is\n * actually advancing across two ticks, then stop — leaving a later real user\n * pause (presenter media controls) alone. A user gesture within the window\n * (real browsers gate autoplay on one) lets the next tick's play() take.\n * Token-guarded, so leaving the slide or disconnecting cancels it.\n */\n private playSceneDocumentMedia(sceneId: string): void {\n if (this.resolveMode() === \"audience\") return;\n const safeId = sceneId.replace(/[\"\\\\]/g, \"\\\\$&\");\n const token = ++this.autoplayToken;\n const state: AutoplayPollState = {\n started: false,\n lastTime: -1,\n advancingTicks: 0,\n waited: 0,\n warned: false,\n };\n const tick = (): void => {\n if (token !== this.autoplayToken) return; // left the slide / disconnected\n const done = this.stepAutoplay(safeId, state);\n state.waited += AUTOPLAY_STEP_MS;\n if (!done && state.waited <= AUTOPLAY_MAX_MS) window.setTimeout(tick, AUTOPLAY_STEP_MS);\n };\n tick();\n }\n\n /** Locate the scene's clip in the composition iframe(s). */\n private findSceneVideo(safeId: string): HTMLVideoElement | null {\n for (const player of this.mediaPlayerElements()) {\n const doc = this.playerFrameDocument(player);\n const video = doc?.querySelector(`[data-composition-id=\"${safeId}\"] video`) ?? null;\n if (video instanceof HTMLVideoElement) return video;\n }\n return null;\n }\n\n /** One autoplay poll step. Returns true once the clip is confirmed playing. */\n private stepAutoplay(safeId: string, state: AutoplayPollState): boolean {\n const video = this.findSceneVideo(safeId);\n if (!video) return false;\n if (!state.started) {\n state.started = true;\n video.muted = this._muted || video.defaultMuted;\n try {\n video.currentTime = 0;\n } catch {\n // not seekable yet — play from wherever it is\n }\n }\n const advancing = !video.paused && video.currentTime > state.lastTime;\n state.lastTime = video.currentTime;\n if (advancing) return ++state.advancingTicks >= 2; // confirmed playing — stop polling\n state.advancingTicks = 0;\n void video.play().catch((err: unknown) => {\n // Expected during the poll: AbortError (a timeline-sync seek interrupts\n // the play) and NotAllowedError (autoplay gated on a user gesture). Surface\n // anything else once — a real failure (bad src, decode) shouldn't be silent.\n const name = err instanceof DOMException ? err.name : \"\";\n if (name !== \"AbortError\" && name !== \"NotAllowedError\" && !state.warned) {\n state.warned = true;\n console.warn(\"[hyperframes-slideshow] autoplay play() failed:\", err);\n }\n });\n return false;\n }\n\n private presenterNotesDeckKey(): string {\n const explicit = this.getAttribute(\"notes-storage-key\")?.trim();\n if (explicit) return explicit;\n\n const playerSrc = this.querySelector(\"hyperframes-player\")?.getAttribute(\"src\") ?? \"\";\n let resolvedPlayerSrc = playerSrc;\n try {\n const baseHref = typeof location !== \"undefined\" ? location.href : \"http://localhost/\";\n resolvedPlayerSrc = new URL(playerSrc, baseHref).href;\n } catch {\n // Keep the raw src when URL construction is unavailable.\n }\n\n const locationKey =\n typeof location !== \"undefined\" ? `${location.origin}${location.pathname}` : \"\";\n const title = this.ownerDocument.title;\n return `${locationKey}|${title}|${resolvedPlayerSrc}`;\n }\n\n private presenterNotesStorageKey(slide: SlideNotesTarget): string | null {\n const pos = this.controller?.position;\n if (!pos) return null;\n return `${PRESENTER_NOTES_STORAGE_PREFIX}${JSON.stringify([\n this.presenterNotesDeckKey(),\n pos.sequenceId,\n pos.slideIndex,\n slide.sceneId ?? \"\",\n ])}`;\n }\n\n private readPresenterNotes(key: string | null): string | null {\n if (!key) return null;\n if (typeof window === \"undefined\") return null;\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n }\n\n private writePresenterNotes(key: string | null, notes: string): void {\n if (!key) return;\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, notes);\n } catch {\n // localStorage may be disabled or quota-limited; editing still works for\n // the current render even when persistence is unavailable.\n }\n }\n\n private renderPresenter(): void {\n if (!this.controller) return;\n const { counter, currentSlide, nextSlide } = this.controller;\n if (!currentSlide) return;\n\n const elapsedSec =\n this.presenterStartMs !== null ? Math.floor((Date.now() - this.presenterStartMs) / 1000) : 0;\n\n // Pin the live slide to the TOP and reserve the bottom 32% for the notes\n // panel. The player contains the composition, so the FULL slide stays visible\n // (letterboxed) at any width — its bottom is never hidden behind the panel —\n // and it re-fits to the top region on window resize.\n const playerEl = this.querySelector(\"hyperframes-player\");\n if (playerEl instanceof HTMLElement) {\n playerEl.style.top = \"0\";\n playerEl.style.bottom = \"32%\";\n playerEl.style.height = \"auto\";\n }\n\n // Full-overlay chrome (pointer-events:none); the notes panel and nav cluster\n // are the only interactive children.\n const notesStorageKey = this.presenterNotesStorageKey(currentSlide);\n const notes = this.readPresenterNotes(notesStorageKey) ?? currentSlide.notes ?? \"\";\n this.paintChrome(\n buildPresenterLayout({\n notes,\n notesStorageKey,\n nextText: nextPanelText(nextSlide),\n counterText: `${counter.index} / ${counter.total}`,\n elapsedText: formatElapsed(elapsedSec),\n hotspots: currentSlide.hotspots,\n }) + this.buildNavCluster(counter, \"calc(32% + 18px)\"),\n );\n }\n}\n\nfunction nextPanelText(slide: { sceneId: string; notes?: string } | null): string {\n if (slide === null) return \"End of sequence\";\n const firstLine = slide.notes != null ? (slide.notes.split(\"\\n\")[0] ?? \"\") : \"\";\n return firstLine.length > 0\n ? `${escHtml(slide.sceneId)}: ${escHtml(firstLine)}`\n : escHtml(slide.sceneId);\n}\n\nfunction isSlideshowMediaElement(el: Element): el is SlideshowMediaElement {\n const win = el.ownerDocument.defaultView;\n if (!win || typeof win.HTMLMediaElement !== \"function\") return false;\n return el instanceof win.HTMLMediaElement;\n}\n\nfunction finiteMediaNumber(value: number, fallback: number): number {\n return Number.isFinite(value) ? value : fallback;\n}\n\nfunction readScenes(player: HTMLElement): { id: string; start: number; duration: number }[] {\n if (\"scenes\" in player && Array.isArray((player as { scenes: unknown }).scenes)) {\n return (player as { scenes: { id: string; start: number; duration: number }[] }).scenes;\n }\n return [];\n}\n\nconst WAIT_FOR_READY_TIMEOUT_MS = 5000;\n\nfunction waitForReady(player: HTMLElement & { ready?: boolean }): Promise<void> {\n if (player.ready === true) return Promise.resolve();\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const handler = (): void => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n resolve();\n };\n player.addEventListener(\"ready\", handler, { once: true });\n timer = setTimeout(() => {\n player.removeEventListener(\"ready\", handler);\n resolve();\n }, WAIT_FOR_READY_TIMEOUT_MS);\n });\n}\n\n/**\n * Polls `player.scenes` until at least one scene is present, then resolves\n * with the scenes array. Resolves with `[]` if no scenes appear within\n * `timeoutMs` (graceful: explicit startTime/endTime slides still work).\n *\n * Avoids Date.now(): counts poll iterations instead (100ms per iteration).\n *\n * `isCancelled` is checked before each poll iteration; if it returns true\n * the promise resolves with `[]` immediately so the caller can bail out.\n */\nfunction waitForScenes(\n player: HTMLElement,\n timeoutMs: number,\n isCancelled: () => boolean = () => false,\n): Promise<{ id: string; start: number; duration: number }[]> {\n const initial = readScenes(player);\n if (initial.length > 0) return Promise.resolve(initial);\n\n const maxIterations = Math.ceil(timeoutMs / 100);\n\n return new Promise((resolve) => {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let iterations = 0;\n\n const finish = (val: { id: string; start: number; duration: number }[]): void => {\n if (done) return;\n done = true;\n if (timer !== null) clearTimeout(timer);\n player.removeEventListener(\"scenes\", onScenes);\n resolve(val);\n };\n const onScenes = (): void => {\n if (isCancelled()) return finish([]);\n const s = readScenes(player);\n if (s.length > 0) finish(s);\n };\n const poll = (): void => {\n if (done) return;\n if (isCancelled()) return finish([]);\n const cur = readScenes(player);\n if (cur.length > 0) return finish(cur);\n iterations += 1;\n if (iterations >= maxIterations) return finish([]);\n timer = setTimeout(poll, 100);\n };\n\n player.addEventListener(\"scenes\", onScenes);\n timer = setTimeout(poll, 100);\n });\n}\n\n/**\n * Returns a new ResolvedSlideshow with zero-duration (end <= start) slides\n * removed from the main slide list and every sequence's slide list.\n *\n * Valid manifests never produce zero-duration slides — this only drops\n * phantom slides created from partially-specified refs whose scene is absent.\n *\n * Exported as a seam for unit testing.\n */\nexport function dropInvalidSlides(show: ResolvedSlideshow): ResolvedSlideshow {\n const validSlide = (s: { start: number; end: number }): boolean => s.end > s.start;\n\n const slides = show.slides.filter(validSlide);\n\n const sequences: ResolvedSlideshow[\"sequences\"] = {};\n for (const [id, seq] of Object.entries(show.sequences)) {\n sequences[id] = { ...seq, slides: seq.slides.filter(validSlide) };\n }\n\n return { slides, sequences };\n}\n\nfunction escHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nif (!customElements.get(\"hyperframes-slideshow\")) {\n customElements.define(\"hyperframes-slideshow\", HyperframesSlideshow);\n}\n"],"mappings":"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","names":["SLIDESHOW_ISLAND_TYPE","slideshowIslandRegex","flags","escaped","parseSlideshowManifest","html","match","raw","parsed","isManifest","isOptionalNumberArray","v","n","isOptionalBoolean","isSlideRef","r","isSlideSequence","s","o","missingBoundError","sceneId","missing","resolveTimeRange","ref","scene","errors","startTime","endTime","bound","validateFragments","fragments","start","end","f","resolveSlide","sceneById","a","b","resolveSlideshow","manifest","scenes","sequences","seq","slides","allSlides","slide","h","ordered","i","prev","curr","MAIN","SlideshowController","player","show","sequenceId","slides","f","cb","index","slide","fragmentIndex","seekTime","t","target","seq","slideIndex","targetSlides","base","COUNTER_FONT_FAMILY","MEDIA_ACTIONS","MEDIA_NUMBER_FIELDS","MEDIA_BOOLEAN_FIELDS","isRecord","data","isGotoMessage","d","isMediaAction","value","isMediaSender","hasMediaNumberFields","field","hasMediaBooleanFields","isMediaMessage","slideshowChannelName","SlideshowChannel","mode","onGoto","onMedia","e","pos","msg","buildPresenterLayout","opts","esc","escAttr","notes","branches","h","formatElapsed","seconds","m","s","AUTOPLAY_STEP_MS","AUTOPLAY_MAX_MS","isPlayerElement","el","PRESENTER_NOTES_STORAGE_PREFIX","_keyframesInjected","injectKeyframesOnce","style","ENTER_FS_SVG","EXIT_FS_SVG","PRESENT_SVG","COUNTER_FONT_FAMILY","HyperframesSlideshow","attr","c","sep","formatElapsed","SlideshowChannel","msg","gen","playerEl","html","manifest","parseSlideshowManifest","waitForReady","scenes","waitForScenes","resolved","errors","resolveSlideshow","cleaned","dropInvalidSlides","port","t","sceneId","cb","handler","e","detail","SlideshowController","counter","delay","timer","item","player","frame","playerIndex","media","mediaIndex","playerKey","mediaKey","entries","doc","isSlideshowMediaElement","actions","key","action","now","finiteMediaNumber","entry","candidate","allowTimeSync","button","target","active","focused","multiInstance","ambient","data","touch","deltaX","deltaY","currentSlide","hotspotsHtml","h","posStyle","escHtml","bottomCss","variant","options","showPrev","showNext","showLoading","showSound","btnStyle","muteBtnHtml","prevBtnHtml","loadingHtml","nextBtnHtml","presentBtnHtml","isFs","fsLabel","fsBtnHtml","counterPadLeft","counterPadRight","chrome","selector","btn","notesInput","label","muted","safeId","token","state","tick","done","video","advancing","err","name","explicit","playerSrc","resolvedPlayerSrc","baseHref","locationKey","title","slide","pos","notes","nextSlide","elapsedSec","notesStorageKey","buildPresenterLayout","nextPanelText","firstLine","win","value","fallback","readScenes","WAIT_FOR_READY_TIMEOUT_MS","resolve","timeoutMs","isCancelled","initial","maxIterations","iterations","finish","val","onScenes","s","poll","cur","show","validSlide","slides","sequences","id","seq"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/player",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Embeddable web component for HyperFrames compositions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@hyperframes/core": "0.7.
|
|
31
|
+
"@hyperframes/core": "0.7.8"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/bun": "^1.1.0",
|