Watch
0
0
Fork
You've already forked hyperhive
0

frontend: drop the Panel forwarding object and the side-panel-body compat class

Per mara's review: '2 and maybe 1, but 3 also sounds reasonable on first
glance' (against 3 options I posted). Doing 2 and 1, leaving open()/
openNamed() as-is (option 3, tentative only).

Both dashboard/common.js and agent/app.js now export/use the
<hive-side-panel> element instance directly (sidePanel) instead of a
thin Panel = { open, openNamed, refresh, close } object that existed
purely to keep the old call-site shape unchanged. All 6 real call sites
updated to call the element's own methods directly.

The .side-panel-body class each wrapper stamped onto its own instance,
purely so common.css/agent.css's pre-existing content-styling selectors
kept matching, is gone too -- those selectors now use the element's own
tag name as the root (hive-side-panel .md, hive-side-panel .agent-inbox),
which already uniquely identifies the light-DOM instance without a
compatibility class. Verified via headless Chromium/CDP that the
tag-name selectors resolve correctly with no class needed.

Drive-by: removed an unrelated dead Panel import in call.js.
This commit is contained in:
iris 2026-08-01 00:46:02 +02:00 • committed by mara
commit 399a837e17
8 changed files with 73 additions and 90 deletions

View file

@ -901,10 +901,10 @@ pre.diff {
element's own shadow tree) and the styling for the panel's own
content types, which the shared element's shadow tree can never
reach (see @hive/shared/side-panel/hive-side-panel.css's header
comment). `.side-panel-body` below is the compatibility class
app.js's `Panel` wrapper puts directly on its `<hive-side-panel>`
instance so these rules keep reaching the slotted content by
ordinary light-DOM descendant matching. */
comment). The `hive-side-panel .agent-inbox …` rules below use the
element's own tag name as the selector root — no compatibility class
needed, the tag name already uniquely identifies the light-DOM
instance app.js's `sidePanel` creates. */
body.side-panel-resizing {
user-select: none;
}
@ -913,12 +913,12 @@ body.side-panel-resizing * { cursor: ew-resize !important; }
legacy <details>-collapsible variant of .agent-inbox is gone, so
here we strip the inbox-only chrome (background, border-left) and
let the panel body's own padding own the framing. */
.side-panel-body .agent-inbox {
hive-side-panel .agent-inbox {
margin: 0;
font-size: inherit;
color: var(--fg);
}
.side-panel-body .agent-inbox ul {
hive-side-panel .agent-inbox ul {
background: transparent;
border-left: 0;
padding: 0;

View file

@ -38,26 +38,17 @@ window.marked = marked;
bindAsyncForms(() => refreshState());
// ─── side panel (singleton drawer for inbox + loose-ends flyouts) ──────
// Thin wrapper around the shared `<hive-side-panel>` element (see
// @hive/shared/side-panel.js) — created once, eagerly, when this
// The shared `<hive-side-panel>` element (see @hive/shared/side-panel.js
// for the chrome/behavior it owns), created once, eagerly, when this
// module's IIFE runs (ES modules execute after the document is
// parsed, so `document.body` already exists; no per-call lazy-init
// guard to remember for any method, present or future). This UI's
// `open(name, title, content)` always takes an owner name, so it maps
// straight onto the shared element's `openNamed`; `side-panel-body`
// is a plain compatibility class this wrapper puts on its own
// instance so `agent.css`'s existing `.side-panel-body .agent-inbox
// …` rules keep reaching the slotted content by ordinary light-DOM
// descendant matching.
const panelEl = document.createElement('hive-side-panel');
panelEl.classList.add('side-panel-body');
document.body.append(panelEl);
const Panel = {
open: (name, title, content) => panelEl.openNamed(name, title, content),
close: () => panelEl.close(),
refresh: (name, title, content) => panelEl.refresh(name, title, content),
currentOwner: () => panelEl.currentOwner(),
};
// parsed, so `document.body` already exists). Used directly — this
// UI's opens always take an owner name, so call sites use the
// element's own `openNamed(name, title, content)` rather than the
// untyped `open`. `agent.css` reaches the slotted content via a plain
// `hive-side-panel .agent-inbox …` tag-name selector (no compat class
// needed — the element's own tag name already identifies it).
const sidePanel = document.createElement('hive-side-panel');
document.body.append(sidePanel);
// Wire the header pills to open the side panel. Pre-built (vs
// re-building per-click) so the freshest snapshot already lives
@ -67,14 +58,14 @@ window.marked = marked;
const inboxPill = $('inbox-pill');
if (inboxPill) {
inboxPill.addEventListener('click', () => {
Panel.open('inbox', 'inbox · ' + lastInbox.length,
sidePanel.openNamed('inbox', 'inbox · ' + lastInbox.length,
buildInboxList(lastInbox));
});
}
const todosPill = $('todos-pill');
if (todosPill) {
todosPill.addEventListener('click', () => {
Panel.open('todos', 'todos · ' + lastTodos.length,
sidePanel.openNamed('todos', 'todos · ' + lastTodos.length,
buildTodosList(lastTodos));
});
}
@ -336,7 +327,7 @@ window.marked = marked;
else closeOverflowMenu();
}
// Wire once on boot. The trigger itself + click-outside + Escape
// dismissal pattern matches the side-panel flyout (Panel).
// dismissal pattern matches the side-panel flyout (sidePanel).
(function bindOverflowMenu() {
const btn = $('overflow-btn');
if (!btn) return;
@ -807,7 +798,7 @@ window.marked = marked;
function buildLooseEndsList(threads) {
// Returns the <div> the side panel renders. The structural shape
// mirrors the legacy <details>-collapsible block — same CSS rules
// apply via `.side-panel-body .agent-inbox`.
// apply via `hive-side-panel .agent-inbox`.
const wrap = el('div', { class: 'agent-inbox' });
if (!threads.length) {
wrap.append(el('p', { class: 'side-panel-empty' },
@ -907,7 +898,7 @@ window.marked = marked;
const count = $('todos-count');
if (count) count.textContent = todos.length;
if (pill) pill.hidden = todos.length === 0;
Panel.refresh('todos', 'todos · ' + todos.length,
sidePanel.refresh('todos', 'todos · ' + todos.length,
buildTodosList(todos));
}
@ -1073,7 +1064,7 @@ window.marked = marked;
const count = $('inbox-count');
if (count) count.textContent = rows.length;
if (pill) pill.hidden = rows.length === 0;
Panel.refresh('inbox', 'inbox · ' + rows.length, buildInboxList(rows));
sidePanel.refresh('inbox', 'inbox · ' + rows.length, buildInboxList(rows));
}
// Harness reachability badge: derived from the same `s.status` the
// status block reads. Each status maps to a glyph + label + colour

View file

@ -14,7 +14,7 @@
// live-mutation paths call an injected `onCountsChanged` callback the entry
// registers once via `initCall`.
import { $, form, Panel, appendLinkified } from './common.js';
import { $, form, appendLinkified } from './common.js';
import { el } from '@hive/shared/dom.js';
import { themedToast } from '@hive/shared/modal.js';
import { epochSec, fmtAgo, fmtDuration } from './util.js';

View file

@ -376,49 +376,49 @@ code {
not just the shared element's own shadow tree) and the styling for
the panel's own content types, which the shared element's shadow tree
can never reach (see @hive/shared/side-panel/hive-side-panel.css's
header comment). `.side-panel-body` below is the compatibility class
common.js's `Panel` wrapper puts directly on its `<hive-side-panel>`
instance so these rules keep reaching the slotted content by ordinary
light-DOM descendant matching. */
header comment). The `hive-side-panel .md …` rules below use the
element's own tag name as the selector root — no compatibility class
needed, the tag name already uniquely identifies the light-DOM
instance `common.js`'s `sidePanel` creates. */
body.side-panel-resizing {
user-select: none;
}
body.side-panel-resizing * { cursor: ew-resize !important; }
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
.side-panel-body .md > :first-child { margin-top: 0; }
.side-panel-body .md > :last-child { margin-bottom: 0; }
.side-panel-body .md p { margin: 0.5em 0; }
.side-panel-body .md h1,
.side-panel-body .md h2,
.side-panel-body .md h3,
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
.side-panel-body .md code {
hive-side-panel .md { color: var(--fg); line-height: 1.5; }
hive-side-panel .md > :first-child { margin-top: 0; }
hive-side-panel .md > :last-child { margin-bottom: 0; }
hive-side-panel .md p { margin: 0.5em 0; }
hive-side-panel .md h1,
hive-side-panel .md h2,
hive-side-panel .md h3,
hive-side-panel .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
hive-side-panel .md code {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 2px;
padding: 0.1em 0.3em;
font-size: 0.9em;
}
.side-panel-body .md pre {
hive-side-panel .md pre {
background: var(--crust);
border: 1px solid var(--border);
padding: 0.5em 0.7em;
overflow-x: auto;
margin: 0.5em 0;
}
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
.side-panel-body .md a { color: var(--cyan); }
.side-panel-body .md ul,
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
.side-panel-body .md blockquote {
hive-side-panel .md pre code { background: none; border: none; padding: 0; }
hive-side-panel .md a { color: var(--cyan); }
hive-side-panel .md ul,
hive-side-panel .md ol { margin: 0.4em 0; padding-left: 1.5em; }
hive-side-panel .md blockquote {
border-left: 3px solid var(--purple-dim);
padding-left: 0.8em;
margin: 0.4em 0;
color: var(--muted);
}
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
.side-panel-body .md th,
.side-panel-body .md td {
hive-side-panel .md table { border-collapse: collapse; margin: 0.5em 0; }
hive-side-panel .md th,
hive-side-panel .md td {
border: 1px solid var(--border);
padding: 0.2em 0.5em;
}

View file

@ -281,30 +281,21 @@ export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
// ─── side panel ─────────────────────────────────────────────────────────
// Singleton drawer that swipes in from the right. Long content
// (file previews, approval diffs, journald logs, applied config)
// opens here via `Panel.open(title, node)` instead of expanding
// opens here via `sidePanel.open(title, node)` instead of expanding
// inline. Body is swapped on each open; closing just slides out so
// the content stays visible through the transition.
//
// Thin wrapper around the shared `<hive-side-panel>` element (see
// @hive/shared/side-panel.js for the chrome/behavior it owns, including
// drag-to-resize). The instance is created once, eagerly, at module
// evaluation time — ES modules run after the document is parsed (same
// timing `defer` scripts get), so `document.body` already exists here;
// no per-call lazy-init guard needed, and no easy-to-forget step for
// any new method added later. `side-panel-body` is a plain compatibility
// class this wrapper puts on its own instance so `common.css`'s
// existing `.side-panel-body .md …` rules keep reaching the slotted
// content by ordinary light-DOM descendant matching — the shared
// element itself has no idea that class name means anything.
const panelEl = document.createElement('hive-side-panel');
panelEl.classList.add('side-panel-body');
document.body.append(panelEl);
export const Panel = {
open: (title, content) => panelEl.open(title, content),
openNamed: (name, title, content) => panelEl.openNamed(name, title, content),
refresh: (name, title, content) => panelEl.refresh(name, title, content),
close: () => panelEl.close(),
};
// The instance is created once, eagerly, at module evaluation time — ES
// modules run after the document is parsed (same timing `defer` scripts
// get), so `document.body` already exists here. Exported directly (not
// wrapped in a forwarding object) — callers use the element's own
// `open`/`openNamed`/`refresh`/`close`/`currentOwner` methods, see
// @hive/shared/side-panel.js for what they do. `common.css` reaches the
// slotted content via a plain `hive-side-panel .md …` tag-name selector
// (no compatibility class needed — the element's own tag name already
// uniquely identifies it in the light DOM).
export const sidePanel = document.createElement('hive-side-panel');
document.body.append(sidePanel);
// ─── path linkification ─────────────────────────────────────────────────
// Agents constantly drop pointer strings into messages + question
@ -395,14 +386,14 @@ async function openFilePanel(path) {
'(could not load image — it may be missing or over the preview size cap)'));
});
img.src = '/api/state-file?path=' + encodeURIComponent(path);
Panel.open('↳ ' + path, img);
sidePanel.open('↳ ' + path, img);
return;
}
const isMd = /\.(md|markdown)$/i.test(path);
const isSvg = /\.svg$/i.test(path);
const view = el('div');
view.textContent = '(fetching…)';
Panel.open('↳ ' + path, view);
sidePanel.open('↳ ' + path, view);
try {
const text = await fetchStateFile(path);
if (isSvg) {

View file

@ -276,8 +276,8 @@ window.marked = marked;
// NOTIF.bind() (the enable/mute/unmute toggle UI) moved to /settings.html
// with the S3TT1NGS page. The dashboard keeps NOTIF.show() for approval/
// question notifications — it reads the browser permission + localStorage
// mute flag the settings page sets, so no bind() is needed here. Panel
// (backed by <hive-side-panel>) wires its own internals in
// mute flag the settings page sets, so no bind() is needed here. The
// side panel (<hive-side-panel>) wires its own internals in
// connectedCallback — no bind() step needed either.
// ─── live updates: dashboard event stream ──────────────────────────────

View file

@ -9,11 +9,11 @@
it is slotted, so this file needs zero `::slotted()`. The one slotted
node (the caller's opaque content, passed to open()/openNamed()/
refresh()) is intentionally un-styled from in here: each package's own
content-type-specific rules (`.side-panel-body .md` in the dashboard,
`.side-panel-body .agent-inbox` in the agent UI) live in that package's
own global stylesheet and reach the slotted content via a light-DOM
class its own wrapper puts directly on its `<hive-side-panel>` instance
— same architecture floor as `<hive-menu>`'s item-row styling, not a
content-type-specific rules (`hive-side-panel .md` in the dashboard,
`hive-side-panel .agent-inbox` in the agent UI) live in that package's
own global stylesheet and reach the slotted content via the element's
own tag name as the selector root — no compatibility class needed,
same architecture floor as `<hive-menu>`'s item-row styling, not a
scope choice.
`:host(.open)`/`:host(.resizing)` respond to the two state classes the

View file

@ -1,10 +1,10 @@
// hive-side-panel.js — <hive-side-panel>, the generic slide-in drawer
// shadow-DOM custom element behind the dashboard's and the per-agent UI's
// side panel (file previews, diffs, logs, inbox/todo lists — anything too
// long to expand inline). One singleton instance per page, created +
// appended lazily by each package's own thin wrapper
// (dashboard/src/common.js's `Panel`, agent/src/app.js's `Panel`) — same
// lazy-creation pattern `themedToast` uses for its toast-stack container.
// long to expand inline). One singleton instance per page, created once,
// eagerly, by each package's own module (dashboard/src/common.js's
// `sidePanel`, agent/src/app.js's `sidePanel`) and used directly — no
// wrapper object, no per-call lazy-init guard.
//
// Builds all structural chrome itself in `connectedCallback` (backdrop,
// drawer, resize handle, header, title, close button) — shadow-owned,
@ -12,8 +12,9 @@
// caller-built DOM, appended as a light-DOM child and picked up by the
// shadow tree's single default `<slot>`, exactly like `<hive-menu>`'s
// `content` (see that file's header for why). Each package's own global
// stylesheet reaches that content the same way it always did, via a
// light-DOM class its own wrapper puts on the instance it owns.
// stylesheet reaches that content via the element's own tag name as the
// selector root (`hive-side-panel .md`, `hive-side-panel .agent-inbox`)
// — no compatibility class needed.
//
// Public API is instance methods: `open(title, content)`,
// `openNamed(name, title, content)`, `refresh(name, title, content)`,