formstand 0.15.2 → 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,180 @@
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
+
3
178
  ## 0.15.2 — 2026-08-11
4
179
 
5
180
  Identical to 0.15.1, republished under a fresh tag. The v0.15.1 tag was
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;