react-luna-form 0.0.83 → 0.0.85

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.
@@ -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-[...]`.
@@ -0,0 +1,187 @@
1
+ # Sections
2
+
3
+ `sections` is the top level of a form. Every other page describes what goes
4
+ inside one; this page describes the section itself.
5
+
6
+ A section is a **visual grouping and nothing else**. It draws a legend, sets the
7
+ spacing around its fields and can collapse them out of the way, but it does not
8
+ scope names: every field in the form shares one flat namespace, whichever
9
+ section it was declared in. Two fields called `email` in two different sections
10
+ are one field with two renderings, and
11
+ [`buildFormSchema`](../validation/overview.md#headless-validation) produces one
12
+ key for them.
13
+
14
+ ## The shape
15
+
16
+ | Key | Type | Does |
17
+ | ---------------------- | ------------------ | --------------------------------------------- |
18
+ | `fields` | `Fields` | what the section renders |
19
+ | `title` | `string` | the legend |
20
+ | `description` | `string` | help text under the legend |
21
+ | `id` | `string \| number` | the `id` attribute of the rendered `fieldset` |
22
+ | `order` | `number` | position among the other sections |
23
+ | `advanced.compact` | `boolean` | tighter spacing between this section's fields |
24
+ | `advanced.collapsible` | `boolean` | render the section closed, behind a toggle |
25
+ | `advanced.separator` | `boolean` | draw a rule after the section |
26
+
27
+ ```json
28
+ {
29
+ "sections": [
30
+ {
31
+ "id": "payment",
32
+ "title": "Payment Method",
33
+ "description": "All transactions are secure and encrypted",
34
+ "advanced": { "separator": true },
35
+ "fields": [{ "name": "name", "type": "input/text", "label": "Name" }]
36
+ }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ## A section with no title and no description draws nothing
42
+
43
+ There is no empty legend and no wrapper to style around it: the section's fields
44
+ join the flow of the form as if they had been declared without one. This is the
45
+ form to reach for when you want the grouping only for `compact` spacing or for
46
+ `order`.
47
+
48
+ Declaring `advanced.collapsible` on such a section does nothing either — with no
49
+ title there is nothing to click, so the fields render open.
50
+
51
+ ## Title and description are translated, not interpolated
52
+
53
+ Both are resolved against the [`translations`](../fields/list.md#localization)
54
+ dictionary and both render `[text](url)` markdown links. Neither is passed
55
+ through interpolation: a `{context.name}` in a section title stays on screen as
56
+ the literal text `{context.name}`.
57
+
58
+ This is the one place the rule differs. A field's `label` and `description`
59
+ _are_ interpolated, and so is a [column's description](layout.md#description).
60
+ If a section heading has to carry a value, put the value in a field description
61
+ or in a column instead.
62
+
63
+ ## `advanced.compact`
64
+
65
+ Spacing between the fields of this section, tight instead of loose. It is
66
+ declared per section:
67
+
68
+ ```json
69
+ {
70
+ "title": "Connection",
71
+ "advanced": { "compact": true },
72
+ "fields": [{ "name": "host", "type": "input/text" }]
73
+ }
74
+ ```
75
+
76
+ A section that declares nothing inherits `style.compact` from
77
+ [`defineConfig`](../setup.md#style-alert-env), so the form-wide default holds
78
+ and a section only has to speak up to differ from it — including to opt out,
79
+ with `"compact": false` under a compact form.
80
+
81
+ ## `advanced.separator`
82
+
83
+ Draws a horizontal rule **after** the section, between it and whatever follows.
84
+ On the last section it draws a trailing rule, which is rarely what you want.
85
+
86
+ ## `advanced.collapsible`
87
+
88
+ Renders the section closed, with its title as the toggle:
89
+
90
+ ```json
91
+ {
92
+ "title": "Advanced options",
93
+ "advanced": { "collapsible": true },
94
+ "fields": [{ "name": "timeout", "type": "input/number" }]
95
+ }
96
+ ```
97
+
98
+ Three things to know before using it:
99
+
100
+ - **It always starts closed.** There is no key that opens it, and reopening the
101
+ form does not remember that the user had expanded it.
102
+ - **The fields inside stay mounted.** Collapsing hides them; it does not unmount
103
+ them. They keep their values, they still validate, and they are still
104
+ submitted. That is the opposite of what a [`state` action](../events/change.md#what-clearing-a-hidden-target-means)
105
+ does when it hides a field, which unmounts it and clears the value.
106
+ - **The description moves inside.** It renders with the fields rather than under
107
+ the title, so it is not visible while the section is closed.
108
+
109
+ A collapsible section also renders no step number, whatever `advanced.step` says.
110
+
111
+ ## `advanced.step` numbers the sections
112
+
113
+ Declared on the **form**, not on a section, it puts a numbered badge beside each
114
+ section's legend, counting the sections in the order they render:
115
+
116
+ ```tsx
117
+ <Form advanced={{ step: true }} sections={sections} config={config} />
118
+ ```
119
+
120
+ It is a numbering, not a wizard: every section is on screen at once, and the
121
+ library ships nothing that shows them one at a time. The badge appears only on
122
+ sections that draw a legend — a section with no title and no description has
123
+ nowhere to put it, and a collapsible one does not take it.
124
+
125
+ ## `order`, and what disappears
126
+
127
+ `sections` renders in array order unless a section declares `order`. Sections
128
+ carrying one sort ascending; sections without one keep their relative order and
129
+ follow the rest. The same rule applies to the entries inside a section, so a
130
+ field and a column can be interleaved by `order` — see
131
+ [layout.md](layout.md#ordering-fields-and-columns).
132
+
133
+ Two kinds of section never render at all:
134
+
135
+ - **One with no fields.** `"fields": []` is dropped before rendering, legend
136
+ included.
137
+ - **One whose fields are all hidden.** Hidden by `hidden: true` in the JSON or
138
+ by a [`state` action](../events/change.md#2-state-action-stateevent) at run
139
+ time, it makes no difference: when nothing inside a section is visible the
140
+ section goes with it, and it comes back when one of them is shown again. A
141
+ column counts as hidden when every field in it is.
142
+
143
+ A section has **no visibility flag of its own**. Writing `"hidden": true` on a
144
+ section is accepted and ignored; hide its fields instead, or leave the section
145
+ out of the array.
146
+
147
+ ## `id`
148
+
149
+ Lands on the rendered `<fieldset>` as its `id` attribute, for an anchor link or
150
+ a test selector. It is not a name and nothing resolves against it: fields are
151
+ addressed by their own `name` from anywhere in the form.
152
+
153
+ ## Complete example
154
+
155
+ ```json
156
+ {
157
+ "advanced": { "step": true },
158
+ "sections": [
159
+ {
160
+ "id": "contact",
161
+ "title": "Contact",
162
+ "description": "How we reach you about this order",
163
+ "advanced": { "separator": true },
164
+ "fields": [
165
+ { "name": "email", "type": "input/email", "label": "Email" },
166
+ { "name": "phone", "type": "input/tel", "label": "Phone" }
167
+ ]
168
+ },
169
+ {
170
+ "title": "Delivery",
171
+ "advanced": { "compact": true },
172
+ "fields": [
173
+ { "name": "street", "type": "input/text", "label": "Street" },
174
+ { "name": "city", "type": "input/text", "label": "City" }
175
+ ]
176
+ },
177
+ {
178
+ "title": "Advanced options",
179
+ "advanced": { "collapsible": true },
180
+ "order": 99,
181
+ "fields": [
182
+ { "name": "notes", "type": "textarea", "label": "Delivery notes" }
183
+ ]
184
+ }
185
+ ]
186
+ }
187
+ ```
@@ -251,7 +251,7 @@ if (!result.success) {
251
251
  }
252
252
  ```
253
253
 
254
- - **`buildFormSchema(sections, translations?, definition?)`** — walks the form tree (sections → columns → lists → fields) and returns a Zod schema. Lists become arrays of objects, so nested `requiredWhen`/`rules` resolve in **item scope**. `translations` maps messages to localized strings; `definition` resolves `$ref` sources.
254
+ - **`buildFormSchema(sections, translations?, definition?)`** — walks the form tree (sections → columns → lists → fields) and returns a Zod schema. Lists become arrays of objects, so nested `requiredWhen`/`rules` resolve in **item scope**. `translations` maps messages to localized strings; `definition` resolves [`$ref` entries](../structure/definition.md), and has to be the same one the form renders with.
255
255
  - **`collectIssues(error)`** — flattens a `ZodError` into `{ path, message }` entries with **dotted paths** (e.g. `rules.0.value`), including nested list issues.
256
256
 
257
257
  ### What each path validates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-luna-form",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
4
4
  "description": "A React library for building forms based on JSON",
5
5
  "main": "./dist/client/cjs/index.js",
6
6
  "module": "./dist/client/esm/index.js",
@@ -31,11 +31,11 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/react": "19.2.18",
34
- "@types/react-dom": "19.2.4",
34
+ "@types/react-dom": "19.2.5",
35
35
  "eslint-plugin-react": "7.37.5",
36
36
  "eslint-plugin-react-hooks": "7.1.1",
37
- "eslint-plugin-react-refresh": "0.5.4",
38
- "@luna-form/core": "0.0.83"
37
+ "eslint-plugin-react-refresh": "0.5.5",
38
+ "@luna-form/core": "0.0.85"
39
39
  },
40
40
  "license": "Apache-2.0",
41
41
  "repository": {