formstand 0.8.0 → 0.9.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
@@ -2,6 +2,152 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0 — 2026-07-10
6
+
7
+ ### Fixed
8
+
9
+ - `validateFields` no longer fabricates a "required" error for a field
10
+ whose required ancestor is `undefined` (the field-scoped fast path parsed
11
+ the leaf against undefined where the full-form parse keys the error at the
12
+ ancestor) — sync and async agreed again. `slotAtPath` now checks the
13
+ container at each step.
14
+ - `useVariantField` keeps its type guard for **optional/nullable**
15
+ discriminated unions (a missing `NonNullable` let the nullish member
16
+ collapse `keyof` to `never` and leak every key, including the
17
+ discriminant).
18
+ - `devtools` is active only in non-production builds
19
+ (`NODE_ENV !== "production"`), so opting in during development never
20
+ streams a shipped form's state to an end user with the extension.
21
+ - `parseDateText` accepts calendar years under 100 (the Date constructor
22
+ maps 0–99 to 1900–1999; the literal year is forced back on).
23
+ - `DateField` / `dateInputProps` preserve the existing value's time-of-day,
24
+ so re-picking the same day on a timestamped value is no longer spuriously
25
+ dirty (and changing the day keeps the time).
26
+ - `parsePath`'s cached segment array is frozen — it is shared across callers
27
+ now, so external mutation throws instead of corrupting the cache.
28
+ - `useVariantField` docs the write-gating contract (gate variant-field
29
+ writes on the discriminant, like rendering).
30
+
31
+ ### Added
32
+
33
+ - `useVariantField(form, unionPath, field)`: typed access to the
34
+ variant-specific fields of a `z.discriminatedUnion` — the ones
35
+ `FieldPath` omits because they exist in only some union members (the
36
+ discriminant stays a common key, bound with plain `useField`). Typed as
37
+ the field's value across the variants that declare it, widened with
38
+ `| undefined`; a non-variant field or the discriminant itself is a
39
+ compile error. `createFormHooks` gains a bound `use{Name}VariantField`.
40
+ - `persistForm(form, { key })`: the autosave recipe as a first-class
41
+ helper — debounced JSON drafts to any `{ getItem, setItem, removeItem }`
42
+ storage, draft auto-apply on start (`adopt` rebases clean, `restore`
43
+ loads dirty, `manual` waits for you), `clear()` that also cancels
44
+ pending writes, and guarded storage access throughout.
45
+ - Redux DevTools: `createForm(schema, { devtools: "checkout" })` connects
46
+ the form's store to the extension via zustand's middleware — every write
47
+ named, inspectable, and time-travelable. Off by default and inert
48
+ without the extension.
49
+ - Hot-path performance (the 2026-07 review's efficiency findings, no API
50
+ changes): `parsePath` is memoized (every field subscription re-parsed
51
+ its path per store notification); `validateFields` parses just the
52
+ requested subschemas when they're extractable — cross-field rules still
53
+ force the full parse, because extraction bails on refined levels — so a
54
+ wizard step stops paying for the whole schema per click; `diff()`/
55
+ `dirtyFields()` deep-compare each node once instead of once per ancestor
56
+ level; `focusFirstError` runs its focusability DOM walks only on
57
+ name-matched candidates instead of every named control in the document.
58
+ - Dates are a first-class field kind: `DateField` (an `<input type="date">`
59
+ bound to Date-typed paths) joins the shipped components, with
60
+ `dateInputProps` / `dateToInputText` / `parseDateText` exported for
61
+ custom markup and UI-kit adapters. Local calendar-date semantics
62
+ throughout (never `toISOString()` — June 1 stays June 1 west of UTC),
63
+ the `emptyValue` round-trip on clear, and rollover rejection
64
+ (`2026-02-31` is invalid, not March 3).
65
+
66
+ ### CI (no package changes)
67
+
68
+ - Dependabot triage: esbuild pinned to the patched 0.28.1 line via npm
69
+ overrides in the root and cli (dev-server advisories), scoped to spare
70
+ vitepress 1.x's nested vite-5 toolchain (esbuild 0.28 can't downlevel to
71
+ vite 5's browser targets). The remaining vitepress-nested alerts are
72
+ dismissed as tolerable risk — local docs dev/build only — with the
73
+ VitePress 2 migration tracked on the roadmap as the real fix.
74
+ - Workflows declare least-privilege GITHUB_TOKEN permissions (CodeQL:
75
+ actions/missing-workflow-permissions).
76
+
77
+ ### Docs & examples (no package changes)
78
+
79
+ - The Schema builder gains a **Paste a TypeScript type** mode: paste an
80
+ interface or type alias and the same in-browser emitters generate the
81
+ form — no TypeScript compiler in the bundle, a focused ~250-line parser
82
+ covers the CLI's type-mode subset (string/number/boolean/Date, arrays,
83
+ nested objects, string-literal unions, optional/nullable) and degrades
84
+ the rest to a text field with a TODO. Both input modes share one emit
85
+ path.
86
+ - ROADMAP.md: a living plan for the library and the CLI — the 0.9/cli-0.6
87
+ cycle (dates as a first-class field kind, hot-path performance,
88
+ DevTools, CLI config + watch), the next tier, the parking lot, and the
89
+ review's deferred cleanup items.
90
+ - The theme is one preference across the docs site and the playground:
91
+ the playground stores its light/dark choice under VitePress's own
92
+ localStorage key (same origin), so flipping either surface's toggle
93
+ follows you to the other.
94
+ - The demo body caps at a readable 760px measure; legitimately wide demos
95
+ (Perf's grid, the Schema builder, the Invoice table) opt out via an
96
+ explicit WIDE_DEMOS set.
97
+ - The demo card gets a real header: group eyebrow, title, a one-line
98
+ description of what each demo shows (exhaustive over the tab keys, like
99
+ the group map), and the View state / View code toggles plus a copy-link
100
+ button as a right-aligned action cluster — shell chrome separated from
101
+ demo content by a hairline. The nav groups show demo counts, and the
102
+ mobile bottom sheet gains its own Close button (the header toggles sit
103
+ behind it).
104
+
105
+ - Mobile playground shell: a top app bar (menu / title / controls), the
106
+ demo list as an off-canvas drawer that closes on pick, and the View
107
+ state / View code panel as a bottom sheet — all CSS-driven, desktop
108
+ layout unchanged.
109
+ - Light theme: a toggle in the app bar (and sidebar on desktop), defaulting
110
+ to the OS preference and persisted per browser. One variable palette in
111
+ styles.css drives the shell; the MUI bridge and the shadcn scope follow
112
+ the same html[data-theme] switch. Terminal/code surfaces deliberately
113
+ stay dark in both themes.
114
+ - The GitHub link in the shell shows the repo's star count (best-effort,
115
+ cached per session, skipped under tests).
116
+
117
+ ## formstand-cli 0.6.0 — 2026-07-10
118
+
119
+ - Discriminated unions (`z.discriminatedUnion`) at a field position: the
120
+ generator emits a discriminant select plus per-variant conditional
121
+ blocks, binding variant fields through the library's `useVariantField`
122
+ (single-file) or bound `use{Name}VariantField` (module layout) — fully
123
+ typed, no casts. All six emit paths (plain/mui/shadcn × single-file/
124
+ module) typecheck against the real helper. Initial values pick the
125
+ first variant; the schema round-trips. Nested-in-section unions,
126
+ unions in array rows, and non-scalar variant fields keep a TODO.
127
+ - Nested-array row extraction (`--layout module`): an array inside a
128
+ section object, or an array inside an array row, now generates its own
129
+ Row/Rows component pair with a bound `useFieldArray` on the template
130
+ path (two numeric holes for arrays-in-rows,
131
+ `contacts.${parentIndex}.phones.${index}.number`) instead of a TODO
132
+ comment. One extraction level per array — deeper nesting keeps a TODO.
133
+ - `formstand.config.ts` (also `.mts`/`.js`/`.mjs`, or `--config <file>`):
134
+ project defaults for `ui`/`layout`/`sections`/`columns`, written with the
135
+ typed `defineConfig` export. Precedence is flags > config > built-ins,
136
+ and config typos fail as loudly as flag typos.
137
+ - `--watch`: regenerate whenever the input file changes (requires
138
+ `--out`; reruns overwrite their own output). Watches the parent
139
+ directory so editors that save via rename don't kill the watcher.
140
+ - A kind × optional × nullable matrix test pins `emitInitialValues` and
141
+ `blankNeedsCast` agreement in both directions: the no-cast combos must
142
+ typecheck with the checked annotation, and every cast combo is proven
143
+ to genuinely need it.
144
+ - Date fields emit real bindings instead of a text input with a cast and
145
+ a TODO: `DateField` (plain), `muiDateFieldProps` (MUI `type="date"`
146
+ TextField with a floated label), `shadcnDateInputProps` (shadcn Input).
147
+ `DATE_CAST` and the per-backend date TODOs are gone from generated
148
+ output — forms with dates submit successfully out of the box. Requires
149
+ formstand ≥ 0.9 for date fields.
150
+
5
151
  ## 0.8.0 — 2026-07-10
6
152
 
7
153
  #### Fixed (2026-07 full-repo review)
@@ -105,6 +251,14 @@
105
251
  The benchmark writes `field0` instead of the mid-grid field, so the
106
252
  change is visible without scrolling.
107
253
 
254
+ ## formstand-cli 0.5.3 — 2026-07-10
255
+
256
+ ### Fixed
257
+
258
+ - Emitted string literals escape U+2028/U+2029 (JSON.stringify leaves the
259
+ JS line separators raw — a syntax error for pre-ES2019 parsers of the
260
+ GENERATED file, and CodeQL's js/bad-code-sanitization).
261
+
108
262
  ## formstand-cli 0.5.2 — 2026-07-10
109
263
 
110
264
  #### Fixed (2026-07 full-repo review)
package/README.md CHANGED
@@ -429,7 +429,8 @@ form.handleSubmit(async (data) => {
429
429
 
430
430
  See [CONTRIBUTING.md](./CONTRIBUTING.md) — note the repo has three
431
431
  independent npm roots (`.`, `examples/`, `cli/`), and the root test suite
432
- needs `examples/` installed.
432
+ needs `examples/` installed. Where the project is headed lives in
433
+ [ROADMAP.md](./ROADMAP.md).
433
434
 
434
435
  ## License
435
436
 
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var vanilla = require('zustand/vanilla');
4
+ var middleware = require('zustand/middleware');
4
5
  var zod = require('zod');
5
6
  var react = require('react');
6
7
  var jsxRuntime = require('react/jsx-runtime');
@@ -87,7 +88,18 @@ var isIndexSegment = (s) => {
87
88
  const n = Number(s);
88
89
  return Number.isInteger(n) && n >= 0 && String(n) === s;
89
90
  };
90
- var parsePath = (path) => path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s);
91
+ var PARSE_CACHE_MAX = 4096;
92
+ var parseCache = /* @__PURE__ */ new Map();
93
+ var parsePath = (path) => {
94
+ const cached = parseCache.get(path);
95
+ if (cached !== void 0) return cached;
96
+ const segments = Object.freeze(
97
+ path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
98
+ );
99
+ if (parseCache.size >= PARSE_CACHE_MAX) parseCache.clear();
100
+ parseCache.set(path, segments);
101
+ return segments;
102
+ };
91
103
  var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
92
104
  var readSegment = (obj, segment) => {
93
105
  if (obj == null) return void 0;
@@ -100,8 +112,9 @@ var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
100
112
  var slotAtPath = (obj, path) => {
101
113
  const walked = parsePath(path).reduce(
102
114
  (acc, segment) => {
103
- const outOfBounds = typeof segment === "number" && (Array.isArray(acc.current) ? segment >= acc.current.length : !isPlainRecord(acc.current));
104
- return !acc.ok || outOfBounds ? { ok: false, current: void 0 } : { ok: true, current: readSegment(acc.current, segment) };
115
+ const container = acc.current;
116
+ const addressable = typeof segment === "number" ? Array.isArray(container) ? segment < container.length : isPlainRecord(container) : isPlainRecord(container);
117
+ return !acc.ok || !addressable ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
105
118
  },
106
119
  { ok: true, current: obj }
107
120
  );
@@ -265,7 +278,7 @@ var releaseAncestorEntries = (map, path) => {
265
278
  return kept.length === entries.length ? map : Object.fromEntries(kept);
266
279
  };
267
280
  var dirtyPathsOf = (values, initial, prefix) => {
268
- if (valuesEqual(values, initial)) return [];
281
+ if (Object.is(values, initial)) return [];
269
282
  if (isPlainObject(values) && isPlainObject(initial)) {
270
283
  const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
271
284
  const childPaths = [...keys].flatMap(
@@ -275,9 +288,11 @@ var dirtyPathsOf = (values, initial, prefix) => {
275
288
  prefix === "" ? k : `${prefix}.${k}`
276
289
  )
277
290
  );
278
- return childPaths.length > 0 ? childPaths : [prefix];
291
+ if (childPaths.length > 0) return childPaths;
292
+ const sameKeySet = Object.keys(values).length === Object.keys(initial).length && Object.keys(values).every((k) => Object.hasOwn(initial, k));
293
+ return sameKeySet ? [] : [prefix];
279
294
  }
280
- return [prefix];
295
+ return valuesEqual(values, initial) ? [] : [prefix];
281
296
  };
282
297
  var FORM_SEQ_KEY = "__form__";
283
298
  var SUBMIT_SEQ_KEY = "__submit__";
@@ -339,7 +354,18 @@ var createForm = (schema, options) => {
339
354
  mode: initialMode,
340
355
  reValidateMode: initialReValidateMode
341
356
  };
342
- const store = vanilla.createStore(() => initial);
357
+ const store = options.devtools === void 0 || options.devtools === false ? vanilla.createStore(() => initial) : vanilla.createStore()(
358
+ middleware.devtools(() => initial, {
359
+ name: typeof options.devtools === "string" ? options.devtools : "formstand",
360
+ // Non-production only, matching zustand's own devtools default:
361
+ // opting in during development must NOT stream every keystroke to
362
+ // an end user who happens to have the extension installed, where
363
+ // a module-singleton form's unbounded time-travel history is a
364
+ // real memory + serialization cost. Guarded for environments
365
+ // without process (a bundler defines NODE_ENV in real dev).
366
+ enabled: typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production"
367
+ })
368
+ );
343
369
  const commitFullPass = (state, next) => errorChannels(state, {
344
370
  schemaErrors: reuseErrorRefs(next, state.schemaErrors)
345
371
  });
@@ -368,7 +394,38 @@ var createForm = (schema, options) => {
368
394
  return result.kind === "pending" ? result.promise.then((r) => r.kind === "valid") : result.kind === "valid";
369
395
  }
370
396
  try {
371
- const result = validateSync(schema, store.getState().values);
397
+ const values = store.getState().values;
398
+ const scopes = paths.map((path) => ({
399
+ path,
400
+ scope: fieldScopeFor(path, values)
401
+ }));
402
+ if (scopes.every(
403
+ ({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
404
+ )) {
405
+ const merged = scopes.reduce(
406
+ (acc, { path, scope }) => scope.kind === "skip" ? acc : {
407
+ ...acc,
408
+ ...scopeSettledResult(
409
+ validateSync(scope.schema, scope.value),
410
+ path,
411
+ true
412
+ )
413
+ },
414
+ {}
415
+ );
416
+ store.setState((state) => ({
417
+ ...state,
418
+ ...errorChannels(state, {
419
+ schemaErrors: reuseErrorRefs(
420
+ mergeScopedErrors(state.schemaErrors, merged, paths),
421
+ state.schemaErrors
422
+ ),
423
+ serverErrors: omitScope(state.serverErrors, paths)
424
+ })
425
+ }));
426
+ return Object.values(merged).flat().length === 0;
427
+ }
428
+ const result = validateSync(schema, values);
372
429
  const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
373
430
  store.setState((state) => ({
374
431
  ...state,
@@ -968,6 +1025,74 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
968
1025
  return true;
969
1026
  }
970
1027
  };
1028
+
1029
+ // src/core/persist.ts
1030
+ var defaultStorage = () => {
1031
+ try {
1032
+ return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage;
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ };
1037
+ var persistForm = (form, options) => {
1038
+ const storage = options.storage ?? defaultStorage();
1039
+ const debounceMs = options.debounceMs ?? 300;
1040
+ const apply = options.apply ?? "adopt";
1041
+ const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1042
+ const restore = () => {
1043
+ try {
1044
+ const raw = storage === null ? null : storage.getItem(options.key);
1045
+ if (raw === null) return false;
1046
+ applyDraft(JSON.parse(raw));
1047
+ return true;
1048
+ } catch {
1049
+ return false;
1050
+ }
1051
+ };
1052
+ const writeDraft = (values) => {
1053
+ try {
1054
+ storage?.setItem(options.key, JSON.stringify(values));
1055
+ } catch {
1056
+ }
1057
+ };
1058
+ const timerRef = {
1059
+ current: null
1060
+ };
1061
+ const cancelPending = () => {
1062
+ if (timerRef.current !== null) {
1063
+ clearTimeout(timerRef.current);
1064
+ timerRef.current = null;
1065
+ }
1066
+ };
1067
+ if (apply !== "manual") {
1068
+ restore();
1069
+ }
1070
+ const unsubscribe = form.watchValues((values) => {
1071
+ cancelPending();
1072
+ if (debounceMs === 0) {
1073
+ writeDraft(values);
1074
+ } else {
1075
+ timerRef.current = setTimeout(() => {
1076
+ timerRef.current = null;
1077
+ writeDraft(values);
1078
+ }, debounceMs);
1079
+ }
1080
+ });
1081
+ return Object.freeze({
1082
+ restore,
1083
+ clear: () => {
1084
+ cancelPending();
1085
+ try {
1086
+ storage?.removeItem(options.key);
1087
+ } catch {
1088
+ }
1089
+ },
1090
+ dispose: () => {
1091
+ cancelPending();
1092
+ unsubscribe();
1093
+ }
1094
+ });
1095
+ };
971
1096
  var useForm = (schema, options) => {
972
1097
  const formRef = react.useRef(null);
973
1098
  if (formRef.current === null) {
@@ -1130,6 +1255,14 @@ function useField(form, pathArg, options) {
1130
1255
  [slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
1131
1256
  );
1132
1257
  }
1258
+
1259
+ // src/react/useVariantField.ts
1260
+ function useVariantField(form, unionPath, field) {
1261
+ return useField(
1262
+ form,
1263
+ `${unionPath}.${field}`
1264
+ );
1265
+ }
1133
1266
  var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1134
1267
  var EMPTY_ITEMS = [];
1135
1268
  var reconcileIds = (prev, nextItems) => {
@@ -1271,6 +1404,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
1271
1404
  var createFormHooks = (form, name) => {
1272
1405
  const structural = form;
1273
1406
  const useBoundField = (path, options) => useField(structural, path, options);
1407
+ const useBoundVariantField = (unionPath, field) => useVariantField(
1408
+ structural,
1409
+ unionPath,
1410
+ field
1411
+ );
1274
1412
  const useBoundFieldArray = (path) => useFieldArray(structural, path);
1275
1413
  const useBoundSelector = (selector) => useFormSelector(structural, selector);
1276
1414
  const useBoundSelectorShallow = (selector) => useFormSelectorShallow(structural, selector);
@@ -1282,6 +1420,7 @@ var createFormHooks = (form, name) => {
1282
1420
  const prefix = capitalize(name ?? "");
1283
1421
  return {
1284
1422
  [`use${prefix}Field`]: useBoundField,
1423
+ [`use${prefix}VariantField`]: useBoundVariantField,
1285
1424
  [`use${prefix}FieldArray`]: useBoundFieldArray,
1286
1425
  [`use${prefix}Selector`]: useBoundSelector,
1287
1426
  [`use${prefix}SelectorShallow`]: useBoundSelectorShallow,
@@ -1326,6 +1465,48 @@ var numberInputProps = (field) => ({
1326
1465
  },
1327
1466
  onBlur: field.onBlur
1328
1467
  });
1468
+ var dateToInputText = (value) => {
1469
+ if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
1470
+ return "";
1471
+ }
1472
+ const pad = (part) => String(part).padStart(2, "0");
1473
+ return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
1474
+ };
1475
+ var parseDateText = (text) => {
1476
+ if (text.trim() === "") return { kind: "empty" };
1477
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1478
+ if (match === null) return { kind: "invalid" };
1479
+ const [, year, month, day] = match;
1480
+ const y = Number(year);
1481
+ const date = new Date(y, Number(month) - 1, Number(day));
1482
+ if (y < 100) date.setFullYear(y);
1483
+ return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
1484
+ };
1485
+ var dateInputProps = (field) => ({
1486
+ type: "date",
1487
+ name: field.path,
1488
+ value: dateToInputText(field.value),
1489
+ "aria-invalid": ariaInvalid(field),
1490
+ onChange: (e) => {
1491
+ const parsed = parseDateText(e.target.value);
1492
+ if (parsed.kind !== "date") {
1493
+ field.setValue(field.emptyValue);
1494
+ return;
1495
+ }
1496
+ const current = field.value;
1497
+ const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
1498
+ parsed.value.getFullYear(),
1499
+ parsed.value.getMonth(),
1500
+ parsed.value.getDate(),
1501
+ current.getHours(),
1502
+ current.getMinutes(),
1503
+ current.getSeconds(),
1504
+ current.getMilliseconds()
1505
+ ) : parsed.value;
1506
+ field.setValue(next);
1507
+ },
1508
+ onBlur: field.onBlur
1509
+ });
1329
1510
  var checkboxProps = (field) => ({
1330
1511
  type: "checkbox",
1331
1512
  name: field.path,
@@ -1380,6 +1561,33 @@ var TextField = ({
1380
1561
  /* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
1381
1562
  ] });
1382
1563
  };
1564
+ var DateField = ({
1565
+ form,
1566
+ path,
1567
+ label,
1568
+ min,
1569
+ max,
1570
+ ref
1571
+ }) => {
1572
+ const id = react.useId();
1573
+ const errorId = `${id}-error`;
1574
+ const field = useField(form, path);
1575
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
1576
+ label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
1577
+ /* @__PURE__ */ jsxRuntime.jsx(
1578
+ "input",
1579
+ {
1580
+ id,
1581
+ ref,
1582
+ ...dateInputProps(field),
1583
+ min,
1584
+ max,
1585
+ "aria-describedby": hasError(field.error) ? errorId : void 0
1586
+ }
1587
+ ),
1588
+ /* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
1589
+ ] });
1590
+ };
1383
1591
  var IDLE_EDIT = { raw: null, pushed: void 0 };
1384
1592
  var useNumberInput = (field) => {
1385
1593
  const [edit, setEdit] = react.useState(IDLE_EDIT);
@@ -1509,13 +1717,12 @@ var SelectField = ({
1509
1717
  };
1510
1718
 
1511
1719
  // src/react/focusError.ts
1512
- var focusableControls = (scope) => [
1720
+ var namedControls = (scope) => [
1513
1721
  ...scope.querySelectorAll(
1514
1722
  "input[name], select[name], textarea[name]"
1515
1723
  )
1516
- ].filter(
1517
- (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null
1518
- );
1724
+ ];
1725
+ var isFocusCandidate = (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null;
1519
1726
  var holdsFocus = (el) => {
1520
1727
  const rootNode = el.getRootNode();
1521
1728
  const active = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : document.activeElement;
@@ -1535,20 +1742,21 @@ var focusFirstError = (errors, root) => {
1535
1742
  (k) => k !== "" && (errors[k]?.length ?? 0) > 0
1536
1743
  );
1537
1744
  const hasRootError = (errors[""]?.length ?? 0) > 0;
1538
- const controls = focusableControls(scope);
1539
- const fieldMatches = controls.filter(
1540
- (el) => nameMatchesAny(el, erroredPaths)
1541
- );
1745
+ const controls = namedControls(scope);
1746
+ const fieldMatches = controls.filter((el) => nameMatchesAny(el, erroredPaths)).filter(isFocusCandidate);
1542
1747
  const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
1543
- const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
1748
+ const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls.filter(isFocusCandidate) : [];
1544
1749
  return focusFirstOf(candidates);
1545
1750
  };
1546
1751
  var focusField = (path, root) => {
1547
- const controls = focusableControls(root ?? document);
1548
- return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
1752
+ const controls = namedControls(root ?? document);
1753
+ return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls.filter(isFocusCandidate)) : focusFirstOf(
1754
+ controls.filter((el) => nameMatchesAny(el, [path])).filter(isFocusCandidate)
1755
+ );
1549
1756
  };
1550
1757
 
1551
1758
  exports.CheckboxField = CheckboxField;
1759
+ exports.DateField = DateField;
1552
1760
  exports.NumberField = NumberField;
1553
1761
  exports.SelectField = SelectField;
1554
1762
  exports.TextField = TextField;
@@ -1556,6 +1764,8 @@ exports.checkboxProps = checkboxProps;
1556
1764
  exports.createForm = createForm;
1557
1765
  exports.createFormContext = createFormContext;
1558
1766
  exports.createFormHooks = createFormHooks;
1767
+ exports.dateInputProps = dateInputProps;
1768
+ exports.dateToInputText = dateToInputText;
1559
1769
  exports.emptyValueForSchema = emptyValueForSchema;
1560
1770
  exports.flattenIssues = flattenIssues;
1561
1771
  exports.focusField = focusField;
@@ -1564,8 +1774,10 @@ exports.getAtPath = getAtPath;
1564
1774
  exports.isAsyncRequiredError = isAsyncRequiredError;
1565
1775
  exports.numberInputProps = numberInputProps;
1566
1776
  exports.numberToInputText = numberToInputText;
1777
+ exports.parseDateText = parseDateText;
1567
1778
  exports.parseNumberText = parseNumberText;
1568
1779
  exports.parsePath = parsePath;
1780
+ exports.persistForm = persistForm;
1569
1781
  exports.selectProps = selectProps;
1570
1782
  exports.setAtPath = setAtPath;
1571
1783
  exports.shouldValidateOn = shouldValidateOn;
@@ -1580,6 +1792,7 @@ exports.useIsDirty = useIsDirty;
1580
1792
  exports.useIsSubmitting = useIsSubmitting;
1581
1793
  exports.useIsValid = useIsValid;
1582
1794
  exports.useSubmitCount = useSubmitCount;
1795
+ exports.useVariantField = useVariantField;
1583
1796
  exports.validateAsync = validateAsync;
1584
1797
  exports.validateSync = validateSync;
1585
1798
  //# sourceMappingURL=index.cjs.map