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/dist/index.d.cts CHANGED
@@ -67,6 +67,7 @@ type CreateFormOptions<TSchema extends z.ZodType> = Readonly<{
67
67
  mode?: ValidationMode;
68
68
  reValidateMode?: ValidationMode;
69
69
  validateOnMount?: boolean;
70
+ devtools?: boolean | string;
70
71
  }>;
71
72
  type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => void | Promise<void>;
72
73
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
@@ -149,6 +150,24 @@ declare const parsePath: (path: string) => readonly PathSegment[];
149
150
  declare const getAtPath: (obj: unknown, path: string) => unknown;
150
151
  declare const setAtPath: <T>(obj: T, path: string, value: unknown) => T;
151
152
 
153
+ type PersistStorage = Readonly<{
154
+ getItem: (key: string) => string | null;
155
+ setItem: (key: string, value: string) => void;
156
+ removeItem: (key: string) => void;
157
+ }>;
158
+ type PersistOptions<TSchema extends z.ZodType> = Readonly<{
159
+ key: string;
160
+ storage?: PersistStorage;
161
+ debounceMs?: number;
162
+ apply?: "adopt" | "restore" | "manual";
163
+ }>;
164
+ type PersistHandle = Readonly<{
165
+ restore: () => boolean;
166
+ clear: () => void;
167
+ dispose: () => void;
168
+ }>;
169
+ declare const persistForm: <TSchema extends z.ZodType>(form: Form<TSchema>, options: PersistOptions<TSchema>) => PersistHandle;
170
+
152
171
  declare const useForm: <TSchema extends z.ZodType>(schema: TSchema, options: CreateFormOptions<TSchema>) => Form<TSchema>;
153
172
 
