import { fetchConfig, FrameStream } from "../api.js"; import "./portal-controls.js"; import "./portal-viewport.js"; import "./portal-schematic.js"; const APP_STYLES = ` :host { display: grid; grid-template-columns: 280px 1fr; height: 100vh; --border: #2a3044; --panel: #12151f; --text: #e8ecf7; --muted: #8b93a8; --accent: #5b8cff; } main { position: relative; min-width: 0; height: 100%; } portal-viewport { width: 100%; height: 100%; } portal-viewport[hidden] { display: none; } .status { position: absolute; left: 1rem; bottom: 1rem; padding: 0.35rem 0.6rem; background: rgba(0, 0, 0, 0.55); border: 1px solid var(--border); border-radius: 6px; font-size: 0.8rem; color: var(--muted); pointer-events: none; font-family: system-ui, sans-serif; } @media (max-width: 800px) { :host { grid-template-columns: 1fr; grid-template-rows: auto 1fr; } portal-controls { border-right: none; border-bottom: 1px solid var(--border); } } `; export class PortalApp extends HTMLElement { constructor() { super(); /** @type {import('../api.js').PortalConfig | null} */ this._config = null; /** @type {FrameStream | null} */ this._stream = null; /** @type {import('./portal-controls.js').PortalControls | null} */ this._controls = null; /** @type {import('./portal-viewport.js').PortalViewport | null} */ this._viewport = null; /** @type {import('./portal-schematic.js').PortalSchematic | null} */ this._schematic = null; this._settings = { animation: "rainbow", brightness: 0.35, fps: 25, paused: false, identify: false, view3d: true, }; } connectedCallback() { if (this.shadowRoot) return; const root = this.attachShadow({ mode: "open" }); root.innerHTML = `
Loading…
`; this._controls = root.querySelector("portal-controls"); this._viewport = root.querySelector("portal-viewport"); this._schematic = root.querySelector("portal-schematic"); this._status = root.querySelector(".status"); this.addEventListener("portal-settings", (e) => { this._settings = e.detail; this._viewport.hidden = !this._settings.view3d; this._schematic.visible = !this._settings.view3d; if (this._settings.paused && this._settings.identify && this._config) { for (const panel of this._config.panels) { this._viewport.updatePanel(panel.index, [], panel.width, panel.height, true); this._schematic.updatePanel(panel.index, [], panel.width, panel.height, true); } } this._syncStream(); }); this._init(); } disconnectedCallback() { this._stream?.stop(); } async _init() { try { this._config = await fetchConfig(); this._controls.setConfig(this._config); this._viewport.setPanels(this._config.panels); this._schematic.setPanels(this._config.panels); this._status.textContent = "Ready"; this._syncStream(); } catch (err) { this._status.textContent = `Error: ${err.message}`; } } _syncStream() { this._stream?.stop(); if (this._settings.paused) return; this._stream = new FrameStream((payload) => this._onFrame(payload)); this._stream.onDisconnect = () => { this._status.textContent = "Stream disconnected — retrying…"; }; this._stream.start({ animation: this._settings.animation, brightness: this._settings.brightness, fps: this._settings.fps, }); } /** @param {import('../api.js').FramePayload} payload */ _onFrame(payload) { const { identify } = this._settings; for (const panel of payload.panels) { this._viewport.updatePanel(panel.index, panel.rgb, panel.width, panel.height, identify); this._schematic.updatePanel(panel.index, panel.rgb, panel.width, panel.height, identify); } this._status.textContent = `${payload.animation} · frame ${payload.frame}`; } } customElements.define("portal-app", PortalApp);