Components
Form
Two vocabularies, deliberately not one. components/base/dialog-form.tsx is
captioned, bordered and stacked — what the dialogs that are not editing a
thing are built from (feedback, report). components/base/item-form.tsx is the
item editor's: one card per field with the label beside its value, the app's
item sheet translated to the browser. A form that corrects a thing and a form
that files a complaint are not asking the same kind of question.
Both are class strings plus thin wrappers rather than a component per control:
the controls differ — an <input>, a <textarea>, a disclosure, a wall of
chips — and what has to agree between screens is the surface they sit on.
Captioned fields
Field is one labelled control — a real <label>, so the caption is part of
the target. FieldGroup is the same shape around a group of controls, where a
label would hand every click to whichever child it decided was its own; its
action slot rides the caption's trailing edge, which is also where anything
with a button or a counter in its caption row belongs. The control itself takes
the FIELD class — the card's own surface at input scale.
import { FIELD, Field, FieldGroup } from "@/components/base/dialog-form";
<Field label="Title">
<input className={FIELD} placeholder="A name for this item" />
</Field>
<FieldGroup label="Note" action={<span className="text-[0.69rem] text-ink-quiet">240 left</span>}>
<textarea className={FIELD} rows={2} />
</FieldGroup>Item cards
The item editor's field is a card: CARD_LABEL beside the value rather than
stacked above it in uppercase, and CONTROL for the input, which is
transparent because the card is already the surface it sits on. Field is the
<label> version; FieldCard is the bare card, for what a <label> cannot
hold — a disclosure, a wall of chips, a button that would otherwise have its
click handed to the input.
import { CARD_LABEL, CONTROL, Field, FieldCard } from "@/components/base/item-form";
<Field label="Title">
<input className={CONTROL} placeholder="A name for this item" />
</Field>
<FieldCard stacked>
<span className={CARD_LABEL}>Tags</span>
<div className="flex flex-wrap gap-[0.38rem]">{chips}</div>
</FieldCard>Chips
CHIP_ON and CHIP_OFF are the toggle the tag wall is built from. Both states
keep the border — transparent in the on state — or picking a tag would resize
it.
<button className={on ? CHIP_ON : CHIP_OFF}>{tag}</button>Actions
The item editor closes on a row of buttons. They take the site's rounded-md
button corner rather than the app's full-radius pills. ACTION_PRIMARY takes
the row's slack, which is the weighting intended.
ACTION_DANGER is a surface rather than a fill: a red plate would make the one
action that cannot be undone the loudest thing on the row. There is a third,
ACTION_CANCEL, which is tablet:hidden and so absent from the specimen
below — on a pointer device Escape, the scrim and the dialog's close button all
discard already, and a fourth way to do it is a button taking width from Save.
<div className="flex items-center gap-2">
<button className={ACTION_DANGER}>Remove</button>
<button className={ACTION_CANCEL}>Cancel</button>
<button className={ACTION_PRIMARY}>Save</button>
</div>The dialogs built from the captioned vocabulary do not use these — they close
on a BaseButton pair instead, see Button.