swarm-ui: show an agent's details in a popover, not a modal dialog
The details open under the agent badge without dimming or blocking the page; a click outside or Escape closes it. New ui/popover primitive (anchored, portal, no backdrop) with a sample on the components page. A confirm or link dialog opened from inside it stays modal, and the popover ignores clicks and Escape that belong to that dialog. AgentDetailDialog becomes AgentDetailPopover.
This commit is contained in:
parent
74e588415b
commit
f957e19d0d
7 changed files with 221 additions and 42 deletions
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { Button, type ButtonVariant } from "../ui/button/Button.js";
|
||||
import { Dialog } from "../ui/dialog/Dialog.js";
|
||||
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
|
||||
import { Popover } from "../ui/popover/Popover.js";
|
||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||
import { GearIcon } from "@hive/shared/icons.js";
|
||||
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
|
||||
|
|
@ -231,6 +232,33 @@ function DialogSample() {
|
|||
);
|
||||
}
|
||||
|
||||
// `Popover` anchors to the trigger it is given and toggles from it, so the
|
||||
// sample needs the trigger's ref and its own `open` state.
|
||||
function PopoverSample() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorRef = useRef<HTMLSpanElement>(null);
|
||||
return (
|
||||
<>
|
||||
<span ref={anchorRef}>
|
||||
<Button onClick={() => setOpen((o) => !o)}>toggle popover</Button>
|
||||
</span>
|
||||
<Popover
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorRef={anchorRef}
|
||||
label="sample popover"
|
||||
>
|
||||
<Panel title="a popover" icon="🔗" onClose={() => setOpen(false)}>
|
||||
<p>
|
||||
Floats under its trigger and leaves the page behind it clickable.
|
||||
Outside click or Escape closes it.
|
||||
</p>
|
||||
</Panel>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// `plain`: a `Panel`'s own header bar/border is the dialog's
|
||||
// only card, not a second one nested inside `Dialog`'s own — pair
|
||||
// `plain` with `Panel`'s `onClose` so the close button still exists,
|
||||
|
|
@ -511,6 +539,12 @@ export function ComponentsPage() {
|
|||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="Popover">
|
||||
<Sample label="anchored, no backdrop, content supplies its own card">
|
||||
<PopoverSample />
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="ConfirmDialog">
|
||||
<Sample label="modal, cancel/confirm button row">
|
||||
<ConfirmDialogSample />
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
// plain page reload should all re-open the tab instead of landing on a
|
||||
// page with no way back into the tab row. `useDynamicTabs().open()` is
|
||||
// idempotent (dedupes by href), so this is safe to call every mount.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { RouteComponentProps } from "wouter-preact";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import { AgentDetailDialog } from "../../agent/detail/AgentDetailDialog.js";
|
||||
import { AgentDetailPopover } from "../../agent/detail/AgentDetailPopover.js";
|
||||
import { AgentIcon } from "../../agent/AgentIcon.js";
|
||||
import {
|
||||
AgentTermPreview,
|
||||
|
|
@ -48,10 +48,12 @@ export function AgentTerminalPage({
|
|||
// instead of once per terminal `page`.
|
||||
}, [page]);
|
||||
|
||||
// The agent's details open in a dialog over the terminal, so reading or
|
||||
// changing them never means leaving it. A subagent's terminal opens its
|
||||
// agent's details: a subagent has none of its own.
|
||||
// The agent's details open in a popover under its badge, so reading or
|
||||
// changing them never means leaving the terminal, or dimming it. A
|
||||
// subagent's terminal opens its agent's details: a subagent has none of its
|
||||
// own.
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const badgeRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
return (
|
||||
<div class="ui-agent-terminal-page">
|
||||
|
|
@ -61,18 +63,22 @@ export function AgentTerminalPage({
|
|||
fullHeight
|
||||
showHeaderBadges
|
||||
leading={
|
||||
<Badge
|
||||
icon={<AgentIcon name={name} small />}
|
||||
value={name}
|
||||
title={`open ${name}'s details`}
|
||||
onClick={() => setDetailOpen(true)}
|
||||
/>
|
||||
<span ref={badgeRef}>
|
||||
<Badge
|
||||
icon={<AgentIcon name={name} small />}
|
||||
value={name}
|
||||
title={`${detailOpen ? "close" : "open"} ${name}'s details`}
|
||||
expanded={detailOpen}
|
||||
onClick={() => setDetailOpen((o) => !o)}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<AgentDetailDialog
|
||||
<AgentDetailPopover
|
||||
agent={name}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
anchorRef={badgeRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue