swarm-ui: drop the redundant outer dialog card, move close into the panel header
Dialog and Panel both drew their own bordered/backgrounded card in the same --bg-elev, so a Panel-based dialog (create-agent, link-matrix-account) rendered as two concentric cards with a floating close button on the outer one and no purpose to it. Give Dialog a "plain" mode that drops its own card chrome (border, background, padding) and floating close button, and give Panel an optional onClose that renders a close button at the end of its own header row instead. AgentsPage's two Panel-backed dialogs now use plain + Panel's onClose, so the Panel is the dialog's only visible card. ConfirmDialog (no Panel of its own) is unaffected — plain defaults to false, unchanged card + floating close button. Added a ComponentsPage sample demonstrating the plain + onClose pairing. Verified both dialog modes via a real headless-chromium screenshot (plain dialog: single card, close button in the header bar; default dialog: unchanged floating close button).
This commit is contained in:
parent
09d502ea34
commit
8a4c613e4e
8 changed files with 158 additions and 18 deletions
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => setOpen(true)}>open plain dialog</Button>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
label="sample plain dialog"
|
||||||
|
plain
|
||||||
|
>
|
||||||
|
<Panel
|
||||||
|
title="a form-shaped dialog"
|
||||||
|
icon="🔗"
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
One card, one header bar — the close button lives here instead of
|
||||||
|
floating over a second, purposeless outer one.
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Own state for the same reason `TextFieldSample` needs it — a
|
// Own state for the same reason `TextFieldSample` needs it — a
|
||||||
// controlled sample has to actually respond to interaction.
|
// controlled sample has to actually respond to interaction.
|
||||||
function MultiselectFilterSample() {
|
function MultiselectFilterSample() {
|
||||||
|
|
@ -458,6 +488,9 @@ export function ComponentsPage() {
|
||||||
<Sample label="modal, caller-owned content">
|
<Sample label="modal, caller-owned content">
|
||||||
<DialogSample />
|
<DialogSample />
|
||||||
</Sample>
|
</Sample>
|
||||||
|
<Sample label="plain — content supplies its own card + close">
|
||||||
|
<PlainDialogSample />
|
||||||
|
</Sample>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="ConfirmDialog">
|
<Section title="ConfirmDialog">
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,14 @@ type SubmitState =
|
||||||
// exception report on this page.
|
// exception report on this page.
|
||||||
const NAME_PATTERN = "[a-z0-9\\-]{1,63}";
|
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 [name, setName] = useState("");
|
||||||
const [hive, setHive] = useState("");
|
const [hive, setHive] = useState("");
|
||||||
// `null` = still loading, `[]` = loaded but empty (a real, if unusual,
|
// `null` = still loading, `[]` = loaded but empty (a real, if unusual,
|
||||||
|
|
@ -132,7 +139,7 @@ export function CreateAgentForm() {
|
||||||
return (
|
return (
|
||||||
<div class="create-agent-layout">
|
<div class="create-agent-layout">
|
||||||
<div class="create-agent-form-col">
|
<div class="create-agent-form-col">
|
||||||
<Panel title="create agent" icon="🤖">
|
<Panel title="create agent" icon="🤖" onClose={onClose}>
|
||||||
<p class="create-agent-intro">
|
<p class="create-agent-intro">
|
||||||
Create a new agent's swarm-level identity. This only queues the job
|
Create a new agent's swarm-level identity. This only queues the job
|
||||||
— check <Link href="/jobs">jobs</Link> to watch it settle.
|
— check <Link href="/jobs">jobs</Link> to watch it settle.
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,14 @@ const MODE_OPTIONS = [
|
||||||
export function LinkMatrixAccountForm({
|
export function LinkMatrixAccountForm({
|
||||||
hive,
|
hive,
|
||||||
agent,
|
agent,
|
||||||
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
hive: string;
|
hive: string;
|
||||||
agent: 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 [account, setAccount] = useState("");
|
||||||
const [mode, setMode] = useState<Mode>("token");
|
const [mode, setMode] = useState<Mode>("token");
|
||||||
|
|
@ -127,7 +132,11 @@ export function LinkMatrixAccountForm({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel title={`link a matrix account — ${agent}`} icon="🔗">
|
<Panel
|
||||||
|
title={`link a matrix account — ${agent}`}
|
||||||
|
icon="🔗"
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Writes the credential to the swarm secret store and notifies{" "}
|
Writes the credential to the swarm secret store and notifies{" "}
|
||||||
<strong>{hive}</strong> to deliver it. The agent's own matrix daemon
|
<strong>{hive}</strong> to deliver it. The agent's own matrix daemon
|
||||||
|
|
|
||||||
|
|
@ -592,13 +592,15 @@ export function AgentsPage() {
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
onClose={() => setCreateOpen(false)}
|
onClose={() => setCreateOpen(false)}
|
||||||
label="create agent"
|
label="create agent"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
<CreateAgentForm />
|
<CreateAgentForm onClose={() => setCreateOpen(false)} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={matrixTarget !== null}
|
open={matrixTarget !== null}
|
||||||
onClose={() => setMatrixTarget(null)}
|
onClose={() => setMatrixTarget(null)}
|
||||||
label="link a matrix account"
|
label="link a matrix account"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
{/* `matrixTarget.hive` is non-null here — the trigger badge above
|
{/* `matrixTarget.hive` is non-null here — the trigger badge above
|
||||||
is disabled without one, so this can only open with a real
|
is disabled without one, so this can only open with a real
|
||||||
|
|
@ -607,6 +609,7 @@ export function AgentsPage() {
|
||||||
<LinkMatrixAccountForm
|
<LinkMatrixAccountForm
|
||||||
hive={matrixTarget.hive}
|
hive={matrixTarget.hive}
|
||||||
agent={matrixTarget.name}
|
agent={matrixTarget.name}
|
||||||
|
onClose={() => setMatrixTarget(null)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,16 @@
|
||||||
.ui-dialog::backdrop {
|
.ui-dialog::backdrop {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
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 {
|
.ui-dialog-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.75em;
|
top: 0.75em;
|
||||||
|
|
@ -60,3 +70,9 @@
|
||||||
render directly under the button instead of beside it. */
|
render directly under the button instead of beside it. */
|
||||||
padding-right: 3.25em;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,18 @@
|
||||||
// from its former life as a standalone route — reusing that unchanged
|
// from its former life as a standalone route — reusing that unchanged
|
||||||
// is simpler and lower-risk than re-deriving a modal-specific layout
|
// is simpler and lower-risk than re-deriving a modal-specific layout
|
||||||
// for content mara has already reviewed).
|
// 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 `<dialog>`'s own behavior, not this button's.
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
import type { ComponentChildren } from "preact";
|
import type { ComponentChildren } from "preact";
|
||||||
import "./Dialog.css";
|
import "./Dialog.css";
|
||||||
|
|
@ -19,6 +31,7 @@ export function Dialog({
|
||||||
onClose,
|
onClose,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
|
plain = false,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -26,6 +39,10 @@ export function Dialog({
|
||||||
// why content owns its own heading.
|
// why content owns its own heading.
|
||||||
label: string;
|
label: string;
|
||||||
children: ComponentChildren;
|
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<HTMLDialogElement>(null);
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
|
@ -57,7 +74,7 @@ export function Dialog({
|
||||||
return (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
ref={ref}
|
ref={ref}
|
||||||
class="ui-dialog"
|
class={plain ? "ui-dialog ui-dialog-plain" : "ui-dialog"}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
// A click lands on the `<dialog>` element itself (not any child)
|
// A click lands on the `<dialog>` element itself (not any child)
|
||||||
// exactly when it's outside the rendered content box — inside
|
// exactly when it's outside the rendered content box — inside
|
||||||
|
|
@ -67,15 +84,21 @@ export function Dialog({
|
||||||
if (e.target === ref.current) onClose();
|
if (e.target === ref.current) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
{plain ? null : (
|
||||||
type="button"
|
<button
|
||||||
class="ui-dialog-close"
|
type="button"
|
||||||
aria-label="close"
|
class="ui-dialog-close"
|
||||||
onClick={onClose}
|
aria-label="close"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
class={plain ? "ui-dialog-body ui-dialog-body-plain" : "ui-dialog-body"}
|
||||||
>
|
>
|
||||||
✕
|
{children}
|
||||||
</button>
|
</div>
|
||||||
<div class="ui-dialog-body">{children}</div>
|
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,43 @@
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
/* `margin-left: auto` (not `justify-content: space-between` on the
|
/* `margin-left: auto` (not `justify-content: space-between` on the
|
||||||
header) so actions still land at the right edge even on the rare
|
header) so this cluster still lands at the right edge even on the
|
||||||
panel that has actions but no title. */
|
rare panel that has actions/onClose but no title. `actions` and the
|
||||||
.ui-panel-actions {
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin-left: auto;
|
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 {
|
.ui-panel-body {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
// content either). `HivesPage`'s refresh-interval picker is the
|
// content either). `HivesPage`'s refresh-interval picker is the
|
||||||
// motivating caller.
|
// 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
|
// `icon` is a small header glyph, left of the title — the swarm-ui
|
||||||
// design guide's own whimsy reference (before this, the guide only
|
// design guide's own whimsy reference (before this, the guide only
|
||||||
// pointed at the dashboard's matrix-rain background). Grew out of
|
// pointed at the dashboard's matrix-rain background). Grew out of
|
||||||
|
|
@ -33,12 +35,16 @@ export function Panel({
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
actions,
|
actions,
|
||||||
|
onClose,
|
||||||
children,
|
children,
|
||||||
class: extraClass,
|
class: extraClass,
|
||||||
}: {
|
}: {
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
actions?: ComponentChildren;
|
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;
|
children: ComponentChildren;
|
||||||
/** Extra class on the outer `<section>` — e.g. flex-sizing a panel
|
/** Extra class on the outer `<section>` — e.g. flex-sizing a panel
|
||||||
* that's one of several sharing a row (`AgentsPage`'s list/detail
|
* that's one of several sharing a row (`AgentsPage`'s list/detail
|
||||||
|
|
@ -47,7 +53,7 @@ export function Panel({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section class={extraClass ? `ui-panel ${extraClass}` : "ui-panel"}>
|
<section class={extraClass ? `ui-panel ${extraClass}` : "ui-panel"}>
|
||||||
{title || icon || actions ? (
|
{title || icon || actions || onClose ? (
|
||||||
<div class="ui-panel-header">
|
<div class="ui-panel-header">
|
||||||
{icon ? (
|
{icon ? (
|
||||||
<span class="ui-panel-icon" aria-hidden="true">
|
<span class="ui-panel-icon" aria-hidden="true">
|
||||||
|
|
@ -55,7 +61,21 @@ export function Panel({
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{title ? <h2 class="ui-panel-title">{title}</h2> : null}
|
{title ? <h2 class="ui-panel-title">{title}</h2> : null}
|
||||||
{actions ? <div class="ui-panel-actions">{actions}</div> : null}
|
{actions || onClose ? (
|
||||||
|
<div class="ui-panel-end">
|
||||||
|
{actions ? <div class="ui-panel-actions">{actions}</div> : null}
|
||||||
|
{onClose ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-panel-close"
|
||||||
|
aria-label="close"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div class="ui-panel-body">{children}</div>
|
<div class="ui-panel-body">{children}</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue