Files
feedBack/static/panes/pane-launcher.js
topkoa cb425ed48d fix(panes): don't hide a docked pane; don't force display; restore visibility
Six more findings from CodeRabbit on #928. Three are real bugs.

1. THE CHIP HID DOCKED PANES. `_onOpened` decided "did the pane take my
   element?" from `ownerDocument !== document`. That is true for a pane in a
   pop-out window — and false for a pane moved into the DOCK, which lives in
   this very document. So docking a pane stamped `.fb-pane-detached`
   (display:none !important) onto the panel the user was looking at, and put
   the stub next to it instead of at its home.

   The element cannot answer this question — `isConnected` is true in a pane
   window, `ownerDocument` is this one in the dock. Both were live bugs. Ask
   the manager, which knows exactly what it handed to the host:
   `panes.elementOf(id)`. That holds for every host, and for reconciling after
   the fact (detail == null), which is what a plugin rebuilding its panel
   mid-pop-out triggers.

2. `.fb-paned` FORCED `display: block !important`. A panel that is
   `display:flex` or `grid` would be silently re-laid-out while detached —
   the exact opposite of "placement only", and precisely the kind of surprise
   this feature exists to avoid. Removed.

   Making a hidden panel visible is a separate job, and it now belongs to the
   manager, which does it without touching the panel's display MODE: clear
   `hidden`, and clear an inline `display:none` if that is how the panel hides.

3. VISIBILITY IS NOW RESTORED. The hosts used to set `el.hidden = false` and
   never put it back, so the docs' "core only changes placement" was a lie and
   a panel's hidden state was quietly lost. The manager stashes both `hidden`
   and the inline `display` on open and restores them on dock: a panel that was
   closed when you opened its pane from the tray goes back to being closed; one
   that was open stays open.

Plus:

- The launcher rebuilt its whole list on every panes:opened/closed — including
  the one fired by clicking a button in that list — destroying the button under
  the user's finger and dropping focus to <body>. It now restores focus to the
  toggled pane's button.
- `_copyStyles` cloned every stylesheet link, including the panes.css that
  pane.html already loads. Skip sheets the pane document already has.
- Docs: the chip may route to the DOCK, not always a window (it goes through
  detach() → the host router). `header` precedence was documented backwards —
  an explicit `header` wins. And the visibility contract above is now written
  down rather than being a surprise.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 19:24:00 -04:00

72 lines
2.8 KiB
JavaScript

/*
* fee[dB]ack — pane launcher (the "Panes" rail popover).
*
* A chip only works for a pane that already has a dialog to hide. Panes with no
* dialog — a readout, a plugin's optional extra — need somewhere to be opened
* from, so every registered pane gets one: a checkbox list in the rail.
*
* The rail popover is the right home for this precisely because it IS exclusive
* and transient. It's a menu, not a workspace; the panes it opens are the
* workspace, and they persist.
*
* Populated from the registry, so a plugin that calls panes.register() appears
* here with no further work. (The system tray will mirror this list.)
*/
(function () {
'use strict';
const panes = window.feedBack && window.feedBack.panes;
const bus = window.feedBack;
if (!panes || !bus || typeof bus.on !== 'function') return;
let listEl = null;
function render() {
if (!listEl || !listEl.isConnected) listEl = document.getElementById('v3-rail-panes-list');
if (!listEl) return;
const all = panes.list();
// Toggling a pane from this list fires panes:opened/closed, which re-renders
// the list — destroying the very button the user just pressed and dropping
// focus to <body>. Remember which one had it and give it back, so keyboard
// and screen-reader users can toggle several panes without losing their place.
const focusedId = (listEl.contains(document.activeElement) && document.activeElement.dataset)
? document.activeElement.dataset.paneId : null;
listEl.replaceChildren();
if (!all.length) {
const empty = document.createElement('div');
empty.className = 'v3-pop-empty';
empty.textContent = 'No panes available.';
listEl.appendChild(empty);
return;
}
all.forEach((p) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'v3-pop-btn';
b.dataset.paneId = p.id;
b.setAttribute('aria-pressed', p.open ? 'true' : 'false');
b.textContent = (p.open ? '● ' : '○ ') + p.icon + ' ' + p.title;
b.addEventListener('click', (e) => {
e.stopPropagation();
if (panes.isOpen(p.id)) panes.close(p.id); else panes.detach(p.id);
});
listEl.appendChild(b);
if (p.id === focusedId) b.focus();
});
}
// The registry changes when plugins load and when panes open/close. Render is
// cheap and rare (never on a playback path), so just re-run it.
bus.on('panes:registered', render);
bus.on('panes:unregistered', render);
bus.on('panes:opened', render);
bus.on('panes:closed', render);
if (document.readyState !== 'complete') document.addEventListener('DOMContentLoaded', render);
else render();
})();