@hyperframes/player 0.6.113 → 0.6.114

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.
@@ -0,0 +1,83 @@
1
+ "use strict";var y=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var N=(n,e)=>{for(var t in e)y(n,t,{get:e[t],enumerable:!0})},B=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of H(e))!_.call(n,s)&&s!==t&&y(n,s,{get:()=>e[s],enumerable:!(r=F(e,s))||r.enumerable});return n};var D=n=>B(y({},"__esModule",{value:!0}),n);var ne={};N(ne,{HyperframesSlideshow:()=>v,dropInvalidSlides:()=>L});module.exports=D(ne);var G="application/hyperframes-slideshow+json";function O(n="i"){let e=G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`<script[^>]*type=["']${e}["'][^>]*>([\\s\\S]*?)<\\/script>`,n)}function I(n){let t=O("i").exec(n);if(!t||t[1]===void 0)return null;let r=t[1].trim();if(r.length===0)return null;let s=JSON.parse(r);if(!z(s))throw new Error("slideshow island is not a valid SlideshowManifest");return s}function E(n){if(typeof n!="object"||n===null)return!1;let e=n;return!(typeof e.sceneId!="string"||e.fragments!==void 0&&!(Array.isArray(e.fragments)&&e.fragments.every(t=>typeof t=="number"))||e.hotspots!==void 0&&!Array.isArray(e.hotspots))}function j(n){if(typeof n!="object"||n===null)return!1;let e=n;return typeof e.id=="string"&&typeof e.label=="string"&&Array.isArray(e.slides)&&e.slides.every(E)}function z(n){if(typeof n!="object"||n===null||Array.isArray(n))return!1;let e=n;return!(!Array.isArray(e.slides)||!e.slides.every(E)||e.slideSequences!==void 0&&(!Array.isArray(e.slideSequences)||!e.slideSequences.every(j)))}function U(n,e){return`slide "${n}" sets ${e==="startTime"?"endTime":"startTime"} but ${e} cannot be resolved (no scene "${n}")`}function K(n,e,t){let{startTime:r,endTime:s,sceneId:i}=n;if(r!==void 0&&s!==void 0)return{start:r,end:s};if(r===void 0&&s===void 0)return e?{start:e.start,end:e.start+e.duration}:(t.push(`slide references unresolved sceneId "${i}"`),{start:0,end:0});if(!e){let a=r===void 0?"startTime":"endTime";t.push(U(i,a));let l=r??s??0;return{start:l,end:l}}return{start:r??e.start,end:s??e.start+e.duration}}function Y(n,e,t,r,s){for(let i of e)(i<t||i>r)&&s.push(`slide "${n}" fragment ${i} is outside range [${t}, ${r}]`)}function T(n,e,t){let r=e.get(n.sceneId),{start:s,end:i}=K(n,r,t);n.startTime!==void 0&&n.endTime!==void 0&&i<=s&&t.push(`slide "${n.sceneId}" has endTime (${i}) <= startTime (${s})`);let a=[...new Set(n.fragments??[])].sort((l,o)=>l-o);return Y(n.sceneId,a,s,i,t),{...n,start:s,end:i,fragments:a,hotspots:n.hotspots??[]}}function M(n,e){let t=[],r=new Map(e.map(o=>[o.id,o])),s={};for(let o of n.slideSequences??[])Object.prototype.hasOwnProperty.call(s,o.id)&&t.push(`duplicate slideSequence id "${o.id}" \u2014 only the last definition is kept`),s[o.id]={id:o.id,label:o.label,slides:o.slides.map(d=>T(d,r,t))};let i=n.slides.map(o=>T(o,r,t)),a=[...i,...Object.values(s).flatMap(o=>o.slides)];for(let o of a)for(let d of o.hotspots){let u=s[d.target];u?u.slides.length===0&&t.push(`hotspot "${d.id}" targets empty sequence "${d.target}"`):t.push(`hotspot "${d.id}" targets unknown sequence "${d.target}"`)}let l=[...i].sort((o,d)=>o.start-d.start);for(let o=1;o<l.length;o++){let d=l[o-1],u=l[o];d!==void 0&&u!==void 0&&u.start<d.end&&t.push(`main-line slides "${d.sceneId}" and "${u.sceneId}" overlap`)}return{resolved:{slides:i,sequences:s},errors:t}}var f="main";var g=class{constructor(e,t){this.player=e;this.show=t;this.unsub=e.onTimeUpdate(r=>this.onTime(r)),this.enterSlide(0)}player;show;stack=[{sequenceId:f,slideIndex:0,fragmentIndex:-1}];holdAt=null;holdTarget=null;changeCbs=new Set;unsub;dispose(){this.unsub()}slidesOf(e){return e===f?this.show.slides:this.show.sequences[e]?.slides??[]}get frame(){return this.stack[this.stack.length-1]}get currentSlide(){return this.slidesOf(this.frame.sequenceId)[this.frame.slideIndex]}get nextSlide(){return this.slidesOf(this.frame.sequenceId)[this.frame.slideIndex+1]??null}get position(){return{...this.frame}}get counter(){return{index:this.frame.slideIndex+1,total:this.slidesOf(this.frame.sequenceId).length}}get canPrev(){return this.frame.slideIndex>0||this.stack.length>1}get canNext(){let e=this.slidesOf(this.frame.sequenceId);return this.frame.slideIndex+1<e.length||this.stack.length>1}get breadcrumb(){return this.stack.map(e=>e.sequenceId===f?{id:f,label:"Main deck"}:{id:e.sequenceId,label:this.show.sequences[e.sequenceId]?.label??e.sequenceId})}onChange(e){return this.changeCbs.add(e),()=>this.changeCbs.delete(e)}emitChange(){for(let e of this.changeCbs)e()}enterSlide(e){this.frame.slideIndex=e,this.frame.fragmentIndex=-1,this.holdAt=null;let t=this.currentSlide;if(!t)return;let r=t.fragments.length>0?t.fragments[0]??t.end:this.restFrame(t);this.playTo(r),this.emitChange()}restFrame(e){return e.start+(e.end-e.start)*.5}resumeSlide(e,t){this.frame.slideIndex=e,this.frame.fragmentIndex=t;let r=this.currentSlide;if(!r)return;let s=t>=0&&t<r.fragments.length?r.fragments[t]??r.start:r.fragments.length>0?r.start:this.restFrame(r);this.holdAt=null,this.playTo(s),this.emitChange()}nextStop(e,t){return e.fragments[t+1]??e.end}playTo(e){let t=this.currentSlide;this.holdTarget=e,this.holdAt=t?Math.min(e+.2,t.end):e+.2,this.player.seek(e),this.player.play()}onTime(e){if(this.holdAt!==null&&e>=this.holdAt-.001){let t=this.holdTarget;this.holdAt=null,this.holdTarget=null;let r=this.currentSlide;if(r&&t!==null){let s=r.fragments.indexOf(t);s!==-1&&(this.frame.fragmentIndex=s,this.emitChange())}this.player.pause()}}next(){let e=this.currentSlide;if(!e)return;if(this.frame.fragmentIndex+1<e.fragments.length){let s=this.nextStop(e,this.frame.fragmentIndex);this.playTo(s),this.emitChange();return}let r=this.slidesOf(this.frame.sequenceId);this.frame.slideIndex+1<r.length?this.enterSlide(this.frame.slideIndex+1):this.stack.length>1&&this.back()}prev(){if(this.frame.slideIndex>0){this.enterSlide(this.frame.slideIndex-1);return}this.stack.length>1&&this.back()}goToSlide(e){let t=this.slidesOf(this.frame.sequenceId);e>=0&&e<t.length&&this.enterSlide(e)}enterBranch(e){let t=this.show.sequences[e];!t||t.slides.length===0||(this.stack.push({sequenceId:e,slideIndex:0,fragmentIndex:-1}),this.enterSlide(0))}back(){this.stack.length<=1||(this.stack.pop(),this.resumeSlide(this.frame.slideIndex,this.frame.fragmentIndex))}backToMain(){this.stack.length<=1||(this.stack=[this.stack[0]],this.resumeSlide(this.frame.slideIndex,this.frame.fragmentIndex))}syncTo(e,t,r){let s=this.stack[0];if(!s)return;if(this.frame.sequenceId!==e&&(this.stack=[s],e!==f)){let a=this.show.sequences[e];if(!a||a.slides.length===0)return;this.stack.push({sequenceId:e,slideIndex:0,fragmentIndex:-1})}let i=this.slidesOf(this.frame.sequenceId);t<0||t>=i.length||this.resumeSlide(t,r)}};function X(n){if(typeof n!="object"||n===null)return!1;let e=n;return e.type==="goto"&&typeof e.sequenceId=="string"&&typeof e.slideIndex=="number"&&typeof e.fragmentIndex=="number"}function V(){return`hf-slideshow:${typeof location<"u"?location.pathname:""}`}var m=class{constructor(e,t){this.mode=e;this.onGoto=t;try{this.channel=new BroadcastChannel(V())}catch{return}e==="audience"&&(this.channel.onmessage=r=>{X(r.data)&&this.onGoto(r.data)})}mode;onGoto;channel=null;postPosition(e){if(this.mode!=="presenter"||!this.channel)return;let t={type:"goto",...e};this.channel.postMessage(t)}destroy(){this.channel&&(this.channel.onmessage=null,this.channel.close(),this.channel=null)}};function $(n){let e=i=>i.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"),t=i=>e(i).replace(/"/g,"&quot;"),r=n.notes?e(n.notes):'<span style="opacity:.4">No notes for this slide</span>',s=n.hotspots.length?`<div style="display:flex;flex-direction:column;gap:6px;">
2
+ <div style="font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;">Branches</div>
3
+ ${n.hotspots.map(i=>`<button data-hotspot-id="${t(i.id)}" data-hotspot-target="${t(i.target)}" type="button" 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;">&#8627; ${e(i.label)}</button>`).join("")}
4
+ </div>`:"";return`
5
+ <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;">
6
+ <div data-hf-presenter-notes style="flex:1;min-width:0;padding:24px 36px;overflow-y:auto;font-size:21px;line-height:1.55;">${r}</div>
7
+ <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;">
8
+ <div style="font-size:12px;text-transform:uppercase;letter-spacing:.12em;opacity:.55;">Up next</div>
9
+ <div data-hf-presenter-next style="font-size:17px;opacity:.9;line-height:1.4;">${e(n.nextText)}</div>
10
+ ${s}
11
+ <div style="display:flex;gap:34px;margin-top:auto;">
12
+ <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-size:23px;font-variant-numeric:tabular-nums;">${e(n.counterText)}</div></div>
13
+ <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-size:23px;font-variant-numeric:tabular-nums;">${e(n.elapsedText)}</div></div>
14
+ </div>
15
+ </div>
16
+ </div>`.trim()}function S(n){let e=Math.floor(n/60),t=Math.floor(n%60);return`${String(e).padStart(2,"0")}:${String(t).padStart(2,"0")}`}function W(n){return typeof n.seek=="function"&&typeof n.play=="function"&&typeof n.pause=="function"}var R=!1;function J(){if(R)return;R=!0;let n=document.createElement("style");n.textContent=`
17
+ @keyframes hf-hotspot-pulse {
18
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(255,255,255,0.35), 0 4px 16px rgba(0,0,0,0.35); }
19
+ 50% { box-shadow: 0 0 0 8px rgba(255,255,255,0), 0 4px 20px rgba(0,0,0,0.45); }
20
+ }
21
+ @media (prefers-reduced-motion: reduce) {
22
+ .hf-hotspot-pill { animation: none !important; }
23
+ }
24
+ /* Nav-button hover (replaces inline onmouseover/onmouseout \u2014 CSP-safe).
25
+ !important beats the inline base color set on each button. */
26
+ [data-hf-nav-cluster] button:hover {
27
+ background: rgba(255,255,255,0.12) !important;
28
+ color: #fff !important;
29
+ }
30
+ /* When muted, the speaker button stays dimmed on hover so the mute-state
31
+ affordance isn't erased (higher specificity than the rule above). */
32
+ [data-hf-muted] [data-hf-mute]:hover {
33
+ color: rgba(255,255,255,0.6) !important;
34
+ }
35
+ `,document.head.appendChild(n)}var q='<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>',C='<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>',v=class extends HTMLElement{controller=null;offChange=null;chrome=null;touchStartX=0;touchStartY=0;channel=null;presenterStartMs=null;presenterInterval=null;disconnected=!1;initTimer=null;initInFlight=!1;initGeneration=0;_muted=!1;get muted(){return this._muted}resolveMode(){let e=this.getAttribute("mode");if(e)return e;try{return new URLSearchParams(location.search).get("mode")}catch{return null}}static get observedAttributes(){return["sound","mode"]}attributeChangedCallback(){this.controller&&this.render()}connectedCallback(){this.disconnected=!1,this.initInFlight=!1,this.initGeneration+=1,this.tabIndex=0,window.addEventListener("keydown",this.onKey),this.addEventListener("touchstart",this.onTouchStart,{passive:!0}),this.addEventListener("touchend",this.onTouchEnd),window.addEventListener("message",this.onMessage),document.addEventListener("fullscreenchange",this.onFsChange),this.initChannel(),this.initTimer=setTimeout(()=>{this.initTimer=null,this.isConnected&&!this.disconnected&&this.init()},0)}disconnectedCallback(){this.disconnected=!0,this.initGeneration+=1,this.initTimer!==null&&(clearTimeout(this.initTimer),this.initTimer=null),window.removeEventListener("keydown",this.onKey),this.removeEventListener("touchstart",this.onTouchStart),this.removeEventListener("touchend",this.onTouchEnd),window.removeEventListener("message",this.onMessage),document.removeEventListener("fullscreenchange",this.onFsChange),this.offChange?.(),this.offChange=null,this.controller?.dispose?.(),this.controller=null,this.chrome=null,this.channel?.destroy(),this.channel=null,this.presenterInterval!==null&&(clearInterval(this.presenterInterval),this.presenterInterval=null)}__setControllerForTest(e){this.bindController(e)}present(){let e=location.search?"&":"?";window.open(location.href+e+"mode=audience","_blank","noopener,noreferrer"),this.setAttribute("data-hf-presenting","true"),this.presenterStartMs=Date.now(),this.presenterInterval===null&&(this.presenterInterval=setInterval(()=>this.updateElapsed(),1e3)),this.render()}updateElapsed(){if(this.presenterStartMs===null)return;let e=this.chrome?.querySelector("[data-hf-presenter-elapsed]");e&&(e.textContent=S(Math.floor((Date.now()-this.presenterStartMs)/1e3)))}initChannel(){this.resolveMode()==="audience"?this.channel=new m("audience",t=>{this.controller&&this.controller.syncTo?.(t.sequenceId,t.slideIndex,t.fragmentIndex)}):this.channel=new m("presenter",()=>{})}async init(){if(this.initInFlight)return;this.initInFlight=!0;let e=this.initGeneration;try{let t=this.querySelector("hyperframes-player");if(!t||!(t instanceof HTMLElement)||!W(t)||(await ee(t),e!==this.initGeneration))return;let r=this.innerHTML,s;try{s=I(r)}catch{return}if(!s)return;let i=await te(t,2500,()=>e!==this.initGeneration);if(e!==this.initGeneration)return;let{resolved:a,errors:l}=M(s,i);l.length>0&&console.warn("[hyperframes-slideshow] manifest errors:",l);let o=L(a);o.slides.length===0&&s.slides.length>0&&console.error("[hyperframes-slideshow] no main-line slides resolved \u2014 the scene timeline may not have loaded in time, or sceneIds/timing are invalid:",l);let d={seek:u=>t.seek(u),play:()=>t.play(),pause:()=>t.pause(),get currentTime(){return t.currentTime},onTimeUpdate:u=>{let h=c=>{let b=c.detail;u(b.currentTime)};return t.addEventListener("timeupdate",h),()=>t.removeEventListener("timeupdate",h)}};this.bindController(new g(d,o)),i.length===0&&s.slides.length>0&&t.addEventListener("scenes",()=>{e===this.initGeneration&&this.init()},{once:!0})}finally{this.initInFlight=!1}}bindController(e){this.offChange?.(),this.controller?.dispose?.(),this.controller=e,this.offChange=e.onChange(()=>{this.resolveMode()!=="audience"&&this.channel&&this.channel.postPosition(e.position),this.render()}),this.resolveMode()!=="audience"&&this.channel&&this.channel.postPosition(e.position),this.render()}onKey=e=>{if(!this.controller)return;let t=e.target;if(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement||t instanceof HTMLElement&&t.isContentEditable)return;let r=document.activeElement,s=r===this||this.contains(r),i=document.querySelectorAll("hyperframes-slideshow").length>1,a=s||!i&&(r===document.body||r===null);if(e.key==="ArrowRight"){if(!a)return;this.controller.next(),e.preventDefault()}else if(e.key==="ArrowLeft"){if(!a)return;this.controller.prev(),e.preventDefault()}else if(e.key===" "){if(!s)return;this.controller.next(),e.preventDefault()}else if(e.key==="Backspace"){if(!s)return;this.controller.prev(),e.preventDefault()}else if((e.key==="f"||e.key==="F")&&!e.metaKey&&!e.ctrlKey&&!e.altKey){if(!s)return;this.toggleFullscreen(),e.preventDefault()}};onMessage=e=>{if(this.resolveMode()==="audience")return;let t=e.data;!t||!this.controller||(t.type==="next"?this.controller.next():t.type==="prev"?this.controller.prev():t.type==="goto"&&typeof t.slideIndex=="number"?this.controller.goToSlide?.(t.slideIndex):t.type==="back"&&this.controller.back?.())};onTouchStart=e=>{let t=e.touches[0];t&&(this.touchStartX=t.clientX,this.touchStartY=t.clientY)};onTouchEnd=e=>{if(!this.controller)return;let t=e.changedTouches[0];if(!t)return;let r=t.clientX-this.touchStartX,s=t.clientY-this.touchStartY;Math.abs(r)<=40||Math.abs(r)<=Math.abs(s)||(r<0?this.controller.next():this.controller.prev())};render(){if(!this.controller)return;if(this.resolveMode()==="audience"){let{counter:s}=this.controller;this.paintChrome(this.buildNavCluster(s,"28px","fs-only"));return}if(this.getAttribute("data-hf-presenting")==="true"){this.renderPresenter();return}let{counter:e,currentSlide:t}=this.controller;if(!t)return;let r=t.hotspots.map(s=>{let i=s.region?`left:${s.region.x}%;top:${s.region.y}%;`:"right:5%;bottom:18%;";return`<button
36
+ class="hf-hotspot-pill"
37
+ data-hotspot-id="${p(s.id)}"
38
+ data-hotspot-target="${p(s.target)}"
39
+ type="button"
40
+ style="position:absolute;${i}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;"
41
+ aria-label="${p(s.label)}"
42
+ ><span aria-hidden="true" style="font-size:14px;line-height:1;">\u2295</span>${p(s.label)}</button>`}).join("");this.paintChrome(r+this.buildNavCluster(e,"28px"))}paintChrome(e){J(),this.chrome||(this.chrome=document.createElement("div"),this.chrome.setAttribute("data-hf-chrome",""),this.appendChild(this.chrome)),this.chrome.style.cssText="position:absolute;inset:0;pointer-events:none;z-index:10;",this.chrome.innerHTML=e,this.wireChromeButtons()}buildNavCluster(e,t,r="full"){let s=this.controller;if(!s)return"";let i=s.canPrev!==!1,a=s.canNext!==!1,l=this.hasAttribute("sound"),o="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;",h=l?`<button
43
+ data-hf-mute
44
+ type="button"
45
+ aria-label="${this._muted?"Unmute":"Mute"}"
46
+ aria-pressed="${this._muted?"true":"false"}"
47
+ style="${o}${this._muted?"color:rgba(255,255,255,0.45);":""}"
48
+ >${this._muted?'<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>':'<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>'}</button>`:"",c=i?`<button
49
+ data-hf-prev
50
+ type="button"
51
+ aria-label="Previous slide"
52
+ style="${o}" >&#8249;</button>`:"",b=a?`<button
53
+ data-hf-next
54
+ type="button"
55
+ aria-label="Next slide"
56
+ style="${o}" >&#8250;</button>`:"",x=document.fullscreenElement===this,k=`<button
57
+ data-hf-fullscreen
58
+ type="button"
59
+ aria-label="${x?"Exit full screen":"Full screen"}"
60
+ aria-pressed="${x?"true":"false"}"
61
+ style="${o}" >${x?C:q}</button>`;if(r==="fs-only")return`
62
+ <div
63
+ data-hf-nav-cluster
64
+ style="position:absolute;bottom:${t};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;"
65
+ >${k}</div>`;let P=i?"4px":"10px",A=a?"4px":"10px";return`
66
+ <div
67
+ data-hf-nav-cluster
68
+ style="position:absolute;bottom:${t};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;"
69
+ >
70
+ ${h}
71
+ ${l?'<span aria-hidden="true" style="width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;"></span>':""}
72
+ ${c}
73
+ <span
74
+ data-hf-counter
75
+ aria-label="Slide ${e.index} of ${e.total}"
76
+ style="min-width:46px;text-align:center;color:rgba(255,255,255,0.9);font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;letter-spacing:0.02em;padding:0 ${A} 0 ${P};user-select:none;"
77
+ >${e.index}&thinsp;/&thinsp;${e.total}</span>
78
+ ${b}
79
+ <span aria-hidden="true" style="width:1px;height:20px;background:rgba(255,255,255,0.12);margin:0 2px;flex-shrink:0;"></span>
80
+ ${k}
81
+ </div>`}wireChromeButtons(){let e=this.chrome;if(!e)return;let t=e.querySelector("[data-hf-mute]"),r=e.querySelector("[data-hf-prev]"),s=e.querySelector("[data-hf-next]");t&&t.addEventListener("click",()=>this.toggleMute()),r&&r.addEventListener("click",()=>this.controller?.prev()),s&&s.addEventListener("click",()=>this.controller?.next());let i=e.querySelector("[data-hf-fullscreen]");i&&i.addEventListener("click",()=>this.toggleFullscreen());for(let a of e.querySelectorAll("[data-hotspot-id]")){let l=a.getAttribute("data-hotspot-target")??"";a.addEventListener("click",()=>this.controller?.enterBranch?.(l))}}onFsChange=()=>{let e=this.chrome?.querySelector("[data-hf-fullscreen]");if(!e)return;let t=document.fullscreenElement===this;e.innerHTML=t?C:q,e.setAttribute("aria-label",t?"Exit full screen":"Full screen"),e.setAttribute("aria-pressed",t?"true":"false")};toggleFullscreen(){document.fullscreenElement===this?document.exitFullscreen().catch(()=>{}):this.requestFullscreen().catch(()=>{})}toggleMute(){this._muted=!this._muted,this._muted?this.setAttribute("data-hf-muted",""):this.removeAttribute("data-hf-muted"),this.dispatchEvent(new CustomEvent("hf-sound",{detail:{muted:this._muted},bubbles:!0,composed:!0})),this.render()}renderPresenter(){if(!this.controller)return;let{counter:e,currentSlide:t,nextSlide:r}=this.controller;if(!t)return;let s=this.presenterStartMs!==null?Math.floor((Date.now()-this.presenterStartMs)/1e3):0,i=this.querySelector("hyperframes-player");i instanceof HTMLElement&&(i.style.top="0",i.style.bottom="32%",i.style.height="auto"),this.paintChrome($({notes:t.notes??"",nextText:Q(r),counterText:`${e.index} / ${e.total}`,elapsedText:S(s),hotspots:t.hotspots})+this.buildNavCluster(e,"calc(32% + 18px)"))}};function Q(n){if(n===null)return"End of sequence";let e=n.notes!=null?n.notes.split(`
82
+ `)[0]??"":"";return e.length>0?`${p(n.sceneId)}: ${p(e)}`:p(n.sceneId)}function w(n){return"scenes"in n&&Array.isArray(n.scenes)?n.scenes:[]}var Z=5e3;function ee(n){return n.ready===!0?Promise.resolve():new Promise(e=>{let t=null,r=()=>{t!==null&&(clearTimeout(t),t=null),e()};n.addEventListener("ready",r,{once:!0}),t=setTimeout(()=>{n.removeEventListener("ready",r),e()},Z)})}function te(n,e,t=()=>!1){let r=w(n);if(r.length>0)return Promise.resolve(r);let s=Math.ceil(e/100);return new Promise(i=>{let a=!1,l=null,o=0,d=c=>{a||(a=!0,l!==null&&clearTimeout(l),n.removeEventListener("scenes",u),i(c))},u=()=>{if(t())return d([]);let c=w(n);c.length>0&&d(c)},h=()=>{if(a)return;if(t())return d([]);let c=w(n);if(c.length>0)return d(c);if(o+=1,o>=s)return d([]);l=setTimeout(h,100)};n.addEventListener("scenes",u),l=setTimeout(h,100)})}function L(n){let e=s=>s.end>s.start,t=n.slides.filter(e),r={};for(let[s,i]of Object.entries(n.sequences))r[s]={...i,slides:i.slides.filter(e)};return{slides:t,sequences:r}}function p(n){return n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}customElements.get("hyperframes-slideshow")||customElements.define("hyperframes-slideshow",v);0&&(module.exports={HyperframesSlideshow,dropInvalidSlides});
83
+ //# sourceMappingURL=hyperframes-slideshow.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/slideshow/hyperframes-slideshow.ts","../../../core/src/slideshow/parseSlideshow.ts","../../src/slideshow/SlideshowController.ts","../../src/slideshow/slideshowPresenter.ts"],"sourcesContent":["import {\n parseSlideshowManifest,\n resolveSlideshow,\n type ResolvedSlideshow,\n} from \"@hyperframes/core/slideshow\";\nimport { SlideshowController, type PlayerPort } from \"./SlideshowController\";\nimport { SlideshowChannel, buildPresenterLayout, formatElapsed } 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\ntype PlayerElement = HTMLElement & {\n seek(t: number): void;\n play(): void;\n pause(): void;\n readonly currentTime: number;\n readonly ready: boolean;\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\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 @media (prefers-reduced-motion: reduce) {\n .hf-hotspot-pill { 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 /* 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>`;\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 disconnected = false;\n private initTimer: ReturnType<typeof setTimeout> | null = null;\n private initInFlight = false;\n private initGeneration = 0;\n private _muted = false;\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.presenterInterval !== null) {\n clearInterval(this.presenterInterval);\n this.presenterInterval = null;\n }\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 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.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(\"audience\", (msg) => {\n if (!this.controller) return;\n this.controller.syncTo?.(msg.sequenceId, msg.slideIndex, msg.fragmentIndex);\n });\n } else {\n this.channel = new SlideshowChannel(\"presenter\", () => {\n // presenter channel does not receive; posting happens in bindController\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 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 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 // 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 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 bindController(c: ControllerLike): void {\n this.offChange?.();\n this.controller?.dispose?.();\n this.controller = c;\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 // 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 }\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] | [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 ): string {\n const c = this.controller;\n if (!c) return \"\";\n const showPrev = c.canPrev !== false;\n const showNext = c.canNext !== false;\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 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 style=\"${btnStyle}\" >&#8249;</button>`\n : \"\";\n const nextBtnHtml = showNext\n ? `<button\n data-hf-next\n type=\"button\"\n aria-label=\"Next slide\"\n style=\"${btnStyle}\" >&#8250;</button>`\n : \"\";\n const isFs = document.fullscreenElement === this;\n const fsBtnHtml = `<button\n data-hf-fullscreen\n type=\"button\"\n aria-label=\"${isFs ? \"Exit full screen\" : \"Full screen\"}\"\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-size:13px;font-weight:500;font-variant-numeric:tabular-nums;letter-spacing:0.02em;padding:0 ${counterPadRight} 0 ${counterPadLeft};user-select:none;\"\n >${counter.index}&thinsp;/&thinsp;${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 ${fsBtnHtml}\n </div>`;\n }\n\n private wireChromeButtons(): void {\n const chrome = this.chrome;\n if (!chrome) return;\n const muteBtn = chrome.querySelector(\"[data-hf-mute]\");\n const prevBtn = chrome.querySelector(\"[data-hf-prev]\");\n const nextBtn = chrome.querySelector(\"[data-hf-next]\");\n if (muteBtn) muteBtn.addEventListener(\"click\", () => this.toggleMute());\n if (prevBtn) prevBtn.addEventListener(\"click\", () => this.controller?.prev());\n if (nextBtn) nextBtn.addEventListener(\"click\", () => this.controller?.next());\n const fsBtn = chrome.querySelector(\"[data-hf-fullscreen]\");\n if (fsBtn) fsBtn.addEventListener(\"click\", () => this.toggleFullscreen());\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 btn.setAttribute(\"aria-label\", isFs ? \"Exit full screen\" : \"Full screen\");\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 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.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 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 this.paintChrome(\n buildPresenterLayout({\n notes: currentSlide.notes ?? \"\",\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 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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nif (!customElements.get(\"hyperframes-slideshow\")) {\n customElements.define(\"hyperframes-slideshow\", HyperframesSlideshow);\n}\n","// 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 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\";\nconst EPS = 0.001;\n// Seconds to play past a restored/mirrored position so the composition repaints\n// (a bare paused seek doesn't re-render some compositions; pausing on the first\n// timeupdate fires before a paint).\nconst RENDER_NUDGE = 0.2;\n\nexport class SlideshowController {\n private stack: StackFrame[] = [{ sequenceId: MAIN, slideIndex: 0, fragmentIndex: -1 }];\n private holdAt: number | null = null;\n // The logical hold (a fragment time / slide point). playTo() plays a short way\n // PAST it (to holdAt) so the composition repaints; holdTarget is what onTime\n // matches against fragments to advance fragmentIndex.\n private holdTarget: number | null = null;\n private changeCbs = new Set<() => void>();\n private unsub: () => void;\n\n constructor(\n private player: PlayerPort,\n private show: ResolvedSlideshow,\n ) {\n this.unsub = player.onTimeUpdate((t) => this.onTime(t));\n this.enterSlide(0);\n }\n\n // fallow-ignore-next-line unused-class-member\n dispose(): void {\n this.unsub();\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 enterSlide(index: number): void {\n this.frame.slideIndex = index;\n this.frame.fragmentIndex = -1;\n this.holdAt = null;\n const slide = this.currentSlide;\n if (!slide) return;\n // Jump to the slide's first hold and stay there (no auto-progress). With\n // fragments that's the first fragment; without, a settled frame INSIDE the\n // slide (its midpoint) — NOT slide.end, which is the boundary where the next\n // scene begins (else slide 1 would render slide 2's content).\n const firstHold =\n slide.fragments.length > 0 ? (slide.fragments[0] ?? slide.end) : this.restFrame(slide);\n this.playTo(firstHold);\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.holdAt = null;\n this.playTo(seekTime);\n this.emitChange();\n }\n\n private nextStop(slide: ResolvedSlide, fragmentIndex: number): number {\n const next = slide.fragments[fragmentIndex + 1];\n return next ?? slide.end;\n }\n\n /**\n * Jump to hold time `t` and pause there — NO sustained playback, so slides\n * never auto-progress. Seeks just before `t` and plays a short render-nudge\n * ending at `t`: a bare paused seek doesn't repaint some compositions, and\n * pausing on the first timeupdate fires before a paint. onTime() pauses at `t`\n * and advances fragmentIndex when `t` is a fragment boundary.\n */\n private playTo(t: number): void {\n // Seek to the EXACT target so the first repainted frame is the correct one —\n // seeking BEFORE it (as a backward render-nudge) flashes a pre-target frame\n // / the previous scene. Then play a short way PAST it so the composition\n // actually repaints (a bare paused seek doesn't), and onTime() pauses there.\n const slide = this.currentSlide;\n this.holdTarget = t;\n this.holdAt = slide ? Math.min(t + RENDER_NUDGE, slide.end) : t + RENDER_NUDGE;\n this.player.seek(t);\n this.player.play();\n }\n\n private onTime(tt: number): void {\n if (this.holdAt !== null && tt >= this.holdAt - EPS) {\n const target = this.holdTarget;\n this.holdAt = null;\n this.holdTarget = null;\n // Advance fragmentIndex if the logical target is a fragment boundary.\n const slide = this.currentSlide;\n if (slide && target !== null) {\n const fragIdx = slide.fragments.indexOf(target);\n if (fragIdx !== -1) {\n this.frame.fragmentIndex = fragIdx;\n this.emitChange();\n }\n }\n this.player.pause();\n }\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. onTime() advances fragmentIndex at the hold.\n const nextTarget = this.nextStop(slide, this.frame.fragmentIndex);\n this.playTo(nextTarget);\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.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.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.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 const base = this.stack[0];\n if (!base) return;\n if (this.frame.sequenceId !== sequenceId) {\n this.stack = [base];\n if (sequenceId !== MAIN) {\n const seq = this.show.sequences[sequenceId];\n if (!seq || seq.slides.length === 0) return;\n this.stack.push({ sequenceId, slideIndex: 0, fragmentIndex: -1 });\n }\n }\n const slides = this.slidesOf(this.frame.sequenceId);\n if (slideIndex < 0 || slideIndex >= slides.length) return;\n this.resumeSlide(slideIndex, fragmentIndex);\n }\n}\n","export interface PresenterPosition {\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\ninterface GotoMessage {\n type: \"goto\";\n sequenceId: string;\n slideIndex: number;\n fragmentIndex: number;\n}\n\nfunction isGotoMessage(data: unknown): data is GotoMessage {\n if (typeof data !== \"object\" || data === null) 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\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 ) {\n try {\n this.channel = new BroadcastChannel(slideshowChannelName());\n } catch {\n // BroadcastChannel unavailable (e.g. unsupported env); degrade silently.\n return;\n }\n\n if (mode === \"audience\") {\n this.channel.onmessage = (e: MessageEvent) => {\n if (isGotoMessage(e.data)) {\n this.onGoto(e.data);\n }\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 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 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, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n const escAttr = (s: string) => esc(s).replace(/\"/g, \"&quot;\");\n const notes = opts.notes\n ? esc(opts.notes)\n : `<span style=\"opacity:.4\">No notes for this slide</span>`;\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\" 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;\">&#8627; ${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 <div data-hf-presenter-notes style=\"flex:1;min-width:0;padding:24px 36px;overflow-y:auto;font-size:21px;line-height:1.55;\">${notes}</div>\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-size:23px;font-variant-numeric:tabular-nums;\">${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-size:23px;font-variant-numeric:tabular-nums;\">${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"],"mappings":"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","names":["hyperframes_slideshow_exports","__export","HyperframesSlideshow","dropInvalidSlides","__toCommonJS","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","t","MAIN","sequenceId","slides","f","cb","index","slide","firstHold","fragmentIndex","seekTime","tt","target","fragIdx","nextTarget","seq","slideIndex","base","isGotoMessage","data","d","slideshowChannelName","SlideshowChannel","mode","onGoto","e","pos","msg","buildPresenterLayout","opts","esc","s","escAttr","notes","branches","h","formatElapsed","seconds","m","isPlayerElement","el","_keyframesInjected","injectKeyframesOnce","style","ENTER_FS_SVG","EXIT_FS_SVG","HyperframesSlideshow","attr","c","sep","formatElapsed","SlideshowChannel","msg","gen","playerEl","waitForReady","html","manifest","parseSlideshowManifest","scenes","waitForScenes","resolved","errors","resolveSlideshow","cleaned","dropInvalidSlides","port","t","cb","handler","e","detail","SlideshowController","target","active","focused","multiInstance","ambient","data","touch","deltaX","deltaY","counter","currentSlide","hotspotsHtml","h","posStyle","escHtml","bottomCss","variant","showPrev","showNext","showSound","btnStyle","muteBtnHtml","prevBtnHtml","nextBtnHtml","isFs","fsBtnHtml","counterPadLeft","counterPadRight","chrome","muteBtn","prevBtn","nextBtn","fsBtn","btn","nextSlide","elapsedSec","buildPresenterLayout","nextPanelText","slide","firstLine","readScenes","player","WAIT_FOR_READY_TIMEOUT_MS","resolve","timer","timeoutMs","isCancelled","initial","maxIterations","done","iterations","finish","val","onScenes","s","poll","cur","show","validSlide","slides","sequences","id","seq"]}
@@ -0,0 +1,113 @@
1
+ import { ResolvedSlideshow } from '@hyperframes/core/slideshow';
2
+
3
+ interface Hotspot {
4
+ id: string;
5
+ label: string;
6
+ target: string;
7
+ region?: {
8
+ x: number;
9
+ y: number;
10
+ w: number;
11
+ h: number;
12
+ };
13
+ }
14
+ interface ControllerLike {
15
+ next(): void;
16
+ prev(): void;
17
+ onChange(cb: () => void): () => void;
18
+ readonly counter: {
19
+ index: number;
20
+ total: number;
21
+ };
22
+ readonly breadcrumb: {
23
+ id: string;
24
+ label: string;
25
+ }[];
26
+ readonly currentSlide: {
27
+ hotspots: Hotspot[];
28
+ notes?: string;
29
+ sceneId?: string;
30
+ } | undefined;
31
+ readonly nextSlide: {
32
+ sceneId: string;
33
+ notes?: string;
34
+ } | null;
35
+ readonly position: {
36
+ sequenceId: string;
37
+ slideIndex: number;
38
+ fragmentIndex: number;
39
+ };
40
+ readonly canPrev?: boolean;
41
+ readonly canNext?: boolean;
42
+ goToSlide?(index: number): void;
43
+ syncTo?(sequenceId: string, slideIndex: number, fragmentIndex: number): void;
44
+ enterBranch?(id: string): void;
45
+ back?(): void;
46
+ backToMain?(): void;
47
+ dispose?(): void;
48
+ }
49
+ declare class HyperframesSlideshow extends HTMLElement {
50
+ private controller;
51
+ private offChange;
52
+ private chrome;
53
+ private touchStartX;
54
+ private touchStartY;
55
+ private channel;
56
+ private presenterStartMs;
57
+ private presenterInterval;
58
+ private disconnected;
59
+ private initTimer;
60
+ private initInFlight;
61
+ private initGeneration;
62
+ private _muted;
63
+ /** Whether audio is currently muted. Reflects `data-hf-muted` attribute. */
64
+ get muted(): boolean;
65
+ /** Mode resolves from the `mode` attribute, falling back to the URL query
66
+ * (?mode=audience) so the audience window opened by present() is detected. */
67
+ private resolveMode;
68
+ static get observedAttributes(): string[];
69
+ attributeChangedCallback(): void;
70
+ connectedCallback(): void;
71
+ disconnectedCallback(): void;
72
+ /** Test seam: inject a controller without a live player. */
73
+ __setControllerForTest(c: ControllerLike): void;
74
+ /**
75
+ * Opens an audience window and switches this element to presenter layout.
76
+ * Audience window URL: current page URL with `mode=audience` query param.
77
+ */
78
+ present(): void;
79
+ /**
80
+ * Update only the elapsed readout. Re-rendering the whole chrome every second
81
+ * (the old behavior) rebuilt the nav buttons' DOM on each tick — they
82
+ * flickered and clicks landing mid-rebuild were dropped.
83
+ */
84
+ private updateElapsed;
85
+ private initChannel;
86
+ private init;
87
+ private bindController;
88
+ private onKey;
89
+ private onMessage;
90
+ private onTouchStart;
91
+ private onTouchEnd;
92
+ private render;
93
+ /** Ensure the overlay chrome layer exists, set its content, and wire its buttons. */
94
+ private paintChrome;
95
+ private buildNavCluster;
96
+ private wireChromeButtons;
97
+ private onFsChange;
98
+ private toggleFullscreen;
99
+ private toggleMute;
100
+ private renderPresenter;
101
+ }
102
+ /**
103
+ * Returns a new ResolvedSlideshow with zero-duration (end <= start) slides
104
+ * removed from the main slide list and every sequence's slide list.
105
+ *
106
+ * Valid manifests never produce zero-duration slides — this only drops
107
+ * phantom slides created from partially-specified refs whose scene is absent.
108
+ *
109
+ * Exported as a seam for unit testing.
110
+ */
111
+ declare function dropInvalidSlides(show: ResolvedSlideshow): ResolvedSlideshow;
112
+
113
+ export { HyperframesSlideshow, dropInvalidSlides };
@@ -0,0 +1,113 @@
1
+ import { ResolvedSlideshow } from '@hyperframes/core/slideshow';
2
+
3
+ interface Hotspot {
4
+ id: string;
5
+ label: string;
6
+ target: string;
7
+ region?: {
8
+ x: number;
9
+ y: number;
10
+ w: number;
11
+ h: number;
12
+ };
13
+ }
14
+ interface ControllerLike {
15
+ next(): void;
16
+ prev(): void;
17
+ onChange(cb: () => void): () => void;
18
+ readonly counter: {
19
+ index: number;
20
+ total: number;
21
+ };
22
+ readonly breadcrumb: {
23
+ id: string;
24
+ label: string;
25
+ }[];
26
+ readonly currentSlide: {
27
+ hotspots: Hotspot[];
28
+ notes?: string;
29
+ sceneId?: string;
30
+ } | undefined;
31
+ readonly nextSlide: {
32
+ sceneId: string;
33
+ notes?: string;
34
+ } | null;
35
+ readonly position: {
36
+ sequenceId: string;
37
+ slideIndex: number;
38
+ fragmentIndex: number;
39
+ };
40
+ readonly canPrev?: boolean;
41
+ readonly canNext?: boolean;
42
+ goToSlide?(index: number): void;
43
+ syncTo?(sequenceId: string, slideIndex: number, fragmentIndex: number): void;
44
+ enterBranch?(id: string): void;
45
+ back?(): void;
46
+ backToMain?(): void;
47
+ dispose?(): void;
48
+ }
49
+ declare class HyperframesSlideshow extends HTMLElement {
50
+ private controller;
51
+ private offChange;
52
+ private chrome;
53
+ private touchStartX;
54
+ private touchStartY;
55
+ private channel;
56
+ private presenterStartMs;
57
+ private presenterInterval;
58
+ private disconnected;
59
+ private initTimer;
60
+ private initInFlight;
61
+ private initGeneration;
62
+ private _muted;
63
+ /** Whether audio is currently muted. Reflects `data-hf-muted` attribute. */
64
+ get muted(): boolean;
65
+ /** Mode resolves from the `mode` attribute, falling back to the URL query
66
+ * (?mode=audience) so the audience window opened by present() is detected. */
67
+ private resolveMode;
68
+ static get observedAttributes(): string[];
69
+ attributeChangedCallback(): void;
70
+ connectedCallback(): void;
71
+ disconnectedCallback(): void;
72
+ /** Test seam: inject a controller without a live player. */
73
+ __setControllerForTest(c: ControllerLike): void;
74
+ /**
75
+ * Opens an audience window and switches this element to presenter layout.
76
+ * Audience window URL: current page URL with `mode=audience` query param.
77
+ */
78
+ present(): void;
79
+ /**
80
+ * Update only the elapsed readout. Re-rendering the whole chrome every second
81
+ * (the old behavior) rebuilt the nav buttons' DOM on each tick — they
82
+ * flickered and clicks landing mid-rebuild were dropped.
83
+ */
84
+ private updateElapsed;
85
+ private initChannel;
86
+ private init;
87
+ private bindController;
88
+ private onKey;
89
+ private onMessage;
90
+ private onTouchStart;
91
+ private onTouchEnd;
92
+ private render;
93
+ /** Ensure the overlay chrome layer exists, set its content, and wire its buttons. */
94
+ private paintChrome;
95
+ private buildNavCluster;
96
+ private wireChromeButtons;
97
+ private onFsChange;
98
+ private toggleFullscreen;
99
+ private toggleMute;
100
+ private renderPresenter;
101
+ }
102
+ /**
103
+ * Returns a new ResolvedSlideshow with zero-duration (end <= start) slides
104
+ * removed from the main slide list and every sequence's slide list.
105
+ *
106
+ * Valid manifests never produce zero-duration slides — this only drops
107
+ * phantom slides created from partially-specified refs whose scene is absent.
108
+ *
109
+ * Exported as a seam for unit testing.
110
+ */
111
+ declare function dropInvalidSlides(show: ResolvedSlideshow): ResolvedSlideshow;
112
+
113
+ export { HyperframesSlideshow, dropInvalidSlides };