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 +622 -1
- package/README.md +17 -13
- package/dist/index.cjs +485 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -57
- package/dist/index.d.ts +117 -57
- package/dist/index.js +486 -274
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,627 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|