From 188bdaa837829ea6a8ea5dd024095d3a50d9428d Mon Sep 17 00:00:00 2001 From: topkoa Date: Sun, 12 Jul 2026 18:12:46 -0400 Subject: [PATCH] feat(panes)!: move the real element, instead of rebuilding it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut of this got the model wrong. A pane was a SECOND implementation of the plugin's panel — its own sliders, its own styling, driven over a cross-realm bridge (ctx, a state store, capability RPC, mirrorGlobal, a stream sampler). Popping out gave you something that resembled the panel you popped, and every feature it did not reimplement (presets, tabs, EQ, language) was simply gone. What a user wants from "pop this out" is the thing they popped out. So: MOVE THE REAL ELEMENT. Same-origin windows can adopt each other's nodes, and an adopted node keeps its event listeners and its closures. The panel goes on running the plugin's own code, against the plugin's own state, in the plugin's own realm — it is merely being DISPLAYED in another window. Copy the app's stylesheets into that window and it looks identical too, because it is identical. The plugin's side collapses to two lines: feedBack.panes.register({ id, title, element: () => panelEl }); feedBack.panes.attachChip(panelEl, id); and everything comes along: the CSS, the listeners, the presets, the state. Nothing to keep in step, because there is no second copy. Deleted, all of it now pointless: pane-bridge (ctx + transports), pane-hub (the cross-realm server), pane-runtime (the pane realm's boot), pane-streams (the rAF sampler that existed because an AnalyserNode can't cross a window), pane-mirror (mirrorGlobal), pane-plugins + the manifest `panes[]` key and its server-side validation, panes.state(), and both built-in demo panes. ~1200 lines. None of it was wrong — it was all correct machinery for the wrong problem. Consequences worth knowing: - The window MUST be opened by the renderer with window.open(), not by the desktop's main process: a window we did not open gives this realm no handle to its document, and without the handle there is nothing to adopt into. Electron turns the same-origin window.open() into a real BrowserWindow anyway (setWindowOpenHandler → 'allow'), so we get the OS window AND the live DOM link. The desktop side finds it by frame name. - `.fb-paned` neutralises PLACEMENT only (position/inset/width/z-index/shadow). A plugin panel is nearly always a fixed overlay pinned to a corner of the app; alone in a 380px window that positioning is nonsense. Colours, borders, padding, fonts and the panel's own internal layout are untouched — the whole promise is that what you popped out is what you get. - The element is returned to its EXACT home on dock: same parent, same position among its siblings. - The plugin's code still runs in the main window. So a document.body .appendChild() inside a panel (a tooltip, a popover) lands in the main window, not the pane — anchor to the panel instead. And a continuously animating panel may run slowly while the main window is backgrounded, since its rAF lives there. Both documented. Signed-off-by: topkoa --- docs/plugin-panes.md | 310 ++++++++-------------- plugins/__init__.py | 78 ------ static/panes/builtin/mixer-pane.js | 162 ----------- static/panes/builtin/now-playing.js | 113 -------- static/panes/pane-bridge.js | 398 ---------------------------- static/panes/pane-desktop-host.js | 108 -------- static/panes/pane-desktop.js | 47 ++++ static/panes/pane-dock.js | 54 ++-- static/panes/pane-hub.js | 236 ----------------- static/panes/pane-manager.js | 384 ++++++++++----------------- static/panes/pane-mirror.js | 80 ------ static/panes/pane-plugins.js | 122 --------- static/panes/pane-runtime.js | 181 ------------- static/panes/pane-streams.js | 137 ---------- static/panes/pane-window-host.js | 182 +++++++++---- static/panes/pane.html | 30 +-- static/panes/panes.css | 169 ++++-------- static/v3/index.html | 22 +- 18 files changed, 518 insertions(+), 2295 deletions(-) delete mode 100644 static/panes/builtin/mixer-pane.js delete mode 100644 static/panes/builtin/now-playing.js delete mode 100644 static/panes/pane-bridge.js delete mode 100644 static/panes/pane-desktop-host.js create mode 100644 static/panes/pane-desktop.js delete mode 100644 static/panes/pane-hub.js delete mode 100644 static/panes/pane-mirror.js delete mode 100644 static/panes/pane-plugins.js delete mode 100644 static/panes/pane-runtime.js delete mode 100644 static/panes/pane-streams.js diff --git a/docs/plugin-panes.md b/docs/plugin-panes.md index fe28bda..4cfb6b4 100644 --- a/docs/plugin-panes.md +++ b/docs/plugin-panes.md @@ -1,249 +1,147 @@ # Detachable panes (`window.feedBack.panes`) -A **pane** is live UI that stays put: a mixer, a camera rig, a readout, a settings -board. You author it once, and the host decides where it lives — docked beside the -player, or popped out into its own OS window that remembers where you put it and -minimizes to the system tray. +Pop a panel out of the app into its own OS window, and leave it there: while you +play, across song switches, on a second monitor, minimized to the system tray. -Panes exist because the player's rail popovers are **exclusive**: opening one closes -the last. You cannot watch the mixer while riding the camera, and both vanish the -moment you want to look at the highway. Panes are non-exclusive, and they survive -song switches. +Panes exist because the player's rail popovers are **exclusive** — opening one +closes the last. You cannot watch the mixer while riding the camera, and both +vanish the moment you want to look at the highway. --- -## The two-line version +## The whole idea, in one sentence -If your plugin already has a dialog, give it a pop-out chip: +**We move the real element.** + +Not a copy of your panel. Not a re-implementation of it in the pop-out window. +The actual DOM node. Same-origin windows can adopt each other's nodes, and an +adopted node keeps its event listeners and its closures — so your panel goes on +running *your* code, against *your* state, in *your* realm. The app's stylesheets +are copied into the pane window, so it looks identical too. + +What you popped out is what you get. That is the promise, and it is the reason +there is no `ctx`, no state mirroring, no cross-window RPC and no second copy of +your UI to keep in step with the first. Those are all solutions to a problem we +simply do not have. + +--- + +## Adding a pane to your plugin + +Two lines. ```js feedBack.panes.register({ id: 'camera_director', title: 'Camera Director', icon: '🎥', - mount(root, ctx) { root.appendChild(buildCameraUI(ctx)); }, // your existing builder - unmount(root) { root.replaceChildren(); }, + element: () => panelEl, // your existing panel, as it is }); -feedBack.panes.attachChip(myDialogEl, 'camera_director'); +feedBack.panes.attachChip(panelEl, 'camera_director'); ``` -`attachChip()` injects **the** standard ⇱ button — same glyph, same place, same -behaviour everywhere. Clicking it opens the pane in its host and **hides your -dialog**, leaving a "⇲ … is popped out" stub in its place. Closing the pane -un-hides your dialog and restores the chip. +`attachChip()` injects **the** standard pop-out chip (`⇱`) — same glyph, same +place, same behaviour in every plugin. Clicking it moves your panel into a window +and leaves a "⇲ … is popped out" stub in its place; clicking the stub brings it +back, to exactly the spot it left. Core owns the chip, the hiding and the stub, so +you write no show/hide logic. -**You write no show/hide logic.** Core owns it, so that every plugin's pop-out -behaves identically — which is the entire point. +That's it. Your sliders, your presets, your tabs, your CSS, your event handlers, +your state — all of it comes along, because none of it moved anywhere except into +a different window's document. ---- +### `element` is a function for a reason -## The one rule +It is resolved at open time, not at registration. Plugins commonly build their +panel lazily on first use, or rebuild it wholesale when something changes (Camera +Director rebuilds its panel on every mode change). Asking for it when we need it +means we always move the live one. -> **`mount(root, ctx)` runs in a realm that may not have the app in it.** +### The one thing core changes about your element -Docked, your pane runs in the main window with everything present. Popped out, it -runs in a **separate JS realm** — a different window, with no `window.highway`, no -`window.feedBack.capabilities`, no `