velxio/frontend/src/lib/openDeviceGateway.ts

103 lines
4.4 KiB
TypeScript

/**
* openDeviceGateway
*
* Opens an emulated board's IoT-gateway page in a small, FLOATING, draggable
* panel docked to the top-right — NOT a modal overlay and NOT a new tab.
*
* Why not a new tab: the Raspberry Pi Pico W emulation runs in THIS browser
* tab, driven by requestAnimationFrame. A new tab backgrounds the emulation
* tab, the browser pauses its rAF, the simulated chip freezes, and the gateway
* can no longer reach the server on it (request times out / 502). An in-tab
* iframe keeps the emulation in the foreground so the chip keeps answering.
*
* Why not a modal: the panel must NOT block the canvas or the editor — the
* user needs to watch the board react (LED/relay) and keep pressing buttons,
* wiring components, editing code while the device page is open. So: no
* backdrop, draggable by its title bar, resizable, parked out of the way.
*
* (The ESP32 doesn't need any of this — its server runs in QEMU on the
* backend, immune to tab visibility — so it still opens in a new tab.)
*/
const PANEL_ID = 'velxio-device-gateway-panel';
export function openDeviceGateway(url: string): void {
if (typeof document === 'undefined') return;
document.getElementById(PANEL_ID)?.remove();
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.style.cssText =
'position:fixed;top:64px;right:20px;z-index:100000;' +
'width:340px;height:500px;display:flex;flex-direction:column;' +
'background:#1e1e1e;border:1px solid #444;border-radius:10px;overflow:hidden;' +
'box-shadow:0 12px 48px rgba(0,0,0,0.55);resize:both;min-width:240px;min-height:220px;';
const bar = document.createElement('div');
bar.style.cssText =
'flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;' +
'padding:7px 10px;background:#2d2d2e;color:#ddd;cursor:move;user-select:none;' +
'font:12px -apple-system,BlinkMacSystemFont,sans-serif;border-bottom:1px solid #444;';
const title = document.createElement('span');
title.textContent = 'Device web page';
title.style.cssText = 'font-weight:600;white-space:nowrap;';
const right = document.createElement('div');
right.style.cssText = 'display:flex;align-items:center;gap:12px;';
const reload = document.createElement('button');
reload.type = 'button';
reload.textContent = 'Reload';
reload.style.cssText = 'background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:12px;padding:0;';
// No "open in a new tab" affordance: a background tab pauses the chip's
// rAF loop, freezing the emulated server (502). The in-tab iframe is the
// only way the Pico W's device page stays reachable.
const close = document.createElement('button');
close.type = 'button';
close.textContent = 'Close';
close.style.cssText = 'background:#444;border:none;color:#fff;cursor:pointer;font-size:12px;border-radius:4px;padding:3px 9px;';
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.cssText = 'flex:1 1 auto;border:none;width:100%;background:#fff;';
reload.onclick = () => { iframe.src = url; };
close.onclick = () => panel.remove();
// Drag by the title bar. Switch from right-anchored to left/top on grab so
// the panel follows the cursor cleanly.
bar.addEventListener('mousedown', (e) => {
if (e.target !== bar && e.target !== title) return; // not on a button
e.preventDefault();
const rect = panel.getBoundingClientRect();
const dx = e.clientX - rect.left;
const dy = e.clientY - rect.top;
panel.style.right = 'auto';
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
// While dragging, don't let the iframe swallow mouse events.
iframe.style.pointerEvents = 'none';
const onMove = (m: MouseEvent) => {
const maxX = window.innerWidth - 60;
const maxY = window.innerHeight - 30;
panel.style.left = Math.max(0, Math.min(maxX, m.clientX - dx)) + 'px';
panel.style.top = Math.max(0, Math.min(maxY, m.clientY - dy)) + 'px';
};
const onUp = () => {
iframe.style.pointerEvents = '';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
right.append(reload, close);
bar.append(title, right);
panel.append(bar, iframe);
document.body.append(panel);
}