react-luna-form 0.0.83 → 0.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/index.md CHANGED
@@ -9,21 +9,27 @@ field descriptors is the single source for what renders **and** for the Zod
9
9
  schema that validates it. Most mistakes come from assuming otherwise.
10
10
 
11
11
  Start at [setup.md](setup.md) if you are wiring the form up for the first
12
- time: entry points, `defineConfig`, and the difference between the client and
13
- server `Form`.
12
+ time: installing, styling, entry points, `defineConfig`, and the difference
13
+ between the client and server `Form`.
14
14
 
15
15
  ## Where to look
16
16
 
17
17
  | You are | Read |
18
18
  | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ |
19
+ | Grouping fields into a titled, collapsible or numbered section | [structure/sections.md](structure/sections.md) |
20
+ | Putting fields side by side, or a label beside its control | [structure/layout.md](structure/layout.md) |
21
+ | Reusing one piece of JSON across the form | [structure/definition.md](structure/definition.md) |
19
22
  | Declaring a text, number, date or password field | [fields/input.md](fields/input.md) |
20
23
  | Declaring a `select`, `radio` or `chips` field, or filling one from a source | [fields/select.md](fields/select.md) |
21
24
  | Declaring a repeatable group of fields | [fields/list.md](fields/list.md) |
22
25
  | Looking for a date range, a color or another ready-made picker | [fields/specialized-selectors.md](fields/specialized-selectors.md) |
23
26
  | Implementing a field type this library does not ship | [fields/custom-inputs.md](fields/custom-inputs.md) |
27
+ | Filling a `select` from an API | [fields/data-source.md](fields/data-source.md) |
24
28
  | Making a field required, or validating against another field | [validation/overview.md](validation/overview.md) |
29
+ | Handling the submit, a server action or the error summary | [forms/submit.md](forms/submit.md) |
25
30
  | Showing, hiding, clearing or filling a field when another one changes | [events/change.md](events/change.md) |
26
- | Formatting an interpolated value inside a label or a message | [interpolation/format-filters.md](interpolation/format-filters.md) |
31
+ | Writing a `{placeholder}` in a label, a description or an event | [interpolation/overview.md](interpolation/overview.md) |
32
+ | Formatting an interpolated value as currency, a date or a duration | [interpolation/format-filters.md](interpolation/format-filters.md) |
27
33
 
28
34
  ## Three things that surprise people
29
35
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  Format filters transform interpolated values into locale-aware strings using a pipe syntax inspired by Vue and Angular.
4
4
 
5
+ This page is the filter reference. What a placeholder resolves against in the first place — and it is not the same set everywhere — is [Interpolation](overview.md).
6
+
5
7
  ## Where filters can be used
6
8
 
7
9
  Filters work in any string passed through Luna Form's interpolation engine, including:
@@ -27,6 +29,8 @@ Multiple filters can be chained; the output of the previous filter becomes the i
27
29
 
