react-luna-form 0.0.81 → 0.0.83

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,175 @@
1
+ # Specialized Selectors
2
+
3
+ Specialized selectors in Luna Form come with pre-defined data sources, such as months, years, or timezones. These allow you to quickly implement common form fields without manually defining a `source` array.
4
+
5
+ ## Date & Time Selectors
6
+
7
+ ### 1. Month Selector (`select/month`)
8
+
9
+ Renders a dropdown with the 12 months of the year.
10
+
11
+ ```json
12
+ {
13
+ "name": "expiry_month",
14
+ "type": "select/month",
15
+ "label": "Expiration Month",
16
+ "required": true
17
+ }
18
+ ```
19
+
20
+ ### 2. Year Selector (`select/year`)
21
+
22
+ Renders a dropdown with a range of years.
23
+
24
+ - **Default behavior**: Shows the current year plus the next 5 years.
25
+ - **Customization**: Use `advanced.length.min` and `advanced.length.max` to define the start and end years relative to the current year.
26
+
27
+ ```json
28
+ {
29
+ "name": "expiry_year",
30
+ "type": "select/year",
31
+ "label": "Expiration Year",
32
+ "advanced": {
33
+ "length": {
34
+ "min": 0,
35
+ "max": 10
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### 3. Day Selector (`select/day`)
42
+
43
+ Renders a dropdown with days 1 through 31.
44
+
45
+ ```json
46
+ {
47
+ "name": "birth_day",
48
+ "type": "select/day",
49
+ "label": "Day"
50
+ }
51
+ ```
52
+
53
+ ### 4. Timezone Selector (`select/timezone`)
54
+
55
+ Provides a comprehensive list of global timezones.
56
+
57
+ ```json
58
+ {
59
+ "name": "user_timezone",
60
+ "type": "select/timezone",
61
+ "label": "Your Timezone",
62
+ "defaultValue": "America/New_York"
63
+ }
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Utility Selectors
69
+
70
+ ### 5. Active/Binary Selector (`select/active`)
71
+
72
+ A simple binary toggle, typically rendered as "Yes" and "No".
73
+
74
+ ```json
75
+ {
76
+ "name": "is_active",
77
+ "type": "select/active",
78
+ "label": "Account Active?",
79
+ "defaultValue": "true"
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## specialized Chips
86
+
87
+ These types render as buttons (tags) instead of dropdowns.
88
+
89
+ ### 6. Day Chips (`chips/day`)
90
+
91
+ Renders chips for the 7 days of the week.
92
+
93
+ ```json
94
+ {
95
+ "name": "available_days",
96
+ "type": "chips/day",
97
+ "label": "Select Days",
98
+ "advanced": {
99
+ "multiple": true
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### 7. Month Chips (`chips/month`)
105
+
106
+ Renders chips for the 12 months of the year.
107
+
108
+ ```json
109
+ {
110
+ "name": "subscription_months",
111
+ "type": "chips/month",
112
+ "label": "Subscription Months"
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Localization
119
+
120
+ The option labels of these selectors are produced by the framework, not written in the schema, so most of them are resolved through the form's `lang` rather than against the `translations` dictionary the way an inline [array source](select.md#translating-options) is.
121
+
122
+ - `select/month`, `select/day`, `chips/day` and `chips/month` take their names from the form's `lang`, resolved through `Intl`. Without `lang` they fall back to the runtime locale, and a malformed tag (`es_MX`, `español`) falls back too rather than failing.
123
+ - `select/year` renders plain numbers.
124
+ - `select/timezone` renders its region groups and zone names in English.
125
+ - `select/active` is the exception: `Yes` and `No` are authored copy rather than locale data, so they **are** resolved against the dictionary, using their English text as the key — the same way the built-in `(Optional)` suffix is. The library ships the Spanish for both, so `lang: "es"` alone is enough; see [built-in translations](list.md#built-in-translations).
126
+
127
+ ```json
128
+ {
129
+ "lang": "es",
130
+ "sections": [
131
+ {
132
+ "fields": [{ "name": "month", "type": "select/month" }]
133
+ }
134
+ ]
135
+ }
136
+ ```
137
+
138
+ The dropdown lists `enero` through `diciembre`, while the submitted value stays the month number. Setting `lang` also keeps the server and client renders on the same locale; without it each side resolves its own default, which can differ.
139
+
140
+ `select/active` instead reads its two labels from the dictionary, and the library already ships the Spanish for them:
141
+
142
+ ```json
143
+ {
144
+ "lang": "es",
145
+ "sections": [
146
+ {
147
+ "fields": [{ "name": "active", "type": "select/active" }]
148
+ }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ The dropdown lists `Sí` and `No`, while the submitted value stays the boolean. A `lang` with no built-in dictionary renders them in English, and a form that wants different wording declares the keys itself:
154
+
155
+ ```json
156
+ {
157
+ "lang": "es",
158
+ "translations": { "es": { "Yes": "Activo", "No": "Inactivo" } }
159
+ }
160
+ ```
161
+
162
+ To control the wording further — or to localize any of the locale-driven selectors — declare an explicit `source`. It overrides the built-in options and, being an array, its labels go through the dictionary:
163
+
164
+ ```json
165
+ {
166
+ "name": "active",
167
+ "type": "select/active",
168
+ "source": [
169
+ { "value": "true", "label": "active_yes" },
170
+ { "value": "false", "label": "active_no" }
171
+ ]
172
+ }
173
+ ```
174
+
175
+ For `select/timezone` the override must keep the group shape (`{ "label": "...", "items": [...] }`) that the grouped selector expects.
package/docs/index.md ADDED
@@ -0,0 +1,53 @@
1
+ # react-luna-form
2
+
3
+ Reference for the JSON that drives a luna form. These files ship inside the
4
+ published package, so what you are reading always describes the version you have
5
+ installed.
6
+
7
+ Forms are declared as data, not composed as components: a `sections` array of
8
+ field descriptors is the single source for what renders **and** for the Zod
9
+ schema that validates it. Most mistakes come from assuming otherwise.
10
+
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`.
14
+
15
+ ## Where to look
16
+
17
+ | You are | Read |
18
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ |
19
+ | Declaring a text, number, date or password field | [fields/input.md](fields/input.md) |
20
+ | Declaring a `select`, `radio` or `chips` field, or filling one from a source | [fields/select.md](fields/select.md) |
21
+ | Declaring a repeatable group of fields | [fields/list.md](fields/list.md) |
22
+ | Looking for a date range, a color or another ready-made picker | [fields/specialized-selectors.md](fields/specialized-selectors.md) |
23
+ | Implementing a field type this library does not ship | [fields/custom-inputs.md](fields/custom-inputs.md) |
24
+ | Making a field required, or validating against another field | [validation/overview.md](validation/overview.md) |
25
+ | 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) |
27
+
28
+ ## Three things that surprise people
29
+
30
+ Read the relevant page before writing the field, not after debugging it.
31
+
32
+ - **A field type nothing implements renders nothing at all.** `config.inputs`
33
+ is an exact map, with no prefix resolution: a `type` that was never
34
+ registered resolves to `undefined` and the field is skipped, on the server
35
+ and on the client alike. No error, no placeholder — the field is simply
36
+ absent from the form.
37
+ - **`defineConfig({ validation })` replaces the defaults, it does not merge.**
38
+ Passing `{ change: false }` also turns off `blur`, `submit` and
39
+ `showError` — the form loses its error summary and nothing says why. Pass
40
+ every key you still want.
41
+ - **A `change` event also runs at mount.** A field that arrives with a value or
42
+ a `defaultValue` fires its `change` events once, so anything they reveal,
43
+ clear or fetch happens before the user touches the form. See
44
+ [events/change.md](events/change.md).
45
+
46
+ ## Entry points
47
+
48
+ | Import | Use |
49
+ | ------------------------ | ------------------------------------------------------------------------------------------- |
50
+ | `react-luna-form` | The `Form` component and client hooks |
51
+ | `react-luna-form/config` | `defineConfig`, `defineInput`, `defineCustomInput` and the rest of the registration helpers |
52
+ | `react-luna-form/schema` | Building and inspecting the Zod schema derived from `sections` |
53
+ | `react-luna-form/server` | Server-safe helpers, usable under the `react-server` condition |
@@ -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.