Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx
iris b25aa157df swarm-ui: give Panel an optional header icon slot
Adds an `icon` prop to Panel (small emoji glyph left of the title,
aria-hidden, chosen per panel with no default) and wires it into every
current Panel caller: hives (bee), create-agent's form (robot) and info
panel (identity card, moved off the info panel's body copy where it
started as a one-off), jobs (puzzle piece), the components gallery
itself (toolbox), and the 404 page (compass). Adds a components-page
demo section and a whimsy-section pointer in the design guide.

Closes: #3508
2026-08-19 17:22:48 +02:00

181 lines
6 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" icon="🧰">
<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 icon (per panel, no default — see the doc comment)">
<Panel title="example title" icon="✨">
panel body content
</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>
);
}