diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index 06d37a0a..2b9d7007 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -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 `` - 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
-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; diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 2af2ab26..729e4b06 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -38,26 +38,17 @@ window.marked = marked; bindAsyncForms(() => refreshState()); // ─── side panel (singleton drawer for inbox + loose-ends flyouts) ────── - // Thin wrapper around the shared `` element (see - // @hive/shared/side-panel.js) — created once, eagerly, when this + // The shared `` 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
the side panel renders. The structural shape // mirrors the legacy
-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 diff --git a/frontend/packages/dashboard/src/call.js b/frontend/packages/dashboard/src/call.js index 9e8df3a6..34902847 100644 --- a/frontend/packages/dashboard/src/call.js +++ b/frontend/packages/dashboard/src/call.js @@ -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'; diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index 7d82770d..d8746d73 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -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 `` - 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; } diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index 5acc6518..6d34c7a7 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -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 `` 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) { diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index fc0ee0e1..9b2af49c 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -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 ) wires its own internals in + // mute flag the settings page sets, so no bind() is needed here. The + // side panel () wires its own internals in // connectedCallback — no bind() step needed either. // ─── live updates: dashboard event stream ────────────────────────────── diff --git a/frontend/packages/shared/src/side-panel/hive-side-panel.css b/frontend/packages/shared/src/side-panel/hive-side-panel.css index 552c4706..51048616 100644 --- a/frontend/packages/shared/src/side-panel/hive-side-panel.css +++ b/frontend/packages/shared/src/side-panel/hive-side-panel.css @@ -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 `` instance - — same architecture floor as ``'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 ``'s item-row styling, not a scope choice. `:host(.open)`/`:host(.resizing)` respond to the two state classes the diff --git a/frontend/packages/shared/src/side-panel/hive-side-panel.js b/frontend/packages/shared/src/side-panel/hive-side-panel.js index a767c66b..01306719 100644 --- a/frontend/packages/shared/src/side-panel/hive-side-panel.js +++ b/frontend/packages/shared/src/side-panel/hive-side-panel.js @@ -1,10 +1,10 @@ // hive-side-panel.js — , 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 ``, exactly like ``'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)`,