const CONTROL_STYLES = ` :host { display: block; border-right: 1px solid var(--border, #2a3044); background: var(--panel, #12151f); padding: 1rem; overflow-y: auto; color: var(--text, #e8ecf7); font-family: system-ui, sans-serif; } h1 { margin: 0 0 0.25rem; font-size: 1.15rem; } p { margin: 0 0 1rem; color: var(--muted, #8b93a8); font-size: 0.85rem; line-height: 1.4; } a { color: var(--accent, #5b8cff); } label { display: block; margin: 0.75rem 0 0.35rem; font-size: 0.8rem; color: var(--muted, #8b93a8); } select, input[type="range"], button { width: 100%; } select, button { background: #1a1f2e; color: inherit; border: 1px solid var(--border, #2a3044); border-radius: 6px; padding: 0.45rem 0.6rem; font-size: 0.9rem; } button { margin-top: 0.75rem; cursor: pointer; background: #243049; } button:hover { border-color: var(--accent, #5b8cff); } button.active { background: #2a3f6e; border-color: var(--accent, #5b8cff); } .view-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } `; export class PortalControls extends HTMLElement { constructor() { super(); /** @type {import('../api.js').PortalConfig | null} */ this._config = null; this._animation = "rainbow"; this._brightness = 0.35; this._fps = 25; this._paused = false; this._identify = false; this._view3d = true; } connectedCallback() { if (this.shadowRoot) return; const root = this.attachShadow({ mode: "open" }); root.innerHTML = `

Portal Simulator

Hexagonal LED arch (see photo). Panel 2 top (Pi); 0, 1, 3, 4 walls; floor bare.

`; this._animationEl = root.querySelector("#animation"); this._brightnessEl = root.querySelector("#brightness"); this._fpsEl = root.querySelector("#fps"); this._animationEl.addEventListener("change", () => { this._animation = this._animationEl.value; if (this._config?.defaultFps[this._animation]) { this._fps = Math.min(40, Math.round(this._config.defaultFps[this._animation])); this._fpsEl.value = String(this._fps); } this._emitChange(); }); this._brightnessEl.addEventListener("input", () => { this._brightness = Number(this._brightnessEl.value); this._emitChange(); }); this._fpsEl.addEventListener("input", () => { this._fps = Number(this._fpsEl.value); this._emitChange(); }); root.querySelector('[data-view="3d"]').addEventListener("click", () => { this._view3d = true; this._syncViewButtons(); this._emitChange(); }); root.querySelector('[data-view="flat"]').addEventListener("click", () => { this._view3d = false; this._syncViewButtons(); this._emitChange(); }); root.querySelector('[data-action="identify"]').addEventListener("click", (e) => { this._identify = !this._identify; e.currentTarget.classList.toggle("active", this._identify); this._emitChange(); }); root.querySelector('[data-action="pause"]').addEventListener("click", (e) => { this._paused = !this._paused; e.currentTarget.textContent = this._paused ? "Resume" : "Pause"; this._emitChange(); }); } /** @param {import('../api.js').PortalConfig} config */ setConfig(config) { this._config = config; if (!this.shadowRoot) this.connectedCallback(); this._animationEl.innerHTML = ""; for (const name of config.animations) { if (name === "solid") continue; const opt = document.createElement("option"); opt.value = name; opt.textContent = name; this._animationEl.appendChild(opt); } this._animationEl.value = "rainbow"; this._animation = "rainbow"; this._brightness = config.defaultBrightness ?? 0.35; this._brightnessEl.value = String(this._brightness); this._fps = Math.min(40, Math.round(config.defaultFps.rainbow ?? 25)); this._fpsEl.value = String(this._fps); this._emitChange(); } _syncViewButtons() { const root = this.shadowRoot; root.querySelector('[data-view="3d"]').classList.toggle("active", this._view3d); root.querySelector('[data-view="flat"]').classList.toggle("active", !this._view3d); } _emitChange() { this.dispatchEvent( new CustomEvent("portal-settings", { bubbles: true, composed: true, detail: { animation: this._animation, brightness: this._brightness, fps: this._fps, paused: this._paused, identify: this._identify, view3d: this._view3d, }, }), ); } /** @returns {import('../api.js').PortalConfig | null} */ get config() { return this._config; } } customElements.define("portal-controls", PortalControls);