frontend: split hive-agent-menu's generic dropdown mechanics into a shared hive-menu component

<hive-agent-menu> bundled two concerns: the agent-specific trigger/item
list, and generic "click a trigger, get a positioned dropdown" mechanics
(shadow attach, open/close, singleton close-on-open coordination,
outside-click/Escape handling). Pulled the latter out into a new
@hive/shared/hive-menu.js (<hive-menu>), following the established
per-component-directory + ._opts-before-append shadow-DOM pattern
(<hive-dialog>). <hive-agent-menu> now just builds the "⋮" trigger and
the action list and hands them to an internal <hive-menu> instance.

<hive-menu> takes ownership of every <hive-menu> instance in the app for
singleton coordination (closeAllMenus, renamed from closeAllAgentMenus)
— a deliberate widening from the old per-agent-menu-only tracking, since
the mechanism was never agent-specific to begin with.

The one subtlety worth spelling out: <hive-menu> projects the caller's
opaque trigger/content nodes via named <slot>s rather than moving them
into its own shadow root. That's load-bearing, not cosmetic — if it
re-parented them into its own shadow tree instead, <hive-agent-menu>'s
own classes (.agent-menu-btn, .agent-menu-item, ...) would stop applying,
since a <style> only styles elements within the same shadow tree/document
it's part of, and only slotting (not re-parenting) keeps the caller's
nodes in the caller's own tree for styling purposes. That in turn made
<hive-agent-menu>'s own shadow root redundant once it wasn't the thing
positioning or owning open/close state anymore, so it's dropped in favor
of a plain light-DOM element styled by dashboard.css (already the one
page it renders on) — hive-agent-menu.css is gone, its rules folded into
dashboard.css's per-agent-menu section, minus the positioning rules that
moved into hive-menu.css as the new generic `.menu-dropdown` wrapper.

Verified with a standalone esbuild bundle + a cached nix chromium driven
over raw CDP (no puppeteer/playwright/python3 available): hover-reveal
opacity, dropdown open/close/positioning, outside-click/Escape dismissal,
and cross-instance singleton coordination all behave identically to
before the split.
This commit is contained in:
iris 2026-07-31 22:53:36 +02:00 committed by mara
commit 395c9a6df2
7 changed files with 272 additions and 203 deletions

View file

@ -279,17 +279,90 @@ body.dashboard-shell {
}
/* per-agent three-dot context menu
Positioned after .card-body in the flex row. The menu itself is the
<hive-agent-menu> shadow-DOM custom element (agent-menu/hive-agent-menu.js
+ .css) its own shadow-scoped stylesheet carries the button/dropdown/
item/separator rules and the `:host` rules that play the structural
role this light-DOM wrapper used to play. The one rule that still has
to live out here is the hover reveal: a light-DOM descendant-combinator
selector can't reach into the shadow tree, so it's relayed across the
shadow boundary via the `--menu-btn-opacity` custom property (custom
properties inherit through shadow boundaries) instead the
component's own JS forces the same variable to 1 while its dropdown is
open (see hive-agent-menu.js's open()/close()). */
Positioned after .card-body in the flex row. `<hive-agent-menu>`
(agent-menu/hive-agent-menu.js) is a light-DOM element with no shadow
root of its own the generic dropdown mechanics (shadow attach,
open/close, positioning) live in the shared `<hive-menu>` component
instead (@hive/shared/hive-menu.js), which slots this element's
trigger/content nodes into position rather than moving them into its
own shadow root. That keeps these nodes in the ordinary light DOM the
whole way down, so this ordinary global stylesheet reaches them
directly see hive-agent-menu.js's header for the full reasoning.
`hive-agent-menu` itself just needs the flex/alignment role the old
light-DOM `.agent-menu` wrapper div played in this row; the
`position: relative` an absolutely-positioned dropdown needs now lives
on `<hive-menu>`'s own `:host` instead, since that's the element whose
shadow tree the dropdown is actually positioned within. */
hive-agent-menu {
flex: none;
align-self: flex-start;
margin-top: 0.3em;
}
.agent-menu-btn {
display: block;
background: none;
border: none;
color: var(--subtext0);
font-size: 1.1em;
line-height: 1;
cursor: pointer;
padding: 0.1em 0.4em;
border-radius: 4px;
opacity: var(--menu-btn-opacity, 0);
transition: opacity 120ms, background 120ms, color 120ms;
}
.agent-menu-btn:hover,
.agent-menu-btn:focus-visible {
background: color-mix(in srgb, var(--purple) 10%, transparent);
color: var(--purple);
outline: none;
}
.agent-menu-btn:focus-visible {
outline: 1px solid var(--purple);
}
/* Positioning (`position`/`top`/`right`/`z-index`) is generic and lives
on `<hive-menu>`'s own `.menu-dropdown` wrapper instead this is just
the item list's visual chrome. */
.agent-menu-dropdown {
background: var(--bg-elev);
border: 1px solid var(--purple-dim);
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
list-style: none;
margin: 0;
padding: 0.3em 0;
min-width: 10em;
white-space: nowrap;
}
.agent-menu-item {
display: block;
width: 100%;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 0.88em;
letter-spacing: 0.04em;
text-align: left;
padding: 0.4em 0.9em;
cursor: pointer;
text-decoration: none;
box-sizing: border-box;
}
.agent-menu-item:hover {
background: var(--border);
color: var(--purple);
}
.agent-menu-sep {
height: 1px;
background: var(--purple-dim);
margin: 0.3em 0;
padding: 0;
}
/* Hover reveal: `--menu-btn-opacity` is forced to 1 while a hover
selector (or the trigger's own open state, set by <hive-menu>) applies
inherits down through `hive-agent-menu` `<hive-menu>` the
trigger button same as any custom property, shadow trees included. */
.container-row:hover hive-agent-menu {
--menu-btn-opacity: 1;
}