agent: OverflowMenu — trimmed to rebuild + dashboard link
Deliberately trimmed vs. app.js's `populateOverflowMenu`, which held
dashboard-link/rebuild/new-session/logout/model-picker/effort-picker
all in one flat list — the design guide's own named junk-drawer
anti-example. Model/effort already moved to StatusChips' own
Badge+Dropdown controls; new-session/logout now live as TermInput
slash commands (typed, with their own two-step confirm). What's left —
rebuild + a dashboard back-link — is genuinely menu-shaped (rare, not
tied to any other visible control), so it's still a `⋯` menu, just a
much shorter one.
Reuses `@hive/shared`'s Badge+Dropdown pair (same anchored-popover
shape as StatusChips' model/effort pickers), not a bespoke popover.
Rebuild's confirm is the same "select once to arm, select again to
fire" idea as the slash commands, adapted to a menu click. New
`lib/rebuildAction.ts` — same cross-origin-needs-a-real-form-submit
reasoning as pauseAction.ts (hive-c0re's `/api/rebuild/{name}` has no
CORS header either).
Found + fixed a real layout gap in the shared Dropdown while
screenshot-verifying: its CSS anchors left-edge-to-left-edge, correct
for a picker with room to its right, but the overflow trigger is the
header's right-most element, so the menu ran off-screen. Fixed with a
scoped override in this component's own CSS (`.header-overflow-anchor
.ui-dropdown { left: auto; right: 0; }`) rather than changing the
shared default, which is still correct for every other caller.
This commit is contained in:
parent
d9249a36bf
commit
48a1745bb5
4 changed files with 101 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ import { SidePanel } from './components/SidePanel.js';
|
|||
import { InboxPanel } from './components/InboxPanel.js';
|
||||
import { TodosPanel } from './components/TodosPanel.js';
|
||||
import { TermInput } from './components/TermInput.js';
|
||||
import { OverflowMenu } from './components/OverflowMenu.js';
|
||||
import { useAgentState } from './hooks/useAgentState.js';
|
||||
import { useTodos } from './hooks/useTodos.js';
|
||||
import { fmtAge, fmtTokens } from './lib/format.js';
|
||||
|
|
@ -68,6 +69,10 @@ export function Root() {
|
|||
<>
|
||||
<HeaderPill kind="inbox" icon="📬" label="inbox" count={state?.inbox.length ?? 0} onClick={() => setOpenPanel('inbox')} />
|
||||
<HeaderPill kind="todos" icon="📋" label="todos" count={todos.length} onClick={() => setOpenPanel('todos')} />
|
||||
{/* Needs dashboard_port from a loaded snapshot — omitted (like the
|
||||
* old page's populateOverflowMenu) until the first /api/state
|
||||
* resolves. */}
|
||||
{state ? <OverflowMenu label={state.label} dashboardBase={resolveDashboardBase(state.dashboard_port)} /> : null}
|
||||
</>
|
||||
);
|
||||
// Kept mounted regardless of `openPanel` (open/closed toggles just the
|
||||
|
|
|
|||
19
frontend/packages/agent/src/components/OverflowMenu.css
Normal file
19
frontend/packages/agent/src/components/OverflowMenu.css
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* Anchors <Dropdown> under the "⋯" trigger — same positioning
|
||||
contract as StatusChips.css's `.status-chip-anchor`, kept as its own
|
||||
rule here rather than reused across components (see this file's
|
||||
sibling .tsx comment) so OverflowMenu doesn't depend on StatusChips'
|
||||
incidental CSS. */
|
||||
.header-overflow-anchor {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
/* @hive/shared's Dropdown.css anchors left-edge-to-left-edge, correct
|
||||
for a picker with room to its right (StatusChips' model/effort). The
|
||||
overflow trigger is the header's right-most element instead, so a
|
||||
left anchor pushes the menu off-screen — flip to right-edge-to-
|
||||
right-edge here, scoped to this one consumer rather than changing
|
||||
the shared default (which is still correct for every other caller). */
|
||||
.header-overflow-anchor .ui-dropdown {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
66
frontend/packages/agent/src/components/OverflowMenu.tsx
Normal file
66
frontend/packages/agent/src/components/OverflowMenu.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// <OverflowMenu> — the header's `⋯` trigger. Deliberately trimmed vs.
|
||||
// app.js's `populateOverflowMenu`: that one held dashboard-link/rebuild/
|
||||
// new-session/logout/model-picker/effort-picker all in one flat list —
|
||||
// the design guide's own named anti-example for a junk drawer. The
|
||||
// model/effort pickers already moved to StatusChips' own Badge+Dropdown
|
||||
// controls (this rewrite's first commit); new-session/logout now live
|
||||
// as TermInput slash commands (typed, not menu-clicked, and already
|
||||
// have their own two-step confirm). What's left here — rebuild + a
|
||||
// dashboard back-link — are genuinely menu-shaped (rare, not tied to
|
||||
// any other visible control), so they're still a `⋯` menu, just a much
|
||||
// shorter one.
|
||||
//
|
||||
// Reuses `@hive/shared`'s `Badge`+`Dropdown` — same anchored-popover
|
||||
// pair StatusChips' model/effort pickers use, not a bespoke popover.
|
||||
// Rebuild's confirm is the same "select once to arm, select again to
|
||||
// fire" pattern as TermInput's `/new-session`/`/logout` — adapted here
|
||||
// to a menu click instead of a typed repeat, but the same "requires
|
||||
// deliberate repetition, not a modal" idea.
|
||||
import { useState } from 'preact/hooks';
|
||||
import { Badge } from '@hive/shared/badge.js';
|
||||
import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js';
|
||||
import { submitRebuild } from '../lib/rebuildAction.js';
|
||||
import './OverflowMenu.css';
|
||||
|
||||
export interface OverflowMenuProps {
|
||||
label: string;
|
||||
dashboardBase: string;
|
||||
}
|
||||
|
||||
export function OverflowMenu({ label, dashboardBase }: OverflowMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [armed, setArmed] = useState(false);
|
||||
|
||||
function close() {
|
||||
setOpen(false);
|
||||
setArmed(false);
|
||||
}
|
||||
|
||||
const options: DropdownOption[] = [
|
||||
{ value: 'dashboard', label: '↑ dashboard' },
|
||||
{ value: 'rebuild', label: armed ? '↻ rebuild — click again to confirm' : '↻ rebuild container' },
|
||||
];
|
||||
|
||||
function onSelect(value: string) {
|
||||
if (value === 'dashboard') {
|
||||
window.open(`${dashboardBase}dashboard.html`, '_blank', 'noopener');
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (value === 'rebuild') {
|
||||
if (armed) {
|
||||
submitRebuild(dashboardBase, label);
|
||||
close();
|
||||
} else {
|
||||
setArmed(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="header-overflow-anchor">
|
||||
<Badge value="⋯" title="more actions" onClick={() => setOpen((o) => !o)} expanded={open} />
|
||||
<Dropdown open={open} options={options} label="more actions" onSelect={onSelect} onClose={close} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
frontend/packages/agent/src/lib/rebuildAction.ts
Normal file
11
frontend/packages/agent/src/lib/rebuildAction.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Rebuild POST to the *dashboard's* origin — same cross-origin-needs-a-
|
||||
// real-form-submit reasoning as pauseAction.ts's `submitPauseResume`
|
||||
// (hive-c0re's `/api/rebuild/{name}` returns a plain 200, no CORS
|
||||
// header, so a cross-origin `fetch` can't read the result).
|
||||
export function submitRebuild(dashboardBase: string, label: string): void {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `${dashboardBase}api/rebuild/${label}`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
Loading…
Reference in a new issue