formstand 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,264 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 — 2026-09-16
4
+
5
+ ### Added
6
+
7
+ - **`useFields`: composite fields.** One control backed by several schema
8
+ paths (a coordinate pair behind a DMS input, a range, a date span)
9
+ binds them in one call: `useFields(form, ["latitude", "longitude"])`
10
+ returns the position-typed tuple of full field bindings plus the
11
+ combined view — `error` merged in path order and deduped (a
12
+ cross-field `superRefine` stamping both paths reads once),
13
+ `firstError`, any-of-them `touched`/`dirty`/`isValidating`, and one
14
+ `onBlur` covering every path. The paths tuple must be render-stable;
15
+ React itself throws its hooks-count error otherwise, at the exact
16
+ render that broke the contract.
17
+ - **`field.firstError`.** The first error message or `undefined`, derived
18
+ on `UseFieldReturn` — the display shorthand every consumer (and every
19
+ generated kit adapter) re-derived by hand as `error?.[0]` with an
20
+ empty-array guard. `{field.firstError}` is now the whole helper line;
21
+ the full `field.error` array is unchanged beside it. The CLI keeps
22
+ emitting its own helper for now so generated code stays runnable on
23
+ older formstand; a future major of the generated adapters can lean on
24
+ this instead.
25
+
26
+ ### Fixed
27
+
28
+ - **`useVariantField` accepts row-indexed union paths.** A discriminated
29
+ union that is an array's row item was unbindable: `UnionValueAt`
30
+ resolved paths through `keyof` only, so any array-index segment (a
31
+ template `` `pavements.${number}` `` or a literal `"pavements.0"`)
32
+ collapsed the variant-key set to `never` and the `field` argument
33
+ rejected everything. The type machinery now resolves the array element
34
+ through numeric segments, so a row component binds
35
+ `useVariantField(form, `` `pavements.${index}` ``, "designator")` fully
36
+ typed, in both the plain and the `createFormHooks`-bound flavors.
37
+ Runtime behavior was always correct; only the types refused. Found by
38
+ the airfield-maps trial, whose generated TODO pointed at exactly this
39
+ call.
40
+
41
+ ## formstand-cli 0.15.0 — 2026-09-16
42
+
43
+ ### Changed
44
+
45
+ - **`--form-prop` is single-file only.** With `--layout module` the
46
+ fields are pre-wired to the module's own singleton, so the `form`
47
+ prop was decorative when it told the truth and silently split state
48
+ when it lied (the dev-mode guard existed to catch the lie). The CLI
49
+ now rejects the combination with the fix in the message: the page
50
+ already owns the module's form by importing the exported instance and
51
+ driving it directly. The wizard stops asking the question under the
52
+ module layout, and the playground's command builder disables the
53
+ checkbox. The programmatic `emitModuleForm` surface keeps the option
54
+ for callers who accept the singleton contract knowingly.
55
+
56
+ ### Fixed
57
+
58
+ - **The array-row union TODO gives honest advice.** The single-file
59
+ spelling claimed the union was "not supported"; the module spelling
60
+ recommended a `useVariantField` call that did not typecheck before the
61
+ library fix above. Both now describe the working binding and name the
62
+ formstand 0.16+ floor for row-indexed union paths.
63
+
64
+ ## formstand-cli 0.14.0 — 2026-09-10
65
+
66
+ ### Added
67
+
68
+ - **`component: "textarea"` in the config `fields` block.** The second
69
+ override flavor: a multi-line control for string fields, the binding
70
+ unchanged. Each kit renders its own control (mui `TextField multiline`,
71
+ mantine/chakra/shadcn `Textarea`, antd `Input.TextArea`, plain a native
72
+ `<textarea>`), and it composes with `span` in one entry, so a
73
+ `span: "full"` cover letter can finally look like one. `optionsProp`
74
+ and enum targets are rejected loudly.
75
+ - **JSON Schema defaults and string formats reach the generated
76
+ validator.** A document `default` now emits `.default(value)` in the
77
+ generated zod schema beside the `initialValues` seed it already fed,
78
+ and `format: email`/`uri`/`uuid` emits `z.email()`/`z.url()`/
79
+ `z.uuid()` while the control stays a text field. The emitted schema is
80
+ the runtime source of truth, so it no longer drops contracts the
81
+ source document carried. Note zod's `.default()` makes the input
82
+ optional by design.
83
+
84
+ ### Fixed
85
+
86
+ - **An optional union starts empty and its select can clear it.** An
87
+ optional/nullable discriminated union was seeded with its first
88
+ variant, so an untouched form submitted a variant the user never
89
+ chose, and the discriminant select offered no way back. The union now
90
+ starts `undefined`/`null`, and its select reads the discriminant but
91
+ writes the whole union: the empty choice clears it, a tag starts that
92
+ variant blank. mui's select gains an explicit None item; shadcn's
93
+ Radix select cannot hold an empty item, so there the fixed seed alone
94
+ closes the phantom-submit bug. Required unions are unchanged.
95
+ - **Array add buttons read as the action they perform.** "Add
96
+ references" becomes "Add reference": a conservative singularizer on
97
+ the label's last word, with irregulars passing through unchanged.
98
+ - **A union variant with one field no longer wraps it in a fragment.**
99
+
100
+ ## formstand-cli 0.13.0 — 2026-09-03
101
+
102
+ ### Added
103
+
104
+ - **JSON Schema and OpenAPI inputs.** A `.json` input file is read as a
105
+ bare JSON Schema (2020-12) or an OpenAPI 3.x document: a third
106
+ front-end into the same IR, following type mode's path, so the CLI
107
+ generates the zod schema beside the component (`--schema-out` places
108
+ it; `--layout module` makes it the module's `schema.ts`). The new
109
+ `--schema` flag selects inside the document: a component schema name,
110
+ or a full `#/...` JSON pointer that can reach an operation's request
111
+ body. What translates: `properties`/`required`, `enum`/`const`,
112
+ `oneOf` string consts, discriminated `oneOf` into `useVariantField`
113
+ unions, `allOf` object merging, `$ref` chains, `prefixItems` tuples,
114
+ `format: date`/`date-time` as date fields, primitive `default`s into
115
+ `initialValues`, `description` as helper text, `title` as the label,
116
+ and every nullable dialect (3.1 type arrays, 3.0 `nullable: true`,
117
+ `oneOf` with a null branch). Everything else degrades to the loud TODO
118
+ fallback; Swagger 2.0 and YAML are refused with conversion hints. The
119
+ wizard understands `.json` inputs too, asking `--schema` in place of
120
+ the zod-or-type question.
121
+
122
+ ### Fixed
123
+
124
+ - **A PascalCase schema export no longer collides with its module type
125
+ alias.** `export const MySchema` derives the form name MyForm, whose
126
+ schema type alias was also named MySchema, so the module barrel
127
+ exported two different MySchemas (the value from `schema.ts`, the type
128
+ from `types.ts`) and the generated `index.ts` failed to compile with
129
+ TS2308. The alias now yields the colliding name to the user's value
130
+ and becomes `MySchemaType`, both in `types.ts` and in the
131
+ `--form-prop` component that consumes it.
132
+
133
+ ## formstand-cli 0.12.0 — 2026-08-18
134
+
135
+ ### Added
136
+
137
+ - **`--wizard`: the flag questions asked one at a time.** Which file, zod
138
+ export or TS type, which kit, layout, section chrome, columns, name, and
139
+ where to write — Enter accepts the default on every question, invalid
140
+ answers re-ask, and an existing output target offers `--force` on the
141
+ spot. The interview ends by printing the composed `formstand-gen`
142
+ command before running it, so the run is reproducible without the wizard
143
+ from then on. Strictly opt-in (never a TTY sniff, and the flag runs
144
+ alone), prompts on stderr so stdout output stays pipeable, answers
145
+ pipeable on stdin, and zero new dependencies: numbered readline lists,
146
+ not a prompts library.
147
+ - **Per-field layout placement: `span` in the config `fields` block.** With
148
+ `--columns 2|3`, `"employment.notes": { span: "full" }` gives a field the
149
+ whole row and `{ span: 2 }` two of three columns; it composes with an
150
+ autocomplete override in the same entry. Each backend emits the span in
151
+ the dialect its section grids already use (mui `Grid` size objects, with
152
+ the legacy `item xs/sm` spelling on mui@5/6; mantine `Grid.Col` span
153
+ objects; antd `Col` `xs`/`sm`; shadcn `md:col-span-N`; chakra a `Box`
154
+ with responsive `gridColumn`), so every spelling still collapses to one
155
+ column on a phone. Plain is the one exception, for partial spans only:
156
+ inline styles cannot carry a media query, so a numeric span widens to the
157
+ full row with a comment saying so at the site. A span with no grid to act
158
+ on (a root-level field, array rows, a container, a 1-column form, or a
159
+ module-layout field below a section's direct children) is a loud
160
+ generation-time error, never a silent no-op.
161
+
162
+ ### Fixed
163
+
164
+ - **Generated bodies indent under their real container.** Panel and
165
+ collapsible chrome nests the body's container deeper than the section tag
166
+ (Card > CardContent > Grid), but cells were emitted shallower than the
167
+ container holding them and array row stacks sat beside their wrapper
168
+ cells instead of inside them. Both layouts now shift bodies to the
169
+ chrome's true depth, so generated files survive a host Prettier pass
170
+ without reflowing. Cosmetic only; the demos regenerate with a
171
+ whitespace-only diff.
172
+ - **A union riding a grid cell keeps its vertical rhythm.** Inside an
173
+ object section at `--columns 2|3`, a discriminated union's controls are
174
+ bare siblings in one full-row cell and had lost the spacing the 1-column
175
+ chrome provides; mui and mantine now wrap them in the kit's Stack. antd
176
+ needs no wrapper, since each of its controls is already a vertical Flex.
177
+
178
+ ## 0.15.2 — 2026-08-11
179
+
180
+ Identical to 0.15.1, republished under a fresh tag. The v0.15.1 tag was
181
+ pushed during the August 6 GitHub Actions outage and GitHub dropped the
182
+ tag event outright, so the Publish workflow never ran and 0.15.1 never
183
+ reached npm. The Publish workflow only fires on tag pushes by design (the
184
+ trusted-publishing trust is bound to that trigger), so the recovery is a
185
+ new version under a new tag. No code changes.
186
+
187
+ ## formstand-cli 0.11.1 — 2026-08-11
188
+
189
+ Identical to formstand-cli 0.11.0, republished under a fresh tag for the
190
+ same reason as 0.15.2 above: the cli-v0.11.0 tag event was dropped in the
191
+ August 6 outage and the version never reached npm. No code changes.
192
+
193
+ ## 0.15.1 — 2026-08-06
194
+
195
+ ### Fixed
196
+
197
+ - **Tuple element paths type as their exact element.** `FieldValue` resolved
198
+ a path like `"pair.0"` into `[string, number]` through the generic array
199
+ arm, so the value typed as `string | number` instead of `string`. The
200
+ bundled field components hid this by asserting their own value type; the
201
+ raw hooks exposed it, and a CLI module tuple section over a mixed tuple
202
+ did not compile because its per-kind prop builders need the exact element.
203
+ Tuple index paths now resolve positionally. Types only, no runtime change.
204
+
205
+ ## formstand-cli 0.11.0 — 2026-08-06
206
+
207
+ ### Fixed
208
+
209
+ - **Four edge-config compile and layout bugs in the container migration**,
210
+ all one family: a gate that decides imports or exports did not count every
211
+ shape that renders section chrome.
212
+ - A module union section at `--columns 2|3` rendered its vertical `Stack`
213
+ shell while importing for the grid it no longer renders, a TS2304 in
214
+ mantine and mui output. Imports and shell now derive from one visual.
215
+ - A tuple-only root emitted section chrome (`Typography`/`Title`/`Heading`
216
+ headings, and at `--columns 2|3` the `Row`/`Col` or `Grid` pair) without
217
+ importing any of it; the section-import gate now counts tuples.
218
+ - A module tuple section imported the bound field hook from `hooks.ts`,
219
+ which never exported it for a tuple-only root; the export gate now
220
+ mirrors the tuple section's own import rule.
221
+ - A module nested `<fieldset>` lost its full-row span for the CSS-grid
222
+ backends (plain, shadcn, chakra), collapsing a nested section into one
223
+ grid cell; the span now rides the fieldset whenever no cell wrapper
224
+ exists to carry it.
225
+
226
+ The version matrix now generates tuple-only and root-union schemas against
227
+ every kit's real declarations (`cli/matrix/edgeSchemas.ts`), and the CI
228
+ suite grew matching regression tests, so the class cannot ship green again.
229
+
230
+ ### Changed
231
+
232
+ - **`--columns` output is responsive.** `--columns 2` now means "two columns
233
+ when there is room, one on a phone" in every backend's own dialect: MUI gets
234
+ an `sx` breakpoint object, chakra a responsive `gridTemplateColumns` style
235
+ prop, mantine `SimpleGrid cols={{ base: 1, sm: N }}`, and the inline-style
236
+ backends (plain, antd) a `repeat(auto-fit, minmax(...))` track whose calc
237
+ caps the column count at N while a 220px floor collapses columns that would
238
+ squeeze inputs below a usable width. shadcn already collapsed via
239
+ `md:grid-cols-N`. Fixed grid tracks were why generated multi-column forms
240
+ were unusable on phones; regenerate to pick the new grids up.
241
+ - **antd sections lay out with `Row`/`Col`** instead of an inline CSS grid:
242
+ the kit's own 24-column idiom, responsive via `xs`/`sm` props, in both
243
+ layouts. Nested sections, arrays, unions, and tuples ride spanning or item
244
+ `Col`s; a union section keeps its vertical shell, since its children are
245
+ conditional fragments that cannot each be a `Col`. This is also the shape
246
+ that makes a per-field span a one-prop change later.
247
+ - **mantine sections lay out with `Grid`/`Grid.Col`** instead of
248
+ `SimpleGrid`: responsive span objects (`span={{ base: 12, sm: 6 }}`), and
249
+ the container that can express a per-field span at all, which `SimpleGrid`
250
+ cannot. Same cell rules as antd: leaves ride item Cols, nested content
251
+ spans, 1-column chrome reads as a `Stack`. The version matrix caught one
252
+ invented prop on the way in (`Grid` has no `gutter` in v9), which is
253
+ exactly the drift it exists to catch.
254
+ - **mui sections lay out with `Grid` containers** instead of `sx` CSS grids
255
+ — the same component the hand-written playground demos use — with the
256
+ spelling the pinned major actually accepts: `size={{ xs: 12, sm: 6 }}` on
257
+ v7/v9, legacy `item xs={12} sm={6}` on v5 **and** v6 (v6's size-prop grid
258
+ is the separate `Grid2` import; the matrix against real v6 declarations is
259
+ what established that). 1-column chrome keeps the historical Stack and
260
+ CardContent output.
261
+
3
262
  ## 0.15.0 — 2026-08-04
4
263
 
5
264
  ### Fixed
@@ -40,7 +40,7 @@ type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, Normali
40
40
  type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
41
  [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
42
  }[keyof T & string] : never;
43
- type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
43
+ type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
44
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
45
45
 
46
46
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
@@ -40,7 +40,7 @@ type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, Normali
40
40
  type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
41
  [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
42
  }[keyof T & string] : never;
43
- type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
43
+ type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
44
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
45
45
 
46
46
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-C9hZffCQ.cjs';
3
+ import { F as Form } from '../createForm-jlzK2lCG.cjs';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-C9hZffCQ.js';
3
+ import { F as Form } from '../createForm-jlzK2lCG.js';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
package/dist/index.cjs CHANGED
@@ -1390,6 +1390,8 @@ function useField(form, pathArg, options) {
1390
1390
  initialValue: slice.initialValue,
1391
1391
  emptyValue,
1392
1392
  error: slice.error,
1393
+ // `?.[0]` covers both the undefined channel and an empty array.
1394
+ firstError: slice.error?.[0],
1393
1395
  touched: slice.touched,
1394
1396
  dirty: slice.dirty,
1395
1397
  isValidating: slice.isValidating,
@@ -1669,6 +1671,30 @@ function useFormValues(form) {
1669
1671
  return react$1.useStore(form.store, (state) => state.values);
1670
1672
  }
1671
1673
 
1674
+ // src/react/useFields.ts
1675
+ function useFields(form, paths) {
1676
+ const fields = paths.map(
1677
+ (path) => (
1678
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- a fixed hook count by the stable-arity contract above; each element must be a REAL UseFieldReturn so it spreads into the prop builders
1679
+ useField(form, path)
1680
+ )
1681
+ );
1682
+ const merged = [...new Set(fields.flatMap((field) => field.error ?? []))];
1683
+ return {
1684
+ fields,
1685
+ error: merged.length > 0 ? merged : void 0,
1686
+ firstError: merged[0],
1687
+ touched: fields.some((field) => field.touched),
1688
+ dirty: fields.some((field) => field.dirty),
1689
+ isValidating: fields.some((field) => field.isValidating),
1690
+ onBlur: () => {
1691
+ fields.forEach((field) => {
1692
+ field.onBlur();
1693
+ });
1694
+ }
1695
+ };
1696
+ }
1697
+
1672
1698
  // src/react/inputProps.ts
1673
1699
  var hasFieldError = (error) => error !== void 0 && error.length > 0;
1674
1700
  var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
@@ -2087,6 +2113,7 @@ exports.shouldValidateOn = shouldValidateOn;
2087
2113
  exports.textInputProps = textInputProps;
2088
2114
  exports.useField = useField;
2089
2115
  exports.useFieldArray = useFieldArray;
2116
+ exports.useFields = useFields;
2090
2117
  exports.useForm = useForm;
2091
2118
  exports.useFormError = useFormError;
2092
2119
  exports.useFormSelector = useFormSelector;