diff --git a/.nvmrc b/.nvmrc
deleted file mode 100644
index 8fdd954df..000000000
--- a/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-22
\ No newline at end of file
diff --git a/apps/web/content/docs/dev/passkeys/index.mdx b/apps/web/content/docs/dev/passkeys/index.mdx
index 4108bf0d9..547d133c3 100644
--- a/apps/web/content/docs/dev/passkeys/index.mdx
+++ b/apps/web/content/docs/dev/passkeys/index.mdx
@@ -1,7 +1,7 @@
---
title: Passkeys (WebAuthn)
description: Let members sign in with Face ID, Touch ID, Windows Hello or a security key. Configure the RP ID and origins, run the migration, and learn how the WebAuthn ceremonies work in VitNode.
-icon: Fingerprint
+icon: FingerprintPattern
---
import { Tab, Tabs } from "fumadocs-ui/components/tabs"
diff --git a/apps/web/content/docs/dev/passkeys/meta.json b/apps/web/content/docs/dev/passkeys/meta.json
index f6761fcec..f350bca0e 100644
--- a/apps/web/content/docs/dev/passkeys/meta.json
+++ b/apps/web/content/docs/dev/passkeys/meta.json
@@ -1,6 +1,6 @@
{
"title": "Passkeys",
"description": "Passwordless sign-in with WebAuthn passkeys - Face ID, Touch ID, Windows Hello or a security key",
- "icon": "Fingerprint",
+ "icon": "FingerprintPattern",
"pages": ["index", "using-passkeys", "admincp"]
}
diff --git a/apps/web/content/docs/dev/performance.mdx b/apps/web/content/docs/dev/performance.mdx
index fdb65e648..92728897d 100644
--- a/apps/web/content/docs/dev/performance.mdx
+++ b/apps/web/content/docs/dev/performance.mdx
@@ -65,7 +65,7 @@ Heavy editors (like Tiptap) or complex modals should be lazy-loaded with `React.
```tsx title="plugins/blog/src/views/admin/article-editor.tsx"
import React, { Suspense } from 'react'
-import { Loader } from '@vitnode/core/components/ui/loader'
+import { Spinner } from '@vitnode/core/components/ui/spinner'
// [!code ++:6]
const RichEditor = React.lazy(async () =>
@@ -75,7 +75,7 @@ const RichEditor = React.lazy(async () =>
)
export const ArticleEditor = (props) => (
- }>
+ }>
)
diff --git a/apps/web/content/docs/ui/accordion.mdx b/apps/web/content/docs/ui/accordion.mdx
index 75d60cf12..3050bf2e0 100644
--- a/apps/web/content/docs/ui/accordion.mdx
+++ b/apps/web/content/docs/ui/accordion.mdx
@@ -1,6 +1,7 @@
---
title: Accordion
description: A component that allows users to expand and collapse sections of content.
+icon: Rows3
---
## Preview
diff --git a/apps/web/content/docs/ui/alert-dialog.mdx b/apps/web/content/docs/ui/alert-dialog.mdx
index 0b70f474d..69d6535ca 100644
--- a/apps/web/content/docs/ui/alert-dialog.mdx
+++ b/apps/web/content/docs/ui/alert-dialog.mdx
@@ -1,6 +1,7 @@
---
title: Alert Dialog
description: Display important messages to users in a modal dialog.
+icon: MessageSquareWarning
---
A modal dialog that interrupts the user with important content and expects a
@@ -30,10 +31,10 @@ import { Button } from '@vitnode/core/components/ui/button';
```tsx
- Show Dialog} />
+ Delete account} />
- Are you absolutely sure?
+ Delete your account?
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
@@ -41,7 +42,7 @@ import { Button } from '@vitnode/core/components/ui/button';
Cancel
- Continue
+ Delete account
diff --git a/apps/web/content/docs/ui/alert.mdx b/apps/web/content/docs/ui/alert.mdx
index 9cf761fb7..7cd4b260c 100644
--- a/apps/web/content/docs/ui/alert.mdx
+++ b/apps/web/content/docs/ui/alert.mdx
@@ -1,6 +1,7 @@
---
title: Alert
description: Display a short, important message to users.
+icon: CircleAlert
---
## Preview
diff --git a/apps/web/content/docs/ui/aspect-ratio.mdx b/apps/web/content/docs/ui/aspect-ratio.mdx
new file mode 100644
index 000000000..d1a9f4ef9
--- /dev/null
+++ b/apps/web/content/docs/ui/aspect-ratio.mdx
@@ -0,0 +1,67 @@
+---
+title: Aspect Ratio
+description: Keep images, videos and embeds at the right shape, whatever the width.
+icon: Ratio
+---
+
+## Preview
+
+
+
+## Usage
+
+```ts
+import { AspectRatio } from '@vitnode/core/components/ui/aspect-ratio'
+```
+
+```tsx
+
+
+
+```
+
+The box takes the full width it is given and works out its height from
+`ratio`, so the page doesn't jump around while the image loads. Give the child
+`size-full` and `object-cover` to fill the box without stretching.
+
+## Common ratios
+
+| Ratio | Good for |
+| -------- | ------------------------------ |
+| `16 / 9` | Videos, hero images, banners |
+| `4 / 3` | Photos, product shots |
+| `1` | Avatars, thumbnails, galleries |
+| `9 / 16` | Phone screenshots, stories |
+
+## Video embed
+
+```tsx
+
+
+
+```
+
+## Props
+
+import { TypeTable } from 'fumadocs-ui/components/type-table'
+
+
+
+Every other prop goes straight to the underlying `div`.
diff --git a/apps/web/content/docs/ui/attachment.mdx b/apps/web/content/docs/ui/attachment.mdx
new file mode 100644
index 000000000..a48d82e41
--- /dev/null
+++ b/apps/web/content/docs/ui/attachment.mdx
@@ -0,0 +1,179 @@
+---
+title: Attachment
+description: Upload one file or a sortable gallery, with drag and drop, progress, previews and errors.
+icon: Paperclip
+---
+
+## Preview
+
+
+
+## Usage
+
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+
+
+
+
+Two fields cover every upload: `AutoFormFile` for one file and `AutoFormFiles`
+for many.
+
+```ts
+import { z } from 'zod'
+import { AutoForm } from '@vitnode/core/components/form/auto-form'
+import { AutoFormFile } from '@vitnode/core/components/form/fields/file'
+import { AutoFormFiles } from '@vitnode/core/components/form/fields/files'
+```
+
+The form stores **file ids**, not the files themselves. One file is a number,
+many files are an array of numbers:
+
+```ts
+const formSchema = z.object({
+ avatar: z.number().nullable().default(null),
+ gallery: z.array(z.number()).max(4).default([]),
+})
+```
+
+```tsx
+ (
+
+ ),
+ },
+ {
+ id: 'gallery',
+ component: props => (
+
+ ),
+ },
+ ]}
+/>
+```
+
+
+
+
+
+
+
+```ts
+import {
+ Attachment,
+ AttachmentAction,
+ AttachmentActions,
+ AttachmentContent,
+ AttachmentDescription,
+ AttachmentMedia,
+ AttachmentTitle,
+} from '@vitnode/core/components/ui/attachment'
+```
+
+```tsx
+
+
+
+
+
+ {file.name}
+ 48 KB
+
+
+
+
+
+
+
+```
+
+
+
+
+## Uploading files
+
+`onUpload` receives the `File` the user picked and returns the stored file. The
+field shows a spinner while it runs and an error if it throws, then saves the
+returned `id` into the form.
+
+```ts
+import type { AutoFormFileValue } from '@vitnode/core/components/form/fields/file'
+
+const uploadFile = async (file: File): Promise => {
+ const body = new FormData()
+ body.append('file', file)
+
+ const res = await fetch('/api/my-plugin/uploads', { body, method: 'POST' })
+ if (!res.ok) throw new Error('Upload failed')
+
+ return await res.json() // { id, name, size, url, mimeType? }
+}
+```
+
+Editing something that already has files? Pass them in with `file={existing}`
+on `AutoFormFile` or `files={existing}` on `AutoFormFiles`, and the field shows
+their names and previews.
+
+## Limits
+
+Both fields check files **before** uploading, so nobody waits for a 2 GB video
+just to hear it was too big:
+
+- `maxBytes` sets the largest accepted file. It is required.
+- `allowedExtensions` takes lowercase extensions with a leading dot, like
+ `['.pdf', '.png']`.
+- `allowedMimeTypes` takes MIME types, for example
+ `['image/png', 'application/pdf']`.
+
+`AutoFormFiles` also takes `maxItems` and `minItems`. People can reorder files
+by dragging or with the keyboard. Pass `ordered={false}` when the order doesn't
+matter.
+
+Check the same limits again on your server. The browser check saves people a
+wasted upload, but it can't stop someone determined.
+
+## States
+
+Set `state` on `Attachment` to match what the file is doing:
+
+| State | Looks like |
+| ------------ | ------------------------------------------------ |
+| `done` | The default, a finished file |
+| `idle` | Dashed border, waiting for a file |
+| `uploading` | Shimmering title, dimmed preview |
+| `processing` | Same as uploading, for server-side work |
+| `error` | Red border and description. Say what went wrong. |
+
+## Layouts
+
+- `orientation="vertical"` turns the attachment into a card with a big preview,
+ great for galleries.
+- `size="sm"` or `size="xs"` shrink it for chat composers and tight lists.
+- Wrap several in `AttachmentGroup` for a horizontally scrolling row that fades
+ out at the edges.
+- `AttachmentTrigger` makes the whole attachment clickable. Render it as a link to
+ open the file.
+
+## Accessibility
+
+- Give every `AttachmentAction` an `aria-label` that names the file, like
+ "Remove report.pdf". "Remove" alone is a riddle when there are five files.
+- Previews in `AttachmentMedia` are decorative next to the file name, so
+ `alt=""` is right.
+- The AutoForm fields announce reordering to screen readers and show every
+ rejected file with the reason.
diff --git a/apps/web/content/docs/ui/auto-form.mdx b/apps/web/content/docs/ui/auto-form.mdx
index d43496634..ec219892f 100644
--- a/apps/web/content/docs/ui/auto-form.mdx
+++ b/apps/web/content/docs/ui/auto-form.mdx
@@ -1,6 +1,7 @@
---
title: Auto Form
description: Component creates form based on Zod schemas & TanStack Form with validation
+icon: ClipboardPen
---
## Preview
diff --git a/apps/web/content/docs/ui/avatar.mdx b/apps/web/content/docs/ui/avatar.mdx
new file mode 100644
index 000000000..82b7e8f92
--- /dev/null
+++ b/apps/web/content/docs/ui/avatar.mdx
@@ -0,0 +1,127 @@
+---
+title: Avatar
+description: Profile pictures with a fallback for missing images, status badges and groups that lift on hover.
+icon: CircleUserRound
+---
+
+## Preview
+
+
+
+## Usage
+
+```ts
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from '@vitnode/core/components/ui/avatar'
+```
+
+```tsx
+
+
+ AL
+
+```
+
+The fallback shows while the image loads and stays if it never arrives. A
+broken link turns into tidy initials instead of a sad empty circle.
+
+## Sizes
+
+```tsx
+
+
+
+```
+
+## Badge
+
+Add `AvatarBadge` for a status dot. It takes any background colour, so pick a
+semantic one:
+
+```tsx
+
+
+ AL
+
+
+```
+
+A coloured dot alone doesn't tell everyone that someone is online. Say it in
+text somewhere too, or give the badge an `aria-label`.
+
+## Group
+
+Stack avatars with `AvatarGroup`. Wrap each one in `AvatarGroupItem` to make it
+lift on hover and show the person's name in a tooltip, and finish with
+`AvatarGroupCount` for everyone who didn't fit:
+
+```tsx
+import {
+ AvatarGroup,
+ AvatarGroupCount,
+ AvatarGroupItem,
+} from '@vitnode/core/components/ui/avatar'
+```
+
+```tsx
+
+ {users.map(user => (
+
+
+
+ {user.initials}
+
+
+ ))}
+ +3
+
+```
+
+All items in a group share one tooltip, so moving along the stack glides it
+from face to face instead of blinking a new one open each time.
+
+Plain `Avatar`s work inside `AvatarGroup` too, when you want a quiet stack with
+no hover effect.
+
+## Accessibility
+
+- Always pass `alt` to `AvatarImage`. The person's name is usually perfect.
+- An `AvatarGroupItem` with a `label` can be focused with the keyboard, and the
+ tooltip appears on focus as well as on hover.
+- With reduced motion turned on, avatars in a group stay put and only the
+ tooltip appears.
+
+## Props
+
+import { TypeTable } from 'fumadocs-ui/components/type-table'
+
+### Avatar
+
+
+
+### AvatarGroupItem
+
+
+
+## API Reference
+
+[Base UI - Avatar](https://base-ui.com/react/components/avatar)
diff --git a/apps/web/content/docs/ui/badge.mdx b/apps/web/content/docs/ui/badge.mdx
index 5857aa34d..88a54d043 100644
--- a/apps/web/content/docs/ui/badge.mdx
+++ b/apps/web/content/docs/ui/badge.mdx
@@ -1,6 +1,7 @@
---
title: Badge
description: Display small labels or indicators.
+icon: Tag
---
## Preview
diff --git a/apps/web/content/docs/ui/bubble.mdx b/apps/web/content/docs/ui/bubble.mdx
new file mode 100644
index 000000000..dcb7921a3
--- /dev/null
+++ b/apps/web/content/docs/ui/bubble.mdx
@@ -0,0 +1,197 @@
+---
+title: Bubble
+description: Chat-style message bubbles with variants, alignment, grouping, reactions and clickable suggestions.
+icon: MessageCircleMore
+---
+
+## Preview
+
+
+
+## Usage
+
+```ts
+import {
+ Bubble,
+ BubbleContent,
+ BubbleGroup,
+ BubbleReactions,
+} from '@vitnode/core/components/ui/bubble'
+```
+
+```tsx
+
+ I removed the stale route. You are welcome.
+
+```
+
+A bubble shrinks to fit its text and never grows past 80% of the row, so short
+replies stay short and long ones wrap politely.
+
+## Composition
+
+```text
+BubbleGroup
+├── Bubble
+│ ├── BubbleContent
+│ └── BubbleReactions
+└── Bubble
+ └── BubbleContent
+```
+
+## Variants
+
+```tsx
+
+ Quiet, supporting content.
+
+```
+
+| Variant | Use it for |
+| ------------- | -------------------------------------------------------- |
+| `default` | A strong primary bubble, usually the current user. |
+| `secondary` | The standard neutral bubble. |
+| `muted` | Lower-emphasis replies. |
+| `tinted` | A soft bubble derived from your primary color. |
+| `outline` | A bordered bubble for suggestions or rich content. |
+| `ghost` | Unframed, full-width content such as assistant answers. |
+| `destructive` | Errors and failed actions. |
+
+## Alignment
+
+Messages you sent go on the right, everything else on the left, like every
+chat app you have ever used.
+
+```tsx
+
+ Sent by me.
+
+```
+
+## Grouping
+
+Wrap consecutive bubbles from the same sender in `BubbleGroup` to tighten the
+spacing between them. Set `align` on each `Bubble`, not on the group.
+
+```tsx
+
+
+ First thought.
+
+
+ Second thought, arriving fashionably late.
+
+
+```
+
+## Reactions
+
+`BubbleReactions` pins a small pill to the bubble's edge. Move it with `side`
+(`top` or `bottom`) and `align` (`start` or `end`). It overlaps the bubble, so
+leave a bit more `gap` between rows.
+
+```tsx
+
+ Tests passed on the first try.
+
+ 🚀
+ 👀
+
+
+```
+
+## Links and buttons
+
+Use `render` on `BubbleContent` to turn a bubble into a real link or button,
+such as a quick reply suggestion.
+
+```tsx
+
+ }>
+ Show me the plugin docs
+
+
+```
+
+## Use with Message
+
+Need an avatar, a sender name or a read receipt? Wrap bubbles in a
+[Message](/docs/ui/message). Set `align="end"` on the `Message` once and every
+bubble inside follows it to the end side.
+
+```tsx
+
+
+
+ No need to align me twice.
+
+ Read
+
+
+```
+
+## Accessibility
+
+- `Bubble` is just the visual surface. Put conversation semantics such as
+ `role="log"` and an `aria-label` on the surrounding container.
+- A row of emoji reads poorly on a screen reader ("plus eight", anyone?). Give
+ `BubbleReactions` `role="img"` and a descriptive `aria-label` so it is
+ announced once. When reactions are interactive, render buttons with labels
+ instead.
+- Clickable bubbles should be a real `} />
- Are you absolutely sure?
+ Share this page
- This action cannot be undone. This will permanently delete your account
- and remove your data from our servers.
+ Anyone with the link can read it - no account needed.
+
+
+
+
- Cancel} />
- Yes, delete account
+ Done} />
```
+## Dialog or Alert Dialog?
+
+Use a Dialog for friendly, low-stakes tasks like sharing, quick edits or previews.
+Asking someone to confirm something they can't undo? Reach for the
+[Alert Dialog](/docs/ui/alert-dialog) instead. It can't be dismissed by clicking
+outside, so nobody deletes anything by accident.
+
## API Reference
[Base UI - Dialog](https://base-ui.com/react/components/dialog#api-reference)
diff --git a/apps/web/content/docs/ui/drawer.mdx b/apps/web/content/docs/ui/drawer.mdx
index d31e8c9f0..9b37fb2ab 100644
--- a/apps/web/content/docs/ui/drawer.mdx
+++ b/apps/web/content/docs/ui/drawer.mdx
@@ -1,6 +1,7 @@
---
title: Drawer
description: A component for displaying content in a sliding panel.
+icon: PanelBottom
---
## Preview
diff --git a/apps/web/content/docs/ui/dropdown-menu.mdx b/apps/web/content/docs/ui/dropdown-menu.mdx
index 1120a29ef..b0c41d817 100644
--- a/apps/web/content/docs/ui/dropdown-menu.mdx
+++ b/apps/web/content/docs/ui/dropdown-menu.mdx
@@ -1,6 +1,7 @@
---
title: Dropdown Menu
description: A dropdown menu component for building interactive menus in your application.
+icon: EllipsisVertical
---
## Preview
diff --git a/apps/web/content/docs/ui/editor.mdx b/apps/web/content/docs/ui/editor.mdx
index 3b3ca9e85..62453a8e0 100644
--- a/apps/web/content/docs/ui/editor.mdx
+++ b/apps/web/content/docs/ui/editor.mdx
@@ -1,6 +1,7 @@
---
title: Editor
description: Rich text editor built on TipTap for editing and rendering HTML content.
+icon: PenLine
---
## Preview
diff --git a/apps/web/content/docs/ui/emoji-icon-picker.mdx b/apps/web/content/docs/ui/emoji-icon-picker.mdx
index 501f9014b..4fb53ea0d 100644
--- a/apps/web/content/docs/ui/emoji-icon-picker.mdx
+++ b/apps/web/content/docs/ui/emoji-icon-picker.mdx
@@ -1,6 +1,7 @@
---
title: Emoji & Icon Picker
description: One picker with segmented emoji and icon modes that returns a discriminated value you can store in a single column.
+icon: FaceSlightlySmiling
---
## Preview
diff --git a/apps/web/content/docs/ui/empty.mdx b/apps/web/content/docs/ui/empty.mdx
new file mode 100644
index 000000000..ec18fb4f2
--- /dev/null
+++ b/apps/web/content/docs/ui/empty.mdx
@@ -0,0 +1,188 @@
+---
+title: Empty
+description: Empty states for lists, tables and searches that have nothing to show yet, with an icon, a short message and a next step.
+icon: PackageOpen
+---
+
+## Preview
+
+
+
+## Usage
+
+```ts
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@vitnode/core/components/ui/empty'
+```
+
+```tsx
+
+
+
+
+
+ No projects yet
+
+ You haven't created any projects yet. Start a fresh one or import an
+ existing repository.
+
+
+
+ Create project
+
+
+```
+
+Reach for `Empty` whenever a screen would otherwise show... nothing. A blank
+table, a fresh dashboard, a search with zero hits. The AdminCP uses it for an
+empty dashboard (with an "Edit" button to add widgets) and for navigation lists
+that have no items yet.
+
+## Anatomy
+
+- `Empty` is the centered wrapper. It grows to fill its parent (`flex-1`).
+- `EmptyHeader` groups the media, title and description.
+- `EmptyMedia` holds an icon, avatar or small illustration.
+- `EmptyTitle` is one short line that says what's missing.
+- `EmptyDescription` says why it's empty and what to do about it. Links inside
+ it are underlined automatically.
+- `EmptyContent` holds actions, a search box or anything else the user can act
+ on.
+
+## Media
+
+`EmptyMedia` has two variants. `icon` wraps the icon in a soft muted tile and
+sizes it for you. `default` adds no styling, so it's the one for avatars and
+illustrations.
+
+```tsx
+
+
+
+```
+
+
+
+```tsx
+
+
+
+ AL
+
+
+```
+
+## Outline
+
+`Empty` already has a dashed border style. It just needs a width. Add `border`
+for a drop-zone look, or `border-2` when it sits alone on a page, like the
+AdminCP dashboard.
+
+
+
+```tsx
+...
+```
+
+## No results
+
+A search that finds nothing needs its own empty state, not the same one as "you
+have no data". Echo the query back and offer a way out, like clearing the
+search.
+
+
+
+```tsx
+{results.length === 0 ? (
+
+
+
+
+
+ No integrations found
+
+ Nothing matches "{query}". Check the spelling or try a shorter search.
+
+
+
+ setQuery('')} size="sm" variant="outline">
+ Clear search
+
+
+
+) : (
+
+)}
+```
+
+## Inside a card or sidebar
+
+The default `p-12` is generous on purpose. On phones, `p-6 md:p-12` gives the
+copy room to breathe. In tight spots like a card, a sidebar or a popover, shrink
+the padding and the text:
+
+```tsx
+
+
+ No widgets yet
+
+ Widgets from your plugins will show up here.
+
+
+
+```
+
+## Writing good empty states
+
+An empty state is a tiny piece of onboarding. Five rules for writing one:
+
+- **Say what's missing.** "No projects yet" beats "Nothing here".
+- **Say why.** Is it brand new, filtered out, or did the search miss?
+- **Give the next step.** One primary action, and maybe a secondary one. If the
+ user can't do anything about it, a clear description is enough.
+- **Keep it short.** One title line, one or two sentences. Nobody reads an essay
+ about an empty table.
+- **Skip the blame.** "No results for 'xylphone'" is fine. "You typed it wrong"
+ is not.
+
+## Accessibility
+
+- `EmptyTitle` renders a `div`. If the empty state replaces a whole page
+ section, give the title a heading role so it shows up in the outline:
+ ``.
+- Icons in `EmptyMedia` are decorative, since the title already says what's
+ missing. Use
+ `alt=""` on avatar images for the same reason.
+- When results change while the user types, announce the count in a
+ `role="status"` region (a `sr-only` paragraph works great) so screen reader
+ users know the list just went empty.
+- Use real `Button`s for actions, so they're reachable by keyboard.
+
+## Props
+
+import { TypeTable } from 'fumadocs-ui/components/type-table'
+
+All parts accept the regular `div` props, including `className`.
+
+### EmptyMedia
+
+
+
+## API Reference
+
+[shadcn/ui - Empty](https://ui.shadcn.com/docs/components/base/empty)
diff --git a/apps/web/content/docs/ui/field.mdx b/apps/web/content/docs/ui/field.mdx
new file mode 100644
index 000000000..930e1e0d8
--- /dev/null
+++ b/apps/web/content/docs/ui/field.mdx
@@ -0,0 +1,294 @@
+---
+title: Field
+description: Layout components for labels, descriptions, errors and groups of controls. AutoForm builds every field out of them.
+icon: ListChecks
+---
+
+## Preview
+
+
+
+## Usage
+
+```ts
+import {
+ Field,
+ FieldContent,
+ FieldDescription,
+ FieldError,
+ FieldGroup,
+ FieldLabel,
+ FieldLegend,
+ FieldSeparator,
+ FieldSet,
+ FieldTitle,
+} from '@vitnode/core/components/ui/field'
+```
+
+```tsx
+
+ Username
+
+ Pick something your friends can type.
+
+```
+
+## Should I use this for my form?
+
+Probably not directly. In VitNode, forms are built with
+[AutoForm](/docs/ui/auto-form), and AutoForm already uses these components for
+every field's label, description and error message. You get validation, ids and
+ARIA wiring for free.
+
+Reach for `Field` when you are:
+
+- writing a [custom AutoForm field](#inside-a-custom-autoform-field), so it
+ looks like its built-in neighbours,
+- building a settings-style screen with controls that save instantly or are not
+ part of a form at all (toggles, filters, preferences).
+
+## Anatomy
+
+| Component | What it does |
+| ------------------ | -------------------------------------------------------------------------------------- |
+| `FieldSet` | A semantic `