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.
176 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|