formstand 0.9.0 → 0.11.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,6 +1,627 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.11.0 — 2026-07-29
4
+
5
+ ### Added
6
+
7
+ - **`useNumberInput` is exported** (along with its `NumberInputBinding`
8
+ return type) — the text-preserving number binding behind `NumberField`.
9
+ It was implemented and tested (via `NumberField`) all along, just never
10
+ exported: local raw text while editing (partial entries like `-`, `1.`,
11
+ `85000.` stay visible), keystrokes that parse pushed to the form via the
12
+ shared `parseNumberText` rules, blur snapping the display to the
13
+ canonical value, and an external form-value change while editing
14
+ resetting the local text. Spread it onto an `<input type="text">` for
15
+ custom number inputs and UI-kit adapters — a naive controlled
16
+ `value={String(n)}` binding eats the `.` of `85000.50` and the `-` of
17
+ `-5` as they are typed.
18
+
19
+ ### Fixed
20
+
21
+ - **`focusFirstError` / `focusField` gain an `[id=path]` fallback** for
22
+ paths that match NO named control: composite widgets that render no
23
+ `name` anywhere — Ant Design's `Select`, notably, which has no
24
+ form-posting input — were unreachable by the name walk even though they
25
+ forward `id={path}` to their real focusable control. When a path has no
26
+ name-matched control, the helpers now try the element whose `id` is
27
+ exactly that path (EXACT match only — descendant semantics stay with the
28
+ name walk, since an id names one element), with the same focusability
29
+ filter, post-focus verification, and DOM-order merging as name matches.
30
+ Paths with any named control never consult ids, so existing behavior is
31
+ unchanged wherever a `name` exists.
32
+
33
+ ## 0.10.0 — 2026-07-28
34
+
35
+ ### Breaking
36
+
37
+ - **`validateFields` / `validateFieldsAsync` return discriminated results
38
+ instead of booleans** (2026-07 self-review #1). `validateFields(paths)` now
39
+ returns `{ kind: "valid" }`, `{ kind: "invalid", errors }` (the error map
40
+ scoped to the requested paths), or `{ kind: "pending", promise }` on an
41
+ async schema — mirroring `validateField`. The old `boolean |
42
+ Promise<boolean>` union let a truthy in-flight `Promise` silently pass
43
+ `if (form.validateFields(...))` gates the moment a schema gained an async
44
+ refine. `validateFieldsAsync` resolves with the settled result instead of a
45
+ boolean. Migrate `if (ok)` to `if (result.kind === "valid")`.
46
+ - **`FieldFormApi.validateField` must not throw the async-required signal**
47
+ — `useField` no longer catches it and escalates to `validateFieldAsync`.
48
+ The declared contract (return `{ kind: "pending", promise }`) has been the
49
+ library's own behavior since the async-routing rework; custom
50
+ implementations written against the old throwing style should return the
51
+ pending result instead.
52
+
53
+ ### Added
54
+
55
+ - **`pathDepth` option on `createForm`/`useForm`** — a type-level-only,
56
+ per-form typed-path depth budget (in segments): `createForm(schema, {
57
+ initialValues, pathDepth: 12 })` widens the `FieldPath` union so deeper
58
+ paths bind with full inference, threading through every typed surface
59
+ (`Form<TSchema, D>`, `useField`/`useFieldArray`/`useVariantField`, the
60
+ flag/selector hooks, `createFormHooks`, `createFormContext<S, D>`, the
61
+ bound components' `PathsOf`). The runtime ignores it entirely — path APIs
62
+ always walked any depth — and D infers as a literal from the option.
63
+ Motivated by one global store backing many forms, whose leaves sit past
64
+ the default budget. Raising it is a deliberate TypeScript compile-cost
65
+ trade: the union grows with every extra level, and every path-taking call
66
+ in the editor pays for it. `D` must be a number literal; budgets up to 25
67
+ are supported, and the option is CONSTRAINED to the exported `PathDepth`
68
+ union (`0 | 1 | ... | 25`, exactly the decrement table's range) —
69
+ `pathDepth: 26`, `pathDepth: -1`, a widened `number`-typed variable, and
70
+ a UNION value (`cond ? 12 : 9`, which would infer an unusable
71
+ `Form<S, 9 | 12>`) all fail to compile at the call site instead of
72
+ silently misbehaving. The same `D extends PathDepth` constraint is
73
+ enforced on EVERY generic surface carrying a depth parameter — `Form`
74
+ itself, `createFormContext<S, D>`, `persistForm`, the hooks'
75
+ explicit-`D` positions — so `Form<S, number>` and
76
+ `createFormContext<S, 26>()` are compile errors too, not silently
77
+ unbounded budgets.
78
+ `FieldPath<T, D>` itself (the compat surface for direct users, still
79
+ `D extends number`) NORMALIZES any unusable `D` before recursing: a
80
+ widened `number` (an options object built separately), an out-of-range
81
+ literal (26, -1 — outside the decrement table, where the decrement would
82
+ silently no-op), and a finite union (`9 | 12`, or `PathDepth` itself —
83
+ the decrement distributes over unions and would recurse to the union's
84
+ max) all fall back to the default depth instead of silently building the
85
+ enormous ~25-level union (an empirically verified hazard) or
86
+ hard-erroring TS2615 on recursive value types. The default is now the exported
87
+ `DefaultPathDepth` alias — one source of truth replacing the 22 scattered
88
+ `= 9` literals across the typed surfaces. NOT breaking for existing
89
+ code: `D` is appended LAST with a
90
+ default on every generic surface (the hooks' typed overloads
91
+ forward-reference it from the path parameter's constraint, and the
92
+ selector overloads stay at one type parameter), so existing references —
93
+ `Form<Schema>`, explicit instantiations like
94
+ `useFieldArray<typeof schema, "tags">`, `createFormHooks<S, "name">` —
95
+ keep compiling unchanged; type tests pin the arity, the option's
96
+ rejection cases, the non-literal fallback, and both directions of
97
+ `Form<S, 12>` / `Form<S, 9>` non-assignability.
98
+
99
+ ### Changed
100
+
101
+ - **Default typed-path depth raised from 7 to 9 segments**
102
+ (`FieldPath<T, D = 9>`). Real-world stores kept hitting the 7-segment cap
103
+ (the CLI's nested-array output reaches 7 with just three array levels,
104
+ and one more object wrapper fell off the union). Measured typecheck cost
105
+ of the wider default across this repo (library + tests + examples) is
106
+ noise-level. `Form<Schema>` references keep compiling unchanged — the new
107
+ `D` parameter defaults to 9 everywhere.
108
+
109
+ ### Fixed
110
+
111
+ - `resetField` on a path with no counterpart in `initialValues` (an appended
112
+ array row) no longer writes an explicit `undefined` into the values tree —
113
+ a hole that crashed row components and failed schema validation. The value
114
+ is left unchanged (with a console warning), and error/touched state under
115
+ the path still clears (2026-07 self-review #2).
116
+ - `useFieldArray` row ids stay glued to the right rows even when row values
117
+ are `Object.is`-equal (`remove(0)` on two blank rows handed the survivor
118
+ the removed row's id, resurrecting the wrong React subtree). The core now
119
+ records every array op's exact new→old index mapping in an internal
120
+ per-path op log, and id derivation replays that log — so the fix covers
121
+ hook-issued ops, IMPERATIVE ops (`form.arrayRemove(...)`), and multiple
122
+ ops per render batch alike. All hook instances on the same path share one
123
+ id state, so sibling `useFieldArray` hooks can never disagree on row ids.
124
+ Whole-array writes that bypass the ops (`setValue`, `restore`) still
125
+ reconcile by value as before, and replay keeps its exact progress when a
126
+ chain breaks mid-batch (an op followed by a `setValue` reconciles only the
127
+ remaining hop); consecutive log records compose when the ring fills, so
128
+ arbitrarily long op batches stay fully replayable. Exact replay applies to
129
+ forms created by `createForm`/`useForm` — schema-less custom
130
+ `FieldArrayFormApi` implementations reconcile by value (2026-07
131
+ self-review #3, completed by the deferred store-side follow-up).
132
+ - `flattenIssues` no longer crashes on an issue path of `"__proto__"`
133
+ (reachable via `ctx.addIssue` in a `superRefine`): the accumulator read now
134
+ checks own keys instead of walking the prototype chain (2026-07
135
+ self-review #4).
136
+ - `SelectField` renders its empty option whenever the bound `<select>`
137
+ displays `""` (the condition now derives from `selectProps`' own value
138
+ coercion, covering `undefined`, `null`, AND `""`) — an empty-string value
139
+ no longer displays as the first real option while the form holds `""`.
140
+ An options list that supplies its own `""`-valued entry wins: the
141
+ implicit blank option is suppressed so the explicit labelled one shows.
142
+ - `setError`/server errors keyed at `"__proto__"` are no longer silently
143
+ dropped from the merged `errors` map (`mergeErrorChannels` now checks own
144
+ keys, the same hardening `flattenIssues` got).
145
+
146
+ ### Performance
147
+
148
+ - Async-requiring scopes are latched: once a sync `validate` /
149
+ `validateField` / `validateFields` pass discovers a scope needs async
150
+ parsing, later calls skip the doomed sync parse and go straight to the
151
+ async pass (previously every keystroke on an async schema paid a full sync
152
+ parse that always threw) (2026-07 self-review #5). One nuance: after the
153
+ latch, a value that fails an EARLIER sync check in the same scope also
154
+ returns `pending` (settling via the promise) where it previously settled
155
+ `invalid` synchronously — callers on async schemas must handle `pending`
156
+ regardless, since valid values always produced it. Paths that address no
157
+ slot (out-of-range indices) never consult the latch and still settle
158
+ synchronously.
159
+
160
+ ### Internals
161
+
162
+ - The five bound field components share one `FieldShell` (label + control +
163
+ error line, with the checkbox's label-wrapping layout as a variant) and one
164
+ exported `hasFieldError` predicate — the a11y wiring (`useId`, `-error` id,
165
+ `aria-describedby`) now lives in a single place instead of five copies.
166
+ `hasFieldError` is exported for custom UI-kit adapters.
167
+ - `FieldPathArg` is now actually used by the `useField` / `useFieldArray`
168
+ overloads it describes (it was exported dead).
169
+
170
+ ## formstand-cli 0.9.0 — 2026-07-29
171
+
172
+ ### Added
173
+
174
+ - **Version-aware `--ui`: `mui@5` … `mui@9`** — `--ui mui` can now pin an
175
+ `@mui/material` major: `mui@5`, `mui@6`, `mui@7`, `mui@9` (bare `mui`
176
+ keeps meaning the latest supported major, 9, and its output is
177
+ byte-identical to before). The config file's `ui` key accepts the same
178
+ spellings. Scope: only React-19-capable majors — formstand peers
179
+ `react: ^19`, so MUI 4 and older can never install alongside it — and MUI
180
+ skipped major 8 entirely (7.x jumps to 9 on the registry), so `mui@8`
181
+ and `mui@4` fail with explanations, as do `plain@N`/`shadcn@N` (those
182
+ kits take no version). Internally the flag/config value parses to a
183
+ structured `UiTarget` (`{ kit: "mui", version }` | `{ kit: "plain" }` |
184
+ `{ kit: "shadcn" }`, exported with `parseUiTarget` / `MUI_VERSIONS` from
185
+ `formstand-cli/codegen`), and ONE mui emitter serves every major through
186
+ a small per-version config. The only prop-surface delta across the
187
+ emitted component set (verified empirically against each major's .d.ts)
188
+ is TextField's slot-props API: `mui@5` emits the legacy
189
+ `InputProps: { inputMode }` / `InputLabelProps: { shrink: true }`, v6+
190
+ emit `slotProps.{input,inputLabel}` (v9 removed the legacy spelling;
191
+ v6–7 accept both and get the modern one). Custom `--template` modules
192
+ compose with a versioned `--ui` exactly as they did with bare `mui`
193
+ (a template still overrides the ui entirely, `--layout single` only).
194
+ - **`cli/matrix/`: the UI-kit version-matrix typecheck harness** — an isolated
195
+ workspace (own `package.json` + lockfile; not part of the root or cli
196
+ installs) with every supported `@mui/material` major installed side by
197
+ side under npm aliases (`mui5` … `mui9`). `npm run matrix` (from `cli/`)
198
+ generates a kitchen-sink form plus the nested-array stress form with
199
+ `--ui mui@N` — both layouts, every section/column variant — and
200
+ typechecks the output against each major's real type declarations
201
+ (`@mui/material` path-mapped onto the alias, `formstand` onto the
202
+ library source). A per-version literal-attribute probe restates the
203
+ adapter's TextField props style outside a JSX spread (spreads bypass
204
+ TypeScript's excess-property checks), so a wrong per-version config
205
+ fails the matrix instead of compiling silently. Run it before releasing
206
+ any UI-kit backend change; it is deliberately not part of `npm test`.
207
+ (The chakra, mantine, and antd entries below each extend the harness
208
+ with their kit's alias and job — it ends this cycle proving seven
209
+ targets: four mui majors, chakra 3, mantine 9, antd 6.)
210
+ - **First-class `--ui chakra`: a Chakra UI v3 backend** — both layouts
211
+ (single-file and `--layout module`), the full field-kind surface the mui
212
+ backend covers (strings/numbers/dates/enums/booleans, nested objects,
213
+ arrays with add/remove and recursive nested-row extraction, tuples,
214
+ discriminated unions, TODO degradations), and every `--sections` /
215
+ `--columns` variant. Emits the v3 compound-component API (verified
216
+ against the installed 3.36 declarations, not v2's): `Field.Root` (with
217
+ `invalid`) / `Field.Label` / `Field.ErrorText` for labels and errors,
218
+ `Input` for text/number/date (native bindings — `inputMode="decimal"`,
219
+ `type="date"`), `NativeSelect.Root` + `NativeSelect.Field` for enums (a
220
+ real `<select>`, preferred over the Ark collection `Select` so the
221
+ adapter speaks DOM change events), and `Switch.Root` /
222
+ `Switch.HiddenInput` / `Switch.Control` / `Switch.Thumb` /
223
+ `Switch.Label` for booleans (`checked` + `onCheckedChange` details
224
+ callback, spread on the root where the state lives). Sections render
225
+ `Stack`/`Heading` (flat), `Card.Root`/`Card.Body` (panel), or one
226
+ `Accordion.Root` (`collapsible defaultValue={["section"]}`) per section
227
+ (collapsible); columns use the same CSS-grid shape as the other kits via
228
+ chakra style props. The generated file assumes the host app mounts
229
+ `ChakraProvider` (same policy as the mui backend and its theme). The
230
+ module layout gains a shared `adapter.ts` (`chakraTextInputProps` /
231
+ `useChakraNumberInputProps` / `chakraDateInputProps` /
232
+ `chakraSelectProps` / `chakraSwitchProps` + `fieldError`). chakra takes no version suffix —
233
+ v3 is the only supported major (v2 and older lack the compound API and
234
+ predate formstand's React 19 peer) — with `chakra@3` accepted as the
235
+ explicit spelling and `chakra@1`/`chakra@2` failing with the scope
236
+ rationale like `mui@0`–`mui@4`. The config file's `ui` key accepts the
237
+ same spellings, and the matrix harness gained a `chakra3` alias
238
+ (`npm:@chakra-ui/react@^3`) plus a chakra job — both schemas, both
239
+ layouts, all section styles, and a literal-attribute probe for the
240
+ Input/NativeSelect/Switch prop surfaces — so `npm run matrix` now proves
241
+ five kit targets against their real .d.ts.
242
+ - **First-class `--ui mantine`: a Mantine v9 backend** — both layouts, the
243
+ full field-kind surface the other kit backends cover (strings/numbers/
244
+ dates/enums/booleans, nested objects, arrays with add/remove and
245
+ recursive nested-row extraction, tuples, discriminated unions, TODO
246
+ degradations), and every `--sections` / `--columns` variant. Mantine
247
+ field components carry their own `label` + `error` props, so there is no
248
+ Field wrapper: `TextInput` binds text/number/date natively
249
+ (`inputMode="decimal"`, `type="date"`; Mantine's `NumberInput` widget is
250
+ deliberately NOT used — its `onChange` takes `(value: number | string)`,
251
+ not a DOM event, so it can't share formstand's input-shaped adapters),
252
+ `NativeSelect` binds enums as a real `<select>` with `<option>` children
253
+ (same native-first choice as the chakra backend), and `Switch` binds
254
+ booleans through plain DOM `checked`/`onChange` (a `ChangeEvent`, unlike
255
+ chakra's details callback). Error text rides inside the adapter builders
256
+ (`error: fieldError(field)`), so leaves are one-liners like the mui
257
+ backend's. Sections render `Stack`/`Title` (flat), `Card withBorder` +
258
+ `SimpleGrid` (panel), or `Accordion`/`Accordion.Item`/`Control`/`Panel`
259
+ (collapsible, `defaultValue="section"` + `variant="contained"`); columns
260
+ use `SimpleGrid cols={N}`. The generated file assumes the host app
261
+ mounts `MantineProvider` (same policy as the mui/chakra providers). The
262
+ module layout gains a shared `adapter.ts` (`mantineTextInputProps` /
263
+ `useMantineNumberInputProps` / `mantineDateInputProps` /
264
+ `mantineSelectProps` / `mantineSwitchProps` + `fieldError`). mantine
265
+ takes no version suffix — v9 (the current major) is the only supported
266
+ target, with `mantine@9` accepted as the explicit spelling. The scope is
267
+ empirical: 9.x peers `react ^19.2` while 7.17+/8.x peer `^18 || ^19`
268
+ (7.0 was ^18-only, ≤6 predate the v7 emotion→CSS-modules rewrite), and
269
+ the emitted component surface typechecks IDENTICALLY against 7.17/8.3/9.5
270
+ except the `bdrs` style prop (absent in 7) — so `mantine@0`–`6` fail
271
+ with the React-19/styling-rewrite rationale and `mantine@7`/`@8` fail
272
+ with a precise verified-against-v9-only message instead of a false
273
+ incompatibility claim. The config file's `ui` key accepts the same
274
+ spellings, and the matrix harness gained a `mantine9` alias
275
+ (`npm:@mantine/core@^9.5`, plus its `@mantine/hooks` peer) and a
276
+ mantine job — both schemas, both layouts, all section styles, and a
277
+ literal-attribute probe for the TextInput/NativeSelect/Switch spread
278
+ surfaces — so `npm run matrix` now proves six kit targets against their
279
+ real .d.ts.
280
+ - **First-class `--ui antd`: an Ant Design v6 backend** — both layouts, the
281
+ full field-kind surface the other kit backends cover (strings/numbers/
282
+ dates/enums/booleans, nested objects, arrays with add/remove and
283
+ recursive nested-row extraction, tuples, discriminated unions, TODO
284
+ degradations), and every `--sections` / `--columns` variant. The one
285
+ hard rule: antd's own `Form`/`Form.Item` (name-based bindings, its own
286
+ state store) is NEVER emitted — formstand owns the form state, so the
287
+ generated code binds antd's input components as plain controlled
288
+ components. The bindings, decided empirically against the installed
289
+ 6.5 declarations: `Input` binds text/number/date natively (it extends
290
+ the DOM input props — `inputMode="decimal"`, `type="date"`; antd's
291
+ `InputNumber` is rejected on evidence, its `onChange` is
292
+ `(value: number | null)`, not an event, and the dayjs-value-based
293
+ `DatePicker` is not used — the CLI pulls no date library); enums bind
294
+ antd's `Select`, the backend's ONE value-shaped adapter — antd has no
295
+ native-`<select>` component anywhere, so `onChange` receives the
296
+ selected value directly, `value ?? null` keeps the placeholder visible,
297
+ and there is no `name` (antd's Select renders no form-posting input);
298
+ booleans bind `Checkbox` (its `onChange` is antd's DOM-ish
299
+ `CheckboxChangeEvent` with `e.target.checked`, and it has a real
300
+ `onBlur`) — NOT `Switch`, which has no `onBlur` prop at all and a
301
+ value-shaped `(checked, event)` callback (both rejections verified as
302
+ failing compiles against the real .d.ts). With no `Form.Item` there is
303
+ no built-in error slot: every non-boolean control paints
304
+ `status="error"` (`fieldStatus`) and renders an explicit
305
+ `Typography.Text type="danger" role="alert"` error line, with a plain
306
+ `<label htmlFor>`/`id` pair. Sections render `Flex`/`Typography.Title`
307
+ (flat), `Card variant="outlined"` (panel), or `Collapse` via the items
308
+ API (collapsible — children-panels are deprecated in antd 5+); columns
309
+ use the shared inline-style CSS grid. No provider is required
310
+ (`ConfigProvider` is optional theming). The module layout gains a shared
311
+ `adapter.tsx` (JSX for its `FieldError` line, like shadcn's):
312
+ `antdTextInputProps` / `useAntdNumberInputProps` /
313
+ `antdDateInputProps` / `antdSelectProps` / `antdCheckboxProps` +
314
+ `fieldError` / `fieldStatus` / `FieldError`. antd takes no version suffix — v6 (the current major) is
315
+ the only supported target, with `antd@6` accepted as the explicit
316
+ spelling. The scope is empirical: antd 6.x peers `react >=18` (React 19
317
+ natively in range), while antd 5 nominally peers `react >=16.9` but
318
+ needs the `@ant-design/v5-patch-for-react-19` import in the HOST app on
319
+ React 19 — so `antd@5` fails with a precise
320
+ verified-against-v6-only message (naming the patch, no false
321
+ incompatibility claim) and `antd@0`–`4` fail with the missing-surface
322
+ rationale (no `Flex`, no Collapse items API, no `status` props; 4 and
323
+ older predate the v5 CSS-in-JS rewrite). The config file's `ui` key
324
+ accepts the same spellings, and the matrix harness gained an `antd6`
325
+ alias (`npm:antd@^6.5`) and an antd job — both schemas, both layouts,
326
+ all section styles, and a literal-attribute probe for the
327
+ Input/Select/Checkbox spread surfaces, restating the value-shaped
328
+ Select `onChange` with its explicit parameter type — so
329
+ `npm run matrix` now proves seven kit targets against their real .d.ts.
330
+ - **Playground: the new kit backends as living demos** — three new
331
+ Generated tabs (**Gen: Chakra UI**, **Gen: Mantine**, **Gen: Ant
332
+ Design**) render the SAME Onboarding schema the mui module tab uses,
333
+ generated single-file with `--ui chakra` / `--ui mantine` / `--ui antd`
334
+ and the same `--sections panel --columns 2` chrome — a direct kit
335
+ comparison across the four tabs, with one deliberate second axis: the
336
+ mui tab is `--layout module` (the module-layout showcase) while these
337
+ three are single-file. Committed
338
+ untouched and drift-checked by CI like the other generated demos
339
+ (`scripts/generate-cli-demos.mjs`), rendered behind scoped provider
340
+ bridges that follow the playground's light/dark switch (ChakraProvider +
341
+ a mode class, MantineProvider `forceColorScheme`, antd `ConfigProvider`
342
+ algorithm — antd needs no provider to function), and smoke-tested in
343
+ the root suite (mount, label association, array add/remove per kit).
344
+
345
+ ### Fixed
346
+
347
+ - **Kit number adapters no longer corrupt typed numbers.** The mui, chakra,
348
+ mantine, and antd backends bound numbers as a controlled text input
349
+ (`inputMode="decimal"`) whose onChange immediately reparsed and
350
+ re-rendered the canonical string — so typing `85000.50` yielded
351
+ `8500050` (the `.` eaten as `85000.` reparsed to `85000`), `-5` yielded
352
+ `5`, and `0.050` yielded `50` (jsdom-verified). All four now emit an
353
+ inline `useNumberText` hook replicating formstand's own `useNumberInput`
354
+ semantics exactly (local raw text while editing, valid keystrokes pushed,
355
+ partial entries kept, blur snap, external-change reset) — emitted inline
356
+ rather than imported so generated output keeps its formstand >= 0.3.0
357
+ floor — and their number adapters became use-prefixed HOOKS
358
+ (`useMuiNumberFieldProps` / `useChakraNumberInputProps` /
359
+ `useMantineNumberInputProps` / `useAntdNumberInputProps`) composing kit
360
+ chrome over it, in both layouts. Union variant blocks are conditional
361
+ JSX, so number bindings there hoist
362
+ `const ${var}NumberProps = use...(${var})` next to the field hooks and
363
+ spread the const (React's rules of hooks). plain and shadcn were never
364
+ affected (stateless `type="number"` bindings).
365
+ - **Single-file array sections render the array-level error.** All six
366
+ single-file backends dropped `useFieldArray`'s `.error`, so a
367
+ `z.array().min(1, "add at least one contact")` message was invisible on
368
+ submit; the module layout already rendered it per kit. Each backend now
369
+ emits the same error line its module-layout list shell uses (plain and
370
+ shadcn `<p role="alert">`, mui `Typography color="error"`, chakra
371
+ `Text color="red.500"`, mantine `Text c="red"`, antd
372
+ `Typography.Text role="alert" type="danger"`), between the rows and the
373
+ Add button.
374
+ - **Version-matrix hardening.** (1) The stale-install gate now derives its
375
+ required-alias list from the job list itself and fails loudly naming
376
+ what's missing — it previously checked only 4 of the 7 aliases, so a
377
+ missing `mui6`/`mui7`/`mui9` silently typechecked against the repo
378
+ root's @mui/material copy. (2) Every kit gained a module-layout PANEL
379
+ job (`KitchenSinkPanel`, panel@2col): module-panel emitters were
380
+ previously typechecked nowhere (a deliberate typo in that arm shipped
381
+ green), so the matrix now genuinely covers all three section styles in
382
+ both layouts. (3) The matrix compiles under `exactOptionalPropertyTypes`
383
+ in addition to `strict` — the strictest consumer configuration — after
384
+ fixing the stale antd probe lines, which restated `status={undefined}`,
385
+ a shape the adapter no longer emits (it emits `""`, a member of antd's
386
+ `InputStatus`). (4) The four ~60-line per-kit generator copies collapsed
387
+ into one parameterized `generateKit` with the jobs as data.
388
+ - **Docs corrections**: the module layout works with all six uis (the
389
+ README said three) and the adapter split is named (`adapter.ts` for
390
+ mui/chakra/mantine, `adapter.tsx` for shadcn/antd); the
391
+ formstand >= 0.3.0 requirement explicitly covers mantine and antd (they
392
+ import the same surface); and the playground/README wording about the
393
+ four "CLI output" tabs now acknowledges both axes — the mui tab is
394
+ `--layout module` (doubling as the module-layout showcase) while the
395
+ three kit tabs are single-file, so `--ui` is not the only variable.
396
+ - **antd `fieldStatus` returns `""` — antd's own no-status value — instead
397
+ of `undefined`** when a field has no error. Spreading
398
+ `status: "error" | undefined` onto antd's `status?: InputStatus` props
399
+ fails to compile in host apps built with `exactOptionalPropertyTypes`
400
+ (which rejects explicit `undefined` for optional properties); `""` sits
401
+ in antd's `InputStatus` union precisely for "no status". Caught by
402
+ wiring the playground demo — the examples package compiles with
403
+ `exactOptionalPropertyTypes` on.
404
+
405
+ ## formstand-cli 0.8.0 — 2026-07-28
406
+
407
+ ### Changed
408
+
409
+ - **The typed-path budget follows the library default, now 9 segments**
410
+ (`FORMSTAND_PATH_DEPTH` 7 → 9, tracking `FieldPath<T, D = 9>` in
411
+ src/core/fieldPath.ts). Bindings whose full path is 8 or 9 segments —
412
+ degraded to TODOs by the budget fix below — bind real controls again;
413
+ 10+-segment paths keep the TODO + stderr warning. In-budget output is
414
+ byte-identical (the three-level nested-array snapshot is unchanged). The
415
+ library's new per-form `pathDepth` option can widen a form past 9; a
416
+ future `--path-depth` flag would pair with it (not implemented — bind
417
+ those paths by hand for now).
418
+ - **The default walker nesting budget is now DERIVED from the path budget**
419
+ (`DEFAULT_MAX_DEPTH = FORMSTAND_PATH_DEPTH + 2` = 11, up from the
420
+ standalone 10; the constant moved to a shared `depth` module both walkers
421
+ and emitters import). The walker must reach one level PAST the path
422
+ budget so a leaf of up to 10 segments (path budget + 1) at default flags
423
+ degrades via the PATH budget — a real, correctly typed subtree with a
424
+ depth TODO — rather than via walker truncation, whose wrong-kind string
425
+ fallback poisoned `initialValues` and failed the consumer's typecheck (a
426
+ nullable number at level 10 blanked to `""` instead of `null`). Leaves
427
+ DEEPER than that (11+ segments at default flags) still truncate to the
428
+ string-kind stand-in; the generated file now always compiles anyway (the
429
+ truncated leaf forces the `as unknown as` initialValues cast) and each
430
+ truncated path gets its own stderr warning — see Fixed below. The
431
+ DeepBoundaryForm playground demo no longer needs its `--max-depth 11`
432
+ override, and its regenerated output is byte-identical. `--max-depth`
433
+ remains as an override and the recursion backstop.
434
+ - **One boundary predicate for every depth decision.** The ~38 hand-written
435
+ `>`/`>=` comparisons against `FORMSTAND_PATH_DEPTH` across the
436
+ single-file and module emitters are replaced by a single exported
437
+ `overDepthBudget(spec, segments)` — spec-aware (it picks the
438
+ at-the-budget vs needs-headroom boundary from the spec's kind) and used
439
+ at every depth decision in both layouts and the CLI warnings, so they
440
+ can no longer drift on where degradation starts. (A second predicate for
441
+ spec-less row paths, `pastRowBudget`, briefly existed but had no call
442
+ sites and disagreed with `overDepthBudget`'s boundary — it has been
443
+ deleted.) `depthTodoLine` is likewise the one production of the TODO
444
+ comment shared by both emitters.
445
+
446
+ ### Fixed
447
+
448
+ - **Emitted bindings now respect formstand's typed `FieldPath` budget**
449
+ (7 segments when this fix landed; now 9 — see Changed above) (found by
450
+ dogfooding: the DeepBoundaryForm playground demo). The
451
+ walkers happily emitted 9-segment paths (`l1.l2....l8.leaf`) that fail
452
+ typecheck in every consumer (TS2820, `FieldPath<T, D = 7>` in
453
+ `src/core/fieldPath.ts`). The emitters now count segments of the FULL bound
454
+ path — dots split, a template hole (`${index}`, `${p0}`) one segment each,
455
+ so an array level spends TWO — and a binding that would exceed the budget
456
+ degrades exactly like other unsupported shapes: compiling output with a
457
+ `{/* TODO: path ... exceeds formstand's typed FieldPath depth (7); bind by
458
+ hand */}` comment plus a stderr warning per degraded path. Container
459
+ recursion stops at the boundary (an object whose own path is at 7 segments
460
+ can only produce over-budget children), the subtree is still materialized
461
+ in the zod schema and `initialValues`, and paths exactly AT the limit —
462
+ including 7-segment nested-array templates like
463
+ `` `teams.${p0}.members.${p1}.phones.${index}` `` — keep binding
464
+ byte-identically to before. Covers both layouts, all three UIs, and
465
+ recursive nested-array row extraction; this is deliberately NOT a walker
466
+ depth clamp (walk-depth is the wrong proxy for segments — a coarse cap
467
+ would have broken the working 3-level nested-array output).
468
+ Follow-up alignment: an at-budget array whose ITEM is non-scalar (an
469
+ array-of-arrays, or array rows holding a tuple/union) now emits the DEPTH
470
+ todo at the row site instead of the generic extract-a-row advice — which
471
+ was unachievable there, since the row path itself is past the budget —
472
+ and `overBudgetFieldPaths` (the stderr warning list) mirrors the emitted
473
+ TODOs exactly, one entry per TODO site, per-FIELD for an array's object
474
+ rows (the emitters degrade those field by field). The mirror is
475
+ LAYOUT-AWARE (see Fixed below): the walk takes the emitting layout's
476
+ degradation frontier, since the two layouts stop descending at different
477
+ places.
478
+ - **`.default()` / `.prefault()` values now land in the generated
479
+ `initialValues`** instead of being ignored (`defaultedNumber: z.number()
480
+ .default(42)` emitted `undefined`, contradicting the README). `fromZod`
481
+ captures the value off the zod def into a new optional `defaultValue` on
482
+ the IR's `SharedSpecProps`, and `emitInitialValues` seeds the field with
483
+ it whenever it is a JSON-serializable primitive matching the field kind
484
+ (string / finite number / boolean / declared enum option). Dates and
485
+ object/array defaults keep the blank behavior. A seeded default satisfies
486
+ `z.input`, so it also counts toward the
487
+ checked-annotation-instead-of-cast decision. Type mode is unchanged — TS
488
+ types can't carry runtime defaults, so `fromType` never sets
489
+ `defaultValue`. The README now states the real rule.
490
+ Capture hardening on review: zod v4's `def.defaultValue` is a GETTER that
491
+ already resolves the user's factory, so the walk only READS it — a
492
+ function-valued result (an older shape storing the factory itself, or a
493
+ factory returning a function) is treated as not capturable rather than
494
+ invoked, which previously executed arbitrary user code at generation
495
+ time (a fixture spy proves the resolved function is never called). The
496
+ getter is read TWICE and the value captured only when both reads
497
+ `Object.is`-agree on a JSON primitive, so a non-deterministic factory
498
+ (`Date.now`, `randomUUID`) can never bake a run-dependent literal into
499
+ byte-deterministic output. And a default is NEVER seeded into a
500
+ todo-fallback spec (`z.custom<T>().default(...)` walks as a string
501
+ stand-in whose kind lies about `T`): seeding there broke the generated
502
+ file's checked `initialValues` annotation — the field keeps its blank
503
+ behavior instead.
504
+ - **Walker truncation can no longer ship a non-compiling file, and is never
505
+ silent.** A leaf past the walker nesting budget (11+ segments at default
506
+ flags) degrades to a string-kind stand-in BEFORE its wrappers unwrap, so
507
+ its kind and flags can be wrong (`z.number().nullable()` at 11 segments
508
+ blanked to `""` in a `number | null` slot) — and the checked
509
+ `initialValues` annotation shipped that as a compile error in the
510
+ consumer's tsc. `blankNeedsCast` now treats a REQUIRED todo-bearing leaf
511
+ (truncated specs are always required-flagged; a required `z.custom`
512
+ fallback hits the same rule) as not input-satisfying, forcing the
513
+ `as unknown as` cast so the generated file always compiles — in both
514
+ layouts, which share the one predicate. Optional/nullable fallbacks keep
515
+ the checked annotation (their `undefined`/`null` blanks genuinely satisfy
516
+ `z.input` — those flags only exist when real wrappers unwrapped). And the
517
+ truncation is mirrored on stderr: the walkers stamp a recognizable marker
518
+ todo (`NESTING_LIMIT_TODO`), `truncatedFieldPaths` collects the truncated
519
+ paths, and the CLI prints
520
+ `warning: path "…" exceeds the walker nesting budget (11); field degraded
521
+ to a placeholder — raise --max-depth or bind by hand` per path.
522
+ - **Depth warnings no longer promise TODOs the chosen layout never emits.**
523
+ `overBudgetFieldPaths` walked one fixed shape while the two layouts stop
524
+ descending at different frontiers: the single-file layout does not descend
525
+ into array-of-array items (generic extract-a-row TODO, no depth TODO
526
+ inside), and the module layout does not recurse into OBJECT fields of an
527
+ array row (generic bind-by-hand TODO) — so for those shapes the stderr
528
+ warnings claimed depth TODOs that weren't in the file (always
529
+ over-promising, never under-reporting). The walk now takes the emitting
530
+ layout's `DepthWarningFrontier` (`depthWarningFrontier("single" |
531
+ "module")`; the single-file frontier is the default, preserving existing
532
+ programmatic behavior), and `warnDegradedBindings` passes the CLI's
533
+ `--layout`. Tests pin warnings-per-TODO equality for both divergent
534
+ shapes in both layouts.
535
+ - **Refused `.default()` captures now warn instead of degrading silently.**
536
+ Every mode that leaves a declared default unseeded is mirrored on stderr:
537
+ the capture guard's refusals (function-valued resolved default,
538
+ non-deterministic two-read disagreement, throwing getter) are recorded on
539
+ the IR as `droppedDefault`, and emit-time refusals (todo-fallback specs,
540
+ kind-mismatched or non-finite values, undeclared enum options, dates,
541
+ container defaults) are detected off the captured `defaultValue` — the
542
+ CLI prints `warning: field "…" has a .default() the CLI could not capture
543
+ (non-primitive, non-deterministic, or degraded field); it starts blank —
544
+ seed it by hand`, one per field. Fields with no default never warn.
545
+
546
+ ## formstand-cli 0.7.0 — 2026-07-12
547
+
548
+ ### Added
549
+
550
+ - **Tuple support** (`z.tuple([...])` and `[A, B]` in type mode). Tuples were
551
+ degrading to a single string field with a TODO; they now generate fixed
552
+ positional controls, each bound at a static numeric-index path (`coord.0`,
553
+ `coord.1`), in both the single-file and module layouts and all three UIs.
554
+ Scalar elements render a real control; a non-scalar element (object / array /
555
+ union / nested tuple) or a variadic rest degrades to a TODO at just that
556
+ position, so the fixed scalar positions still generate. Generated output is
557
+ typechecked against the real library across all backends.
558
+ - **Recursive nested-array extraction in both layouts.** An array nested
559
+ inside an array row used to generate one level (module) or a TODO
560
+ (single-file). It now recurses to arbitrary depth (bounded by `--max-depth`)
561
+ in **both** layouts: `teams[] › members[] › phones[]` generates a full
562
+ component tree, threading each enclosing row's index down as a `p0`, `p1`, …
563
+ prop. In `--layout module` each level is a `Row`/`Rows` pair; in single-file
564
+ each is a child `{Stem}Rows` component with a typed `form` prop, emitted
565
+ above the main component. Array-of-arrays sections extract an inner row
566
+ component too (module). A non-array shape inside a row (nested object /
567
+ union / tuple) still degrades to a TODO. Every generated tree is typechecked
568
+ against the real library across all three UIs.
569
+ - `--max-depth <n>` flag (and a `maxDepth` argument on `fromZod` / `fromType`)
570
+ for the schema/type nesting budget — the number of levels the walkers
571
+ descend before a level degrades to a string + TODO. The default rose from 6
572
+ to **10**, so deeper schemas generate fully. Recursion is still caught
573
+ directly by a seen-set (a self-referential schema → TODO), with the depth
574
+ budget as the backstop for getter-minted schemas; the IR is always finite.
575
+
576
+ ### Fixed
577
+
578
+ - An array whose item is itself non-scalar (an array-of-arrays, or an array of
579
+ tuples/unions) no longer emits an empty `{/* unreachable */}` row in the
580
+ single-file layout — it now emits a clear TODO to extract a row component,
581
+ matching the documented nested-array behavior.
582
+
583
+ ### Docs & examples (no package changes)
584
+
585
+ - Corrected the CLI README / code-generation guide, which claimed several
586
+ already-resolved limitations: `date` fields have emitted real `DateField` /
587
+ date-input bindings since 0.9 (not a text-input TODO), and the module layout
588
+ extracts one real nested-array row level (not a blanket TODO).
589
+ - Schema builder: **paste a zod schema**. An **Import code…** modal takes a
590
+ TypeScript type _or_ a `z.object(...)` schema — by paste or by picking a
591
+ `.ts` file — and generates the form from it. The zod source is evaluated in
592
+ the browser against the bundled zod (the REPL trust model: your own code, in
593
+ your own tab, nothing fetched or sent) and walked by the real `fromZod`, so
594
+ it shares the paste-a-type mode's exact IR → emitters path. Switching the
595
+ dialect swaps in a worked sample while the field is untouched.
596
+
597
+ ## formstand-cli 0.6.2 — 2026-07-10
598
+
599
+ ### Added
600
+
601
+ - Browser-safe programmatic API via the new `formstand-cli/codegen` subpath.
602
+ Everything downstream of the IR — `fromZod`, every emitter
603
+ (`emitPlainForm` / `emitMuiForm` / `emitShadcnForm` / `emitTemplateForm` /
604
+ `emitModuleForm`, `emitZodSchema` / `emitInitialValues`), `joinModuleFiles`,
605
+ `defineTemplate`, `labelFromName` and the casing helpers — is a pure string
606
+ builder with no `fs`/`path` and no TypeScript compiler, so it bundles for the
607
+ browser (the docs Schema-builder generates forms client-side through it). The
608
+ main `formstand-cli` entry still re-exports all of it and adds the
609
+ Node-oriented `fromType` / `defineConfig`.
610
+
611
+ ## formstand-cli 0.6.1 — 2026-07-10
612
+
613
+ ### Added
614
+
615
+ - Custom templates (`--template <file>` or `template:` in the config): a
616
+ `defineTemplate({ name, imports, leaf })` module overrides the per-kind
617
+ field rendering for a UI kit formstand doesn't ship (Mantine, Chakra,
618
+ in-house), inheriting the generated form's scaffold — sections, arrays,
619
+ discriminated unions, submit. Each leaf renderer gets a context of
620
+ JS-expression strings (`bind`, `field`, `label`, `options`), and the same
621
+ renderer works both in the generated wrapper component and inside union
622
+ variant blocks. Unlisted kinds fall back to plain. `--layout single`
623
+ only for now; overrides `--ui`.
624
+
4
625
 
5
626
  ## 0.9.0 — 2026-07-10
6
627