28
30
  Locale-aware filters read the locale from `config.env.locale`. If not set, filters fall back to the runtime default (typically the browser's locale).
29
31
 
32
+ It applies wherever a filter is written — a label, a description, a `value` payload — so one setting keeps them consistent. See [Interpolation](overview.md#the-locale-filters-use).
33
+
30
34
  ```ts
31
35
  const config = {
32
36
  env: {
@@ -0,0 +1,179 @@
1
+ # Interpolation
2
+
3
+ A `{placeholder}` in a form is resolved against a set of values that depends on
4
+ **where the placeholder is written**. There are two of them, and most confusion
5
+ about interpolation is one being read as the other.
6
+
7
+ - **In rendered text** — a field's `label`, a field or column `description` —
8
+ the values are `context`, `env` and, in a description, that field's own
9
+ `value`.
10
+ - **In an event payload** — what a [`value` action](../events/change.md#1-value-action-valueevent)
11
+ writes, and the `url` and `body` of a [`source` action](../events/change.md#3-source-action-sourceevent) —
12
+ the values are **the selected option itself**: `{value}`, `{label}` and every
13
+ other key the option carries.
14
+
15
+ [Format filters](format-filters.md) work in both.
16
+
17
+ ## Where each namespace applies
18
+
19
+ | Written in | Resolves against |
20
+ | --------------------------------- | --------------------------------------- |
21
+ | a field `label` | `context`, `env` |
22
+ | a field `description` | `context`, `env`, `value` |
23
+ | a column `description` | `context`, `env` |
24
+ | a `state` action's `description` | `context`, `env`, `value` of the target |
25
+ | a `value` action's payload | the selected option |
26
+ | a `source` action's `url`, `body` | the selected option |
27
+ | a section `title`, `description` | nothing — see below |
28
+
29
+ ## Rendered text
30
+
31
+ `context` is a prop of the form and `env` is a key of
32
+ [`defineConfig`](../setup.md#style-alert-env). Both are addressed by name, with
33
+ dot notation for anything nested:
34
+
35
+ ```tsx
36
+ <Form
37
+ sections={sections}
38
+ config={config}
39
+ context={{ user: { firstName: 'Jane' }, plan: 'pro' }}
40
+ />
41
+ ```
42
+
43
+ ```json
44
+ {
45
+ "name": "greeting",
46
+ "type": "input/text",
47
+ "label": "Welcome {context.user.firstName}",
48
+ "description": "You are on the {context.plan} plan, billed in {env.currency}"
49
+ }
50
+ ```
51
+
52
+ Three details decide most of what you will hit:
53
+
54
+ - **`{value}` is a description's, not a label's.** A description is rendered
55
+ with the field's current value in scope, so `{value}` there tracks what the
56
+ user typed. A label is not: the same placeholder in a `label` resolves to
57
+ nothing and stays on screen as the literal `{value}`.
58
+ - **Interpolation runs first, translation second.** The resolved string is what
59
+ gets looked up in the [`translations`](../fields/list.md#localization)
60
+ dictionary, so a label written as `Welcome {context.user.firstName}` is
61
+ looked up as `Welcome Jane` — which is not a key anybody wrote. Translate the
62
+ whole sentence with the placeholder inside it (`"welcome_user": "Hola
63
+ {context.user.firstName}"` keyed on the source text) rather than expecting the
64
+ key to survive.
65
+ - **A section heading is not interpolated at all.** `title` and `description` on
66
+ a [section](../structure/sections.md#title-and-description-are-translated-not-interpolated)
67
+ are translated and take markdown links, but a placeholder in one is printed
68
+ as written. A column description is interpolated, so that is the place to put
69
+ one.
70
+
71
+ ## Event payloads
72
+
73
+ What a change event carries is the **selected option**, not the form's values.
74
+ For a field whose options are declared or fetched, that is the whole row behind
75
+ the selection:
76
+
77
+ ```json
78
+ {
79
+ "name": "country",
80
+ "type": "select",
81
+ "source": [
82
+ { "value": "cl", "label": "Chile", "dial": "+56" },
83
+ { "value": "mx", "label": "Mexico", "dial": "+52" }
84
+ ],
85
+ "event": {
86
+ "change": [
87
+ {
88
+ "action": "value",
89
+ "value": { "phone_prefix": "{dial}", "country_name": "{label}" }
90
+ }
91
+ ]
92
+ }
93
+ }
94
+ ```
95
+
96
+ Selecting Chile writes `+56` and `Chile`. The keys available are the keys of
97
+ that option: there is no `{context.*}` or `{env.*}` here, and no way to read
98
+ another field's value.
99
+
100
+ Two shapes to keep in mind:
101
+
102
+ - **A text, textarea or date field carries `{ value }` and nothing else.** There
103
+ is no option row behind it, so `{value}` is the only placeholder that resolves
104
+ in its events.
105
+ - **A `chips` field carries its array** — see
106
+ [change.md](../events/change.md#when-a-change-event-runs-at-mount). `{value}`
107
+ is the array itself, and there is no `{label}` to read.
108
+
109
+ When the option rows do not use `value` as their identifying key — a remote
110
+ source mapped with `advanced.options` — the row is found through
111
+ [`advanced.entity`](../fields/select.md#advancedentity). Without it the event
112
+ sees only `{ value }` and every other placeholder stays literal.
113
+
114
+ ### A `state` description is interpolated by the field that shows it
115
+
116
+ Unlike `value` and `source`, a [`state` action](../events/change.md#2-state-action-stateevent)
117
+ stores its `description` as written and the **target** interpolates it when it
118
+ renders. So `{value}` in that description is the target field's own value, not
119
+ the value of the field whose change set it.
120
+
121
+ Aimed at the field that raised the event — the common case — the two are the
122
+ same thing, which is why this rarely comes up:
123
+
124
+ ```json
125
+ {
126
+ "action": "state",
127
+ "target": "price",
128
+ "state": { "description": "Total to pay: {value | currency:USD}" }
129
+ }
130
+ ```
131
+
132
+ Aimed at a different field, `{value}` reads that other field, and it is empty
133
+ until somebody fills it.
134
+
135
+ ## Resolution rules
136
+
137
+ - **Dot notation walks objects.** `{context.user.firstName}`, `{env.branding.name}`.
138
+ - **A key that resolves to nothing is left alone.** The placeholder stays in the
139
+ text exactly as written — `{context.missing}` renders as `{context.missing}`.
140
+ This is deliberate: a half-rendered sentence is easier to spot than a blank.
141
+ - **So is a value that is not a scalar.** A placeholder standing for an object or
142
+ an array is left literal in text. The one exception is a `value` action, where
143
+ a template that is _nothing but_ the placeholder hands the structure over
144
+ untouched — that is what fills a
145
+ [list](../events/change.md#targeting-a-list).
146
+ - **An unknown filter leaves the whole placeholder.** `{total | nope}` renders as
147
+ `{total | nope}` rather than dropping the value.
148
+ - **Spaces are trimmed.** `{ value }` and `{value}` are the same placeholder, as
149
+ are `{total|currency:USD}` and `{total | currency:USD}`.
150
+
151
+ ## The locale filters use
152
+
153
+ Locale-aware filters read `config.env.locale`:
154
+
155
+ ```ts
156
+ defineConfig({ inputs: [...], env: { locale: 'es-ES', currency: 'EUR' } })
157
+ ```
158
+
159
+ It reaches every filter the form renders — a label and a description format the
160
+ same value the same way. With no `locale` set, filters fall back to the locale of
161
+ whatever is running the code: the browser in the client form, the process in the
162
+ server one, which are not always the same machine. Set it once in `env` and both
163
+ renders agree.
164
+
165
+ ## An unresolved placeholder cancels a request
166
+
167
+ A `source.url` still holding a `{placeholder}` after interpolation is **not
168
+ fetched**. The field renders with no options, no request appears in the network
169
+ panel, and nothing is logged: from the outside it looks exactly like an empty
170
+ response.
171
+
172
+ That is the intended behaviour — a cascading select should not call
173
+ `/api/states?country={value}` before a country is picked — but it is also what a
174
+ typo produces. A field that never fetches is worth checking for a placeholder
175
+ that nothing resolves, before checking the network.
176
+
177
+ A URL that _is_ fully resolved and still comes back empty is a different
178
+ problem: see [`fetcher.remotePatterns`](../setup.md#fetcherremotepatterns),
179
+ which does log when it blocks one.
package/docs/setup.md CHANGED
@@ -4,6 +4,55 @@ How a form is configured and rendered. The pages under `fields/`,
4
4
  `validation/` and `events/` describe the JSON; this one describes the code
5
5
  around it.
6
6
 
7
+ ## Installing
8
+
9
+ ```bash
10
+ pnpm add react-luna-form
11
+ ```
12
+
13
+ The package carries no dependencies of its own: everything it needs is a peer
14
+ dependency, so your application resolves one copy of React, Zod and the rest.
15
+
16
+ | Peer | Version | Used for |
17
+ | ----------------------- | ------------------- | ------------------------------------------- |
18
+ | `react`, `react-dom` | `^19.0.0` | rendering, and `useActionState` on submit |
19
+ | `zod` | `^4.0.0` | the schema derived from `sections` |
20
+ | `jotai`, `jotai-family` | `^2.0.0` / `^1.0.0` | the value, error and source stores |
21
+ | `swr` | `^2.0.0` | fetching a field's remote `source` |
22
+ | `date-fns` | `^4.0.0` | date and time formatting, `duration` filter |
23
+ | `fast-equals` | `^6.0.0` | change detection in the stores |
24
+ | `tailwind-merge` | `^3.5.0` | merging the classes the components render |
25
+
26
+ None of them is optional, and none is bundled. A package manager that installs
27
+ peers automatically (npm 7+, or pnpm with `auto-install-peers=true`) brings
28
+ them in for you; anywhere else, install them alongside the library. A missing
29
+ peer surfaces as a module resolution error at import time, not as a form that
30
+ renders badly.
31
+
32
+ ## Styling
33
+
34
+ The components render Tailwind utility classes and **the build ships no CSS**.
35
+ With [Tailwind CSS v4](https://tailwindcss.com) installed, point it at the
36
+ package so the classes inside the published bundle are generated:
37
+
38
+ ```css
39
+ @import 'tailwindcss';
40
+
41
+ @source '../node_modules/react-luna-form';
42
+ ```
43
+
44
+ Without that `@source` line every class the library renders is scanned out as
45
+ unused: the markup is correct, the labels and inputs are all there, and none of
46
+ the layout is — which reads as a broken build rather than a missing line of
47
+ CSS.
48
+
49
+ Every element also carries a `data-slot` attribute — `field`, `field-label`,
50
+ `field-control`, `field-set`, `field-set-content`, `field-content`,
51
+ `field-group`, `field-separator`, `column`, `list-item-card` — so a design
52
+ system can restyle the structure without patching the library. The components
53
+ you register yourself are yours to style; these slots cover the scaffolding
54
+ around them.
55
+
7
56
  ## Entry points
8
57
 
9
58
  | Import | Contains |
@@ -57,7 +106,7 @@ Pass every key you still want.
57
106
 
58
107
  ### `fetcher.remotePatterns`
59
108
 
60
- Which hosts a field's remote `source` may call. The rules:
109
+ Which hosts a field's remote `source` may call — the source itself is documented in [fields/data-source.md](fields/data-source.md). The rules:
61
110
 
62
111
  | Value | Effect |
63
112
  | ------- | ------------------------------------ |
@@ -95,19 +144,21 @@ Check the console when a remote source comes back empty.
95
144
  The client form. It owns its own state, so it is the one to use for anything
96
145
  interactive.
97
146
 
98
- | Prop | Purpose |
99
- | ----------------------------------------- | ---------------------------------------------------------------------- |
100
- | `sections` | the field descriptors — the form itself |
101
- | `config` | the result of `defineConfig` |
102
- | `value` | the current values |
103
- | `action` | server action invoked on submit, receives the form data and the schema |
104
- | `onSuccess` | called with the parsed data after `action` succeeds |
105
- | `onValueChange` | called with `{ name, value }` on every change |
106
- | `readOnly` | renders every field disabled |
107
- | `translations` | per-locale strings for labels, options and messages |
108
- | `lang` | the locale to resolve `translations` against |
109
- | `context` | arbitrary values available to interpolation |
110
- | `definition`, `advanced.step`, `children` | multi-step and custom controls |
147
+ | Prop | Purpose |
148
+ | --------------- | --------------------------------------------------------------------------------------------------------------- |
149
+ | `sections` | the field descriptors — the form itself |
150
+ | `config` | the result of `defineConfig` |
151
+ | `value` | the current values |
152
+ | `action` | server action invoked on submit, receives the form data and the schema — see [forms/submit.md](forms/submit.md) |
153
+ | `onSuccess` | called with the parsed data after `action` succeeds |
154
+ | `onValueChange` | called with `{ name, value }` on every change |
155
+ | `readOnly` | renders every field disabled |
156
+ | `translations` | per-locale strings for labels, options and messages |
157
+ | `lang` | the locale to resolve `translations` against |
158
+ | `context` | arbitrary values available to interpolation |
159
+ | `definition` | the JSON that [`$ref`](structure/definition.md) entries resolve against |
160
+ | `advanced.step` | numbers the [sections](structure/sections.md#advancedstep-numbers-the-sections) |
161
+ | `children` | the form's control area — see [forms/submit.md](forms/submit.md#nothing-submits-without-a-control) |
111
162
 
112
163
  ### `Form` from `react-luna-form/server`
113
164
 
@@ -0,0 +1,99 @@
1
+ # Definition and `$ref`
2
+
3
+ A definition names a piece of JSON once so the rest of the form can point at it.
4
+ Anywhere inside `sections`, an object shaped `{ "$ref": "#/definition/<path>" }`
5
+ is replaced by whatever `<path>` holds in the `definition` you pass alongside
6
+ them.
7
+
8
+ ```tsx
9
+ <Form sections={sections} definition={definition} config={config} />
10
+ ```
11
+
12
+ ```json
13
+ {
14
+ "definition": {
15
+ "sources": {
16
+ "countries": { "url": "/api/countries", "namespace": "results" }
17
+ }
18
+ },
19
+ "sections": [
20
+ {
21
+ "fields": [
22
+ {
23
+ "name": "origin",
24
+ "type": "select",
25
+ "label": "Origin",
26
+ "source": { "$ref": "#/definition/sources.countries" }
27
+ },
28
+ {
29
+ "name": "destination",
30
+ "type": "select",
31
+ "label": "Destination",
32
+ "source": { "$ref": "#/definition/sources.countries" }
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ ```
39
+
40
+ ## The path is dotted, not slashed
41
+
42
+ Everything after `#/definition/` is a **dot path** into the definition object:
43
+ `#/definition/sources.countries`, never `#/definition/sources/countries`. The
44
+ prefix is the only part that uses slashes, and it looks enough like a JSON
45
+ Pointer to be written the wrong way on the first try.
46
+
47
+ ## What can be a reference
48
+
49
+ Any object in the tree. The `$ref` object is replaced whole, so a reference can
50
+ stand for a `source`, an array of options, a whole field, or the `fields` of a
51
+ section:
52
+
53
+ ```json
54
+ {
55
+ "definition": {
56
+ "email": {
57
+ "name": "email",
58
+ "type": "input/email",
59
+ "label": "Email",
60
+ "required": true
61
+ }
62
+ },
63
+ "sections": [{ "fields": [{ "$ref": "#/definition/email" }] }]
64
+ }
65
+ ```
66
+
67
+ References inside a definition are resolved too, so one entry can be built out
68
+ of others.
69
+
70
+ ## The rules
71
+
72
+ - **Resolution happens once, before anything else.** The tree is resolved first,
73
+ and only then filtered and sorted, so an `order` or a `hidden` that arrives
74
+ through a reference behaves like one written in place.
75
+ - **An unresolved path leaves the reference standing.** Nothing throws: the node
76
+ stays as the literal `{ "$ref": ... }` object. What that looks like on screen
77
+ depends on where it was — a field whose `source` is still a reference renders
78
+ with no options at all, which is the usual way a typo in the path is noticed.
79
+ - **An empty or missing `definition` resolves nothing.** `definition={{}}` is
80
+ treated as absent, and every `$ref` in the form is left as written.
81
+ - **A cycle stops instead of hanging.** Two entries pointing at each other
82
+ resolve until the loop closes and leave the reference that would repeat
83
+ unresolved.
84
+
85
+ ## Pass it everywhere the sections go
86
+
87
+ `definition` is a prop of both the client and the server `Form`, and the third
88
+ argument of the headless builder:
89
+
90
+ ```ts
91
+ import { buildFormSchema } from 'react-luna-form/schema'
92
+
93
+ const schema = buildFormSchema(sections, translations, definition)
94
+ ```
95
+
96
+ A schema built without the definition sees `$ref` objects where the form sees
97
+ fields — the rendered form and the validated shape drift apart, which is the one
98
+ thing [the schema being derived from the same descriptors](../validation/overview.md#headless-validation)
99
+ is meant to prevent.
@@ -0,0 +1,189 @@
1
+ # Layout
2
+
3
+ Inside a section, fields stack one per row. Everything else — fields side by
4
+ side, a label beside its control — is opt in, and this page is where those keys
5
+ live.
6
+
7
+ ## Columns (`type: "column"`)
8
+
9
+ A `column` is a grid that holds fields. It is declared among the fields of a
10
+ section (or of a [list](../fields/list.md) row) and takes a `fields` array of
11
+ its own:
12
+
13
+ ```json
14
+ {
15
+ "type": "column",
16
+ "advanced": { "cols": 2 },
17
+ "fields": [
18
+ { "name": "first_name", "type": "input/text", "label": "First name" },
19
+ { "name": "last_name", "type": "input/text", "label": "Last name" }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ A column has no `name` and holds no value. It groups its fields visually and
25
+ nothing more: they keep the names they declare, and the submitted object is the
26
+ same one you would get without it.
27
+
28
+ | Key | Type | Does |
29
+ | --------------- | ---------------------- | ------------------------------------------ |
30
+ | `type` | `"column"` | required, exactly this string |
31
+ | `fields` | `Array<Field \| List>` | what the grid holds |
32
+ | `advanced.cols` | `1 \| 2 \| 3` | how many columns the grid has, default `2` |
33
+ | `description` | `string \| object` | help text under the grid |
34
+ | `order` | `number` | position among the section's other entries |
35
+
36
+ ### `cols`, and the values that are not
37
+
38
+ `cols` accepts `1`, `2` and `3`. **Anything else falls back to `2`** —
39
+ `"cols": 4`, `"cols": 0`, a string, or the key left out entirely all produce the
40
+ same two-column grid, with no error and no warning. A four-column layout is not
41
+ something to reach for by writing `4`; it does not exist.
42
+
43
+ The grid is one column below the `md` breakpoint and `cols` columns from there
44
+ up, so a column layout stacks on a phone whatever it says.
45
+
46
+ ### A field's own `cols` is a span
47
+
48
+ Inside a column, a field's `advanced.cols` says how many of the grid's columns
49
+ that field takes:
50
+
51
+ ```json
52
+ {
53
+ "type": "column",
54
+ "advanced": { "cols": 3 },
55
+ "fields": [
56
+ {
57
+ "name": "card_number",
58
+ "type": "input/text",
59
+ "label": "Card number",
60
+ "advanced": { "cols": 2 }
61
+ },
62
+ { "name": "cvc", "type": "input/text", "label": "CVC" }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ The card number takes two of the three columns and the CVC takes the third.
68
+ Spans of `1`, `2` and `3` are honoured and anything else is ignored, leaving the
69
+ field at one cell — unlike the container's `cols`, an out-of-range span does not
70
+ fall back to a default.
71
+
72
+ The same key means different things in the two places, which is worth reading
73
+ twice: on a `column` it is **how many columns the grid has**, on a field inside
74
+ it, **how many of them the field covers**.
75
+
76
+ A field's span outside a column does nothing at all. The class is emitted, the
77
+ parent is not a grid, and the field goes on filling the width.
78
+
79
+ ### Description
80
+
81
+ A column's `description` renders **under** the grid, not above it, and unlike a
82
+ [section's description](sections.md#title-and-description-are-translated-not-interpolated)
83
+ it goes through interpolation: `{context.*}`, `{env.*}` and
84
+ [format filters](../interpolation/format-filters.md) all resolve. It also takes
85
+ the `{ title, message, collapsed }` object form that a field description takes.
86
+
87
+ ### What a column can hold
88
+
89
+ Fields and [lists](../fields/list.md). A column inside a list row is supported
90
+ and keeps its grid; the fields inside it are named by the row's path like any
91
+ other leaf.
92
+
93
+ A column has no visibility of its own: `hidden` on a column is not read. Hide
94
+ the fields it holds instead — and note that when every field in a column is
95
+ hidden, the column counts as hidden for the
96
+ [section's visibility](sections.md#order-and-what-disappears), while the grid
97
+ element itself stays in the layout.
98
+
99
+ ### Labels truncate inside a grid
100
+
101
+ A label that is longer than its cell is truncated to one line inside a column,
102
+ where the same label wraps outside one. This keeps the inputs of a row aligned
103
+ with each other instead of one sinking below its neighbour. Write the short
104
+ version in the `label` and put the rest in the `description`.
105
+
106
+ ## Ordering fields and columns
107
+
108
+ Every entry in a `fields` array — a field, a column, a list — sorts by `order`
109
+ before rendering. Entries carrying one sort ascending; entries without one hold
110
+ their relative position and follow the rest. Because a column sorts as a single
111
+ entry, `order` interleaves whole columns with the plain fields around them, not
112
+ the fields inside them.
113
+
114
+ ## Orientation (`advanced.horizontal`)
115
+
116
+ By default a field renders its label above its control. `advanced.horizontal`
117
+ puts them side by side from the `md` breakpoint up:
118
+
119
+ ```json
120
+ {
121
+ "name": "timeout",
122
+ "type": "input/number",
123
+ "label": "Timeout",
124
+ "advanced": { "horizontal": true }
125
+ }
126
+ ```
127
+
128
+ A form that wants every field that way sets it once as
129
+ `style.horizontal` in [`defineConfig`](../setup.md#style-alert-env). A field
130
+ that declares nothing inherits it; one that declares `false` opts out of it.
131
+
132
+ Two consequences:
133
+
134
+ - **`radio` and `checkbox` are always horizontal**, whatever the key or the
135
+ form-wide style says. Their control belongs beside its label, so neither is
136
+ read for them.
137
+ - **The description moves.** A vertical field renders its description after the
138
+ control; a horizontal one renders it under the label, in the label's own
139
+ column.
140
+
141
+ The rendered field carries `data-orientation="horizontal"` or `"vertical"`,
142
+ which is the attribute to key CSS on rather than the class names.
143
+
144
+ ## `advanced.reverse` (checkbox only)
145
+
146
+ A `checkbox` renders its box first and its label after it. `reverse: false`
147
+ swaps them:
148
+
149
+ ```json
150
+ {
151
+ "name": "accepted",
152
+ "type": "checkbox",
153
+ "label": "I accept the terms",
154
+ "advanced": { "reverse": false }
155
+ }
156
+ ```
157
+
158
+ The default is the reversed order, so this key exists to turn it **off**, and
159
+ `"reverse": true` changes nothing. It is read only for `checkbox`; on a `radio`,
160
+ an `input/*` or anything else it is ignored.
161
+
162
+ ## Spacing
163
+
164
+ Spacing between fields is a property of the section that holds them, not of the
165
+ field: see [`advanced.compact`](sections.md#advancedcompact).
166
+
167
+ ## Styling hooks
168
+
169
+ Every structural element carries a `data-slot`, so a design system can restyle
170
+ the scaffolding without patching the library. The components you register in
171
+ [`defineConfig`](../setup.md#inputs-required) are yours already; these are the
172
+ parts around them.
173
+
174
+ | `data-slot` | Wraps |
175
+ | ------------------- | ------------------------------------------------------- |
176
+ | `field-set` | one section, the rendered `<fieldset>` |
177
+ | `field-set-content` | the body of a collapsible section |
178
+ | `field-group` | the stack of entries inside a section |
179
+ | `field-separator` | the rule drawn by `advanced.separator` |
180
+ | `column` | the grid of a `column` |
181
+ | `field` | one field, with `data-orientation` on it |
182
+ | `field-label` | the `<label>` |
183
+ | `field-content` | the label and, when horizontal, the description with it |
184
+ | `field-control` | the form's own control area — where `children` renders |
185
+ | `list-item-card` | one row of a [list](../fields/list.md) |
186
+
187
+ A field also carries `data-invalid` while it holds an error and `data-readonly`
188
+ while it is disabled, both readable from the descendants with
189
+ `group-data-[...]`.