mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-19 06:52:38 +00:00
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 <topkoa@gmail.com>
189 lines
6.5 KiB
CSS
189 lines
6.5 KiB
CSS
/* fee[dB]ack — detachable panes.
|
|
*
|
|
* Hand-authored (not Tailwind-scanned) so a runtime-installed plugin gets the chip
|
|
* and the dock without shipping its own stylesheet — the same reason .fb-selectable
|
|
* is hand-authored in core CSS.
|
|
*
|
|
* Z-index: the dock is a child of <body>, so it is NOT on the ladder from
|
|
* docs/plugin-v3-ui.md (transport 20, rail 30, popovers 40) — those numbers live
|
|
* *inside* #player's stacking context, and #player itself is `position:fixed;
|
|
* z-index:100` covering the viewport. A dock below 100 is invisible on the one
|
|
* screen panes exist for. Body-level ladder: #player 100 < dock 110 < toasts 120
|
|
* < modals 200.
|
|
*/
|
|
|
|
/* ── The popped-out element ──────────────────────────────────────────────────
|
|
*
|
|
* The single most important rule in this file.
|
|
*
|
|
* A plugin's panel is almost always a fixed overlay pinned to a corner of the app:
|
|
* `position:fixed; top:72px; right:18px; width:288px; z-index:99999`, with a drop
|
|
* shadow and a max-height sized against the viewport. Inside a dock card, or alone
|
|
* in a 320px window, every one of those is wrong — it would float 72px down from
|
|
* the top of its own window, still 288px wide, still casting a shadow over nothing.
|
|
*
|
|
* So we neutralise PLACEMENT and nothing else. Colours, borders, radius, padding,
|
|
* fonts, the panel's own internal layout: all untouched, because the whole promise
|
|
* of this feature is that what you popped out is what you get. */
|
|
.fb-paned {
|
|
position: static !important;
|
|
inset: auto !important;
|
|
margin: 0 !important;
|
|
width: 100% !important;
|
|
max-width: none !important;
|
|
max-height: none !important;
|
|
z-index: auto !important;
|
|
box-shadow: none !important;
|
|
/* A panel hidden until its launcher is clicked is being shown on purpose now. */
|
|
display: block !important;
|
|
}
|
|
|
|
/* ── The pop-out chip ────────────────────────────────────────────────────── */
|
|
|
|
.fb-pane-chip {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
border: 1px solid rgba(51, 65, 85, .7);
|
|
border-radius: .4rem;
|
|
background: rgba(30, 41, 59, .8);
|
|
color: #94a3b8;
|
|
font-size: .8rem;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
transition: color .15s, border-color .15s, background .15s;
|
|
}
|
|
.fb-pane-chip:hover {
|
|
color: #e2e8f0;
|
|
border-color: #4080e0;
|
|
background: rgba(64, 128, 224, .18);
|
|
}
|
|
.fb-pane-chip:focus-visible { outline: 2px solid #4080e0; outline-offset: 1px; }
|
|
|
|
/* The panel, while its pane is popped out. A dedicated class rather than
|
|
.hidden/[hidden]: the panels we attach to toggle those themselves, and two owners
|
|
of one class is a bug waiting for a bad day. */
|
|
.fb-pane-detached { display: none !important; }
|
|
|
|
/* What the user sees in the panel's place. */
|
|
.fb-pane-stub {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
padding: .35rem .6rem;
|
|
border: 1px dashed rgba(64, 128, 224, .55);
|
|
border-radius: .5rem;
|
|
background: rgba(64, 128, 224, .08);
|
|
color: #93b4e8;
|
|
font-size: .72rem;
|
|
cursor: pointer;
|
|
transition: background .15s, border-color .15s;
|
|
}
|
|
.fb-pane-stub:hover { background: rgba(64, 128, 224, .18); border-color: #4080e0; }
|
|
.fb-pane-stub-glyph { font-size: .85rem; }
|
|
|
|
/* ── The dock ────────────────────────────────────────────────────────────── */
|
|
|
|
.fb-pane-dock {
|
|
position: fixed;
|
|
top: 4.5rem;
|
|
right: 1rem;
|
|
bottom: 1rem;
|
|
z-index: 110; /* above #player (100), below toasts (120) */
|
|
width: 22rem;
|
|
max-width: calc(100vw - 2rem);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: .6rem;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
/* A frame around cards, not a surface — the empty space below them must never
|
|
eat a click meant for the highway. */
|
|
pointer-events: none;
|
|
scrollbar-width: thin;
|
|
}
|
|
.fb-pane-dock.is-empty { display: none; }
|
|
|
|
.fb-pane-card {
|
|
pointer-events: auto;
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: rgba(15, 23, 42, .96);
|
|
border: 1px solid rgba(51, 65, 85, .6);
|
|
border-radius: .9rem;
|
|
box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.fb-pane-card-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
padding: .5rem .7rem;
|
|
border-bottom: 1px solid rgba(51, 65, 85, .5);
|
|
background: rgba(30, 41, 59, .6);
|
|
}
|
|
.fb-pane-card-title {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: .78rem;
|
|
font-weight: 600;
|
|
color: #cbd5e1;
|
|
}
|
|
.fb-pane-card-btn {
|
|
flex: 0 0 auto;
|
|
width: 1.4rem;
|
|
height: 1.4rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
border-radius: .35rem;
|
|
background: transparent;
|
|
color: #94a3b8;
|
|
font-size: .75rem;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
}
|
|
.fb-pane-card-btn:hover { background: rgba(51, 65, 85, .7); color: #e2e8f0; }
|
|
.fb-pane-card-btn:focus-visible { outline: 2px solid #4080e0; outline-offset: 1px; }
|
|
|
|
.fb-pane-card-body { overflow: auto; }
|
|
|
|
/* focus(id) — a brief highlight, so re-opening an already-open pane says so
|
|
instead of appearing to do nothing. */
|
|
.fb-pane-card.is-flash { animation: fb-pane-flash .7s ease-out; }
|
|
@keyframes fb-pane-flash {
|
|
0% { border-color: #4080e0; box-shadow: 0 0 0 3px rgba(64, 128, 224, .35), 0 12px 40px rgba(0, 0, 0, .5); }
|
|
100% { border-color: rgba(51, 65, 85, .6); box-shadow: 0 12px 40px rgba(0, 0, 0, .5); }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fb-pane-card.is-flash { animation: none; }
|
|
}
|
|
|
|
/* ── The pop-out window (static/panes/pane.html) ─────────────────────────────
|
|
*
|
|
* The window's own chrome — everything INSIDE it is the adopted element, styled by
|
|
* the app's stylesheets, which the host copies into this document. */
|
|
|
|
html.fb-pane-window,
|
|
html.fb-pane-window body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
background: #0f172a;
|
|
}
|
|
html.fb-pane-window body { display: flex; flex-direction: column; overflow: hidden; }
|
|
#fb-pane-root {
|
|
flex: 1 1 auto;
|
|
overflow: auto;
|
|
padding: .6rem;
|
|
}
|