Compare commits

..
6 changed files with 228 additions and 404 deletions

View file

@ -1,181 +0,0 @@
// hive-agent-menu.js — <hive-agent-menu>, the per-agent "⋮" context-menu
// custom element. One instance per agent card, appended after
// .card-body in the container row's flex layout (swarm.js's
// buildContainerLi via buildAgentMenu).
//
// Deliberately a *light-DOM* element with no shadow root of its own: all
// the generic dropdown-menu mechanics (shadow attach, open/close state,
// positioning, singleton coordination, outside-click/Escape) now live in
// the shared `<hive-menu>` component (@hive/shared/hive-menu.js) — this
// element's whole job is building the agent-specific trigger button +
// item list and handing them to an internal `<hive-menu>` via its
// `._opts = { trigger, content }` contract, same convention `<hive-
// dialog>` uses (custom elements can't take constructor args via
// `document.createElement`). Its own shadow root would've bought nothing
// once `<hive-menu>` owns shadow/positioning/open-close, so it doesn't
// have one — which means its `.agent-menu-*` classes need a stylesheet
// that actually reaches them. Since `<hive-menu>` slots this element's
// nodes in rather than moving them into its own shadow root (see
// hive-menu.js's header for why), they stay in the light DOM the whole
// way down — reachable by dashboard.css's ordinary global rules (there's
// no hive-agent-menu.css anymore; its old rules moved there, minus the
// pure positioning rule which is now generic and lives in `<hive-menu>`'s
// own shadow-scoped `.menu-dropdown`).
import { el } from '@hive/shared/dom.js';
import { themedConfirm, themedToast } from '@hive/shared/modal.js';
import '@hive/shared/hive-menu.js'; // registers <hive-menu> — side-effect import
// Single-agent POST helper shared by all menu items.
async function agentMenuPost(actionPath, name, body, graceful) {
const url = actionPath + encodeURIComponent(name) + (graceful ? '?graceful=true' : '');
try {
const resp = await fetch(url, {
method: 'POST',
headers: body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {},
body: body ? new URLSearchParams(body) : undefined,
redirect: 'manual',
});
const ok = resp.ok || resp.type === 'opaqueredirect'
|| (resp.status >= 200 && resp.status < 400);
if (!ok) {
const text = await resp.text().catch(() => '');
themedToast('action failed: ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
}
} catch (err) {
themedToast('action failed: ' + err, { type: 'error' });
}
}
class HiveAgentMenu extends HTMLElement {
connectedCallback() {
const { c, forgeBase } = this._opts || {};
const btn = el('button', {
type: 'button',
class: 'agent-menu-btn',
title: `actions for ${c.name}`,
'aria-label': `actions for ${c.name}`,
'aria-haspopup': 'menu',
'aria-expanded': 'false',
}, '⋮');
const dropdown = el('ul', { class: 'agent-menu-dropdown', role: 'menu' });
const close = () => this._menu.close();
const menuItem = (label, opts) => {
const li = el('li', { role: 'presentation' });
const item = el('button', {
type: 'button',
class: 'agent-menu-item',
role: 'menuitem',
}, label);
item.addEventListener('click', async () => {
close();
let graceful = false;
if (opts.confirm) {
const r = await themedConfirm({
message: opts.confirm,
danger: true,
confirmLabel: opts.confirmLabel || 'confirm',
checkboxes: opts.graceful
? [{ name: 'graceful', label: opts.gracefulLabel || 'stop gracefully — let the agent finish its turn and flush state before the container stops' }]
: [],
});
if (!r) return;
graceful = !!r.graceful;
}
await agentMenuPost(opts.action, c.name, opts.body || null, graceful);
});
li.append(item);
return li;
};
const menuSep = () => el('li', { class: 'agent-menu-sep', role: 'separator' });
// Navigation link item (opens in same tab by default).
const menuLink = (label, href, title) => {
const li = el('li', { role: 'presentation' });
const a = el('a', {
class: 'agent-menu-item',
href,
role: 'menuitem',
title: title || '',
}, label);
a.addEventListener('click', close);
li.append(a);
return li;
};
// Show only actions that are applicable in the current state.
if (c.running) {
dropdown.append(
menuItem('↺ R3ST4RT', {
action: '/api/restart/',
confirm: `restart ${c.name}?`,
graceful: true,
gracefulLabel: 'restart gracefully — let the agent finish its turn and flush state before the container restarts',
}),
menuItem('■ ST0P', { action: '/api/kill/', confirm: `stop ${c.name}?`, confirmLabel: '■ stop', graceful: true }),
);
} else {
dropdown.append(
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
);
}
// Pause/resume is orthogonal to running: a paused stopped agent boots
// paused; a paused running agent keeps its container but drives no turns.
if (c.paused) {
dropdown.append(
menuItem('▶ R3SUM3', { action: '/api/resume/', confirm: `resume ${c.name}? the turn loop restarts and drains queued messages.` }),
);
} else {
dropdown.append(
menuItem('⏸ P4US3', { action: '/api/pause/', confirm: `pause ${c.name}? parks the turn loop — inbox messages queue unacked.` }),
);
}
dropdown.append(
menuSep(),
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
menuSep(),
// Deep-link to the AGENT log tab pre-filtered to this container.
// The ?agent= param is read by logs.js on load and pre-selects this
// agent's journal without extra clicks.
menuLink('journal logs →',
`/logs.html?agent=${encodeURIComponent(c.name)}#agent`,
`view ${c.name} journal logs`),
);
dropdown.append(
menuSep(),
menuItem('DESTR0Y', {
action: '/api/destroy/',
confirm: `destroy ${c.name}? container removed; state + creds kept.`,
}),
menuItem('PURG3', {
action: '/api/destroy/',
body: { purge: 'on' },
confirm: `PURGE ${c.name}? WIPES container, config history, claude creds, and notes. no undo.`,
}),
);
if (c.deployed_sha && forgeBase) {
const li = el('li', { role: 'presentation' });
const a = el('a', {
class: 'agent-menu-item',
href: `${forgeBase}/agent-configs/${encodeURIComponent(c.name)}/commit/${c.deployed_sha}`,
target: '_blank',
rel: 'noopener',
role: 'menuitem',
title: 'deployed config commit on forge',
}, `deployed:${c.deployed_sha}`);
li.append(a);
dropdown.append(menuSep(), li);
}
this._menu = document.createElement('hive-menu');
this._menu._opts = { trigger: btn, content: dropdown };
this.append(this._menu);
}
}
customElements.define('hive-agent-menu', HiveAgentMenu);

View file

@ -279,41 +279,53 @@ body.dashboard-shell {
}
/* per-agent three-dot context menu
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 {
Positioned after .card-body in the flex row. */
.agent-menu {
flex: none;
position: relative;
align-self: flex-start;
margin-top: 0.3em;
}
/* .agent-menu-btn carries no styling of its own anymore the trigger is
a top-level `slot="trigger"` node inside <hive-menu>'s shadow tree, so
its base icon-button chrome now lives in hive-menu.css's
`::slotted([slot='trigger'])` rules instead (reachable and genuinely
generic any <hive-menu> trigger gets the same treatment). The class
itself stays, just as an identification hook (aria-label/title
already carry the real semantics). */
/* Box chrome (background/border/radius/shadow/min-width/white-space) now
lives on <hive-menu>'s own `.menu-dropdown` wrapper (shadow-DOM-owned
markup, no slotting constraint) this is just the `<ul>`'s own
list-reset + item padding, which only the caller (owning the actual
list markup) can set. */
.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: 0;
transition: opacity 120ms, background 120ms, color 120ms;
}
.container-row:hover .agent-menu-btn,
.agent-menu.open .agent-menu-btn {
opacity: 1;
}
.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);
}
.agent-menu-dropdown {
position: absolute;
right: 0;
top: calc(100% + 2px);
z-index: 50;
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;
@ -340,13 +352,6 @@ hive-agent-menu {
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;
}
/* Pending state splits queued vs running. */
.container-row.pending .actions { opacity: 0.4; pointer-events: none; }
.container-row.pending-running {

View file

@ -11,8 +11,6 @@ import { themedConfirm, themedToast } from '@hive/shared/modal.js';
import {
containersState, questionsState,
} from './state.js';
import { closeAllMenus } from '@hive/shared/hive-menu.js';
import './agent-menu/hive-agent-menu.js'; // registers <hive-agent-menu> — side-effect import
// Context-window badge thresholds. Preferred source is each container's
// `context_window_tokens` from /api/state (the real window for the model
@ -47,6 +45,8 @@ const transientsState = new Map();
// tab-gated visibility).
const selectionState = new Set();
let openAgentMenu = null; // currently open dropdown element, or null
// ─── rebuild queue ──────────────────────────────────────────────────────────
export function syncRebuildQueueFromSnapshot(s) {
@ -186,19 +186,192 @@ document.addEventListener('click', (e) => {
// stopped; rebuild + destroy/purge always shown.
// The button is CSS-invisible until the row is hovered (or menu is
// open) so it doesn't clutter quiet rows.
// Rendering + agent-specific interaction lives in the <hive-agent-menu>
// custom element (./agent-menu/hive-agent-menu.js, imported above for its
// customElements.define side effect); the generic dropdown mechanics
// (open/close, positioning, singleton coordination — closed via
// `closeAllMenus` below) live in the shared `<hive-menu>` component it
// composes internally (@hive/shared/hive-menu.js). This is a thin
// wrapper matching <hive-dialog>'s `._opts`-before-append convention,
// since a custom element created via `document.createElement` can't
// take constructor args.
function closeAllAgentMenus() {
if (!openAgentMenu) return;
openAgentMenu.hidden = true;
const wrap = openAgentMenu.closest('.agent-menu');
if (wrap) {
wrap.classList.remove('open');
const btn = wrap.querySelector('.agent-menu-btn');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
openAgentMenu = null;
}
// Close on any click outside an agent-menu element.
document.addEventListener('click', (e) => {
if (!e.target.closest('.agent-menu')) closeAllAgentMenus();
}, true);
// Close on Escape. stopImmediatePropagation so the selection-clear
// handler on the same element doesn't also fire when a menu is open.
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && openAgentMenu) {
closeAllAgentMenus();
e.stopImmediatePropagation();
}
}, true);
// Single-agent POST helper shared by all menu items.
async function agentMenuPost(actionPath, name, body, graceful) {
const url = actionPath + encodeURIComponent(name) + (graceful ? '?graceful=true' : '');
try {
const resp = await fetch(url, {
method: 'POST',
headers: body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {},
body: body ? new URLSearchParams(body) : undefined,
redirect: 'manual',
});
const ok = resp.ok || resp.type === 'opaqueredirect'
|| (resp.status >= 200 && resp.status < 400);
if (!ok) {
const text = await resp.text().catch(() => '');
themedToast('action failed: ' + resp.status + (text ? '\n\n' + text : ''), { type: 'error' });
}
} catch (err) {
themedToast('action failed: ' + err, { type: 'error' });
}
}
function buildAgentMenu(c, forgeBase) {
const menu = document.createElement('hive-agent-menu');
menu._opts = { c, forgeBase };
return menu;
const wrap = el('div', { class: 'agent-menu' });
const btn = el('button', {
type: 'button',
class: 'agent-menu-btn',
title: `actions for ${c.name}`,
'aria-label': `actions for ${c.name}`,
'aria-haspopup': 'menu',
'aria-expanded': 'false',
}, '⋮');
const dropdown = el('ul', { class: 'agent-menu-dropdown', hidden: true, role: 'menu' });
function menuItem(label, opts) {
const li = el('li', { role: 'presentation' });
const item = el('button', {
type: 'button',
class: 'agent-menu-item',
role: 'menuitem',
}, label);
item.addEventListener('click', async () => {
closeAllAgentMenus();
let graceful = false;
if (opts.confirm) {
const r = await themedConfirm({
message: opts.confirm,
danger: true,
confirmLabel: opts.confirmLabel || 'confirm',
checkboxes: opts.graceful
? [{ name: 'graceful', label: opts.gracefulLabel || 'stop gracefully — let the agent finish its turn and flush state before the container stops' }]
: [],
});
if (!r) return;
graceful = !!r.graceful;
}
await agentMenuPost(opts.action, c.name, opts.body || null, graceful);
});
li.append(item);
return li;
}
function menuSep() {
return el('li', { class: 'agent-menu-sep', role: 'separator' });
}
// Navigation link item (opens in same tab by default).
function menuLink(label, href, title) {
const li = el('li', { role: 'presentation' });
const a = el('a', {
class: 'agent-menu-item',
href,
role: 'menuitem',
title: title || '',
}, label);
a.addEventListener('click', () => closeAllAgentMenus());
li.append(a);
return li;
}
// Show only actions that are applicable in the current state.
if (c.running) {
dropdown.append(
menuItem('↺ R3ST4RT', {
action: '/api/restart/',
confirm: `restart ${c.name}?`,
graceful: true,
gracefulLabel: 'restart gracefully — let the agent finish its turn and flush state before the container restarts',
}),
menuItem('■ ST0P', { action: '/api/kill/', confirm: `stop ${c.name}?`, confirmLabel: '■ stop', graceful: true }),
);
} else {
dropdown.append(
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
);
}
// Pause/resume is orthogonal to running: a paused stopped agent boots
// paused; a paused running agent keeps its container but drives no turns.
if (c.paused) {
dropdown.append(
menuItem('▶ R3SUM3', { action: '/api/resume/', confirm: `resume ${c.name}? the turn loop restarts and drains queued messages.` }),
);
} else {
dropdown.append(
menuItem('⏸ P4US3', { action: '/api/pause/', confirm: `pause ${c.name}? parks the turn loop — inbox messages queue unacked.` }),
);
}
dropdown.append(
menuSep(),
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
menuSep(),
// Deep-link to the AGENT log tab pre-filtered to this container.
// The ?agent= param is read by logs.js on load and pre-selects this
// agent's journal without extra clicks.
menuLink('journal logs →',
`/logs.html?agent=${encodeURIComponent(c.name)}#agent`,
`view ${c.name} journal logs`),
);
dropdown.append(
menuSep(),
menuItem('DESTR0Y', {
action: '/api/destroy/',
confirm: `destroy ${c.name}? container removed; state + creds kept.`,
}),
menuItem('PURG3', {
action: '/api/destroy/',
body: { purge: 'on' },
confirm: `PURGE ${c.name}? WIPES container, config history, claude creds, and notes. no undo.`,
}),
);
if (c.deployed_sha && forgeBase) {
const li = el('li', { role: 'presentation' });
const a = el('a', {
class: 'agent-menu-item',
href: `${forgeBase}/agent-configs/${encodeURIComponent(c.name)}/commit/${c.deployed_sha}`,
target: '_blank',
rel: 'noopener',
role: 'menuitem',
title: 'deployed config commit on forge',
}, `deployed:${c.deployed_sha}`);
li.append(a);
dropdown.append(menuSep(), li);
}
btn.addEventListener('click', (e) => {
e.stopPropagation();
const wasHidden = dropdown.hidden;
closeAllAgentMenus();
if (wasHidden) {
dropdown.hidden = false;
btn.setAttribute('aria-expanded', 'true');
wrap.classList.add('open');
openAgentMenu = dropdown;
}
});
wrap.append(btn, dropdown);
return wrap;
}
// ─── port conflicts ──────────────────────────────────────────────────────────
@ -230,7 +403,7 @@ function derivePortConflicts(containers) {
function buildAgentTree(containers) {
// Close any open context menu before replacing the DOM tree — the
// previous dropdown element would otherwise be a stale reference.
closeAllMenus();
closeAllAgentMenus();
const byName = new Map();
for (const c of containers) byName.set(c.name, c);
const children = new Map(); // parent_name -> [child_name, ...]

View file

@ -17,9 +17,7 @@
"./chrome.css": "./src/chrome.css",
"./forms.js": "./src/forms.js",
"./dom.js": "./src/dom.js",
"./modal.js": "./src/modal.js",
"./shadow-css.js": "./src/shadow-css.js",
"./hive-menu.js": "./src/hive-menu/hive-menu.js"
"./modal.js": "./src/modal.js"
},
"files": [
"src/"

View file

@ -1,58 +0,0 @@
/* hive-menu.css scoped stylesheet for the generic <hive-menu>
shadow-DOM custom element (hive-menu.js). Loaded as raw text at build
time (esbuild's `text` loader) and appended as a <style> element
inside the shadow root see @hive/shared/shadow-css.js's header
comment for why a plain <style> tag and not adoptedStyleSheets.
Two kinds of rules live here, split by what they can actually reach:
`:host`/`.menu-dropdown` style shadow-DOM-owned markup this element
builds itself (no slotting constraint at all); `::slotted(...)` rules
reach the caller's *top-level* trigger/content nodes (the ones with a
`slot` attribute) since those are true light-DOM children, just
rendered here. What can NOT live here: anything inside the content
node (e.g. individual dropdown item rows) `::slotted()` only
matches directly-slotted elements, not their descendants, and no CSS
mechanism pierces further than that. That's a hard Shadow DOM
architecture limit, not a scope choice item-level styling stays in
the caller's own stylesheet regardless of how generic it looks. */
:host {
position: relative;
}
.menu-dropdown {
position: absolute;
right: 0;
top: calc(100% + 2px);
z-index: 50;
background: var(--bg-elev);
border: 1px solid var(--purple-dim);
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
min-width: 10em;
white-space: nowrap;
}
/* The trigger is the top-level `slot="trigger"` node reachable, so its
base icon-button chrome (invisible until hover/open, via
`--menu-btn-opacity`) lives here rather than duplicated per caller. */
::slotted([slot='trigger']) {
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;
}
::slotted([slot='trigger']:hover),
::slotted([slot='trigger']:focus-visible) {
background: color-mix(in srgb, var(--purple) 10%, transparent);
color: var(--purple);
outline: none;
}
::slotted([slot='trigger']:focus-visible) {
outline: 1px solid var(--purple);
}

View file

@ -1,113 +0,0 @@
// hive-menu.js — <hive-menu>, the generic dropdown-menu shadow-DOM custom
// element behind any "click a trigger, get a positioned dropdown" UI
// (currently just <hive-agent-menu>'s per-agent "⋮" menu). Owns
// open/close state, trigger + dropdown positioning, singleton
// close-on-open coordination across every `<hive-menu>` instance in the
// app, and the document-level outside-click/Escape listeners. Doesn't
// know or care what's inside the trigger/dropdown — opaque DOM nodes the
// caller hands over via `._opts = { trigger, content }` before appending
// (same `._opts`-before-append convention `<hive-dialog>` uses).
//
// `trigger`/`content` are appended as *light-DOM* children, projected
// into the shadow template via named `<slot>`s rather than moved into
// the shadow root. Load-bearing: it keeps the caller's own class-scoped
// styling applying to the nodes it built — a `<style>` only styles
// elements within the same tree it's part of, and slotted content keeps
// the tree membership of wherever it's actually a light-DOM child, so
// re-parenting into this element's own shadow root would make the
// caller's classes go dark.
//
// `openInstances` tracks every open instance app-wide (a deliberate
// widening from the old per-agent-menu-only coordination) so
// `closeAll()` can close each via its own `.close()`. `closeAllMenus()`
// force-closes everything before tearing down DOM one might anchor off
// of (e.g. swarm.js before replacing the container tree).
//
// The document-level listeners below, registered once: shadow-DOM
// retargeting means `e.target` for a click inside a shadow tree gets
// retargeted past that instance's host, so `e.composedPath()` (includes
// slotted content) tests "did this land inside any open menu" — same
// reasoning as `<hive-dialog>`'s backdrop-click check.
import { el } from '../dom.js';
import { attachShadowCss } from '../shadow-css.js';
import hiveMenuCss from './hive-menu.css';
const openInstances = new Set();
function closeAll() {
for (const inst of [...openInstances]) inst.close();
}
// Exported for any caller that needs to force-close every open menu —
// see module header.
export function closeAllMenus() {
closeAll();
}
// Close on any click outside every currently-open instance.
document.addEventListener('click', (e) => {
if (!openInstances.size) return;
const path = e.composedPath();
for (const inst of [...openInstances]) {
if (!path.includes(inst)) inst.close();
}
}, true);
// Close on Escape. stopImmediatePropagation so a caller's own
// selection-clear Escape handler (e.g. swarm.js's) doesn't also fire
// while a menu is open.
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && openInstances.size) {
closeAll();
e.stopImmediatePropagation();
}
}, true);
class HiveMenu extends HTMLElement {
connectedCallback() {
const { trigger, content } = this._opts || {};
const root = attachShadowCss(this, hiveMenuCss);
// Project the caller's opaque nodes via named slots — see module
// header for why this has to be slotting, not a shadow-root append.
trigger.slot = 'trigger';
content.slot = 'content';
this.append(trigger, content);
const dropdown = el('div', { class: 'menu-dropdown', hidden: true });
dropdown.append(el('slot', { name: 'content' }));
root.append(el('slot', { name: 'trigger' }), dropdown);
this._trigger = trigger;
this._dropdown = dropdown;
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const wasOpen = openInstances.has(this);
closeAll();
if (!wasOpen) this.open();
});
}
disconnectedCallback() {
// Guards against a stale entry if the element is removed from the DOM
// (row re-render, tab switch) while its dropdown was still open.
openInstances.delete(this);
}
open() {
this._dropdown.hidden = false;
this._trigger.setAttribute('aria-expanded', 'true');
this.style.setProperty('--menu-btn-opacity', '1');
openInstances.add(this);
}
close() {
if (!openInstances.has(this)) return;
this._dropdown.hidden = true;
this._trigger.setAttribute('aria-expanded', 'false');
this.style.removeProperty('--menu-btn-opacity');
openInstances.delete(this);
}
}
customElements.define('hive-menu', HiveMenu);