diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 94cec45f..0bdca539 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -101,6 +101,14 @@ export function ComponentsPage() { panel body content, no title + + {}} />} + > + panel body content + +
diff --git a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx index 5df35f72..7149e62f 100644 --- a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx @@ -113,8 +113,10 @@ export function HivesPage() { }); return ( - - + } + > {error ? : null} {!error && hives === null ?

loading…

: null} {hives ? h.name} /> : null} diff --git a/frontend/packages/swarm-ui/src/ui/panel/Panel.css b/frontend/packages/swarm-ui/src/ui/panel/Panel.css index 23a40980..0132d514 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.css +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.css @@ -3,12 +3,26 @@ border-radius: 0.5em; background: var(--bg-elev); } +.ui-panel-header { + display: flex; + align-items: center; + gap: 1em; + padding: 0.75em 1em; + border-bottom: 1px solid var(--border); +} .ui-panel-title { margin: 0; - padding: 0.75em 1em; font-size: 1em; font-weight: 600; - border-bottom: 1px solid var(--border); +} +/* `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 { + display: flex; + align-items: center; + gap: 0.5em; + margin-left: auto; } .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 de8f88db..eea69b56 100644 --- a/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx +++ b/frontend/packages/swarm-ui/src/ui/panel/Panel.tsx @@ -2,13 +2,35 @@ // bordered surface with an optional title, no other opinions. Not a // card-with-actions/footer/whatever kit — those get added the first // time a real page actually needs one, not speculatively ahead of it. +// +// `actions` is that one addition: a slot in the title row, right- +// aligned, for a control that belongs next to the panel's own heading +// rather than its own row inside the body (design guide: "a control +// belongs next to the thing it affects, not tucked into a catch-all +// menu" — and per mara's review on the refresh-interval PR, not a +// dedicated row stealing vertical space from the panel's actual +// content either). `HivesPage`'s refresh-interval picker is the +// motivating caller. import type { ComponentChildren } from 'preact'; import './Panel.css'; -export function Panel({ title, children }: { title?: string; children: ComponentChildren }) { +export function Panel({ + title, + actions, + children, +}: { + title?: string; + actions?: ComponentChildren; + children: ComponentChildren; +}) { return (
- {title ?

{title}

: null} + {title || actions ? ( +
+ {title ?

{title}

: null} + {actions ?
{actions}
: null} +
+ ) : null}
{children}
); diff --git a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css new file mode 100644 index 00000000..0e1a134d --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.css @@ -0,0 +1,42 @@ +/* Compact "clock icon · value · chevron" control — see RefreshInterval.tsx + for why this isn't `SelectField`'s label+bordered-control chrome. Quiet + until interacted with (no border/fill at rest), matching LinksMenu's + header-button treatment so it reads as chrome, not a form. */ +.ui-refresh-picker { + position: relative; + display: inline-flex; + align-items: center; + gap: 0.35em; + min-height: 2.75em; + padding: 0 0.6em; + border: 1px solid transparent; + border-radius: 0.4em; + color: var(--muted); + font-size: 0.9em; + cursor: pointer; +} +.ui-refresh-picker:hover, +.ui-refresh-picker:focus-within { + border-color: var(--border); + background: var(--bg); + color: var(--fg); +} +/* The select drives the interaction (native listbox, keyboard, a11y) but + contributes no chrome of its own — its own box is invisible, sized to + just its selected option's text, and its default arrow is stripped + since the chevron span stands in for it. */ +.ui-refresh-picker-select { + appearance: none; + border: none; + background: none; + color: inherit; + font: inherit; + padding: 0; + margin: 0; + cursor: pointer; +} +.ui-refresh-picker-icon, +.ui-refresh-picker-chevron { + font-size: 0.85em; + line-height: 1; +} diff --git a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx index ca98dac1..5a0979bd 100644 --- a/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx +++ b/frontend/packages/swarm-ui/src/ui/refresh-interval/RefreshInterval.tsx @@ -8,12 +8,25 @@ // file since neither is useful without the other and splitting them // would be two files for one concern. // +// The picker itself is a compact "clock icon · value · chevron" inline +// control, not a labelled `SelectField` — per mara's review, a full +// label+bordered-control form field reads as way too heavy for a +// passive-until-touched setting that lives in a panel's title row +// (`Panel`'s `actions` slot), not a form. Visually quiet (no border/ +// fill until hovered/focused) the same way `LinksMenu`'s header button +// is, so it reads as chrome rather than another input to fill in. A +// native `` for the accessible + // name a sighted-only icon+chevron can't supply). + ); }