swarm-ui: extract ConfirmDialog, use it for the destroy confirm

mara, reviewing the swarm-ui destroy-trigger PR: "why the extra
styling? shouldnt there be a component that does this already?" There
wasnt one -- Dialog is deliberately content-agnostic (see its own
file-top comment), so the destroy confirm had grown its own
page-scoped .agents-destroy-confirm/-actions CSS for what is really a
generic "message + cancel/confirm button row" shape.

Extracted ui/confirm-dialog/ConfirmDialog.tsx: wraps Dialog, owns the
button row, leaves the message body to the caller via children.
AgentsPage now uses it instead of a bare Dialog + bespoke CSS; deleted
the now-unused AgentsPage.css.
This commit is contained in:
iris 2026-09-07 19:02:23 +02:00
commit 82a4324b17
4 changed files with 81 additions and 39 deletions

View file

@ -1,17 +0,0 @@
/* <AgentsPage> the destroy-confirm dialog's own layout. Everything
else on this page (the table, the wanted/destroy badges) draws chrome
from the shared `ui/` kit and needs nothing page-scoped; this file
exists only because the confirm dialog's copy + button row needed
somewhere to live, same reasoning as CreateAgentForm.css owning its
own layout next to the shared form-kit chrome it wraps. */
.agents-destroy-confirm {
display: flex;
flex-direction: column;
gap: 1em;
max-width: 28em;
}
.agents-destroy-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 0.75em;
}

View file

@ -33,6 +33,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge, type BadgeTone } from "@hive/shared/badge.js"; import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Button } from "../ui/button/Button.js"; import { Button } from "../ui/button/Button.js";
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
import { Dialog } from "../ui/dialog/Dialog.js"; import { Dialog } from "../ui/dialog/Dialog.js";
import { Panel } from "../ui/panel/Panel.js"; import { Panel } from "../ui/panel/Panel.js";
import { RelativeTime } from "../ui/relative-time/RelativeTime.js"; import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
@ -43,7 +44,6 @@ import {
} from "../ui/refresh-interval/RefreshInterval.js"; } from "../ui/refresh-interval/RefreshInterval.js";
import { Table, type TableColumn } from "../ui/table/Table.js"; import { Table, type TableColumn } from "../ui/table/Table.js";
import { CreateAgentForm } from "./CreateAgentForm.js"; import { CreateAgentForm } from "./CreateAgentForm.js";
import "./AgentsPage.css";
interface ConfigPrStatus { interface ConfigPrStatus {
pr_number: number; pr_number: number;
@ -415,32 +415,23 @@ export function AgentsPage() {
> >
<CreateAgentForm /> <CreateAgentForm />
</Dialog> </Dialog>
<Dialog <ConfirmDialog
open={destroyTarget !== null} open={destroyTarget !== null}
onClose={() => setDestroyTarget(null)}
label="destroy agent" label="destroy agent"
onCancel={() => setDestroyTarget(null)}
onConfirm={() => destroyTarget && void destroyAgent(destroyTarget)}
confirmLabel="destroy"
> >
{destroyTarget ? ( {destroyTarget ? (
<div class="agents-destroy-confirm"> <p>
<p> Destroy <strong>{destroyTarget.name}</strong>? The hive tears its
Destroy <strong>{destroyTarget.name}</strong>? The hive tears its container down on its next reconcile sweep. This is not reversible
container down on its next reconcile sweep. This is not reversible from here bringing it back means redeploying via "+ agent", which
from here bringing it back means redeploying via "+ agent", reuses the agent's existing identity, config repo, and forge
which reuses the agent's existing identity, config repo, and forge collaborator access rather than starting over.
collaborator access rather than starting over. </p>
</p>
<div class="agents-destroy-confirm-actions">
<Button onClick={() => setDestroyTarget(null)}>cancel</Button>
<Button
variant="primary"
onClick={() => void destroyAgent(destroyTarget)}
>
destroy
</Button>
</div>
</div>
) : null} ) : null}
</Dialog> </ConfirmDialog>
</Panel> </Panel>
); );
} }

View file

@ -0,0 +1,13 @@
/* <ConfirmDialog> body + button-row layout only; `Dialog.css` owns the
surrounding modal chrome, `Button.css` owns the buttons themselves. */
.confirm-dialog {
display: flex;
flex-direction: column;
gap: 1em;
max-width: 28em;
}
.confirm-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.75em;
}

View file

@ -0,0 +1,55 @@
// <ConfirmDialog> — a `Dialog` pre-wired for the "message + cancel/confirm
// button row" shape every confirm-before-acting flow needs. Extracted out
// of AgentsPage's destroy confirmation on mara's review question ("why the
// extra styling? shouldnt there be a component that does this already?")
// — there wasn't one yet, so that page had grown its own
// `.agents-destroy-confirm`/`-actions` layout CSS for what turns out to be
// a generic shape. `Dialog` itself stays opinion-free about content (see
// its own file-top comment); this is the one layer up that isn't, so a
// future confirm flow gets the button row for free instead of another
// page-scoped CSS file.
//
// The message itself stays caller-owned via `children` — only the dialog
// wiring, spacing, and button row are shared.
import type { ComponentChildren } from "preact";
import { Button } from "../button/Button.js";
import { Dialog } from "../dialog/Dialog.js";
import "./ConfirmDialog.css";
export function ConfirmDialog({
open,
label,
onCancel,
onConfirm,
confirmLabel = "confirm",
cancelLabel = "cancel",
confirmDisabled = false,
children,
}: {
open: boolean;
label: string;
onCancel: () => void;
onConfirm: () => void;
confirmLabel?: string;
cancelLabel?: string;
confirmDisabled?: boolean;
children: ComponentChildren;
}) {
return (
<Dialog open={open} onClose={onCancel} label={label}>
<div class="confirm-dialog">
<div class="confirm-dialog-body">{children}</div>
<div class="confirm-dialog-actions">
<Button onClick={onCancel}>{cancelLabel}</Button>
<Button
variant="primary"
onClick={onConfirm}
disabled={confirmDisabled}
>
{confirmLabel}
</Button>
</div>
</div>
</Dialog>
);
}