diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 0e5536cb..a83a5dd1 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -219,6 +219,36 @@ function DialogSample() { ); } +// `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, +// just moved into that header instead of floating over the whole thing. +function PlainDialogSample() { + const [open, setOpen] = useState(false); + return ( + <> + + setOpen(false)} + label="sample plain dialog" + plain + > + setOpen(false)} + > +

+ One card, one header bar — the close button lives here instead of + floating over a second, purposeless outer one. +

+
+
+ + ); +} + // Own state for the same reason `TextFieldSample` needs it — a // controlled sample has to actually respond to interaction. function MultiselectFilterSample() { @@ -458,6 +488,9 @@ export function ComponentsPage() { + + +
diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx index 768b5513..0f234f70 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentForm.tsx @@ -72,7 +72,14 @@ type SubmitState = // exception report on this page. const NAME_PATTERN = "[a-z0-9\\-]{1,63}"; -export function CreateAgentForm() { +export function CreateAgentForm({ + onClose, +}: { + /** Rendered as the form panel's own close button — this form always + * mounts inside a `plain` `Dialog` (see `AgentsPage`), which no longer + * floats its own. */ + onClose?: () => void; +}) { const [name, setName] = useState(""); const [hive, setHive] = useState(""); // `null` = still loading, `[]` = loaded but empty (a real, if unusual, @@ -132,7 +139,7 @@ export function CreateAgentForm() { return (
- +

