formstand 0.10.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,5 +1,35 @@
1
1
  # Changelog
2
2
 
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
+
3
33
  ## 0.10.0 — 2026-07-28
4
34
 
5
35
  ### Breaking
@@ -137,6 +167,241 @@
137
167
  - `FieldPathArg` is now actually used by the `useField` / `useFieldArray`
138
168
  overloads it describes (it was exported dead).
139
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
+
140
405
  ## formstand-cli 0.8.0 — 2026-07-28
141
406
 
142
407
  ### Changed
package/dist/index.cjs CHANGED
@@ -1934,6 +1934,20 @@ var nameMatchesAny = (el, paths) => {
1934
1934
  const name = el.getAttribute("name");
1935
1935
  return name !== null && paths.some((p) => isPathOrChild(name, p));
1936
1936
  };
1937
+ var cssQuoted = (value) => `"${value.replace(/[\\"]/g, (ch) => `\\${ch}`)}"`;
1938
+ var idMatches = (scope, path) => path === "" ? [] : [...scope.querySelectorAll(`[id=${cssQuoted(path)}]`)];
1939
+ var inDomOrder = (els) => [...els].sort(
1940
+ (a, b) => a === b ? 0 : (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 ? -1 : 1
1941
+ );
1942
+ var pathCandidates = (scope, paths) => {
1943
+ const controls = namedControls(scope);
1944
+ const named = controls.filter((el) => nameMatchesAny(el, paths));
1945
+ const unnamedPaths = paths.filter(
1946
+ (p) => !controls.some((el) => nameMatchesAny(el, [p]))
1947
+ );
1948
+ const byId = unnamedPaths.flatMap((p) => idMatches(scope, p));
1949
+ return inDomOrder([.../* @__PURE__ */ new Set([...named, ...byId])]);
1950
+ };
1937
1951
  var focusFirstError = (errors, root) => {
1938
1952
  const scope = root ?? document;
1939
1953
  const erroredPaths = Object.keys(errors).filter(
@@ -1941,16 +1955,16 @@ var focusFirstError = (errors, root) => {
1941
1955
  );
1942
1956
  const hasRootError = (errors[""]?.length ?? 0) > 0;
1943
1957
  const controls = namedControls(scope);
1944
- const fieldMatches = controls.filter((el) => nameMatchesAny(el, erroredPaths)).filter(isFocusCandidate);
1958
+ const fieldMatches = pathCandidates(scope, erroredPaths).filter(
1959
+ isFocusCandidate
1960
+ );
1945
1961
  const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
1946
1962
  const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls.filter(isFocusCandidate) : [];
1947
1963
  return focusFirstOf(candidates);
1948
1964
  };
1949
1965
  var focusField = (path, root) => {
1950
- const controls = namedControls(root ?? document);
1951
- return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls.filter(isFocusCandidate)) : focusFirstOf(
1952
- controls.filter((el) => nameMatchesAny(el, [path])).filter(isFocusCandidate)
1953
- );
1966
+ const scope = root ?? document;
1967
+ return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(namedControls(scope).filter(isFocusCandidate)) : focusFirstOf(pathCandidates(scope, [path]).filter(isFocusCandidate));
1954
1968
  };
1955
1969
 
1956
1970
  exports.CheckboxField = CheckboxField;
@@ -1990,6 +2004,7 @@ exports.useFormSelectorShallow = useFormSelectorShallow;
1990
2004
  exports.useIsDirty = useIsDirty;
1991
2005
  exports.useIsSubmitting = useIsSubmitting;
1992
2006
  exports.useIsValid = useIsValid;
2007
+ exports.useNumberInput = useNumberInput;
1993
2008
  exports.useSubmitCount = useSubmitCount;
1994
2009
  exports.useVariantField = useVariantField;
1995
2010
  exports.validateAsync = validateAsync;