All pages

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.

Note240 left
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.

Tags
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.