react-luna-form 0.0.80 → 0.0.82

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,128 @@
1
+ # Format Filters
2
+
3
+ Format filters transform interpolated values into locale-aware strings using a pipe syntax inspired by Vue and Angular.
4
+
5
+ ## Where filters can be used
6
+
7
+ Filters work in any string passed through Luna Form's interpolation engine, including:
8
+
9
+ - Field `description` (static or applied via a `state` change action)
10
+ - Field `label`
11
+ - `value` event payloads
12
+ - `source.url` and `source.body`
13
+
14
+ ## Syntax
15
+
16
+ ```
17
+ {key | filter:arg1:arg2}
18
+ ```
19
+
20
+ - `key` is the value to look up in the interpolation context. Supports dot notation (`{user.amount}`).
21
+ - After `|` comes one or more filters separated by additional `|` pipes.
22
+ - Each filter accepts optional positional arguments separated by `:`.
23
+
24
+ Multiple filters can be chained; the output of the previous filter becomes the input of the next.
25
+
26
+ ## Locale resolution
27
+
28
+ 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
+
30
+ ```ts
31
+ const config = {
32
+ env: {
33
+ locale: 'es-ES',
34
+ },
35
+ // ...
36
+ }
37
+ ```
38
+
39
+ ## Fallback rules
40
+
41
+ - **Unknown filter name**: the entire placeholder is preserved literally (`{value | foo}` stays as `{value | foo}`).
42
+ - **Invalid input** (NaN for numeric filters, unparseable date for date filters): the filter returns `String(value)` and the surrounding template still renders.
43
+ - **Missing key**: the placeholder is preserved literally.
44
+
45
+ ## Reference
46
+
47
+ ### `currency`
48
+
49
+ Formats a number as currency.
50
+
51
+ - **Syntax**: `{value | currency:CODE}`
52
+ - **Args**:
53
+ - `CODE` (string, default `"USD"`) — ISO 4217 currency code.
54
+ - **Backend**: `Intl.NumberFormat(locale, { style: 'currency', currency: CODE })`.
55
+ - **Examples**:
56
+ - `1234.56` with `currency:USD` and `locale: en-US` → `$1,234.56`.
57
+ - `1234.56` with `currency:EUR` and `locale: es-ES` → `1234,56 €`.
58
+
59
+ ### `percent`
60
+
61
+ Formats a fractional number as a percentage.
62
+
63
+ - **Syntax**: `{value | percent}`
64
+ - **Backend**: `Intl.NumberFormat(locale, { style: 'percent' })`.
65
+ - **Examples**:
66
+ - `0.25` with `locale: en-US` → `25%`.
67
+ - **Note**: input is interpreted as a fraction (`1` is `100%`).
68
+
69
+ ### `number`
70
+
71
+ Formats a number with locale-aware thousands separators.
72
+
73
+ - **Syntax**: `{value | number}`
74
+ - **Backend**: `Intl.NumberFormat(locale)`.
75
+ - **Examples**:
76
+ - `1234567` with `locale: en-US` → `1,234,567`.
77
+ - `1234567` with `locale: es-ES` → `1.234.567`.
78
+
79
+ ### `date`
80
+
81
+ Formats a date or ISO string with a named style.
82
+
83
+ - **Syntax**: `{value | date:STYLE}`
84
+ - **Args**:
85
+ - `STYLE` (string, default `"short"`) — one of `short`, `medium`, `long`, `full`, `relative`. Any other value is passed directly to `date-fns`'s `format` as a pattern.
86
+ - **Backend**: `date-fns` (`format`, `formatDistanceToNow` for `relative`).
87
+ - **Accepts**: `Date`, ISO string, or numeric timestamp (ms).
88
+ - **Examples**:
89
+ - `"2026-05-10"` with `date:long` and `locale: en-US` → `May 10, 2026`.
90
+ - A future date with `date:relative` and `locale: en-US` → `in 7 days`.
91
+
92
+ ### `duration`
93
+
94
+ Produces a localized human-readable duration in years, months, days, hours, minutes, and seconds.
95
+
96
+ - **Syntax**: `{value | duration}` or `{value | duration:UNIT}`
97
+ - **Args**:
98
+ - `UNIT` (string, default `"ms"`) — input unit when the value is a number. One of `ms`, `s`, `min`, `h`, `d`.
99
+ - **Backend**: `date-fns` (`formatDistanceToNow`, `formatDuration`, `intervalToDuration`).
100
+ - **Behavior depends on input type**:
101
+ - **Number** (or numeric string): treated as a duration in `UNIT` units, converted to milliseconds, then formatted as a breakdown (`"1 day 2 hours"`).
102
+ - **`Date` or non-numeric ISO string**: distance between the date and "now" with a suffix (`"3 months ago"`, `"in 2 days"`).
103
+ - **Multipliers to milliseconds**:
104
+
105
+ | Unit | Multiplier |
106
+ | ----- | ---------- |
107
+ | `ms` | 1 |
108
+ | `s` | 1,000 |
109
+ | `min` | 60,000 |
110
+ | `h` | 3,600,000 |
111
+ | `d` | 86,400,000 |
112
+
113
+ - **Examples**:
114
+ - `{age | duration:s}` with `3600` → `1 hour`.
115
+ - `{wait | duration:min}` with `90` → `1 hour 30 minutes`.
116
+ - `{ms | duration}` with `93_600_000` → `1 day 2 hours`.
117
+ - `{createdAt | duration}` with an ISO date in the past → `3 months ago`.
118
+
119
+ ## Supported locales
120
+
121
+ Out of the box, the following locales map to a `date-fns/locale` bundle for `date` (relative and pattern styles) and `duration`:
122
+
123
+ - `en`, `en-US` → `enUS`
124
+ - `es`, `es-ES`, `es-MX` → `es`
125
+
126
+ Other locale codes match by language prefix (e.g. `es-AR` → `es`). If no match is found, `enUS` is used as fallback.
127
+
128
+ `Intl`-backed filters (`currency`, `percent`, `number`) accept any well-formed BCP 47 locale tag supported by the runtime.
package/docs/setup.md ADDED
@@ -0,0 +1,165 @@
1
+ # Setup
2
+
3
+ How a form is configured and rendered. The pages under `fields/`,
4
+ `validation/` and `events/` describe the JSON; this one describes the code
5
+ around it.
6
+
7
+ ## Entry points
8
+
9
+ | Import | Contains |
10
+ | ------------------------ | ----------------------------------------------------------------------------------------- |
11
+ | `react-luna-form` | `Form` (client), `withDeclaredFields`, and the `Sections`, `Source` and `ZodSchema` types |
12
+ | `react-luna-form/server` | `Form` (server) and `Fallback` |
13
+ | `react-luna-form/config` | `defineConfig` and the `define*` registration helpers |
14
+ | `react-luna-form/schema` | `buildFormSchema` and `collectIssues` |
15
+
16
+ ## Configuring
17
+
18
+ `defineConfig` is called once, usually in a `luna.config.ts` at the root of the
19
+ application, and its result is passed to every `Form`.
20
+
21
+ ```ts
22
+ import { defineConfig, defineInput, defineSelect } from 'react-luna-form/config'
23
+
24
+ export default defineConfig({
25
+ inputs: [defineInput(Input), defineSelect(Select)],
26
+ })
27
+ ```
28
+
29
+ ### `inputs` (required)
30
+
31
+ The components that render each field type. Registration is by exact type
32
+ string; a type nothing registers renders nothing at all. The helpers cover the
33
+ types this library ships:
34
+
35
+ | Helper | Registers |
36
+ | ------------------- | --------------------------------------------------------------------------------------------------------------- |
37
+ | `defineInput` | `input`, `input/date`, `input/email`, `input/number`, `input/password`, `input/tel`, `input/text`, `input/time` |
38
+ | `defineSelect` | `select`, `select/active`, `select/day`, `select/month`, `select/timezone`, `select/year` |
39
+ | `defineChips` | `chips`, `chips/day`, `chips/month` |
40
+ | `defineRadio` | `radio` |
41
+ | `defineCheckbox` | `checkbox` |
42
+ | `defineTextArea` | `textarea` |
43
+ | `defineCustomInput` | whatever names you give it — see [fields/custom-inputs.md](fields/custom-inputs.md) |
44
+
45
+ ### `validation`
46
+
47
+ When to validate and whether to summarise errors. All four default to `true`.
48
+
49
+ ```ts
50
+ validation: { blur: true, change: true, showError: true, submit: true }
51
+ ```
52
+
53
+ **This object replaces the defaults, it does not merge into them.** Passing
54
+ `{ change: false }` also turns off `blur`, `submit` and `showError`, so the form
55
+ stops validating on blur and loses its error summary with no indication why.
56
+ Pass every key you still want.
57
+
58
+ ### `fetcher.remotePatterns`
59
+
60
+ Which hosts a field's remote `source` may call. The rules:
61
+
62
+ | Value | Effect |
63
+ | ------- | ------------------------------------ |
64
+ | omitted | **every host is allowed** |
65
+ | `[]` | every external URL is blocked |
66
+ | a list | an external URL must match one entry |
67
+
68
+ A pattern may constrain `protocol`, `hostname` and `port`; every part is
69
+ optional and an omitted part matches anything. Relative URLs count as internal
70
+ and are always allowed.
71
+
72
+ ```ts
73
+ fetcher: {
74
+ remotePatterns: [{ protocol: 'https', hostname: 'api.example.com' }]
75
+ }
76
+ ```
77
+
78
+ A blocked URL is not an error. The library logs
79
+ `URL blocked by remotePatterns: <url>` and the field renders with no options at
80
+ all, which on screen looks like an empty select rather than a rejected request.
81
+ Check the console when a remote source comes back empty.
82
+
83
+ ### `style`, `alert`, `env`
84
+
85
+ - `style` takes `compact`, `horizontal` and `showOptionalLabel`.
86
+ - `alert` is the component that renders the error summary, when `showError`
87
+ is on.
88
+ - `env` is a flat record of values available to interpolation, in labels and in
89
+ descriptions. Its `locale` key is also what those two read to format values.
90
+
91
+ ## Rendering
92
+
93
+ ### `Form` from `react-luna-form`
94
+
95
+ The client form. It owns its own state, so it is the one to use for anything
96
+ interactive.
97
+
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 |
111
+
112
+ ### `Form` from `react-luna-form/server`
113
+
114
+ The same descriptors rendered without client state. It accepts the same props
115
+ minus the interactive ones: there is no `action`, `onSuccess` or
116
+ `onValueChange`. Use it for a form that only needs to be displayed.
117
+
118
+ ### `Fallback`
119
+
120
+ A `Suspense` boundary whose fallback is the form itself in `readOnly` mode:
121
+
122
+ ```tsx
123
+ <Fallback config={config} sections={sections}>
124
+ <SomethingAsync />
125
+ </Fallback>
126
+ ```
127
+
128
+ The placeholder is the real layout rather than a generic skeleton, so nothing
129
+ shifts when the content arrives.
130
+
131
+ ## `withDeclaredFields`
132
+
133
+ A form treats a name missing from its `value` object as a field the caller did
134
+ not mention, and leaves that input showing whatever it was showing. That is
135
+ correct while values are only ever being added, and wrong the moment one is
136
+ lost: reverting to an earlier state does not empty a key, it removes it, and the
137
+ input goes on displaying text that exists nowhere else.
138
+
139
+ ```tsx
140
+ import { withDeclaredFields } from 'react-luna-form'
141
+
142
+ ;<Form
143
+ sections={sections}
144
+ config={config}
145
+ value={withDeclaredFields(sections, value)}
146
+ />
147
+ ```
148
+
149
+ Naming the field is the difference between saying nothing about it and saying it
150
+ holds nothing, and only the second empties the input. Wrapping the value is the
151
+ opt-in: pass the result and the object becomes the form's state.
152
+
153
+ Absence is resolved with the same lookup the form uses, so a name that is a path
154
+ (`basicAuth.username` against a nested object) is handled correctly rather than
155
+ shadowed by a flat key.
156
+
157
+ ## `react-luna-form/schema`
158
+
159
+ `buildFormSchema(sections)` derives the Zod schema from the same descriptors
160
+ that render the form, which is what keeps the rendered fields and the validated
161
+ shape from drifting apart. `collectIssues(error)` flattens a Zod error into
162
+ `{ path, message }` entries.
163
+
164
+ Use them to validate on the server against the same rules the form applies in
165
+ the browser.
@@ -0,0 +1,318 @@
1
+ # Validation
2
+
3
+ Luna Form derives a [Zod](https://zod.dev) schema directly from the form JSON, so the **form definition is the single source of truth for validation**. Each field carries a `validation` object that maps a constraint to its error message; the builder turns those declarations into a runnable schema.
4
+
5
+ The same schema powers two paths:
6
+
7
+ - **In the browser**, the rendered `<Form>` validates on blur/submit.
8
+ - **On the server (or any headless caller)**, `react-luna-form/schema` exposes `buildFormSchema(sections)` so you can run the _exact same_ validation without React — see [Headless validation](#headless-validation).
9
+
10
+ This page documents every key of the `validation` object plus the shared operator vocabulary. Per-field basics also appear in each field's own page (e.g. [Input](../fields/input.md), [Select](../fields/select.md), [List](../fields/list.md)).
11
+
12
+ ---
13
+
14
+ ## The `validation` object
15
+
16
+ `validation` lives on a field and maps a constraint name to a message (or a rule declaration):
17
+
18
+ - **`required`** _(string)_: Message shown when a field marked `required: true` is empty.
19
+ - **`email`** _(string)_: Message for an invalid email format (`input/email`).
20
+ - **`length`** _({ min?: string, max?: string })_: Messages when the value's length breaches `advanced.length`.
21
+ - **`custom`** _(CustomValidation | CustomValidation[])_: Cross-field comparison. See [Custom (cross-field)](#custom-cross-field).
22
+ - **`requiredWhen`** _(WhenRule | WhenRule[])_: Conditionally required. See [requiredWhen](#requiredwhen).
23
+ - **`pattern`** _(PatternRule)_: Regex/format check. See [pattern](#pattern).
24
+ - **`rules`** _(AssertRule[])_: General gated assertions. See [rules](#rules).
25
+
26
+ > Emptiness is normalized for validation: an empty string, a whitespace-only string, `null`/`undefined`, and an empty array all count as "no value". So `pattern` and format rules never fire on an empty field — that is `required`/`requiredWhen`'s job.
27
+
28
+ ---
29
+
30
+ ## `requiredWhen`
31
+
32
+ Makes a field required **only when a condition over sibling data holds**. This is the declarative replacement for wiring "required" to visibility by hand.
33
+
34
+ A `WhenRule` is:
35
+
36
+ - **`field`** _(string, required)_: The sibling field whose value is tested.
37
+ - **`operator`** _(Operator, optional)_: How to compare. Defaults to `eq`. See [Operators](#operators).
38
+ - **`value`** _(string | number | boolean | Array, optional)_: The value to compare against (ignored by the unary operators `exists` / `empty` / `truthy`).
39
+ - **`message`** _(string, optional)_: The error message. Falls back to `validation.required` when omitted.
40
+
41
+ Provide a single rule or an array. **An array is OR** — the field is required if _any_ rule's condition holds.
42
+
43
+ ```json
44
+ {
45
+ "name": "token",
46
+ "type": "input/text",
47
+ "validation": {
48
+ "requiredWhen": {
49
+ "field": "authType",
50
+ "operator": "eq",
51
+ "value": "bearer",
52
+ "message": "Provide the access token"
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Item scope inside lists
59
+
60
+ When a `requiredWhen` field lives inside a [`list`](../fields/list.md), its `field` reference resolves **within the same item**, not the form root. This lets each row validate against its own siblings:
61
+
62
+ ```json
63
+ {
64
+ "name": "rules",
65
+ "type": "list",
66
+ "fields": [
67
+ { "name": "operator", "type": "select" },
68
+ {
69
+ "name": "value",
70
+ "type": "input/text",
71
+ "validation": {
72
+ "requiredWhen": {
73
+ "field": "operator",
74
+ "operator": "nin",
75
+ "value": ["truthy", "exists"],
76
+ "message": "Enter the value to compare against"
77
+ }
78
+ }
79
+ }
80
+ ]
81
+ }
82
+ ```
83
+
84
+ Here `value` is required unless this row's `operator` is `truthy` or `exists`. Item-scoped `requiredWhen` is resolved by the [headless builder](#headless-validation).
85
+
86
+ ---
87
+
88
+ ## `pattern`
89
+
90
+ Validates a string value against a regular expression.
91
+
92
+ - **`regex`** _(string, required)_: The pattern source (compiled with `new RegExp`).
93
+ - **`flags`** _(string, optional)_: Regex flags, e.g. `"i"`.
94
+ - **`message`** _(string, optional)_: The error message.
95
+ - **`allowInterpolation`** _(boolean, optional)_: When `true`, the check is **skipped** if the value is dynamic and only resolved at run time — either an interpolation template (contains `{...}`) or a reference beginning with `@` (e.g. `@Trigger.url`), the convention applications built on this library use for a value resolved elsewhere. Use this for fields that accept dynamic values, e.g. a URL like `{step.url}` or `@step.url`.
96
+
97
+ ```json
98
+ {
99
+ "name": "url",
100
+ "type": "input/text",
101
+ "validation": {
102
+ "pattern": {
103
+ "regex": "^https?://",
104
+ "allowInterpolation": true,
105
+ "message": "The web address must start with http:// or https://"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ `pattern` never fires on an empty value — pair it with `required`/`requiredWhen` when the field is mandatory.
112
+
113
+ ---
114
+
115
+ ## `rules`
116
+
117
+ An escape hatch that keeps complex validation **in JSON**: an array of gated assertions evaluated in order.
118
+
119
+ Each `AssertRule` is:
120
+
121
+ - **`assert`** _(string, required)_: What to check — one of `required`, `minItems`, `maxItems`, `min`, `max`, `oneOf`, `pattern`.
122
+ - **`value`** _(depends on `assert`)_: The bound the assertion checks against (a number, an array for `oneOf`, or a `PatternRule` for `pattern`).
123
+ - **`message`** _(string, required)_: The error shown when the assertion fails.
124
+ - **`when`** _(WhenClause, optional)_: Only run the assertion when this condition holds. Omit to always run it.
125
+
126
+ ### Assertions
127
+
128
+ | `assert` | Passes when | `value` |
129
+ | ---------- | --------------------------------------------------- | ------------- |
130
+ | `required` | The field has a value | — |
131
+ | `minItems` | Array length ≥ value | number |
132
+ | `maxItems` | Array length ≤ value | number |
133
+ | `min` | Size ≥ value (number value, or string/array length) | number |
134
+ | `max` | Size ≤ value | number |
135
+ | `oneOf` | Value is one of the listed values | array |
136
+ | `pattern` | Value matches the pattern | `PatternRule` |
137
+
138
+ ### `when` clause
139
+
140
+ A `when` gate can be a single rule, an array, or an all/any object:
141
+
142
+ - A single **`WhenRule`** — runs when that condition holds.
143
+ - An **array of `WhenRule`** — **AND**: every condition must hold.
144
+ - **`{ all?: WhenRule[]; any?: WhenRule[] }`** — `all` requires every rule, `any` requires at least one; both must be satisfied when present.
145
+
146
+ ```json
147
+ {
148
+ "name": "port",
149
+ "type": "input/number",
150
+ "validation": {
151
+ "rules": [
152
+ {
153
+ "when": { "field": "kind", "operator": "eq", "value": "custom" },
154
+ "assert": "required",
155
+ "message": "Port is required for custom connections"
156
+ },
157
+ {
158
+ "assert": "oneOf",
159
+ "value": ["eq", "neq", "gt"],
160
+ "message": "Choose a valid operator"
161
+ }
162
+ ]
163
+ }
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## `custom` (cross-field)
170
+
171
+ Compares this field's value against **another field's value** using an operator. Provide one rule or an array (all are checked).
172
+
173
+ - **`field`** _(string)_: The other field to compare against.
174
+ - **`operator`** _(Operator, optional)_: Defaults to `eq`.
175
+ - **`message`** _(string)_: The error shown when the comparison fails.
176
+
177
+ ```json
178
+ {
179
+ "name": "confirmPassword",
180
+ "type": "input/text",
181
+ "required": true,
182
+ "validation": {
183
+ "custom": {
184
+ "field": "password",
185
+ "operator": "eq",
186
+ "message": "Passwords must match"
187
+ }
188
+ }
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Operators
195
+
196
+ Operators are shared across `custom`, `requiredWhen`, `rules` gates, and [change-event conditions](../events/change.md). Each compares a `current` value against a `value`:
197
+
198
+ | Operator | True when | Notes |
199
+ | ------------ | ---------------------------------------------- | --------------------------- |
200
+ | `eq` | `current === value` | default |
201
+ | `neq` | `current !== value` | |
202
+ | `in` | `value` (array) contains `current` | |
203
+ | `nin` | `value` (array) does **not** contain `current` | |
204
+ | `gt` / `gte` | `current > value` / `>=` | numeric |
205
+ | `lt` / `lte` | `current < value` / `<=` | numeric |
206
+ | `contains` | `current` (string/array) contains `value` | |
207
+ | `exists` | `current` has a value | **unary** — `value` ignored |
208
+ | `empty` | `current` has no value (inverse of `exists`) | **unary** — `value` ignored |
209
+ | `truthy` | `current` is truthy | **unary** — `value` ignored |
210
+
211
+ `contains`, `exists`, `empty`, and `truthy` are the newest additions; the rest have always been available.
212
+
213
+ ---
214
+
215
+ ## List length
216
+
217
+ A [`list`](../fields/list.md) can require a minimum/maximum number of items. The bounds live on `advanced.length`, and the messages on `validation.length`:
218
+
219
+ ```json
220
+ {
221
+ "name": "rules",
222
+ "type": "list",
223
+ "advanced": { "length": { "min": 1, "max": 5 } },
224
+ "validation": {
225
+ "length": {
226
+ "min": "Add at least one condition",
227
+ "max": "You can add up to five conditions"
228
+ }
229
+ },
230
+ "fields": [{ "name": "label", "type": "input/text" }]
231
+ }
232
+ ```
233
+
234
+ A missing list key is treated as an empty list, so `length.min` still fails on an absent value rather than silently passing. List length is enforced by the [headless builder](#headless-validation).
235
+
236
+ ---
237
+
238
+ ## Headless validation
239
+
240
+ The server-safe entry `react-luna-form/schema` exposes the builder with **no React dependency**, so a backend can validate a stored configuration with the same rules the form uses:
241
+
242
+ ```ts
243
+ import { buildFormSchema, collectIssues } from 'react-luna-form/schema'
244
+
245
+ const schema = buildFormSchema(sections) // sections: the form JSON
246
+ const result = schema.safeParse(config)
247
+
248
+ if (!result.success) {
249
+ const issues = collectIssues(result.error)
250
+ // [{ path: 'rules.0.value', message: 'Enter the value to compare against' }]
251
+ }
252
+ ```
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.
255
+ - **`collectIssues(error)`** — flattens a `ZodError` into `{ path, message }` entries with **dotted paths** (e.g. `rules.0.value`), including nested list issues.
256
+
257
+ ### What each path validates
258
+
259
+ | Rule | Rendered `<Form>` submit | `buildFormSchema` (headless) |
260
+ | --------------------------------------------- | ------------------------ | ---------------------------- |
261
+ | `required`, `email`, `length` (field) | ✅ | ✅ |
262
+ | `custom` | ✅ | ✅ |
263
+ | `requiredWhen` (top-level) | ✅ | ✅ |
264
+ | `pattern` | ✅ | ✅ |
265
+ | `rules` (top-level) | ✅ | ✅ |
266
+ | `requiredWhen` **inside a list** (item scope) | — | ✅ |
267
+ | List `length` | — | ✅ |
268
+
269
+ The headless builder is the superset: item-scoped conditions and list length require the nested tree it produces.
270
+
271
+ ### Nested (dotted) field names
272
+
273
+ A field whose `name` contains a dot — e.g. `basicAuth.username` — is grouped into a **nested object** by `buildFormSchema`, matching the nested config the runtime persists (the `<Form>` submits flat keys and then un-flattens them). So `{ "name": "basicAuth.username" }` validates against `{ basicAuth: { username } }`, and its issue path is `basicAuth.username`. The nested group is optional as a whole: a config may omit it entirely (e.g. no `basicAuth` when auth is off) and rely on `requiredWhen` for the conditional requirement.
274
+
275
+ ### Hidden fields
276
+
277
+ `buildFormSchema` treats a `hidden: true` field as **not structurally required**, even if it declares `required: true`. A hidden field is only shown once an event reveals it, so its requirement is conditional by nature — express it with [`requiredWhen`](#requiredwhen) (typically keyed on the same condition that reveals it). This keeps the headless schema from over-requiring fields the rendered form never mounts. `pattern`/format rules on a hidden field still fire when it holds a value.
278
+
279
+ ---
280
+
281
+ ## Complete example
282
+
283
+ ```json
284
+ {
285
+ "sections": [
286
+ {
287
+ "fields": [
288
+ { "name": "authType", "type": "select" },
289
+ {
290
+ "name": "token",
291
+ "type": "input/text",
292
+ "validation": {
293
+ "requiredWhen": {
294
+ "field": "authType",
295
+ "operator": "eq",
296
+ "value": "bearer",
297
+ "message": "Provide the access token"
298
+ }
299
+ }
300
+ },
301
+ {
302
+ "name": "url",
303
+ "type": "input/text",
304
+ "required": true,
305
+ "validation": {
306
+ "required": "The web address is required",
307
+ "pattern": {
308
+ "regex": "^https?://",
309
+ "allowInterpolation": true,
310
+ "message": "Use a valid http(s) address"
311
+ }
312
+ }
313
+ }
314
+ ]
315
+ }
316
+ ]
317
+ }
318
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-luna-form",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
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",
@@ -15,7 +15,8 @@
15
15
  "validation"
16
16
  ],
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "docs"
19
20
  ],
20
21
  "peerDependencies": {
21
22
  "date-fns": "^4.0.0",
@@ -34,7 +35,7 @@
34
35
  "eslint-plugin-react": "7.37.5",
35
36
  "eslint-plugin-react-hooks": "7.1.1",
36
37
  "eslint-plugin-react-refresh": "0.5.4",
37
- "@luna-form/core": "0.0.80"
38
+ "@luna-form/core": "0.0.82"
38
39
  },
39
40
  "license": "Apache-2.0",
40
41
  "repository": {