Create a new agent's swarm-level identity. This only queues the job — check jobs to watch it settle. diff --git a/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx b/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx index 081c0b81..de08cdbe 100644 --- a/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx +++ b/frontend/packages/swarm-ui/src/pages/LinkMatrixAccountForm.tsx @@ -63,9 +63,14 @@ const MODE_OPTIONS = [ export function LinkMatrixAccountForm({ hive, agent, + onClose, }: { hive: string; agent: string; + /** Rendered as the panel header's own close button — this form always + * mounts inside a `plain` `Dialog` (see `AgentsPage`), which no longer + * floats its own. */ + onClose?: () => void; }) { const [account, setAccount] = useState(""); const [mode, setMode] = useState("token"); @@ -127,7 +132,11 @@ export function LinkMatrixAccountForm({ } return ( - +

Writes the credential to the swarm secret store and notifies{" "} {hive} to deliver it. The agent's own matrix daemon diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx index 6ed2fb62..4370d6bf 100644 --- a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx @@ -592,13 +592,15 @@ export function AgentsPage() { open={createOpen} onClose={() => setCreateOpen(false)} label="create agent" + plain > - + setCreateOpen(false)} />

setMatrixTarget(null)} label="link a matrix account" + plain > {/* `matrixTarget.hive` is non-null here — the trigger badge above is disabled without one, so this can only open with a real @@ -607,6 +609,7 @@ export function AgentsPage() { setMatrixTarget(null)} /> ) : null} diff --git a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css index 49706dca..2c82b413 100644 --- a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css +++ b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.css @@ -32,6 +32,16 @@ .ui-dialog::backdrop { background: rgba(0, 0, 0, 0.5); } +/* `plain` (see Dialog.tsx's file-top comment): drop this dialog's own + card look entirely — sizing/backdrop/scroll behavior above are the + only things left that are actually this component's job once the + content supplies its own bordered card (a `Panel`). */ +.ui-dialog-plain { + border: none; + border-radius: 0; + background: none; + padding: 0; +} .ui-dialog-close { position: absolute; top: 0.75em; @@ -60,3 +70,9 @@ render directly under the button instead of beside it. */ padding-right: 3.25em; } +/* `plain` has no floating close button of its own to clear — the + content's own `Panel` header carries one instead (see `.ui-panel-close` + in Panel.css). */ +.ui-dialog-body-plain { + padding-right: 0; +} diff --git a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx index f699a6d2..bf98cb81 100644 --- a/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx +++ b/frontend/packages/swarm-ui/src/ui/dialog/Dialog.tsx @@ -10,6 +10,18 @@ // from its former life as a standalone route — reusing that unchanged // is simpler and lower-risk than re-deriving a modal-specific layout // for content mara has already reviewed). +// +// `plain` (mara, screenshot of the matrix-account dialog: "the outer +// card does not serve a purpose. move the close btn into the bar and +// drop one layer"): content that's already a `Panel` — a bordered +// card with its own header bar — nested inside this dialog's own +// bordered card produced two concentric boxes in the same `--bg-elev`, +// one serving no purpose. `plain` drops this dialog's own card chrome +// (border/background/padding) and its floating close button, so a +// `Panel` (with its own `onClose` wired into its header) becomes the +// dialog's sole visible card instead of a second one nested inside it. +// Escape and backdrop-click still close it either way — those are the +// native ``'s own behavior, not this button's. import { useEffect, useRef } from "preact/hooks"; import type { ComponentChildren } from "preact"; import "./Dialog.css"; @@ -19,6 +31,7 @@ export function Dialog({ onClose, label, children, + plain = false, }: { open: boolean; onClose: () => void; @@ -26,6 +39,10 @@ export function Dialog({ // why content owns its own heading. label: string; children: ComponentChildren; + /** Drop this dialog's own card chrome + floating close button — see + * file-top comment. Pair with a `Panel` (or similar) that renders its + * own header + `onClose` button as the dialog's content. */ + plain?: boolean; }) { const ref = useRef(null); @@ -57,7 +74,7 @@ export function Dialog({ return ( ` element itself (not any child) // exactly when it's outside the rendered content box — inside @@ -67,15 +84,21 @@ export function Dialog({ if (e.target === ref.current) onClose(); }} > - + )} +
- ✕ - -
{children}
+ {children} +
); } diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.css b/frontend/packages/swarm-ui/src/ui/panel/Panel.css index 6b4b9f10..ebd739be 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.css +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.css @@ -20,14 +20,43 @@ line-height: 1; } /* `margin-left: auto` (not `justify-content: space-between` on the - header) so actions still land at the right edge even on the rare - panel that has actions but no title. */ -.ui-panel-actions { + header) so this cluster still lands at the right edge even on the + rare panel that has actions/onClose but no title. `actions` and the + close button (`onClose`) share this one wrapper rather than each + getting their own `margin-left: auto` slot — see Panel.tsx's file-top + comment. */ +.ui-panel-end { display: flex; align-items: center; gap: 0.5em; margin-left: auto; } +.ui-panel-actions { + display: flex; + align-items: center; + gap: 0.5em; +} +/* Same interactive treatment as `Dialog`'s own `.ui-dialog-close` + (border/background on hover), scaled down to fit a header row instead + of floating absolutely over the whole card. */ +.ui-panel-close { + display: flex; + align-items: center; + justify-content: center; + width: 1.8em; + height: 1.8em; + border: 1px solid transparent; + border-radius: 0.4em; + background: none; + color: var(--fg); + font-size: 1em; + line-height: 1; + cursor: pointer; +} +.ui-panel-close:hover { + border-color: var(--border); + background: var(--bg); +} .ui-panel-body { padding: 1em; } diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx index 56afdd7b..b1aa05fd 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx @@ -12,6 +12,8 @@ // content either). `HivesPage`'s refresh-interval picker is the // motivating caller. // +// `onClose`: end-of-header close (✕) for `Dialog`'s `plain` mode. +// // `icon` is a small header glyph, left of the title — the swarm-ui // design guide's own whimsy reference (before this, the guide only // pointed at the dashboard's matrix-rain background). Grew out of @@ -33,12 +35,16 @@ export function Panel({ title, icon, actions, + onClose, children, class: extraClass, }: { title?: string; icon?: string; actions?: ComponentChildren; + /** Renders a close (✕) button at the end of the header — see file-top + * comment. Omit for a panel that isn't a dialog's own content. */ + onClose?: () => void; children: ComponentChildren; /** Extra class on the outer `
` — e.g. flex-sizing a panel * that's one of several sharing a row (`AgentsPage`'s list/detail @@ -47,7 +53,7 @@ export function Panel({ }) { return (
- {title || icon || actions ? ( + {title || icon || actions || onClose ? (
{icon ? ( ) : null} {title ?

{title}

: null} - {actions ?
{actions}
: null} + {actions || onClose ? ( +
+ {actions ?
{actions}
: null} + {onClose ? ( + + ) : null} +
+ ) : null}
) : null}
{children}