hyperhive/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx
iris fc44891ab5 swarm-ui: compact refresh-interval picker, moved into the panel header
Per mara's review on this PR: the labelled SelectField-based picker
read as way too heavy for a passive-until-touched setting - a full
label+bordered-control form field row above the table. Replace it with
a compact inline "clock icon - value - chevron" control (a native
<select> still drives the interaction, just stripped of SelectField/
FormField's chrome), quiet until hovered/focused the same way
LinksMenu's header button is.

Also move it out of the panel body entirely: Panel gains an `actions`
slot in its title row (right-aligned via margin-left: auto), so the
picker sits next to the "hives" heading instead of taking its own row
and pushing the table down - per the design guide's own "a control
belongs next to the thing it affects" rule.

Verified with headless chromium screenshots (full page + a tight
close-up crop) against the built bundle: the picker now reads as
"(clock) 30s (chevron)" inline with the panel title, no extra vertical
space taken from the table. Added a Panel "with actions" demo to
ComponentsPage.
2026-08-18 23:28:47 +02:00

176 lines
5.8 KiB
TypeScript

// <ComponentsPage> — living style guide for every ui primitive this
// package ships. One section per component, each mode it supports
// rendered side by side with a caption naming the mode — so a visual
// regression or a missing variant is obvious at a glance, and a new
// primitive gets a section here the same day it's added. Sample data
// only, no network calls — this page must render the same whether
// swarm-controller's API is up or not.
import { useState } from 'preact/hooks';
import type { ComponentChildren } from 'preact';
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 { StatusChip, type ChipTone } from '../ui/status-chip/StatusChip.js';
import { Table, type TableColumn } from '../ui/table/Table.js';
import { TextField } from '../ui/text-field/TextField.js';
import { SelectField, type SelectOption } from '../ui/select-field/SelectField.js';
import { Button, type ButtonVariant } from '../ui/button/Button.js';
import './ComponentsPage.css';
function Section({ title, children }: { title: string; children: ComponentChildren }) {
return (
<section class="components-section">
<h2 class="components-section-title">{title}</h2>
<div class="components-section-body">{children}</div>
</section>
);
}
function Sample({ label, children }: { label: string; children: ComponentChildren }) {
return (
<div class="components-sample">
<div class="components-sample-label">{label}</div>
<div class="components-sample-content">{children}</div>
</div>
);
}
const CHIP_TONES: ChipTone[] = ['neutral', 'positive', 'warning', 'negative'];
interface Row {
name: string;
detail: string;
}
const TABLE_COLUMNS: TableColumn<Row>[] = [
{ key: 'name', header: 'name', render: (r) => r.name },
{ key: 'detail', header: 'detail', render: (r) => r.detail },
{ key: 'status', header: 'status', render: () => <StatusChip tone="positive" label="ok" /> },
];
const TABLE_ROWS: Row[] = [
{ name: 'alpha', detail: 'sample row one' },
{ name: 'beta', detail: 'sample row two' },
];
const SELECT_OPTIONS: SelectOption[] = [
{ value: 'alpha', label: 'alpha' },
{ value: 'beta', label: 'beta' },
];
const BUTTON_VARIANTS: ButtonVariant[] = ['primary', 'default'];
// Controlled samples need their own state to actually type/select into —
// module-level consts can't do that, hence these two small wrappers
// rather than inline JSX in the page body below.
function TextFieldSample() {
const [value, setValue] = useState('');
return <TextField id="sample-text-field" label="agent name" value={value} onInput={setValue} />;
}
function SelectFieldSample() {
const [value, setValue] = useState('alpha');
return (
<SelectField
id="sample-select-field"
label="hive"
value={value}
onChange={setValue}
options={SELECT_OPTIONS}
/>
);
}
function RefreshIntervalPickerSample() {
const [value, setValue] = useState<RefreshIntervalMs>(30_000);
return <RefreshIntervalPicker id="sample-refresh-interval" value={value} onChange={setValue} />;
}
export function ComponentsPage() {
return (
<Panel title="components">
<p class="components-intro">
Every primitive in <code>src/ui/</code>, shown in each mode it supports. Sample data
only nothing here calls the API.
</p>
<Section title="Panel">
<Sample label="with title">
<Panel title="example title">panel body content</Panel>
</Sample>
<Sample label="without title">
<Panel>panel body content, no title</Panel>
</Sample>
<Sample label="with actions (e.g. HivesPage's refresh picker)">
<Panel
title="example title"
actions={<RefreshIntervalPicker id="sample-panel-actions" value={30_000} onChange={() => {}} />}
>
panel body content
</Panel>
</Sample>
</Section>
<Section title="StatusChip">
<div class="components-chip-row">
{CHIP_TONES.map((tone) => (
<Sample key={tone} label={tone}>
<StatusChip tone={tone} label={tone} />
</Sample>
))}
</div>
</Section>
<Section title="Table">
<Sample label="populated">
<Table columns={TABLE_COLUMNS} rows={TABLE_ROWS} rowKey={(r) => r.name} />
</Sample>
<Sample label="empty">
<Table columns={TABLE_COLUMNS} rows={[]} rowKey={(r) => r.name} />
</Sample>
</Section>
<Section title="RelativeTime">
<div class="components-chip-row">
<Sample label="5s ago, ticking">
<RelativeTime epochMs={Date.now() - 5_000} />
</Sample>
<Sample label="2h ago, ticking">
<RelativeTime epochMs={Date.now() - 2 * 3_600_000} />
</Sample>
</div>
</Section>
<Section title="RefreshIntervalPicker">
<Sample label="editable">
<RefreshIntervalPickerSample />
</Sample>
</Section>
<Section title="TextField">
<Sample label="editable">
<TextFieldSample />
</Sample>
</Section>
<Section title="SelectField">
<Sample label="editable">
<SelectFieldSample />
</Sample>
</Section>
<Section title="Button">
<div class="components-chip-row">
{BUTTON_VARIANTS.map((variant) => (
<Sample key={variant} label={variant}>
<Button variant={variant}>{variant}</Button>
</Sample>
))}
<Sample label="disabled">
<Button disabled>disabled</Button>
</Sample>
</div>
</Section>
</Panel>
);
}