154
173
  type FormProviderProps<TSchema extends z.ZodType> = Readonly<{
@@ -198,6 +217,16 @@ declare function useField<TValue = unknown>(form: FieldFormApi & {
198
217
  }, path: string | ((state: FormState<unknown>) => string), options?: UseFieldOptions): UseFieldReturn<TValue>;
199
218
  declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
200
219
 
220
+ type VariantFieldValue<V, TField extends string> = V extends unknown ? TField extends keyof V ? V[TField] : never : never;
221
+ type CommonKeys<V> = keyof V;
222
+ type AllKeys<V> = V extends unknown ? keyof V : never;
223
+ type VariantKeys<V> = Exclude<AllKeys<V>, CommonKeys<V>> & string;
224
+ type UnionValueAt<TValues, P extends string> = P extends keyof TValues ? NonNullable<TValues[P]> : P extends `${infer Head}.${infer Tail}` ? Head extends keyof TValues ? UnionValueAt<NonNullable<TValues[Head]>, Tail> : never : never;
225
+ declare function useVariantField<TSchema extends z.ZodType, P extends string, TField extends VariantKeys<UnionValueAt<z.input<TSchema>, P>>>(form: Form<TSchema>, unionPath: P, field: TField): UseFieldReturn<VariantFieldValue<UnionValueAt<z.input<TSchema>, P>, TField> | undefined>;
226
+ declare function useVariantField<TValue = unknown>(form: FieldFormApi & {
227
+ readonly schema?: undefined;
228
+ }, unionPath: string, field: string): UseFieldReturn<TValue | undefined>;
229
+
201
230
  type ReadonlyStore$1<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
202
231
  type FieldArrayFormApi = Readonly<{
203
232
  store: ReadonlyStore$1<FormState<unknown>>;
@@ -233,6 +262,7 @@ type BoundUseField<TSchema extends z.ZodType> = {
233
262
  (pathSelector: (state: FormState<z.input<TSchema>>) => string, options?: UseFieldOptions): UseFieldReturn<unknown>;
234
263
  <P extends FieldPath<z.input<TSchema>>>(path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
235
264
  };
265
+ type BoundUseVariantField<TSchema extends z.ZodType> = <P extends string, TField extends VariantKeys<UnionValueAt<z.input<TSchema>, P>>>(unionPath: P, field: TField) => UseFieldReturn<VariantFieldValue<UnionValueAt<z.input<TSchema>, P>, TField> | undefined>;
236
266
  type BoundUseFieldArray<TSchema extends z.ZodType> = {
237
267
  (pathSelector: (state: FormState<z.input<TSchema>>) => string): UseFieldArrayReturn<unknown>;
238
268
  <P extends FieldPath<z.input<TSchema>>>(path: P): UseFieldArrayReturn<ArrayItemOf<FieldValue<z.input<TSchema>, P>>>;
@@ -241,6 +271,8 @@ type BoundUseSelector<TSchema extends z.ZodType> = <U>(selector: (state: FormSta
241
271
  type BoundUseFlag<TSchema extends z.ZodType> = (path?: FieldPath<z.input<TSchema>>) => boolean;
242
272
  type FormHooks<TSchema extends z.ZodType, N extends string> = Readonly<{
243
273
  [K in `use${Capitalize<N>}Field`]: BoundUseField<TSchema>;
274
+ } & {
275
+ [K in `use${Capitalize<N>}VariantField`]: BoundUseVariantField<TSchema>;
244
276
  } & {
245
277
  [K in `use${Capitalize<N>}FieldArray`]: BoundUseFieldArray<TSchema>;
246
278
  } & {
@@ -300,6 +332,15 @@ type TextFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
300
332
  ref?: FieldRef<HTMLInputElement>;
301
333
  }>;
302
334
  declare const TextField: <F extends FieldFormApi>({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps<F>) => react.JSX.Element;
335
+ type DateFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
336
+ form: F;
337
+ path: PathsOf<F>;
338
+ label?: ReactNode;
339
+ min?: string;
340
+ max?: string;
341
+ ref?: FieldRef<HTMLInputElement>;
342
+ }>;
343
+ declare const DateField: <F extends FieldFormApi>({ form, path, label, min, max, ref, }: DateFieldProps<F>) => react.JSX.Element;
303
344
  type NumberFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
304
345
  form: F;
305
346
  path: PathsOf<F>;
@@ -347,6 +388,14 @@ type NumberInputProps = Readonly<{
347
388
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
348
389
  onBlur: () => void;
349
390
  }>;
391
+ type DateInputProps = Readonly<{
392
+ type: "date";
393
+ name: string;
394
+ value: string;
395
+ "aria-invalid": true | undefined;
396
+ onChange: (e: ChangeEvent<HTMLInputElement>) => void;
397
+ onBlur: () => void;
398
+ }>;
350
399
  type CheckboxProps = Readonly<{
351
400
  type: "checkbox";
352
401
  name: string;
@@ -374,7 +423,18 @@ type ParsedNumberText = Readonly<{
374
423
  declare const parseNumberText: (text: string) => ParsedNumberText;
375
424
  declare const textInputProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => TextInputProps;
376
425
  declare const numberInputProps: <T extends number | null | undefined>(field: UseFieldReturn<T>) => NumberInputProps;
426
+ declare const dateToInputText: (value: Date | null | undefined) => string;
427
+ type ParsedDateText = Readonly<{
428
+ kind: "empty";
429
+ }> | Readonly<{
430
+ kind: "date";
431
+ value: Date;
432
+ }> | Readonly<{
433
+ kind: "invalid";
434
+ }>;
435
+ declare const parseDateText: (text: string) => ParsedDateText;
436
+ declare const dateInputProps: <T extends Date | null | undefined>(field: UseFieldReturn<T>) => DateInputProps;
377
437
  declare const checkboxProps: <T extends boolean | null | undefined>(field: UseFieldReturn<T>) => CheckboxProps;
378
438
  declare const selectProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => SelectProps;
379
439
 
380
- export { type BoolMap, type BoundUseField, type BoundUseFieldArray, type BoundUseFlag, type BoundUseSelector, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormHooks, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type PathsOf, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, createFormHooks, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
440
+ export { type BoolMap, type BoundUseField, type BoundUseFieldArray, type BoundUseFlag, type BoundUseSelector, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, DateField, type DateFieldProps, type DateInputProps, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormHooks, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedDateText, type ParsedNumberText, type PathSegment, type PathsOf, type PersistHandle, type PersistOptions, type PersistStorage, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, useVariantField, validateAsync, validateSync };
package/dist/index.d.ts CHANGED
@@ -67,6 +67,7 @@ type CreateFormOptions<TSchema extends z.ZodType> = Readonly<{
67
67
  mode?: ValidationMode;
68
68
  reValidateMode?: ValidationMode;
69
69
  validateOnMount?: boolean;
70
+ devtools?: boolean | string;
70
71
  }>;
71
72
  type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => void | Promise<void>;
72
73
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
@@ -149,6 +150,24 @@ declare const parsePath: (path: string) => readonly PathSegment[];
149
150
  declare const getAtPath: (obj: unknown, path: string) => unknown;
150
151
  declare const setAtPath: <T>(obj: T, path: string, value: unknown) => T;
151
152
 
153
+ type PersistStorage = Readonly<{
154
+ getItem: (key: string) => string | null;
155
+ setItem: (key: string, value: string) => void;
156
+ removeItem: (key: string) => void;
157
+ }>;
158
+ type PersistOptions<TSchema extends z.ZodType> = Readonly<{
159
+ key: string;
160
+ storage?: PersistStorage;
161
+ debounceMs?: number;
162
+ apply?: "adopt" | "restore" | "manual";
163
+ }>;
164
+ type PersistHandle = Readonly<{
165
+ restore: () => boolean;
166
+ clear: () => void;
167
+ dispose: () => void;
168
+ }>;
169
+ declare const persistForm: <TSchema extends z.ZodType>(form: Form<TSchema>, options: PersistOptions<TSchema>) => PersistHandle;
170
+
152
171
  declare const useForm: <TSchema extends z.ZodType>(schema: TSchema, options: CreateFormOptions<TSchema>) => Form<TSchema>;
153
172
 
154
173
  type FormProviderProps<TSchema extends z.ZodType> = Readonly<{
@@ -198,6 +217,16 @@ declare function useField<TValue = unknown>(form: FieldFormApi & {
198
217
  }, path: string | ((state: FormState<unknown>) => string), options?: UseFieldOptions): UseFieldReturn<TValue>;
199
218
  declare function useField<TSchema extends z.ZodType, P extends FieldPath<z.input<TSchema>>>(form: Form<TSchema>, path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
200
219
 
220
+ type VariantFieldValue<V, TField extends string> = V extends unknown ? TField extends keyof V ? V[TField] : never : never;
221
+ type CommonKeys<V> = keyof V;
222
+ type AllKeys<V> = V extends unknown ? keyof V : never;
223
+ type VariantKeys<V> = Exclude<AllKeys<V>, CommonKeys<V>> & string;
224
+ type UnionValueAt<TValues, P extends string> = P extends keyof TValues ? NonNullable<TValues[P]> : P extends `${infer Head}.${infer Tail}` ? Head extends keyof TValues ? UnionValueAt<NonNullable<TValues[Head]>, Tail> : never : never;
225
+ declare function useVariantField<TSchema extends z.ZodType, P extends string, TField extends VariantKeys<UnionValueAt<z.input<TSchema>, P>>>(form: Form<TSchema>, unionPath: P, field: TField): UseFieldReturn<VariantFieldValue<UnionValueAt<z.input<TSchema>, P>, TField> | undefined>;
226
+ declare function useVariantField<TValue = unknown>(form: FieldFormApi & {
227
+ readonly schema?: undefined;
228
+ }, unionPath: string, field: string): UseFieldReturn<TValue | undefined>;
229
+
201
230
  type ReadonlyStore$1<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
202
231
  type FieldArrayFormApi = Readonly<{
203
232
  store: ReadonlyStore$1<FormState<unknown>>;
@@ -233,6 +262,7 @@ type BoundUseField<TSchema extends z.ZodType> = {
233
262
  (pathSelector: (state: FormState<z.input<TSchema>>) => string, options?: UseFieldOptions): UseFieldReturn<unknown>;
234
263
  <P extends FieldPath<z.input<TSchema>>>(path: P, options?: UseFieldOptions): UseFieldReturn<FieldValue<z.input<TSchema>, P>>;
235
264
  };
265
+ type BoundUseVariantField<TSchema extends z.ZodType> = <P extends string, TField extends VariantKeys<UnionValueAt<z.input<TSchema>, P>>>(unionPath: P, field: TField) => UseFieldReturn<VariantFieldValue<UnionValueAt<z.input<TSchema>, P>, TField> | undefined>;
236
266
  type BoundUseFieldArray<TSchema extends z.ZodType> = {
237
267
  (pathSelector: (state: FormState<z.input<TSchema>>) => string): UseFieldArrayReturn<unknown>;
238
268
  <P extends FieldPath<z.input<TSchema>>>(path: P): UseFieldArrayReturn<ArrayItemOf<FieldValue<z.input<TSchema>, P>>>;
@@ -241,6 +271,8 @@ type BoundUseSelector<TSchema extends z.ZodType> = <U>(selector: (state: FormSta
241
271
  type BoundUseFlag<TSchema extends z.ZodType> = (path?: FieldPath<z.input<TSchema>>) => boolean;
242
272
  type FormHooks<TSchema extends z.ZodType, N extends string> = Readonly<{
243
273
  [K in `use${Capitalize<N>}Field`]: BoundUseField<TSchema>;
274
+ } & {
275
+ [K in `use${Capitalize<N>}VariantField`]: BoundUseVariantField<TSchema>;
244
276
  } & {
245
277
  [K in `use${Capitalize<N>}FieldArray`]: BoundUseFieldArray<TSchema>;
246
278
  } & {
@@ -300,6 +332,15 @@ type TextFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
300
332
  ref?: FieldRef<HTMLInputElement>;
301
333
  }>;
302
334
  declare const TextField: <F extends FieldFormApi>({ form, path, label, placeholder, type, autoComplete, ref, }: TextFieldProps<F>) => react.JSX.Element;
335
+ type DateFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
336
+ form: F;
337
+ path: PathsOf<F>;
338
+ label?: ReactNode;
339
+ min?: string;
340
+ max?: string;
341
+ ref?: FieldRef<HTMLInputElement>;
342
+ }>;
343
+ declare const DateField: <F extends FieldFormApi>({ form, path, label, min, max, ref, }: DateFieldProps<F>) => react.JSX.Element;
303
344
  type NumberFieldProps<F extends FieldFormApi = FieldFormApi> = Readonly<{
304
345
  form: F;
305
346
  path: PathsOf<F>;
@@ -347,6 +388,14 @@ type NumberInputProps = Readonly<{
347
388
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
348
389
  onBlur: () => void;
349
390
  }>;
391
+ type DateInputProps = Readonly<{
392
+ type: "date";
393
+ name: string;
394
+ value: string;
395
+ "aria-invalid": true | undefined;
396
+ onChange: (e: ChangeEvent<HTMLInputElement>) => void;
397
+ onBlur: () => void;
398
+ }>;
350
399
  type CheckboxProps = Readonly<{
351
400
  type: "checkbox";
352
401
  name: string;
@@ -374,7 +423,18 @@ type ParsedNumberText = Readonly<{
374
423
  declare const parseNumberText: (text: string) => ParsedNumberText;
375
424
  declare const textInputProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => TextInputProps;
376
425
  declare const numberInputProps: <T extends number | null | undefined>(field: UseFieldReturn<T>) => NumberInputProps;
426
+ declare const dateToInputText: (value: Date | null | undefined) => string;
427
+ type ParsedDateText = Readonly<{
428
+ kind: "empty";
429
+ }> | Readonly<{
430
+ kind: "date";
431
+ value: Date;
432
+ }> | Readonly<{
433
+ kind: "invalid";
434
+ }>;
435
+ declare const parseDateText: (text: string) => ParsedDateText;
436
+ declare const dateInputProps: <T extends Date | null | undefined>(field: UseFieldReturn<T>) => DateInputProps;
377
437
  declare const checkboxProps: <T extends boolean | null | undefined>(field: UseFieldReturn<T>) => CheckboxProps;
378
438
  declare const selectProps: <T extends string | null | undefined>(field: UseFieldReturn<T>) => SelectProps;
379
439
 
380
- export { type BoolMap, type BoundUseField, type BoundUseFieldArray, type BoundUseFlag, type BoundUseSelector, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormHooks, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedNumberText, type PathSegment, type PathsOf, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, createFormHooks, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
440
+ export { type BoolMap, type BoundUseField, type BoundUseFieldArray, type BoundUseFlag, type BoundUseSelector, CheckboxField, type CheckboxFieldProps, type CheckboxProps, type CreateFormOptions, DateField, type DateFieldProps, type DateInputProps, type ErrorMap, type FieldArrayEntry, type FieldArrayFormApi, type FieldFormApi, type FieldPath, type FieldPathArg, type FieldRef, type FieldSnapshot, type FieldValidationResult, type FieldValue, type Form, type FormContextApi, type FormHooks, type FormProviderProps, type FormState, type FormStateApi, type InvalidSubmitHandler, NumberField, type NumberFieldProps, type NumberInputProps, type ParsedDateText, type ParsedNumberText, type PathSegment, type PathsOf, type PersistHandle, type PersistOptions, type PersistStorage, type ReadonlyStoreApi, type ResetOptions, SelectField, type SelectFieldOption, type SelectFieldProps, type SelectProps, type SettledFieldValidationResult, type SettledValidationResult, type SubmitHandler, type SubmitOptions, type SubmitResult, TextField, type TextFieldProps, type TextInputProps, type UseFieldArrayReturn, type UseFieldOptions, type UseFieldReturn, type ValidationMode, type ValidationResult, type ValidationTrigger, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, useVariantField, validateAsync, validateSync };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createStore } from 'zustand/vanilla';
2
+ import { devtools } from 'zustand/middleware';
2
3
  import { z } from 'zod';
3
4
  import { useRef, useEffect, createContext, useMemo, useCallback, useState, useId, useContext } from 'react';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -85,7 +86,18 @@ var isIndexSegment = (s) => {
85
86
  const n = Number(s);
86
87
  return Number.isInteger(n) && n >= 0 && String(n) === s;
87
88
  };
88
- var parsePath = (path) => path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s);
89
+ var PARSE_CACHE_MAX = 4096;
90
+ var parseCache = /* @__PURE__ */ new Map();
91
+ var parsePath = (path) => {
92
+ const cached = parseCache.get(path);
93
+ if (cached !== void 0) return cached;
94
+ const segments = Object.freeze(
95
+ path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
96
+ );
97
+ if (parseCache.size >= PARSE_CACHE_MAX) parseCache.clear();
98
+ parseCache.set(path, segments);
99
+ return segments;
100
+ };
89
101
  var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
90
102
  var readSegment = (obj, segment) => {
91
103
  if (obj == null) return void 0;
@@ -98,8 +110,9 @@ var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
98
110
  var slotAtPath = (obj, path) => {
99
111
  const walked = parsePath(path).reduce(
100
112
  (acc, segment) => {
101
- const outOfBounds = typeof segment === "number" && (Array.isArray(acc.current) ? segment >= acc.current.length : !isPlainRecord(acc.current));
102
- return !acc.ok || outOfBounds ? { ok: false, current: void 0 } : { ok: true, current: readSegment(acc.current, segment) };
113
+ const container = acc.current;
114
+ const addressable = typeof segment === "number" ? Array.isArray(container) ? segment < container.length : isPlainRecord(container) : isPlainRecord(container);
115
+ return !acc.ok || !addressable ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
103
116
  },
104
117
  { ok: true, current: obj }
105
118
  );
@@ -263,7 +276,7 @@ var releaseAncestorEntries = (map, path) => {
263
276
  return kept.length === entries.length ? map : Object.fromEntries(kept);
264
277
  };
265
278
  var dirtyPathsOf = (values, initial, prefix) => {
266
- if (valuesEqual(values, initial)) return [];
279
+ if (Object.is(values, initial)) return [];
267
280
  if (isPlainObject(values) && isPlainObject(initial)) {
268
281
  const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
269
282
  const childPaths = [...keys].flatMap(
@@ -273,9 +286,11 @@ var dirtyPathsOf = (values, initial, prefix) => {
273
286
  prefix === "" ? k : `${prefix}.${k}`
274
287
  )
275
288
  );
276
- return childPaths.length > 0 ? childPaths : [prefix];
289
+ if (childPaths.length > 0) return childPaths;
290
+ const sameKeySet = Object.keys(values).length === Object.keys(initial).length && Object.keys(values).every((k) => Object.hasOwn(initial, k));
291
+ return sameKeySet ? [] : [prefix];
277
292
  }
278
- return [prefix];
293
+ return valuesEqual(values, initial) ? [] : [prefix];
279
294
  };
280
295
  var FORM_SEQ_KEY = "__form__";
281
296
  var SUBMIT_SEQ_KEY = "__submit__";
@@ -337,7 +352,18 @@ var createForm = (schema, options) => {
337
352
  mode: initialMode,
338
353
  reValidateMode: initialReValidateMode
339
354
  };
340
- const store = createStore(() => initial);
355
+ const store = options.devtools === void 0 || options.devtools === false ? createStore(() => initial) : createStore()(
356
+ devtools(() => initial, {
357
+ name: typeof options.devtools === "string" ? options.devtools : "formstand",
358
+ // Non-production only, matching zustand's own devtools default:
359
+ // opting in during development must NOT stream every keystroke to
360
+ // an end user who happens to have the extension installed, where
361
+ // a module-singleton form's unbounded time-travel history is a
362
+ // real memory + serialization cost. Guarded for environments
363
+ // without process (a bundler defines NODE_ENV in real dev).
364
+ enabled: typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production"
365
+ })
366
+ );
341
367
  const commitFullPass = (state, next) => errorChannels(state, {
342
368
  schemaErrors: reuseErrorRefs(next, state.schemaErrors)
343
369
  });
@@ -366,7 +392,38 @@ var createForm = (schema, options) => {
366
392
  return result.kind === "pending" ? result.promise.then((r) => r.kind === "valid") : result.kind === "valid";
367
393
  }
368
394
  try {
369
- const result = validateSync(schema, store.getState().values);
395
+ const values = store.getState().values;
396
+ const scopes = paths.map((path) => ({
397
+ path,
398
+ scope: fieldScopeFor(path, values)
399
+ }));
400
+ if (scopes.every(
401
+ ({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
402
+ )) {
403
+ const merged = scopes.reduce(
404
+ (acc, { path, scope }) => scope.kind === "skip" ? acc : {
405
+ ...acc,
406
+ ...scopeSettledResult(
407
+ validateSync(scope.schema, scope.value),
408
+ path,
409
+ true
410
+ )
411
+ },
412
+ {}
413
+ );
414
+ store.setState((state) => ({
415
+ ...state,
416
+ ...errorChannels(state, {
417
+ schemaErrors: reuseErrorRefs(
418
+ mergeScopedErrors(state.schemaErrors, merged, paths),
419
+ state.schemaErrors
420
+ ),
421
+ serverErrors: omitScope(state.serverErrors, paths)
422
+ })
423
+ }));
424
+ return Object.values(merged).flat().length === 0;
425
+ }
426
+ const result = validateSync(schema, values);
370
427
  const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
371
428
  store.setState((state) => ({
372
429
  ...state,
@@ -966,6 +1023,74 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
966
1023
  return true;
967
1024
  }
968
1025
  };
1026
+
1027
+ // src/core/persist.ts
1028
+ var defaultStorage = () => {
1029
+ try {
1030
+ return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage;
1031
+ } catch {
1032
+ return null;
1033
+ }
1034
+ };
1035
+ var persistForm = (form, options) => {
1036
+ const storage = options.storage ?? defaultStorage();
1037
+ const debounceMs = options.debounceMs ?? 300;
1038
+ const apply = options.apply ?? "adopt";
1039
+ const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1040
+ const restore = () => {
1041
+ try {
1042
+ const raw = storage === null ? null : storage.getItem(options.key);
1043
+ if (raw === null) return false;
1044
+ applyDraft(JSON.parse(raw));
1045
+ return true;
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ };
1050
+ const writeDraft = (values) => {
1051
+ try {
1052
+ storage?.setItem(options.key, JSON.stringify(values));
1053
+ } catch {
1054
+ }
1055
+ };
1056
+ const timerRef = {
1057
+ current: null
1058
+ };
1059
+ const cancelPending = () => {
1060
+ if (timerRef.current !== null) {
1061
+ clearTimeout(timerRef.current);
1062
+ timerRef.current = null;
1063
+ }
1064
+ };
1065
+ if (apply !== "manual") {
1066
+ restore();
1067
+ }
1068
+ const unsubscribe = form.watchValues((values) => {
1069
+ cancelPending();
1070
+ if (debounceMs === 0) {
1071
+ writeDraft(values);
1072
+ } else {
1073
+ timerRef.current = setTimeout(() => {
1074
+ timerRef.current = null;
1075
+ writeDraft(values);
1076
+ }, debounceMs);
1077
+ }
1078
+ });
1079
+ return Object.freeze({
1080
+ restore,
1081
+ clear: () => {
1082
+ cancelPending();
1083
+ try {
1084
+ storage?.removeItem(options.key);
1085
+ } catch {
1086
+ }
1087
+ },
1088
+ dispose: () => {
1089
+ cancelPending();
1090
+ unsubscribe();
1091
+ }
1092
+ });
1093
+ };
969
1094
  var useForm = (schema, options) => {
970
1095
  const formRef = useRef(null);
971
1096
  if (formRef.current === null) {
@@ -1128,6 +1253,14 @@ function useField(form, pathArg, options) {
1128
1253
  [slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
1129
1254
  );
1130
1255
  }
1256
+
1257
+ // src/react/useVariantField.ts
1258
+ function useVariantField(form, unionPath, field) {
1259
+ return useField(
1260
+ form,
1261
+ `${unionPath}.${field}`
1262
+ );
1263
+ }
1131
1264
  var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1132
1265
  var EMPTY_ITEMS = [];
1133
1266
  var reconcileIds = (prev, nextItems) => {
@@ -1269,6 +1402,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
1269
1402
  var createFormHooks = (form, name) => {
1270
1403
  const structural = form;
1271
1404
  const useBoundField = (path, options) => useField(structural, path, options);
1405
+ const useBoundVariantField = (unionPath, field) => useVariantField(
1406
+ structural,
1407
+ unionPath,
1408
+ field
1409
+ );
1272
1410
  const useBoundFieldArray = (path) => useFieldArray(structural, path);
1273
1411
  const useBoundSelector = (selector) => useFormSelector(structural, selector);
1274
1412
  const useBoundSelectorShallow = (selector) => useFormSelectorShallow(structural, selector);
@@ -1280,6 +1418,7 @@ var createFormHooks = (form, name) => {
1280
1418
  const prefix = capitalize(name ?? "");
1281
1419
  return {
1282
1420
  [`use${prefix}Field`]: useBoundField,
1421
+ [`use${prefix}VariantField`]: useBoundVariantField,
1283
1422
  [`use${prefix}FieldArray`]: useBoundFieldArray,
1284
1423
  [`use${prefix}Selector`]: useBoundSelector,
1285
1424
  [`use${prefix}SelectorShallow`]: useBoundSelectorShallow,
@@ -1324,6 +1463,48 @@ var numberInputProps = (field) => ({
1324
1463
  },
1325
1464
  onBlur: field.onBlur
1326
1465
  });
1466
+ var dateToInputText = (value) => {
1467
+ if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
1468
+ return "";
1469
+ }
1470
+ const pad = (part) => String(part).padStart(2, "0");
1471
+ return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
1472
+ };
1473
+ var parseDateText = (text) => {
1474
+ if (text.trim() === "") return { kind: "empty" };
1475
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1476
+ if (match === null) return { kind: "invalid" };
1477
+ const [, year, month, day] = match;
1478
+ const y = Number(year);
1479
+ const date = new Date(y, Number(month) - 1, Number(day));
1480
+ if (y < 100) date.setFullYear(y);
1481
+ return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
1482
+ };
1483
+ var dateInputProps = (field) => ({
1484
+ type: "date",
1485
+ name: field.path,
1486
+ value: dateToInputText(field.value),
1487
+ "aria-invalid": ariaInvalid(field),
1488
+ onChange: (e) => {
1489
+ const parsed = parseDateText(e.target.value);
1490
+ if (parsed.kind !== "date") {
1491
+ field.setValue(field.emptyValue);
1492
+ return;
1493
+ }
1494
+ const current = field.value;
1495
+ const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
1496
+ parsed.value.getFullYear(),
1497
+ parsed.value.getMonth(),
1498
+ parsed.value.getDate(),
1499
+ current.getHours(),
1500
+ current.getMinutes(),
1501
+ current.getSeconds(),
1502
+ current.getMilliseconds()
1503
+ ) : parsed.value;
1504
+ field.setValue(next);
1505
+ },
1506
+ onBlur: field.onBlur
1507
+ });
1327
1508
  var checkboxProps = (field) => ({
1328
1509
  type: "checkbox",
1329
1510
  name: field.path,
@@ -1378,6 +1559,33 @@ var TextField = ({
1378
1559
  /* @__PURE__ */ jsx(ErrorText, { id: errorId, error: field.error })
1379
1560
  ] });
1380
1561
  };
1562
+ var DateField = ({
1563
+ form,
1564
+ path,
1565
+ label,
1566
+ min,
1567
+ max,
1568
+ ref
1569
+ }) => {
1570
+ const id = useId();
1571
+ const errorId = `${id}-error`;
1572
+ const field = useField(form, path);
1573
+ return /* @__PURE__ */ jsxs("div", { className: "zf-field", children: [
1574
+ label !== void 0 ? /* @__PURE__ */ jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
1575
+ /* @__PURE__ */ jsx(
1576
+ "input",
1577
+ {
1578
+ id,
1579
+ ref,
1580
+ ...dateInputProps(field),
1581
+ min,
1582
+ max,
1583
+ "aria-describedby": hasError(field.error) ? errorId : void 0
1584
+ }
1585
+ ),
1586
+ /* @__PURE__ */ jsx(ErrorText, { id: errorId, error: field.error })
1587
+ ] });
1588
+ };
1381
1589
  var IDLE_EDIT = { raw: null, pushed: void 0 };
1382
1590
  var useNumberInput = (field) => {
1383
1591
  const [edit, setEdit] = useState(IDLE_EDIT);
@@ -1507,13 +1715,12 @@ var SelectField = ({
1507
1715
  };
1508
1716
 
1509
1717
  // src/react/focusError.ts
1510
- var focusableControls = (scope) => [
1718
+ var namedControls = (scope) => [
1511
1719
  ...scope.querySelectorAll(
1512
1720
  "input[name], select[name], textarea[name]"
1513
1721
  )
1514
- ].filter(
1515
- (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null
1516
- );
1722
+ ];
1723
+ var isFocusCandidate = (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null;
1517
1724
  var holdsFocus = (el) => {
1518
1725
  const rootNode = el.getRootNode();
1519
1726
  const active = rootNode instanceof Document || rootNode instanceof ShadowRoot ? rootNode.activeElement : document.activeElement;
@@ -1533,19 +1740,19 @@ var focusFirstError = (errors, root) => {
1533
1740
  (k) => k !== "" && (errors[k]?.length ?? 0) > 0
1534
1741
  );
1535
1742
  const hasRootError = (errors[""]?.length ?? 0) > 0;
1536
- const controls = focusableControls(scope);
1537
- const fieldMatches = controls.filter(
1538
- (el) => nameMatchesAny(el, erroredPaths)
1539
- );
1743
+ const controls = namedControls(scope);
1744
+ const fieldMatches = controls.filter((el) => nameMatchesAny(el, erroredPaths)).filter(isFocusCandidate);
1540
1745
  const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
1541
- const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
1746
+ const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls.filter(isFocusCandidate) : [];
1542
1747
  return focusFirstOf(candidates);
1543
1748
  };
1544
1749
  var focusField = (path, root) => {
1545
- const controls = focusableControls(root ?? document);
1546
- return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
1750
+ const controls = namedControls(root ?? document);
1751
+ return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls.filter(isFocusCandidate)) : focusFirstOf(
1752
+ controls.filter((el) => nameMatchesAny(el, [path])).filter(isFocusCandidate)
1753
+ );
1547
1754
  };
1548
1755
 
1549
- export { CheckboxField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseNumberText, parsePath, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, validateAsync, validateSync };
1756
+ export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, isAsyncRequiredError, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, shouldValidateOn, textInputProps, useField, useFieldArray, useForm, useFormError, useFormSelector, useFormSelectorShallow, useIsDirty, useIsSubmitting, useIsValid, useSubmitCount, useVariantField, validateAsync, validateSync };
1550
1757
  //# sourceMappingURL=index.js.map
1551
1758
  //# sourceMappingURL=index.js.map