fix(panes): get the element out before the pane window's document dies

Docking a popped-out panel brought it home DEAD. It rendered perfectly —
right markup, right size, right place — and every control in it was inert:
the close button, the sliders, the presets, even the pop-out chip. A
photograph of a panel.

Closing a pane window tears down its document, and the panel was still
inside it. The node itself survives (the manager holds a reference), but
every event listener in its subtree goes with the document that hosted
them. Two paths did this:

  1. closePane() called the host's unplace() — which closes the window —
     BEFORE adopting the element back. Order is now reversed, and the
     comment says why so nobody helpfully "tidies" it back.

  2. The user closing the pane window themselves was only noticed by the
     `closed` poll, which by definition runs AFTER the document is gone.
     The window now gets a `beforeunload` listener that brings the element
     home while its document is still alive.

That listener has to be attached AFTER /pane loads: window.open() hands
back a throwaway about:blank document, and anything registered on it is
discarded when the real page replaces it. This is the same trap that made
the pane window blank in the first place — adopt into about:blank and the
panel is destroyed a moment later — and it is now handled in both places.

The `closed` poll stays, but only as a last-resort net for a CRASHED pane
window, where nothing can be saved.

Also fixed while chasing this:

  - The chip stamped `.fb-pane-detached` (display:none !important) onto the
    element to hide it in the main window — and that element is the one we
    move, so the class travelled with it and blanked the pane window. The
    chip now only hides an element the pane did NOT take, and marks the hole
    with its stub otherwise. "Did not take" is an ownerDocument test, not
    isConnected: a panel sitting in a pane window IS connected, just not
    here, and a plugin that rebuilds its panel (Camera Director does, on
    every mode change) re-runs attachChip while popped out.

  - The stub was inserted "before the element", which is nowhere — the
    element has left the document. The manager now hands over the element's
    recorded home, and the stub goes there.

  - GET /pane sent no cache headers. A stale copy is especially nasty here:
    the opener waits for an element inside that page before adopting, so an
    old cached version means the pane window just sits there blank.

Verified in the desktop app: pop out, use the controls in the pane window,
dock back, use them again. Panel comes home alive.

Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-12 18:47:38 -04:00
parent 9e9f0fdac6
commit 1e5282e27e
4 changed files with 771 additions and 605 deletions
+11 -6
View File
@@ -1719,9 +1719,14 @@ def index_v3():
@app.get("/pane")
def pane_host():
# The document a popped-out pane runs in. Deliberately NOT the app shell with
# a query flag (the splitscreen follower's approach): that loads the library,
# the highway and the whole v3 shell only to hide them again, and pays for it
# with anti-flash hacks in three files. This page loads the pane runtime and
# nothing else. See docs/plugin-panes.md.
return FileResponse(str(STATIC_DIR / "panes" / "pane.html"))
# The document a popped-out pane is displayed in. It builds nothing: the opener
# MOVES the real panel element into it (document.adoptNode) and copies the app's
# stylesheets across. See docs/plugin-panes.md.
#
# no-cache, matching the /static mount's contract (_RevalidatedStaticFiles). A
# stale copy of this page is especially nasty: the opener waits for an element
# inside it before adopting, so an old cached version means the pane window
# simply sits there blank.
resp = FileResponse(str(STATIC_DIR / "panes" / "pane.html"))
resp.headers["Cache-Control"] = "no-cache"
return resp