// src/player/ControlsOverlay.ts
import type { ControlsMode } from './types';
export interface ControlsHandlers {
onPlayPause: () => void;
onPrev: () => void;
onNext: () => void;
onFullscreen: () => void;
}
/** Plain-DOM control bar for the player. Lives inside the element's shadow
* root; styling is provided by the host's shadow stylesheet. */
export class ControlsOverlay {
readonly root: HTMLDivElement;
private readonly playBtn: HTMLButtonElement;
private readonly prevBtn: HTMLButtonElement;
private readonly nextBtn: HTMLButtonElement;
constructor(doc: Document, mode: ControlsMode, hasPlaylist: boolean, h: ControlsHandlers) {
const root = doc.createElement('div');
root.className = `phys-controls phys-controls--${mode}`;
this.prevBtn = this.button(doc, '‹', 'Previous preset', h.onPrev);
this.playBtn = this.button(doc, '⏸', 'Play / pause', h.onPlayPause);
this.nextBtn = this.button(doc, '›', 'Next preset', h.onNext);
const fsBtn = this.button(doc, '⛶', 'Fullscreen', h.onFullscreen);
// Only offer fullscreen where it's actually available. Inside an <iframe> embed this is false
// unless the host page granted `allow="fullscreen"`, so the button self-hides instead of being a
// dead control whose requestFullscreen() is silently rejected.
fsBtn.hidden = !doc.fullscreenEnabled;
this.prevBtn.hidden = !hasPlaylist;
this.nextBtn.hidden = !hasPlaylist;
root.append(this.prevBtn, this.playBtn, this.nextBtn, fsBtn);
this.root = root;
}
private button(
doc: Document,
label: string,
aria: string,
onClick: () => void,
): HTMLButtonElement {
const b = doc.createElement('button');
b.type = 'button';
b.textContent = label;
b.setAttribute('aria-label', aria);
b.addEventListener('click', (e) => {
e.stopPropagation();
onClick();
});
return b;
}
setPaused(paused: boolean): void {
this.playBtn.textContent = paused ? '▶' : '⏸';
}
}