/** Accessible, capture-free command panel for authored office screens. */ import type { MediaSurfaceDescriptor } from "./presentation.ts"; export interface OfficeScreenPanelOptions { container: HTMLElement; surfaces: readonly MediaSurfaceDescriptor[]; onSelect?: (surface: MediaSurfaceDescriptor) => void; /** Intent only. The caller may prompt for capture after receiving it. */ onRequestShare?: (surface: MediaSurfaceDescriptor) => void; /** Intent only. The caller owns presenter stop/revoke policy and media. */ onStopShare?: (surface: MediaSurfaceDescriptor) => void; onViewerOptIn?: (surface: MediaSurfaceDescriptor, optedIn: boolean) => void; } export interface OfficeScreenPanelState { open: boolean; selectedId: string | null; /** Explicit, local viewing choices. Empty initially and after disposal. */ optedInScreenIds: string[]; surfaces: MediaSurfaceDescriptor[]; } export interface OfficeScreenPanel { root: HTMLElement; open(): OfficeScreenPanelState; update(surfaces: readonly MediaSurfaceDescriptor[]): OfficeScreenPanelState; close(): OfficeScreenPanelState; state(): OfficeScreenPanelState; dispose(): void; } const STYLES = ` .tera-screen-panel { width:min(520px,calc(100vw - 32px)); max-height:min(720px,calc(100dvh - 32px)); overflow:auto; padding:20px; color:#e8edf2; background:rgba(9,13,18,.94); border:1px solid rgba(255,255,255,.14); border-radius:8px; font:12px/1.5 ui-monospace,monospace; } .tera-screen-panel__title { margin:0 0 4px; font-size:17px; } .tera-screen-panel__intro,.tera-screen-panel__empty { color:rgba(255,255,255,.65); } .tera-screen-panel__list { display:grid; gap:8px; margin:16px 0; } .tera-screen-panel__screen { width:100%; padding:10px; color:inherit; text-align:left; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.13); border-radius:5px; cursor:pointer; } .tera-screen-panel__screen[aria-pressed="true"] { border-color:#f2b134; background:rgba(242,177,52,.1); } .tera-screen-panel__meta { display:block; color:rgba(255,255,255,.58); } .tera-screen-panel__actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; } .tera-screen-panel__button { min-height:40px; padding:8px 12px; color:inherit; background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.15); border-radius:5px; cursor:pointer; } .tera-screen-panel__button:disabled { opacity:.4; cursor:not-allowed; } .tera-screen-panel :focus-visible { outline:2px solid #f2b134; outline-offset:2px; } `; let panelSequence = 0; export function createOfficeScreenPanel(options: OfficeScreenPanelOptions): OfficeScreenPanel { if (!options.container || typeof options.container.append !== "function") { throw new RangeError("office screen panel: container must be an HTMLElement"); } let surfaces = validateSurfaces(options.surfaces); let selectedId: string | null = surfaces[0]?.screenId ?? null; const optedIn = new Set(); let isOpen = false; let disposed = false; let invoker: HTMLElement | null = null; const doc = options.container.ownerDocument; const number = ++panelSequence; const titleId = `tera-screen-panel-title-${number}`; const introId = `tera-screen-panel-intro-${number}`; const root = doc.createElement("section"); root.className = "tera-screen-panel"; root.setAttribute("role", "dialog"); root.setAttribute("aria-modal", "true"); root.setAttribute("aria-labelledby", titleId); root.setAttribute("aria-describedby", introId); root.setAttribute("aria-hidden", "true"); root.hidden = true; const style = doc.createElement("style"); style.textContent = STYLES; const title = doc.createElement("h2"); title.id = titleId; title.className = "tera-screen-panel__title"; title.textContent = "Office screens"; const intro = doc.createElement("p"); intro.id = introId; intro.className = "tera-screen-panel__intro"; intro.textContent = "Media is off by default. Choose a screen, then explicitly opt in to view or request sharing."; const list = doc.createElement("div"); list.className = "tera-screen-panel__list"; list.setAttribute("role", "list"); const actions = doc.createElement("div"); actions.className = "tera-screen-panel__actions"; const optButton = button(doc, "Opt in to view", "opt-in"); const shareButton = button(doc, "Share screen", "share"); const stopButton = button(doc, "Stop / revoke", "stop"); const closeButton = button(doc, "Close", "close"); actions.append(optButton, shareButton, stopButton, closeButton); root.append(style, title, intro, list, actions); options.container.append(root); function selected(): MediaSurfaceDescriptor | null { return surfaces.find((surface) => surface.screenId === selectedId) ?? null; } function render(): void { for (const child of [...list.children]) child.remove(); if (surfaces.length === 0) { const empty = doc.createElement("p"); empty.className = "tera-screen-panel__empty"; empty.textContent = "No authored office screens are available."; list.append(empty); } for (const surface of surfaces) { const row = button(doc, "", "select"); row.className = "tera-screen-panel__screen"; row.setAttribute("role", "listitem"); row.setAttribute("data-screen-id", surface.screenId); row.setAttribute("aria-pressed", String(surface.screenId === selectedId)); const name = doc.createElement("span"); name.textContent = surface.screenId; const meta = doc.createElement("span"); meta.className = "tera-screen-panel__meta"; const room = surface.roomId ?? "Unassigned room"; const status = surface.bound ? "Media active" : "Media off"; meta.textContent = `${surface.levelId} · ${room} · ${status}`; row.append(name, meta); row.addEventListener("click", () => { selectedId = surface.screenId; render(); options.onSelect?.(copySurface(surface)); }); list.append(row); } const surface = selected(); const viewing = surface ? optedIn.has(surface.screenId) : false; optButton.disabled = surface === null; shareButton.disabled = surface === null || !viewing; stopButton.disabled = surface === null || !surface.bound; optButton.textContent = viewing ? "Stop viewing" : "Opt in to view"; optButton.setAttribute("aria-pressed", String(viewing)); } optButton.addEventListener("click", () => { const surface = selected(); if (!surface) return; const next = !optedIn.has(surface.screenId); if (next) optedIn.add(surface.screenId); else optedIn.delete(surface.screenId); render(); options.onViewerOptIn?.(copySurface(surface), next); }); shareButton.addEventListener("click", () => { const surface = selected(); if (surface) options.onRequestShare?.(copySurface(surface)); }); stopButton.addEventListener("click", () => { const surface = selected(); if (surface?.bound) options.onStopShare?.(copySurface(surface)); }); closeButton.addEventListener("click", () => close()); function snapshot(): OfficeScreenPanelState { return { open: isOpen, selectedId, optedInScreenIds: [...optedIn], surfaces: surfaces.map(copySurface), }; } function close(): OfficeScreenPanelState { if (disposed || !isOpen) return snapshot(); isOpen = false; root.hidden = true; root.setAttribute("aria-hidden", "true"); invoker?.focus(); invoker = null; return snapshot(); } root.addEventListener("keydown", (event) => { if (event.key === "Escape") { event.preventDefault(); close(); return; } if (event.key !== "Tab") return; const focusable = controls(); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable.at(-1); if (!first || !last) return; if (!event.shiftKey && doc.activeElement === last) { event.preventDefault(); first.focus(); } else if (event.shiftKey && doc.activeElement === first) { event.preventDefault(); last.focus(); } }); function controls(): HTMLElement[] { const rows = [...list.children].filter((child): child is HTMLElement => (child as HTMLElement).getAttribute("data-screen-id") !== null, ); return [...rows, optButton, shareButton, stopButton, closeButton].filter( (control) => !(control as HTMLButtonElement).disabled, ); } render(); return { root, open() { if (disposed) return snapshot(); const active = doc.activeElement as HTMLElement | null; invoker = active && typeof active.focus === "function" ? active : null; isOpen = true; root.hidden = false; root.setAttribute("aria-hidden", "false"); controls()[0]?.focus(); return snapshot(); }, update(next) { if (disposed) return snapshot(); surfaces = validateSurfaces(next); const ids = new Set(surfaces.map((surface) => surface.screenId)); for (const id of optedIn) if (!ids.has(id)) optedIn.delete(id); if (selectedId === null || !ids.has(selectedId)) selectedId = surfaces[0]?.screenId ?? null; render(); return snapshot(); }, close, state: snapshot, dispose() { if (disposed) return; close(); disposed = true; optedIn.clear(); surfaces = []; selectedId = null; root.remove(); }, }; } function validateSurfaces(input: readonly MediaSurfaceDescriptor[]): MediaSurfaceDescriptor[] { if (!Array.isArray(input)) throw new TypeError("office screen panel: surfaces must be an array"); const seen = new Set(); return input.map((surface) => { if (!surface || typeof surface !== "object") throw new TypeError("office screen panel: invalid surface"); for (const field of ["screenId", "officeId", "levelId", "kind"] as const) { if (typeof surface[field] !== "string" || surface[field].length === 0) { throw new TypeError(`office screen panel: invalid ${field}`); } } if (surface.roomId !== null && typeof surface.roomId !== "string") { throw new TypeError("office screen panel: invalid roomId"); } if (typeof surface.bound !== "boolean") throw new TypeError("office screen panel: invalid bound state"); if (seen.has(surface.screenId)) throw new TypeError("office screen panel: duplicate screenId"); seen.add(surface.screenId); return copySurface(surface); }); } function copySurface(surface: MediaSurfaceDescriptor): MediaSurfaceDescriptor { return { ...surface }; } function button(doc: Document, label: string, action: string): HTMLButtonElement { const value = doc.createElement("button"); value.type = "button"; value.className = "tera-screen-panel__button"; value.textContent = label; value.setAttribute("data-action", action); return value; }