swarm-ui: shared form-field kit (TextField, SelectField, Button)

Closes #3448.

New ui/ primitives: FormField (shared label+control wrapper),
TextField, SelectField, Button — each with a min-height touch target
(2.75em ~= 44px, WCAG 2.5.5) per mara's #3447 ask, and a max-width
instead of a fixed width so the control caps on desktop without
overflowing a narrow/touch viewport.

CreateAgentPage's name field + submit button now come from the kit
instead of page-scoped CSS; ComponentsPage gets a section for each new
primitive with an editable sample.
This commit is contained in:
iris 2026-08-18 20:28:50 +02:00
commit ba873926fa
9 changed files with 288 additions and 57 deletions

View file

@ -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() {
<Link href="/jobs">jobs</Link> to watch it settle.
</p>
<form class="create-agent-form" onSubmit={submit}>
<div class="create-agent-field">
<label class="create-agent-label" for="agent-name">
agent name
</label>
<input
id="agent-name"
class="create-agent-input"
type="text"
value={name}
pattern={NAME_PATTERN}
title="1-63 chars: lowercase letters, digits, hyphens"
required
onInput={(e) => setName(e.currentTarget.value)}
/>
</div>
<button class="create-agent-submit" type="submit" disabled={result.status === 'submitting'}>
<TextField
id="agent-name"
label="agent name"
value={name}
pattern={NAME_PATTERN}
title="1-63 chars: lowercase letters, digits, hyphens"
required
onInput={setName}
/>
<Button variant="primary" type="submit" disabled={result.status === 'submitting'}>
{result.status === 'submitting' ? 'creating…' : 'create'}
</button>
</Button>
</form>
{result.status === 'done' && (
<p class="create-agent-result create-agent-result-ok">