swarm-ui: extract Card/MultiselectFilter/SplitView per component-first design
Mara: "result looks like the shape i am looking for, but the code does not. you did not follow component first principle" - the card's clickable/selectable mechanics, the card-view filter trigger+popover, and the list+detail split layout were all one-off page-local JSX in AgentsPage.tsx instead of docs/web-ui/design-guide.md's "Component-first design" primitives. Three new ui/ components, each with a same-day /components demo section per that doc's own rule: - ui/card/Card.tsx - the role=button/keyboard-activation/selected mechanics AgentCard now wraps agent-specific content around, instead of owning them itself. - ui/multiselect-filter/MultiselectFilter.tsx - the checkbox-list trigger+popover control. This was also a straight duplicate of Table's own inline popover content once AgentsPage's filter toolbar needed the identical thing; Table now renders the same MultiselectFilterOptions piece too (keeping its own th-anchored trigger and fixed+portal positioning, which are genuinely table-specific), not a second copy. - ui/split-view/SplitView.tsx - the list+detail flex-wrap layout, no opinion on what's inside either pane. AgentsPage.tsx's own CSS shrinks to just the agent-specific content inside these primitives (card line/message layout, detail-panel field grid, the name-search input) - the container/positioning rules moved to each component's own colocated CSS. No behavior change for any other Table caller (HivesPage, IssueReportPage, the components demo's own Table samples) - the popover's visual output is identical, just sourced from the shared component instead of inline JSX. Verified: typecheck/build clean, real screenshots of both AgentsPage (pixel-identical to before) and the three new /components sections, plus a live click confirming MultiselectFilter's popover opens correctly on the demo page too.
This commit is contained in:
parent
60d7e8ce84
commit
7003560569
11 changed files with 646 additions and 400 deletions
|
|
@ -7,12 +7,18 @@
|
|||
// swarm-controller's API is up or not.
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { Card } from "../ui/card/Card.js";
|
||||
import {
|
||||
MultiselectFilter,
|
||||
type MultiselectFilterState,
|
||||
} from "../ui/multiselect-filter/MultiselectFilter.js";
|
||||
import { Panel } from "../ui/panel/Panel.js";
|
||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
||||
import {
|
||||
RefreshIntervalPicker,
|
||||
type RefreshIntervalMs,
|
||||
} from "../ui/refresh-interval/RefreshInterval.js";
|
||||
import { SplitView } from "../ui/split-view/SplitView.js";
|
||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||
import { TextField } from "../ui/text-field/TextField.js";
|
||||
import {
|
||||
|
|
@ -212,6 +218,31 @@ function DialogSample() {
|
|||
);
|
||||
}
|
||||
|
||||
// Own state for the same reason `TextFieldSample` needs it — a
|
||||
// controlled sample has to actually respond to interaction.
|
||||
function MultiselectFilterSample() {
|
||||
const [filter, setFilter] = useState<MultiselectFilterState>({
|
||||
values: [],
|
||||
negate: false,
|
||||
});
|
||||
return (
|
||||
<MultiselectFilter
|
||||
label="hive"
|
||||
options={["pr1ma", "sec0nd"]}
|
||||
filter={filter}
|
||||
onToggle={(v) =>
|
||||
setFilter((f) => ({
|
||||
...f,
|
||||
values: f.values.includes(v)
|
||||
? f.values.filter((x) => x !== v)
|
||||
: [...f.values, v],
|
||||
}))
|
||||
}
|
||||
onNegateChange={(negate) => setFilter((f) => ({ ...f, negate }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmDialogSample() {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
|
|
@ -269,6 +300,23 @@ export function ComponentsPage() {
|
|||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="SplitView">
|
||||
<Sample label="primary + secondary, side by side (wraps to stacked on a narrow viewport)">
|
||||
<SplitView
|
||||
primary={
|
||||
<Panel title="list">
|
||||
primary content — e.g. AgentsPage's roster
|
||||
</Panel>
|
||||
}
|
||||
secondary={
|
||||
<Panel title="detail">
|
||||
secondary content — e.g. the selected row
|
||||
</Panel>
|
||||
}
|
||||
/>
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="Table">
|
||||
<Sample label="populated">
|
||||
<Table
|
||||
|
|
@ -288,6 +336,35 @@ export function ComponentsPage() {
|
|||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="Card">
|
||||
<div class="components-chip-row">
|
||||
<Sample label="static (no onClick)">
|
||||
<Card>
|
||||
<strong>alpha</strong>
|
||||
<div>plain content, not clickable</div>
|
||||
</Card>
|
||||
</Sample>
|
||||
<Sample label="interactive">
|
||||
<Card onClick={() => {}}>
|
||||
<strong>beta</strong>
|
||||
<div>click, or Enter/Space when focused</div>
|
||||
</Card>
|
||||
</Sample>
|
||||
<Sample label="selected (e.g. the row shown in a detail panel)">
|
||||
<Card selected onClick={() => {}}>
|
||||
<strong>gamma</strong>
|
||||
<div>same card, with selected set</div>
|
||||
</Card>
|
||||
</Sample>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="MultiselectFilter">
|
||||
<Sample label="trigger + checkbox popover">
|
||||
<MultiselectFilterSample />
|
||||
</Sample>
|
||||
</Section>
|
||||
|
||||
<Section title="RelativeTime">
|
||||
<div class="components-chip-row">
|
||||
<Sample label="5s ago, ticking">
|
||||
|
|
|
|||
Loading…
Reference in a new issue