103 lines
4.4 KiB
TypeScript
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);
|
|
}
|