formstand 0.14.0 → 0.15.2

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,115 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.2 — 2026-08-11
4
+
5
+ Identical to 0.15.1, republished under a fresh tag. The v0.15.1 tag was
6
+ pushed during the August 6 GitHub Actions outage and GitHub dropped the
7
+ tag event outright, so the Publish workflow never ran and 0.15.1 never
8
+ reached npm. The Publish workflow only fires on tag pushes by design (the
9
+ trusted-publishing trust is bound to that trigger), so the recovery is a
10
+ new version under a new tag. No code changes.
11
+
12
+ ## formstand-cli 0.11.1 — 2026-08-11
13
+
14
+ Identical to formstand-cli 0.11.0, republished under a fresh tag for the
15
+ same reason as 0.15.2 above: the cli-v0.11.0 tag event was dropped in the
16
+ August 6 outage and the version never reached npm. No code changes.
17
+
18
+ ## 0.15.1 — 2026-08-06
19
+
20
+ ### Fixed
21
+
22
+ - **Tuple element paths type as their exact element.** `FieldValue` resolved
23
+ a path like `"pair.0"` into `[string, number]` through the generic array
24
+ arm, so the value typed as `string | number` instead of `string`. The
25
+ bundled field components hid this by asserting their own value type; the
26
+ raw hooks exposed it, and a CLI module tuple section over a mixed tuple
27
+ did not compile because its per-kind prop builders need the exact element.
28
+ Tuple index paths now resolve positionally. Types only, no runtime change.
29
+
30
+ ## formstand-cli 0.11.0 — 2026-08-06
31
+
32
+ ### Fixed
33
+
34
+ - **Four edge-config compile and layout bugs in the container migration**,
35
+ all one family: a gate that decides imports or exports did not count every
36
+ shape that renders section chrome.
37
+ - A module union section at `--columns 2|3` rendered its vertical `Stack`
38
+ shell while importing for the grid it no longer renders, a TS2304 in
39
+ mantine and mui output. Imports and shell now derive from one visual.
40
+ - A tuple-only root emitted section chrome (`Typography`/`Title`/`Heading`
41
+ headings, and at `--columns 2|3` the `Row`/`Col` or `Grid` pair) without
42
+ importing any of it; the section-import gate now counts tuples.
43
+ - A module tuple section imported the bound field hook from `hooks.ts`,
44
+ which never exported it for a tuple-only root; the export gate now
45
+ mirrors the tuple section's own import rule.
46
+ - A module nested `<fieldset>` lost its full-row span for the CSS-grid
47
+ backends (plain, shadcn, chakra), collapsing a nested section into one
48
+ grid cell; the span now rides the fieldset whenever no cell wrapper
49
+ exists to carry it.
50
+
51
+ The version matrix now generates tuple-only and root-union schemas against
52
+ every kit's real declarations (`cli/matrix/edgeSchemas.ts`), and the CI
53
+ suite grew matching regression tests, so the class cannot ship green again.
54
+
55
+ ### Changed
56
+
57
+ - **`--columns` output is responsive.** `--columns 2` now means "two columns
58
+ when there is room, one on a phone" in every backend's own dialect: MUI gets
59
+ an `sx` breakpoint object, chakra a responsive `gridTemplateColumns` style
60
+ prop, mantine `SimpleGrid cols={{ base: 1, sm: N }}`, and the inline-style
61
+ backends (plain, antd) a `repeat(auto-fit, minmax(...))` track whose calc
62
+ caps the column count at N while a 220px floor collapses columns that would
63
+ squeeze inputs below a usable width. shadcn already collapsed via
64
+ `md:grid-cols-N`. Fixed grid tracks were why generated multi-column forms
65
+ were unusable on phones; regenerate to pick the new grids up.
66
+ - **antd sections lay out with `Row`/`Col`** instead of an inline CSS grid:
67
+ the kit's own 24-column idiom, responsive via `xs`/`sm` props, in both
68
+ layouts. Nested sections, arrays, unions, and tuples ride spanning or item
69
+ `Col`s; a union section keeps its vertical shell, since its children are
70
+ conditional fragments that cannot each be a `Col`. This is also the shape
71
+ that makes a per-field span a one-prop change later.
72
+ - **mantine sections lay out with `Grid`/`Grid.Col`** instead of
73
+ `SimpleGrid`: responsive span objects (`span={{ base: 12, sm: 6 }}`), and
74
+ the container that can express a per-field span at all, which `SimpleGrid`
75
+ cannot. Same cell rules as antd: leaves ride item Cols, nested content
76
+ spans, 1-column chrome reads as a `Stack`. The version matrix caught one
77
+ invented prop on the way in (`Grid` has no `gutter` in v9), which is
78
+ exactly the drift it exists to catch.
79
+ - **mui sections lay out with `Grid` containers** instead of `sx` CSS grids
80
+ — the same component the hand-written playground demos use — with the
81
+ spelling the pinned major actually accepts: `size={{ xs: 12, sm: 6 }}` on
82
+ v7/v9, legacy `item xs={12} sm={6}` on v5 **and** v6 (v6's size-prop grid
83
+ is the separate `Grid2` import; the matrix against real v6 declarations is
84
+ what established that). 1-column chrome keeps the historical Stack and
85
+ CardContent output.
86
+
87
+ ## 0.15.0 — 2026-08-04
88
+
89
+ ### Fixed
90
+
91
+ - **`persistForm` no longer applies a draft from a different schema shape.**
92
+ The stored JSON was cast straight to the values type and adopted, so a draft
93
+ written before a schema change rebased the form onto values its own schema
94
+ rejects. Because the default `adopt` mode clears errors and resets the
95
+ baseline, the form then read **clean** while holding them, which is the worst
96
+ version of the failure.
97
+
98
+ A draft is now ignored when its shape conflicts with the form's: a path
99
+ holding a string where the form expects an object, an array of strings where
100
+ it expects rows. Only overlapping paths are compared, and only their kinds,
101
+ so a half-filled draft still restores (a draft is work in progress and would
102
+ fail full validation by design), and so does one that filled an optional the
103
+ initial values left empty.
104
+
105
+ ### Added
106
+
107
+ - **`persistForm`'s `version` option.** Renames and removals are invisible to
108
+ the shape guard above, because JSON drops undefined slots and an absent key
109
+ is indistinguishable from an unfilled optional. `version` covers those: it is
110
+ stored with the draft and a mismatch discards it. Leaving it unset keeps the
111
+ stored format byte-identical, so existing drafts survive the upgrade.
112
+
3
113
  ## 0.14.0 — 2026-07-31
4
114
 
5
115
  ### Added
package/README.md CHANGED
@@ -12,6 +12,7 @@ Zod-schema-first form state for React 19, backed by zustand.
12
12
  - **Field arrays** with stable IDs that survive reorders, inserts, and removes.
13
13
  - **Bound input components** with the accessibility wiring already done.
14
14
  - **A generator**: [`formstand-cli`](https://scrumrot.github.io/formstand/documentation/cli/) writes the whole component from your schema, for six UI kits.
15
+ - **[Devtools](https://scrumrot.github.io/formstand/documentation/devtools)**: `formstand/devtools` is a panel showing every field, both error channels, and the live diff. Renders nothing in production.
15
16
 
16
17
  ## Install
17
18
 
@@ -111,6 +112,7 @@ PATCH-ready payload of exactly what changed.
111
112
  | Components | `TextField`, `NumberField`, `DateField`, `CheckboxField`, `SelectField`, the prop builders, `useNumberInput`, `focusField`/`focusFirstError` | [Components & bindings](https://scrumrot.github.io/formstand/documentation/api/components) |
112
113
  | Validation | five modes plus `reValidateMode`, field-scoped passes, async refines with race handling, per-field `debounceMs` | [Validation](https://scrumrot.github.io/formstand/documentation/validation) |
113
114
  | Utilities | `parsePath`/`getAtPath`/`setAtPath`, `validateSync`/`validateAsync`, `persistForm`, and the full type list | [Utilities & types](https://scrumrot.github.io/formstand/documentation/api/utilities) |
115
+ | Devtools | `<FormstandDevtools form={form} />` from `formstand/devtools` | [Devtools](https://scrumrot.github.io/formstand/documentation/devtools) |
114
116
 
115
117
  Coming from react-hook-form? The
116
118
  [migration guide](https://scrumrot.github.io/formstand/documentation/migrating-from-react-hook-form)
@@ -40,7 +40,7 @@ type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, Normali
40
40
  type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
41
  [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
42
  }[keyof T & string] : never;
43
- type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
43
+ type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
44
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
45
45
 
46
46
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
@@ -40,7 +40,7 @@ type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, Normali
40
40
  type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
41
  [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
42
  }[keyof T & string] : never;
43
- type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
43
+ type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
44
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
45
45
 
46
46
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-C9hZffCQ.cjs';
3
+ import { F as Form } from '../createForm-jlzK2lCG.cjs';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-C9hZffCQ.js';
3
+ import { F as Form } from '../createForm-jlzK2lCG.js';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
package/dist/index.cjs CHANGED
@@ -1140,6 +1140,25 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
1140
1140
  };
1141
1141
 
1142
1142
  // src/core/persist.ts
1143
+ var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1144
+ var conflictsWith = (draft, reference) => {
1145
+ if (draft === void 0 || draft === null) return false;
1146
+ if (reference === void 0 || reference === null) return false;
1147
+ const draftKind = kindOf(draft);
1148
+ const referenceKind = kindOf(reference);
1149
+ if (draftKind !== referenceKind) return true;
1150
+ if (draftKind === "array") {
1151
+ const [d] = draft;
1152
+ const [r] = reference;
1153
+ return conflictsWith(d, r);
1154
+ }
1155
+ if (draftKind === "object") {
1156
+ const d = draft;
1157
+ const r = reference;
1158
+ return Object.keys(d).some((key) => conflictsWith(d[key], r[key]));
1159
+ }
1160
+ return false;
1161
+ };
1143
1162
  var defaultStorage = () => {
1144
1163
  try {
1145
1164
  return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage;
@@ -1152,11 +1171,29 @@ var persistForm = (form, options) => {
1152
1171
  const debounceMs = options.debounceMs ?? 300;
1153
1172
  const apply = options.apply ?? "adopt";
1154
1173
  const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1174
+ const referenceShape = () => {
1175
+ try {
1176
+ return JSON.parse(JSON.stringify(form.getState().initialValues));
1177
+ } catch {
1178
+ return void 0;
1179
+ }
1180
+ };
1155
1181
  const restore = () => {
1156
1182
  try {
1157
1183
  const raw = storage === null ? null : storage.getItem(options.key);
1158
1184
  if (raw === null) return false;
1159
- applyDraft(JSON.parse(raw));
1185
+ const parsed = JSON.parse(raw);
1186
+ const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
1187
+ if (options.version !== void 0 && !versioned) return false;
1188
+ const wrapper = parsed;
1189
+ if (versioned && wrapper.__v !== options.version) return false;
1190
+ if (versioned && wrapper.values === void 0) return false;
1191
+ const values = versioned ? wrapper.values : parsed;
1192
+ const reference = referenceShape();
1193
+ if (reference !== void 0 && conflictsWith(values, reference)) {
1194
+ return false;
1195
+ }
1196
+ applyDraft(values);
1160
1197
  return true;
1161
1198
  } catch {
1162
1199
  return false;
@@ -1164,7 +1201,12 @@ var persistForm = (form, options) => {
1164
1201
  };
1165
1202
  const writeDraft = (values) => {
1166
1203
  try {
1167
- storage?.setItem(options.key, JSON.stringify(values));
1204
+ storage?.setItem(
1205
+ options.key,
1206
+ JSON.stringify(
1207
+ options.version === void 0 ? values : { __v: options.version, values }
1208
+ )
1209
+ );
1168
1210
  } catch {
1169
1211
  }
1170
1212
  };