swarm-ui: the agents page is one column of cards that open in place
The roster loses its second column and its table view. Selecting a card opens its details under it as four folded sections: overview, settings, accounts and the terminal. The status, message and wanted rows, which the card header already shows, are gone from the details. The terminal page's details dialog keeps the flat list.
This commit is contained in:
parent
04fcb4ca5c
commit
2eaa02e892
3 changed files with 555 additions and 541 deletions
|
|
@ -42,3 +42,14 @@
|
|||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The header is what opens the card: it reads as a control without a box. */
|
||||
.ui-agent-card-row[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The opened body, set off from the header by a rule. */
|
||||
.ui-agent-card-detail {
|
||||
margin-block-start: 0.75em;
|
||||
padding-block-start: 0.75em;
|
||||
border-block-start: 1px solid var(--border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,55 @@
|
|||
// <AgentCard> — one card per roster agent (mara: "main view: name,
|
||||
// status, message, wanted" / "message as second line" / "more like
|
||||
// card per agent") — the FilterableView `view === "cards"` alternative
|
||||
// to the original `Table`, not its replacement (see `AgentsPage`'s own
|
||||
// comment). Clicking a card opens a detail panel repeating this same
|
||||
// info plus what doesn't fit here (hive, config-PR link, matrix
|
||||
// link-account) — mara: "the info from main list should be included in
|
||||
// the agent view" too, not just the leftovers.
|
||||
// card per agent"). The header is those four things; selecting it opens the
|
||||
// card in place into the agent's details (mara: "one column of expandable
|
||||
// cards"), so there is no second panel to select into. The header already
|
||||
// shows the agent's status, message and wanted state, so the opened body is the
|
||||
// rest: `AgentDetail`'s folded sections (`inCard`).
|
||||
//
|
||||
// Built on the shared `Card` primitive (`ui/card/`) for the clickable/
|
||||
// selectable-container mechanics — the `stopPropagation` wrapper around
|
||||
// `WantedMenu` below keeps its own clicks (mouse or keyboard-synthesized,
|
||||
// `WantedMenu` hosts real `<button>`s) from also firing `Card`'s
|
||||
// `onClick` and opening the detail panel.
|
||||
// The `stopPropagation` wrappers around the login and `WantedMenu` controls
|
||||
// keep their own clicks (mouse or keyboard-synthesized, they host real
|
||||
// `<button>`s) from also toggling the card. The wanted menu carries the destroy
|
||||
// option here, since no detail row repeats it any more.
|
||||
import { useState } from "preact/hooks";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import type { ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { AgentIcon } from "../AgentIcon.js";
|
||||
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
||||
import { AgentDetail } from "../detail/AgentDetail.js";
|
||||
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
||||
import type { AgentActions } from "./AgentActions.js";
|
||||
import { WantedMenu } from "./WantedMenu.js";
|
||||
import "./AgentCard.css";
|
||||
|
||||
export function AgentCard({
|
||||
row,
|
||||
pending,
|
||||
error,
|
||||
selected,
|
||||
onSelectUp,
|
||||
onSelectOffline,
|
||||
onSelectPaused,
|
||||
onOpenDetail,
|
||||
actions,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
pending: boolean;
|
||||
error: ProblemDetails | undefined;
|
||||
/** This row is the one currently shown in the detail panel — a
|
||||
* highlight only (the panel itself is the source of truth), so a
|
||||
* page-refresh landing on a still-valid selection reads as obviously
|
||||
* "that one" rather than a silent selection nothing marks. */
|
||||
selected: boolean;
|
||||
onSelectUp: (row: AgentRow) => void;
|
||||
onSelectOffline: (row: AgentRow) => void;
|
||||
onSelectPaused: (row: AgentRow) => void;
|
||||
onOpenDetail: (row: AgentRow) => void;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
const { tone, label } = FRESHNESS[row.freshness];
|
||||
const [loggingIn, setLoggingIn] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const error = actions.actionErrors.get(row.name);
|
||||
const toggle = () => setOpen((was) => !was);
|
||||
return (
|
||||
<Card selected={selected} onClick={() => onOpenDetail(row)}>
|
||||
<div class="ui-agent-card-row">
|
||||
<Card selected={open}>
|
||||
<div
|
||||
class="ui-agent-card-row"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={open}
|
||||
onClick={toggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgentIcon name={row.name} />
|
||||
<div class="ui-agent-card-body">
|
||||
<div class="ui-agent-card-line1">
|
||||
|
|
@ -104,11 +103,11 @@ export function AgentCard({
|
|||
>
|
||||
<WantedMenu
|
||||
row={row}
|
||||
pending={pending}
|
||||
showDestroy={false}
|
||||
onSelectUp={onSelectUp}
|
||||
onSelectOffline={onSelectOffline}
|
||||
onSelectPaused={onSelectPaused}
|
||||
pending={actions.pendingAgents.has(row.name)}
|
||||
onSelectUp={actions.selectUp}
|
||||
onSelectOffline={actions.selectOffline}
|
||||
onSelectPaused={actions.selectPaused}
|
||||
onDestroy={actions.requestDestroy}
|
||||
/>
|
||||
{error ? (
|
||||
<Badge
|
||||
|
|
@ -124,6 +123,11 @@ export function AgentCard({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{open ? (
|
||||
<div class="ui-agent-card-detail">
|
||||
<AgentDetail row={row} actions={actions} withTerminal inCard />
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ export function WantedMenu({
|
|||
* only the table's "wanted" column needs it, to escape
|
||||
* `.ui-table-scroll`'s clip on the table's last row (the bug `Dropdown`'s
|
||||
* own `portal` prop exists to fix in the first place).
|
||||
* Neither the card list nor the detail panel has a clipping ancestor,
|
||||
* and inside the detail panel specifically `portal` is actively wrong:
|
||||
* Neither the card list nor the details dialog has a clipping ancestor,
|
||||
* and inside the details dialog specifically `portal` is actively wrong:
|
||||
* a `position: fixed` element appended to `document.body` renders
|
||||
* *behind* an open native `<dialog>` (the dialog is promoted to the
|
||||
* browser's top layer, which composites above ordinary body content
|
||||
* regardless of z-index) — found while screenshotting this exact
|
||||
* dropdown open inside the detail panel, only the portion extending
|
||||
* dropdown open inside the details dialog, only the portion extending
|
||||
* past the dialog's own edge was visible.
|
||||
*/
|
||||
portal?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue