react-luna-form 0.0.86 → 0.0.88

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.
Files changed (85) hide show
  1. package/README.md +15 -10
  2. package/dist/client/cjs/index.js +2 -1
  3. package/dist/client/esm/index.js +2 -1
  4. package/dist/config/cjs/index.js +1 -1
  5. package/dist/config/esm/index.js +1 -1
  6. package/dist/schema/cjs/index.js +1 -1
  7. package/dist/schema/esm/index.js +1 -1
  8. package/dist/server/cjs/index.js +1 -1
  9. package/dist/server/esm/index.js +1 -1
  10. package/dist/types/luna-core/handle/source-event.d.ts +2 -1
  11. package/dist/types/luna-core/handle/value-event.d.ts +2 -1
  12. package/dist/types/luna-core/helper/input.d.ts +16 -7
  13. package/dist/types/luna-core/type.d.ts +15 -4
  14. package/dist/types/luna-core/util/constant.d.ts +4 -0
  15. package/dist/types/luna-core/util/date.d.ts +132 -6
  16. package/dist/types/luna-core/util/extract.d.ts +21 -0
  17. package/dist/types/luna-core/util/format.d.ts +11 -0
  18. package/dist/types/luna-core/util/is-input.d.ts +3 -2
  19. package/dist/types/luna-core/util/pattern.d.ts +28 -0
  20. package/dist/types/luna-core/util/prepare.d.ts +3 -2
  21. package/dist/types/luna-core/util/schema.d.ts +2 -1
  22. package/dist/types/luna-core/util/string.d.ts +7 -0
  23. package/dist/types/luna-react/src/client/component/field/field-list-preview-item.d.ts +3 -3
  24. package/dist/types/luna-react/src/client/component/field/field-preview-item.d.ts +4 -2
  25. package/dist/types/luna-react/src/client/component/field/field-preview-value.d.ts +4 -1
  26. package/dist/types/luna-react/src/client/component/field/field-preview.d.ts +6 -4
  27. package/dist/types/luna-react/src/{component → client/component}/field/field-set-advanced.d.ts +1 -1
  28. package/dist/types/luna-react/src/client/component/field/field-set.d.ts +11 -0
  29. package/dist/types/luna-react/src/client/component/field/field.d.ts +20 -11
  30. package/dist/types/luna-react/src/client/component/field-description.d.ts +2 -0
  31. package/dist/types/luna-react/src/{component → client/component}/formatted-description.d.ts +3 -3
  32. package/dist/types/luna-react/src/client/component/guard/list-guard.d.ts +1 -1
  33. package/dist/types/luna-react/src/client/component/input-strategies.d.ts +10 -2
  34. package/dist/types/luna-react/src/client/component/slot/slot.d.ts +1 -2
  35. package/dist/types/luna-react/src/client/hook/use-entry-atom.d.ts +18 -0
  36. package/dist/types/luna-react/src/client/hook/use-input-core.d.ts +4 -4
  37. package/dist/types/luna-react/src/client/hook/use-latest.d.ts +22 -0
  38. package/dist/types/luna-react/src/client/hook/use-live-item-value.d.ts +17 -3
  39. package/dist/types/luna-react/src/client/hook/use-value.d.ts +1 -1
  40. package/dist/types/luna-react/src/client/lib/error-store.d.ts +3 -4
  41. package/dist/types/luna-react/src/client/lib/host-value-store.d.ts +13 -15
  42. package/dist/types/luna-react/src/client/lib/list-store.d.ts +0 -2
  43. package/dist/types/luna-react/src/client/lib/resolve-value.d.ts +0 -5
  44. package/dist/types/luna-react/src/client/lib/source-store.d.ts +7 -4
  45. package/dist/types/luna-react/src/client/lib/state-store.d.ts +0 -4
  46. package/dist/types/luna-react/src/client/lib/store-helper.d.ts +5 -15
  47. package/dist/types/luna-react/src/client/lib/value-store.d.ts +0 -4
  48. package/dist/types/luna-react/src/component/column.d.ts +4 -2
  49. package/dist/types/luna-react/src/component/description.d.ts +9 -0
  50. package/dist/types/luna-react/src/component/field/field-list.d.ts +2 -3
  51. package/dist/types/luna-react/src/component/field/{field-set.d.ts → field-set-static.d.ts} +1 -1
  52. package/dist/types/luna-react/src/component/field/field.d.ts +4 -4
  53. package/dist/types/luna-react/src/component/field-description.d.ts +5 -4
  54. package/dist/types/luna-react/src/component/form.d.ts +10 -0
  55. package/dist/types/luna-react/src/component/input/input-base.d.ts +3 -3
  56. package/dist/types/luna-react/src/component/input-group.d.ts +4 -2
  57. package/dist/types/luna-react/src/component/input-label.d.ts +4 -2
  58. package/dist/types/luna-react/src/component/slot/list-slot.d.ts +1 -1
  59. package/dist/types/luna-react/src/component/slot/slot-base.d.ts +4 -3
  60. package/dist/types/luna-react/src/component/slot/slot-create.d.ts +2 -3
  61. package/dist/types/luna-react/src/component/slot/slot-list.d.ts +2 -3
  62. package/dist/types/luna-react/src/component/slot/slot.d.ts +1 -2
  63. package/dist/types/luna-react/src/config/index.d.ts +1 -0
  64. package/dist/types/luna-react/src/lib/visibility.d.ts +3 -0
  65. package/dist/types/luna-react/src/server/component/field.d.ts +2 -0
  66. package/dist/types/luna-react/src/server/component/input.d.ts +2 -2
  67. package/dist/types/luna-react/src/server/component/list-slot.d.ts +2 -0
  68. package/dist/types/luna-react/src/server/component/slot.d.ts +14 -0
  69. package/dist/types/luna-react/src/type.d.ts +2 -1
  70. package/docs/events/change.md +4 -1
  71. package/docs/fields/custom-inputs.md +153 -4
  72. package/docs/fields/data-source.md +5 -3
  73. package/docs/fields/input.md +175 -3
  74. package/docs/fields/list.md +1 -1
  75. package/docs/fields/specialized-selectors.md +51 -12
  76. package/docs/forms/submit.md +13 -14
  77. package/docs/index.md +2 -1
  78. package/docs/interpolation/format-filters.md +28 -29
  79. package/docs/interpolation/overview.md +44 -14
  80. package/docs/setup.md +56 -35
  81. package/docs/structure/definition.md +45 -6
  82. package/docs/validation/overview.md +15 -12
  83. package/package.json +5 -5
  84. package/dist/types/luna-react/src/client/component/field/field-with-state.d.ts +0 -13
  85. package/dist/types/luna-react/src/client/hook/use-format.d.ts +0 -5
@@ -25,21 +25,21 @@ Filters work in any string passed through Luna Form's interpolation engine, incl
25
25
 
26
26
  Multiple filters can be chained; the output of the previous filter becomes the input of the next.
27
27
 
28
- ## Locale resolution
28
+ ## Language
29
29
 
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).
30
+ Filters format in the form's `lang`, the tag that also picks its translations
31
+ and names its months:
31
32
 
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
-
34
- ```ts
35
- const config = {
36
- env: {
37
- locale: 'es-ES',
38
- },
39
- // ...
40
- }
33
+ ```tsx
34
+ <Form sections={sections} config={config} lang="es-ES" />
41
35
  ```
42
36
 
37
+ It applies wherever a filter is written — a label, a description, a `value` or
38
+ `source` payload — so one setting keeps them consistent. Without `lang`, or with
39
+ one that is no language tag or names no language the runtime has, filters write
40
+ English, on every machine. See
41
+ [Interpolation](overview.md#the-language-filters-use).
42
+
43
43
  ## Fallback rules
44
44
 
45
45
  - **Unknown filter name**: the entire placeholder is preserved literally (`{value | foo}` stays as `{value | foo}`).
@@ -57,8 +57,8 @@ Formats a number as currency.
57
57
  - `CODE` (string, default `"USD"`) — ISO 4217 currency code.
58
58
  - **Backend**: `Intl.NumberFormat(locale, { style: 'currency', currency: CODE })`.
59
59
  - **Examples**:
60
- - `1234.56` with `currency:USD` and `locale: en-US` → `$1,234.56`.
61
- - `1234.56` with `currency:EUR` and `locale: es-ES` → `1234,56 €`.
60
+ - `1234.56` with `currency:USD` and `lang: en-US` → `$1,234.56`.
61
+ - `1234.56` with `currency:EUR` and `lang: es-ES` → `1234,56 €`.
62
62
 
63
63
  ### `percent`
64
64
 
@@ -67,7 +67,7 @@ Formats a fractional number as a percentage.
67
67
  - **Syntax**: `{value | percent}`
68
68
  - **Backend**: `Intl.NumberFormat(locale, { style: 'percent' })`.
69
69
  - **Examples**:
70
- - `0.25` with `locale: en-US` → `25%`.
70
+ - `0.25` with `lang: en-US` → `25%`.
71
71
  - **Note**: input is interpreted as a fraction (`1` is `100%`).
72
72
 
73
73
  ### `number`
@@ -77,8 +77,8 @@ Formats a number with locale-aware thousands separators.
77
77
  - **Syntax**: `{value | number}`
78
78
  - **Backend**: `Intl.NumberFormat(locale)`.
79
79
  - **Examples**:
80
- - `1234567` with `locale: en-US` → `1,234,567`.
81
- - `1234567` with `locale: es-ES` → `1.234.567`.
80
+ - `1234567` with `lang: en-US` → `1,234,567`.
81
+ - `1234567` with `lang: es-ES` → `1.234.567`.
82
82
 
83
83
  ### `date`
84
84
 
@@ -87,11 +87,13 @@ Formats a date or ISO string with a named style.
87
87
  - **Syntax**: `{value | date:STYLE}`
88
88
  - **Args**:
89
89
  - `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.
90
- - **Backend**: `date-fns` (`format`, `formatDistanceToNow` for `relative`).
90
+ - **Backend**: `Intl.DateTimeFormat` with `dateStyle` for the named styles, `Intl.RelativeTimeFormat` for `relative`, and `date-fns`'s `format` for a pattern.
91
91
  - **Accepts**: `Date`, ISO string, or numeric timestamp (ms).
92
+ - **`relative`** is measured from [`context.now`](overview.md#the-instant-relative-dates-use), the instant the host passes, never from the clock of the machine that renders it. Without it the date is shown in the `medium` style.
92
93
  - **Examples**:
93
- - `"2026-05-10"` with `date:long` and `locale: en-US` → `May 10, 2026`.
94
- - A future date with `date:relative` and `locale: en-US` → `in 7 days`.
94
+ - `"2026-05-10"` with `date:long` and `lang: en-US` → `May 10, 2026`.
95
+ - `"2026-05-10"` with `date:long` and `lang: es-ES` → `10 de mayo de 2026`.
96
+ - `"2026-10-12T12:00:00Z"` with `date:relative`, `context.now` at `"2026-10-05T12:00:00Z"` and `lang: en-US` → `in 7 days`; with no `context.now` → `Oct 12, 2026`.
95
97
 
96
98
  ### `duration`
97
99
 
@@ -100,10 +102,10 @@ Produces a localized human-readable duration in years, months, days, hours, minu
100
102
  - **Syntax**: `{value | duration}` or `{value | duration:UNIT}`
101
103
  - **Args**:
102
104
  - `UNIT` (string, default `"ms"`) — input unit when the value is a number. One of `ms`, `s`, `min`, `h`, `d`.
103
- - **Backend**: `date-fns` (`formatDistanceToNow`, `formatDuration`, `intervalToDuration`).
105
+ - **Backend**: `Intl.RelativeTimeFormat` for a date, and `date-fns` (`formatDuration`, `intervalToDuration`) for a number.
104
106
  - **Behavior depends on input type**:
105
107
  - **Number** (or numeric string): treated as a duration in `UNIT` units, converted to milliseconds, then formatted as a breakdown (`"1 day 2 hours"`).
106
- - **`Date` or non-numeric ISO string**: distance between the date and "now" with a suffix (`"3 months ago"`, `"in 2 days"`).
108
+ - **`Date` or non-numeric ISO string**: distance between the date and [`context.now`](overview.md#the-instant-relative-dates-use) with a suffix (`"3 months ago"`, `"in 2 days"`). Without `context.now` the date is shown in the `medium` style.
107
109
  - **Multipliers to milliseconds**:
108
110
 
109
111
  | Unit | Multiplier |
@@ -118,15 +120,12 @@ Produces a localized human-readable duration in years, months, days, hours, minu
118
120
  - `{age | duration:s}` with `3600` → `1 hour`.
119
121
  - `{wait | duration:min}` with `90` → `1 hour 30 minutes`.
120
122
  - `{ms | duration}` with `93_600_000` → `1 day 2 hours`.
121
- - `{createdAt | duration}` with an ISO date in the past → `3 months ago`.
122
-
123
- ## Supported locales
123
+ - `{createdAt | duration}` with `"2026-07-07T12:00:00Z"` and `context.now` at `"2026-10-05T12:00:00Z"` → `3 months ago`.
124
124
 
125
- Out of the box, the following locales map to a `date-fns/locale` bundle for `date` (relative and pattern styles) and `duration`:
125
+ ## Supported languages
126
126
 
127
- - `en`, `en-US` → `enUS`
128
- - `es`, `es-ES`, `es-MX` → `es`
127
+ The named date styles, `relative`, `currency`, `percent` and `number` are written by `Intl`, in any well-formed BCP 47 tag the runtime supports. In English the named styles read as they always did: `10/2/26`, `Oct 2, 2026`, `October 2, 2026`, `Friday, October 2, 2026`.
129
128
 
130
- Other locale codes match by language prefix (e.g. `es-AR` → `es`). If no match is found, `enUS` is used as fallback.
129
+ `relative` writes the distance in the largest unit that holds it once rounded: seconds, minutes, hours, days up to a month, months up to a year, and then years. `in 40 seconds`, `in 3 hours`, `in 1 month`; 59 minutes and 40 seconds is `1 hour ago`, and no distance at all is `now` (`ahora` in Spanish).
131
130
 
132
- `Intl`-backed filters (`currency`, `percent`, `number`) accept any well-formed BCP 47 locale tag supported by the runtime.
131
+ A date pattern (`date:dd/MM/yyyy`) and a `duration` of a number are written by `date-fns`, which the library ships in English and Spanish. A tag matches by its base language (`es-AR` writes Spanish), and any other language gets English names and units.
@@ -33,13 +33,18 @@ about interpolation is one being read as the other.
33
33
  dot notation for anything nested:
34
34
 
35
35
  ```tsx
36
- <Form
37
- sections={sections}
38
- config={config}
39
- context={{ user: { firstName: 'Jane' }, plan: 'pro' }}
40
- />
36
+ const context = { user: { firstName: 'Jane' }, plan: 'pro' }
37
+
38
+ export function Page() {
39
+ return <Form sections={sections} config={config} context={context} />
40
+ }
41
41
  ```
42
42
 
43
+ Keep `context` the same object while nothing in it changes, as here, or with
44
+ `useMemo` when it comes from state: every field renders again when it is a new
45
+ one. See
46
+ [What the host knows](../structure/definition.md#what-the-host-knows-context).
47
+
43
48
  ```json
44
49
  {
45
50
  "name": "greeting",
@@ -148,19 +153,44 @@ until somebody fills it.
148
153
  - **Spaces are trimmed.** `{ value }` and `{value}` are the same placeholder, as
149
154
  are `{total|currency:USD}` and `{total | currency:USD}`.
150
155
 
151
- ## The locale filters use
156
+ ## The language filters use
157
+
158
+ Filters format in the form's language, its `lang`: the same tag that picks its
159
+ [translations](../fields/list.md#built-in-translations) and names its months.
160
+
161
+ ```tsx
162
+ <Form sections={sections} config={config} lang="es-CO" />
163
+ ```
164
+
165
+ It reaches every filter the form runs: a label, a description and the payload
166
+ of a `value` or `source` action format the same value the same way. Without
167
+ `lang`, with one that is no language tag (`es_MX`), or with one the runtime has
168
+ no language for (`sp`), the form speaks English, on the server and in the
169
+ browser alike, whatever language their machines speak. Its translations follow
170
+ the same tag, so a form never writes in two languages.
171
+ `config.env.locale` is not read; a development build names one left in the
172
+ config.
152
173
 
153
- Locale-aware filters read `config.env.locale`:
174
+ ## The instant relative dates use
154
175
 
155
- ```ts
156
- defineConfig({ inputs: [...], env: { locale: 'es-ES', currency: 'EUR' } })
176
+ The form keeps no clock. A [`date:relative`](format-filters.md#date) or a [`duration`](format-filters.md#duration) of a date is measured from `context.now`, an ISO date, time and offset the host passes, such as `2026-10-05T19:30:00-05:00` or what `toISOString()` gives:
177
+
178
+ ```tsx
179
+ // Worked out once per request, on the server.
180
+ const context = { now: new Date().toISOString(), renewal: plan.renewsAt }
181
+
182
+ <Form sections={sections} context={context} config={config} />
183
+ ```
184
+
185
+ ```json
186
+ { "label": "Renews {context.renewal | date:relative}" }
157
187
  ```
158
188
 
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.
189
+ With `now` at `2026-10-05T12:00:00Z` and the renewal at `2026-10-10T12:00:00Z`, the label reads `Renews in 5 days` on the server and in the browser alike, whatever their clocks say. Without `context.now`, or with one that is no ISO instant, the date is shown in the `medium` style: `Renews Oct 10, 2026`.
190
+
191
+ The offset is what makes it an instant. Without one, `2026-10-05T12:00:00` or `2026-10-05` would be read in the zone of the machine that renders it, so the form takes it for no instant. So is a `Date` or a timestamp: a development build names each once for the `context` that carries it. The date being measured is read the way the [`date`](format-filters.md#date) filter reads any date, so give it with its offset too: a day alone is midnight where the code runs, and the distance to it changes with the machine.
192
+
193
+ `now` is the one key of `context` the form reads on its own, wherever the text it formats can read `context`: a label and a description. A [`value` or `source` payload](#event-payloads) resolves against the selected option and nothing else, so a relative date written there shows the date. A [`select/timezone`](../fields/specialized-selectors.md#4-timezone-selector-selecttimezone) labels its zones for the same instant.
164
194
 
165
195
  ## An unresolved placeholder cancels a request
166
196
 
package/docs/setup.md CHANGED
@@ -13,15 +13,15 @@ pnpm add react-luna-form
13
13
  The package carries no dependencies of its own: everything it needs is a peer
14
14
  dependency, so your application resolves one copy of React, Zod and the rest.
15
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 |
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` | `^2.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.7.0` | merging the classes the components render |
25
25
 
26
26
  None of them is optional, and none is bundled. A package manager that installs
27
27
  peers automatically (npm 7+, or pnpm with `auto-install-peers=true`) brings
@@ -48,20 +48,26 @@ CSS.
48
48
 
49
49
  Every element also carries a `data-slot` attribute — `field`, `field-label`,
50
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.
51
+ `field-group`, `field-separator`, `collapsible-content`, `column`,
52
+ `list-item-card` — so a design system can restyle the structure without
53
+ patching the library. The components you register yourself are yours to style;
54
+ these slots cover the scaffolding around them.
55
55
 
56
56
  ## Entry points
57
57
 
58
58
  | Import | Contains |
59
59
  | ------------------------ | ----------------------------------------------------------------------------------------- |
60
60
  | `react-luna-form` | `Form` (client), `withDeclaredFields`, and the `Sections`, `Source` and `ZodSchema` types |
61
- | `react-luna-form/server` | `Form` (server) and `Fallback` |
61
+ | `react-luna-form/server` | `Form` (server) and `Fallback`, usable under the `react-server` condition |
62
62
  | `react-luna-form/config` | `defineConfig` and the `define*` registration helpers |
63
63
  | `react-luna-form/schema` | `buildFormSchema` and `collectIssues` |
64
64
 
65
+ The client bundle is published carrying a `'use client'` directive, so an App
66
+ Router project imports `Form` straight from a Server Component and needs no
67
+ wrapper module of its own to say it. The server bundle carries none and
68
+ resolves under the `react-server` condition, which is what lets a Server
69
+ Component render the form itself.
70
+
65
71
  ## Configuring
66
72
 
67
73
  `defineConfig` is called once, usually in a `luna.config.ts` at the root of the
@@ -99,10 +105,9 @@ When to validate and whether to summarise errors. All four default to `true`.
99
105
  validation: { blur: true, change: true, showError: true, submit: true }
100
106
  ```
101
107
 
102
- **This object replaces the defaults, it does not merge into them.** Passing
103
- `{ change: false }` also turns off `blur`, `submit` and `showError`, so the form
104
- stops validating on blur and loses its error summary with no indication why.
105
- Pass every key you still want.
108
+ This object merges into the defaults, so pass only what you want to change.
109
+ `{ change: false }` stops the form validating as the user types and leaves the
110
+ other three alone.
106
111
 
107
112
  ### `fetcher.remotePatterns`
108
113
 
@@ -127,7 +132,8 @@ fetcher: {
127
132
  A blocked URL is not an error. The library logs
128
133
  `URL blocked by remotePatterns: <url>` and the field renders with no options at
129
134
  all, which on screen looks like an empty select rather than a rejected request.
130
- Check the console when a remote source comes back empty.
135
+ Check the console when a remote source comes back empty — see
136
+ [Warnings](#warnings) for when that line is written.
131
137
 
132
138
  ### `style`, `alert`, `env`
133
139
 
@@ -135,7 +141,22 @@ Check the console when a remote source comes back empty.
135
141
  - `alert` is the component that renders the error summary, when `showError`
136
142
  is on.
137
143
  - `env` is a flat record of values available to interpolation, in labels and in
138
- descriptions. Its `locale` key is also what those two read to format values.
144
+ descriptions. It holds the application's values: the language is the form's
145
+ `lang`, and a development build names an `env.locale` nothing reads.
146
+
147
+ ## Warnings
148
+
149
+ Some of what can go wrong is nobody's to fix but the author of the form, and
150
+ the library says so on the console rather than on screen: a `pattern` that does
151
+ not compile, a field name that reaches a prototype and is left out of the
152
+ submit, the error an `action` threw, a remote `source` that failed or was
153
+ blocked. Every one of those lines is prefixed `[Luna Form]`.
154
+
155
+ They are written outside production and silent inside it, and which one you are
156
+ in is your build's answer to give: the published bundle leaves
157
+ `process.env.NODE_ENV` for your bundler to resolve instead of deciding while it
158
+ is being packaged. A development build of your application shows them and a
159
+ production build does not. Where nothing defines it at all, they are shown.
139
160
 
140
161
  ## Rendering
141
162
 
@@ -144,21 +165,21 @@ Check the console when a remote source comes back empty.
144
165
  The client form. It owns its own state, so it is the one to use for anything
145
166
  interactive.
146
167
 
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) |
168
+ | Prop | Purpose |
169
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
170
+ | `sections` | the field descriptors — the form itself |
171
+ | `config` | the result of `defineConfig` |
172
+ | `value` | the current values |
173
+ | `action` | server action invoked on submit, receives the form data and the schema — see [forms/submit.md](forms/submit.md) |
174
+ | `onSuccess` | called with the parsed data after `action` succeeds |
175
+ | `onValueChange` | called with `{ name, value }` on every change |
176
+ | `readOnly` | renders every field disabled |
177
+ | `translations` | per-language strings for labels, options and messages |
178
+ | `lang` | the form's language: its translations, its month and day names, its filters and the dates it shows; English without one |
179
+ | `context` | arbitrary values available to interpolation, and to a `$ref` under `#/context/` |
180
+ | `definition` | the JSON that [`$ref`](structure/definition.md) entries resolve against |
181
+ | `advanced.step` | numbers the [sections](structure/sections.md#advancedstep-numbers-the-sections) |
182
+ | `children` | the form's control area — see [forms/submit.md](forms/submit.md#nothing-submits-without-a-control) |
162
183
 
163
184
  ### `Form` from `react-luna-form/server`
164
185
 
@@ -67,6 +67,42 @@ section:
67
67
  References inside a definition are resolved too, so one entry can be built out
68
68
  of others.
69
69
 
70
+ ## What the host knows: `#/context/`
71
+
72
+ A definition is JSON the form reuses. What the host only knows when it renders,
73
+ such as the options a user may pick or the nights already booked, is passed as
74
+ the form's `context` prop instead and read with the same syntax, under
75
+ `#/context/`:
76
+
77
+ ```json
78
+ {
79
+ "name": "room",
80
+ "type": "select",
81
+ "source": { "$ref": "#/context/rooms" }
82
+ }
83
+ ```
84
+
85
+ ```tsx
86
+ const context = useMemo(() => ({ rooms }), [rooms])
87
+
88
+ <Form sections={sections} context={context} config={config} />
89
+ ```
90
+
91
+ Both roots resolve the same way and follow the rules below. A path with no root
92
+ reads the definition, as it always has, and an entry of the definition can
93
+ point into `#/context/`.
94
+
95
+ One key of `context` is read by the form on its own: `now`, the instant
96
+ [relative dates](../interpolation/overview.md#the-instant-relative-dates-use)
97
+ are measured from and a time zone select labels its zones for. The form keeps
98
+ no clock, so without it there is no "now".
99
+
100
+ Keep `context` the same object while nothing in it changes, and hand over a new
101
+ one when something does. Every field renders again when `context` is a new
102
+ object, since a label or a description may interpolate it, and a field whose
103
+ JSON reads `#/context/` is resolved again as well. A value changed in place, in
104
+ the same object, is not something the form can notice.
105
+
70
106
  ## The rules
71
107
 
72
108
  - **Resolution happens once, before anything else.** The tree is resolved first,
@@ -76,24 +112,27 @@ of others.
76
112
  stays as the literal `{ "$ref": ... }` object. What that looks like on screen
77
113
  depends on where it was — a field whose `source` is still a reference renders
78
114
  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.
115
+ A path that holds nothing is unresolved too: a key that is missing, and one
116
+ the host left `undefined` or `null`.
117
+ - **An empty or missing root resolves nothing under it.** `definition={{}}` is
118
+ treated as absent, and so is `context={{}}`: every `$ref` into a root that is
119
+ absent is left as written.
81
120
  - **A cycle stops instead of hanging.** Two entries pointing at each other
82
121
  resolve until the loop closes and leave the reference that would repeat
83
122
  unresolved.
84
123
 
85
124
  ## Pass it everywhere the sections go
86
125
 
87
- `definition` is a prop of both the client and the server `Form`, and the third
88
- argument of the headless builder:
126
+ `definition` and `context` are props of both the client and the server `Form`,
127
+ and the third and fourth arguments of the headless builder:
89
128
 
90
129
  ```ts
91
130
  import { buildFormSchema } from 'react-luna-form/schema'
92
131
 
93
- const schema = buildFormSchema(sections, translations, definition)
132
+ const schema = buildFormSchema(sections, translations, definition, context)
94
133
  ```
95
134
 
96
- A schema built without the definition sees `$ref` objects where the form sees
135
+ A schema built without them sees `$ref` objects where the form sees
97
136
  fields — the rendered form and the validated shape drift apart, which is the one
98
137
  thing [the schema being derived from the same descriptors](../validation/overview.md#headless-validation)
99
138
  is meant to prevent.
@@ -17,7 +17,10 @@ This page documents every key of the `validation` object plus the shared operato
17
17
 
18
18
  - **`required`** _(string)_: Message shown when a field marked `required: true` is empty.
19
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`.
20
+ - **`date`** _(string)_: Message when an `input/date` holds text that is no day. See [Input](../fields/input.md#what-a-date-holds).
21
+ - **`length`** _({ min?: string, max?: string })_: Messages when the value breaches `advanced.length`: its length, a number's range, or a date's first and last day. See [Input](../fields/input.md#the-first-and-the-last-day).
22
+ - **`reserved`** _(string)_: Message when an `input/date` holds a day its `advanced.reserved` lists. See [Input](../fields/input.md#days-nobody-can-pick).
23
+ - **`range`** _(string)_: Message when an `input/date` range has an end missing, or its last day before its first. See [Input](../fields/input.md#a-range-of-days).
21
24
  - **`step`** _(string)_: Message when an `input/number` is off its step: a decimal on a whole number, or a value off `advanced.step`. See [Input](../fields/input.md).
22
25
  - **`custom`** _(CustomValidation | CustomValidation[])_: Cross-field comparison. See [Custom (cross-field)](#custom-cross-field).
23
26
  - **`requiredWhen`** _(WhenRule | WhenRule[])_: Conditionally required. See [requiredWhen](#requiredwhen).
@@ -111,7 +114,7 @@ Validates a string value against a regular expression.
111
114
 
112
115
  `pattern` never fires on an empty value — pair it with `required`/`requiredWhen` when the field is mandatory.
113
116
 
114
- A `regex` or `flags` that does not compile is a mistake in the definition, not in the value. The rule then holds back every value it checks with its `message`, as it does a value that does not match, and the form keeps working. The same goes for a `pattern` among [`rules`](#rules).
117
+ A `regex` or `flags` that does not compile is a mistake in the definition, not in the value. The rule then holds back every value it checks with its `message`, as it does a value that does not match, and the form keeps working. The same goes for a `pattern` among [`rules`](#rules). Since the `message` is written for whoever fills the form in, the expression that failed to compile is named on the console instead — see [Warnings](../setup.md#warnings).
115
118
 
116
119
  A `regex` that compiles runs as written, on what the user types: in the browser when the form is submitted, and on the server when it validates with `buildFormSchema`. Nothing inspects it first, so it is part of the code the form ships with. Accept one only from someone you would let write that code.
117
120
 
@@ -260,20 +263,20 @@ if (!result.success) {
260
263
  }
261
264
  ```
262
265
 
263
- - **`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.
266
+ - **`buildFormSchema(sections, translations?, definition?, context?)`** — 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` and `context` resolve [`$ref` entries](../structure/definition.md), and have to be the same ones the form renders with. A date comes out of it as `yyyy-MM-dd`, as it does from the form's submit.
264
267
  - **`collectIssues(error)`** — flattens a `ZodError` into `{ path, message }` entries with **dotted paths** (e.g. `rules.0.value`), including nested list issues.
265
268
 
266
269
  ### What each path validates
267
270
 
268
- | Rule | Rendered `<Form>` submit | `buildFormSchema` (headless) |
269
- | --------------------------------------------- | ------------------------ | ---------------------------- |
270
- | `required`, `email`, `length`, `step` (field) | ✅ | ✅ |
271
- | `custom` | ✅ | ✅ |
272
- | `requiredWhen` (top-level) | ✅ | ✅ |
273
- | `pattern` | ✅ | ✅ |
274
- | `rules` (top-level) | ✅ | ✅ |
275
- | `requiredWhen` **inside a list** (item scope) | — | ✅ |
276
- | List `length` | — | ✅ |
271
+ | Rule | Rendered `<Form>` submit | `buildFormSchema` (headless) |
272
+ | -------------------------------------------------------------------------- | ------------------------ | ---------------------------- |
273
+ | `required`, `email`, `length`, `step`, `date`, `reserved`, `range` (field) | ✅ | ✅ |
274
+ | `custom` | ✅ | ✅ |
275
+ | `requiredWhen` (top-level) | ✅ | ✅ |
276
+ | `pattern` | ✅ | ✅ |
277
+ | `rules` (top-level) | ✅ | ✅ |
278
+ | `requiredWhen` **inside a list** (item scope) | — | ✅ |
279
+ | List `length` | — | ✅ |
277
280
 
278
281
  The headless builder is the superset: item-scoped conditions and list length require the nested tree it produces.
279
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-luna-form",
3
- "version": "0.0.86",
3
+ "version": "0.0.88",
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",
@@ -22,20 +22,20 @@
22
22
  "date-fns": "^4.0.0",
23
23
  "fast-equals": "^6.0.0",
24
24
  "jotai": "^2.0.0",
25
- "jotai-family": "^1.0.0",
26
25
  "react": "^19.0.0",
27
26
  "react-dom": "^19.0.0",
28
27
  "swr": "^2.0.0",
29
- "tailwind-merge": "^3.5.0",
28
+ "tailwind-merge": "^3.7.0",
30
29
  "zod": "^4.0.0"
31
30
  },
32
31
  "devDependencies": {
33
- "@luna-form/core": "0.0.86",
32
+ "@luna-form/core": "0.0.88",
34
33
  "@types/react": "19.3.0",
35
34
  "@types/react-dom": "19.3.0",
36
35
  "eslint-plugin-react": "7.37.5",
37
36
  "eslint-plugin-react-hooks": "7.1.1",
38
- "eslint-plugin-react-refresh": "0.5.7"
37
+ "eslint-plugin-react-refresh": "0.5.7",
38
+ "jotai-family": "1.1.0"
39
39
  },
40
40
  "license": "Apache-2.0",
41
41
  "repository": {
@@ -1,13 +0,0 @@
1
- export declare function withError<P extends {
2
- errors?: Record<string, string[]>;
3
- field: {
4
- name: string;
5
- };
6
- }>(Component: React.ComponentType<P>): (props: Readonly<P>) => import("react").JSX.Element | null;
7
- export declare function withState<P extends {
8
- disabled?: boolean;
9
- field: {
10
- name: string;
11
- hidden?: boolean;
12
- };
13
- }>(Component: React.ComponentType<P>): (props: Readonly<P>) => import("react").JSX.Element | null;
@@ -1,5 +0,0 @@
1
- import { type Field } from '../../../../luna-core/index';
2
- export declare function useFormat(field: Field): {
3
- dateFormat: import('../../../../luna-core/index').DateFormat | null;
4
- timeFormat: import('../../../../luna-core/index').TimeFormat | null;
5
- };