diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index 1159b62c..ae82075b 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -5,10 +5,14 @@ // 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 { 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 }) { @@ -47,6 +51,34 @@ const TABLE_ROWS: Row[] = [ { 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 ; +} + +function SelectFieldSample() { + const [value, setValue] = useState('alpha'); + return ( + + ); +} + export function ComponentsPage() { return ( @@ -82,6 +114,31 @@ export function ComponentsPage() { r.name} /> + +
+ + + +
+ +
+ + + +
+ +
+
+ {BUTTON_VARIANTS.map((variant) => ( + + + + ))} + + + +
+
); } diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css index 61f8f368..789a6403 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css @@ -1,7 +1,7 @@ -/* — the create-agent form. First form in this package, - so its input/button chrome lives here rather than in `ui/` — see the - component's own comment for why. Reuses the same base16 slots - (../theme.css) every other component draws from. */ +/* — the create-agent form. Input/button chrome now + comes from the shared `ui/` form kit (`TextField`/`Button`); this + file only owns the page's own layout + copy. Reuses the same base16 + slots (../theme.css) every other component draws from. */ .create-agent-intro { margin: 0 0 1.5em; color: var(--muted); @@ -12,37 +12,6 @@ gap: 0.75em; flex-wrap: wrap; } -.create-agent-field { - display: flex; - flex-direction: column; - gap: 0.3em; -} -.create-agent-label { - font-size: 0.85em; - color: var(--muted); -} -.create-agent-input { - background: var(--bg); - color: var(--fg); - border: 1px solid var(--purple-dim); - border-radius: 0.3em; - padding: 0.4em 0.6em; - font: inherit; - min-width: 16em; -} -.create-agent-submit { - background: var(--purple-dim); - color: var(--fg); - border: none; - border-radius: 0.3em; - padding: 0.5em 1.2em; - font: inherit; - cursor: pointer; -} -.create-agent-submit:disabled { - opacity: 0.6; - cursor: default; -} .create-agent-result { margin-top: 1em; } diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx index ed500fe8..35fc4722 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx @@ -16,16 +16,17 @@ // one path segment deep — a real bug, filed separately rather than // fixed here, but reason enough to avoid a nested route today. // -// First real form in this package — no shared form kit exists yet, so -// the input/button styling below is scoped to `CreateAgentPage.css` -// rather than promoted into `ui/`. Promote the day a second page needs -// one, same "don't build ahead of a second caller" rule `Panel`'s own -// comment states. +// First real form in this package — its name field + submit button now +// come from the shared `ui/` form kit (`TextField`/`Button`) rather than +// page-scoped input/button chrome, so a hive-picker soon landing on this +// same page has something to reuse instead of copying this page's CSS. import { useState } from 'preact/hooks'; import { Link } from 'wouter-preact'; import { ApiErrorPanel } from '@hive/shared/api-error-panel.js'; import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js'; import { Panel } from '../ui/panel/Panel.js'; +import { TextField } from '../ui/text-field/TextField.js'; +import { Button } from '../ui/button/Button.js'; import './CreateAgentPage.css'; interface CreateAgentResponse { @@ -85,24 +86,18 @@ export function CreateAgentPage() { jobs to watch it settle.

-
- - setName(e.currentTarget.value)} - /> -
- + {result.status === 'done' && (

diff --git a/frontend/packages/swarm-ui/src/ui/button/Button.css b/frontend/packages/swarm-ui/src/ui/button/Button.css new file mode 100644 index 00000000..1c5e42a6 --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/button/Button.css @@ -0,0 +1,26 @@ +/* + ); +} diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css new file mode 100644 index 00000000..29257bdd --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css @@ -0,0 +1,32 @@ +/* — label stacked above its control, plus the shared + `.ui-form-control` chrome every text/select input in the kit draws + from (one class, so the two never drift). `max-width` + `width: 100%` + rather than a fixed `width`: caps the control on a wide desktop + viewport without forcing an overflow on a narrow/touch one. + `min-height` is a touch-target floor (44px at the default 16px root + font — WCAG 2.5.5's minimum), not a visual choice — it's the same on + every control in the kit whether or not it's ever used on a touch + device, since the alternative is a component that behaves differently + per input method. Colours are the shared base16-derived vars + (../../theme.css), never literal. */ +.ui-form-field { + display: flex; + flex-direction: column; + gap: 0.3em; +} +.ui-form-field-label { + font-size: 0.85em; + color: var(--muted); +} +.ui-form-control { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--purple-dim); + border-radius: 0.3em; + padding: 0.4em 0.6em; + font: inherit; + width: 100%; + max-width: 16em; + box-sizing: border-box; + min-height: 2.75em; +} diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx b/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx new file mode 100644 index 00000000..18317e0f --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.tsx @@ -0,0 +1,27 @@ +// — label + control wrapper shared by every labelled form +// control (`TextField`, `SelectField`, …). Not exported as a standalone +// primitive callers reach for directly — it only exists so those two +// don't each reinvent the label/spacing chrome; a bare labelled `

` +// wrapper has no independent identity worth a `/components` entry of +// its own. +import type { ComponentChildren } from 'preact'; +import './FormField.css'; + +export function FormField({ + label, + htmlFor, + children, +}: { + label: string; + htmlFor: string; + children: ComponentChildren; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/frontend/packages/swarm-ui/src/ui/select-field/SelectField.tsx b/frontend/packages/swarm-ui/src/ui/select-field/SelectField.tsx new file mode 100644 index 00000000..982b0fbd --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/select-field/SelectField.tsx @@ -0,0 +1,46 @@ +// — a labelled ` onChange((e.target as HTMLSelectElement).value)} + > + {options.map((o) => ( + + ))} + + + ); +} diff --git a/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx b/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx new file mode 100644 index 00000000..c759e6f6 --- /dev/null +++ b/frontend/packages/swarm-ui/src/ui/text-field/TextField.tsx @@ -0,0 +1,44 @@ +// — a labelled single-line text input, the shared control +// every page-level form (`CreateAgentPage` today) reaches for instead +// of hand-rolling its own label/input pair. No textarea/multi-line +// mode — promote that the day a real caller needs one, same "don't +// build ahead of a caller" rule the rest of `ui/` follows. +import { FormField } from '../form-field/FormField.js'; + +export function TextField({ + id, + label, + value, + onInput, + type = 'text', + pattern, + title, + required, + placeholder, +}: { + id: string; + label: string; + value: string; + onInput: (value: string) => void; + type?: string; + pattern?: string; + title?: string; + required?: boolean; + placeholder?: string; +}) { + return ( + + onInput((e.target as HTMLInputElement).value)} + /> + + ); +}