Create & edit pages
Making one record, or changing one. This is the payment link page, measured — the product page is the same shape: a 440px form column on the left that scrolls on its own with a hairline down its right edge, and the rest of the island given to a preview of what the form produces, on a dotted canvas.
Sections are a 13px label over their control — no uppercase, no card around a single field. Controls that belong together sit in a 14px-radius bordered card. A toggle is a 40px row with a 24px icon tile and a switch, and the whole row is the control. Save and Cancel do not go on the page. They go in the header bar through titleBar, where they stay put while the form scrolls.
.folio-formEverything on the right is a preview. When the thing beside the form is real content — a key pool, a file list — use FormPane instead: plain, scrolling, and still there under 1024px.
import {
FormLayout, FormPage, FormPreview, FormPane, FormSection, FormCard, FormStack,
OptionRow, AddButton, Segmented, FormHint, FormCount, FormEmpty,
} from '@/components/form-page'
// The actions belong to the page, so the page declares them into the bar.
useTitleBar({
crumbs: [{ id: 'list', label: 'Payment links', onClick: back }, { id: 'new', label: 'New link' }],
primaryAction: { label: saving ? 'Saving…' : 'Create link', icon: 'check',
disabled: !ready || saving, loading: saving, onAction: save },
secondaryActions: [{ label: 'Cancel', onAction: back }],
})
<FormLayout>
<FormPage loading={isLoading}> {/* lays the column's own skeleton over the body */}
<FormSection title="Products">
<FormStack>
<FormCard padded>
<Select …/>
<OptionRow icon={<HashSolid />} label="Let customers change quantity" checked={qty} onCheckedChange={setQty} />
</FormCard>
<AddButton onClick={addProduct}><PlusSolid />Add product</AddButton>
</FormStack>
</FormSection>
<FormSection title="Link URL">
<Input value={slug} onChange={…} />
<FormHint>Auto-filled from the first product. Edit it any time.</FormHint>
</FormSection>
<FormSection>
<Segmented value={pricing} onChange={setPricing} options={[{ value: 'one_time', label: 'One-time' }, …]} />
</FormSection>
<FormSection title="Checkout options">
<FormCard>
<OptionRow icon={<TruckSolid />} label="Collect shipping address" checked={ship} onCheckedChange={setShip} />
<OptionRow icon={<ListSolid />} label="Custom fields" checked={fields} onCheckedChange={setFields}>
{/* unfolds inside the card while on */}
</OptionRow>
</FormCard>
</FormSection>
</FormPage>
<FormPreview>{/* what the form produces, centred on the dotted canvas */}</FormPreview>
</FormLayout>A create page and an edit page are the same page. The differences are all in the bar: the crumb reads “New link” or the record’s name, the primary action reads Create or Save, and on an edit page it is disabled until something is actually dirty — a live Save button on an untouched form is a lie about what will happen.
Rules
- 440px, one column, scrolling on its own. The rest of the island is the preview. Under 1024px the form takes everything and the preview is hidden.
- A section is a 13px medium label over its control. A single field gets no card; grouped controls get a 14px-radius bordered card.
- Fields are filled (
--secondary), 40px, 12px radius, and step to--mutedon focus rather than growing a ring. - A toggle is an
OptionRow: icon tile, label, switch, and the whole 40px row flips it. What it enables unfolds beneath it inside the same card. - Primary and secondary actions go in the header bar. Disable the primary action until the form is valid and dirty, and show
loadingwhile it saves. - Errors render where the work is: an
ErrorNoteat the top of the column, or a redFormHintunder the field. Never only in a toast.