@zeno-lib/forms 0.2.0 → 0.2.1

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.
Files changed (41) hide show
  1. package/dist/create-zeno-form.d.mts +118 -0
  2. package/dist/create-zeno-form.mjs +121 -0
  3. package/dist/form-element.d.mts +13 -0
  4. package/dist/form-element.mjs +31 -0
  5. package/dist/index.d.mts +11 -0
  6. package/dist/index.mjs +10 -0
  7. package/dist/lib/action-result.d.mts +22 -0
  8. package/dist/lib/action-result.mjs +32 -0
  9. package/dist/lib/apply-validation-error.d.mts +6 -0
  10. package/dist/lib/apply-validation-error.mjs +41 -0
  11. package/dist/lib/aria.d.mts +4 -0
  12. package/dist/lib/aria.mjs +7 -0
  13. package/dist/lib/contexts.d.mts +5 -0
  14. package/dist/lib/contexts.mjs +7 -0
  15. package/dist/lib/formatted-number.d.mts +56 -0
  16. package/dist/lib/formatted-number.mjs +138 -0
  17. package/dist/lib/schema-defaults.d.mts +17 -0
  18. package/dist/lib/schema-defaults.mjs +75 -0
  19. package/dist/lib/schema-required.d.mts +25 -0
  20. package/dist/lib/schema-required.mjs +72 -0
  21. package/dist/lib/submit-action.d.mts +64 -0
  22. package/dist/lib/submit-action.mjs +43 -0
  23. package/dist/lib/use-form-dialog.d.mts +73 -0
  24. package/dist/lib/use-form-dialog.mjs +90 -0
  25. package/dist/lib/use-formatted-number.d.mts +61 -0
  26. package/dist/lib/use-formatted-number.mjs +104 -0
  27. package/dist/lib/use-is-invalid.d.mts +7 -0
  28. package/dist/lib/use-is-invalid.mjs +31 -0
  29. package/dist/lib/use-rebased-default-values.d.mts +10 -0
  30. package/dist/lib/use-rebased-default-values.mjs +21 -0
  31. package/dist/lib/use-unsaved-changes-warning.d.mts +6 -0
  32. package/dist/lib/use-unsaved-changes-warning.mjs +24 -0
  33. package/dist/lib/validation-error.d.mts +12 -0
  34. package/dist/lib/validation-error.mjs +11 -0
  35. package/dist/lib/validation-logic.d.mts +5 -0
  36. package/dist/lib/validation-logic.mjs +114 -0
  37. package/dist/lib/validation-modes.d.mts +16 -0
  38. package/dist/lib/validation-modes.mjs +38 -0
  39. package/dist/tanstack.d.mts +1 -0
  40. package/dist/tanstack.mjs +2 -0
  41. package/package.json +23 -7
@@ -0,0 +1,118 @@
1
+ import { ValidationMode } from "./lib/validation-modes.mjs";
2
+ import { DeepKeys, FormAsyncValidateOrFn, FormOptions, FormValidateOrFn } from "@tanstack/react-form";
3
+ import { ComponentProps, ReactNode } from "react";
4
+ //#region src/create-zeno-form.d.ts
5
+ type StandardSchema<T> = {
6
+ readonly "~standard": {
7
+ readonly validate: (value: unknown) => unknown;
8
+ readonly types?: {
9
+ readonly input: T;
10
+ readonly output: T;
11
+ };
12
+ };
13
+ };
14
+ type PartialFormData<T> = T extends readonly unknown[] ? T : T extends Date | RegExp | ((...args: never[]) => unknown) ? T : T extends object ? { [K in keyof T]?: PartialFormData<T[K]>; } : T;
15
+ type ZenoFormExtras<TFormData> = {
16
+ /**
17
+ * If `true`, shipped fields skip rendering their inline `<FieldError>`
18
+ * message. Fields still flip `data-invalid` / `aria-invalid`.
19
+ */
20
+ hideFieldErrors?: boolean;
21
+ /**
22
+ * Show a `*` next to the label of every field the schema treats as required.
23
+ * Defaults to `true`. Required-ness is detected by probing the schema.
24
+ */
25
+ requiredIndicator?: boolean;
26
+ /**
27
+ * Warn the user before they navigate away with unsaved changes.
28
+ * `"if-changed"` (or `true`) warns while values differ from defaults;
29
+ * `"if-touched"` warns after any edit.
30
+ */
31
+ unsavedChangesWarning?: boolean | "if-changed" | "if-touched";
32
+ /**
33
+ * Initial values, relaxed to a deep partial so schema-provided defaults can
34
+ * be omitted. See `lib/schema-defaults.ts`.
35
+ */
36
+ defaultValues?: PartialFormData<TFormData>;
37
+ };
38
+ type SchemaPathExtras<TFormData> = {
39
+ schema: StandardSchema<TFormData>;
40
+ validators?: ValidationMode;
41
+ validationLogic?: never;
42
+ };
43
+ type ManualPathExtras = {
44
+ schema?: never;
45
+ };
46
+ type SchemaFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues" | "validators" | "validationLogic"> & SchemaPathExtras<TFormData> & ZenoFormExtras<TFormData>;
47
+ type ManualFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues"> & ManualPathExtras & ZenoFormExtras<TFormData>;
48
+ type UseFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = SchemaFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> | ManualFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>;
49
+ type FormDataOf<F> = F extends {
50
+ state: {
51
+ values: infer V;
52
+ };
53
+ } ? V : never;
54
+ type AnyAppForm = {
55
+ AppField: unknown;
56
+ state: {
57
+ values: unknown;
58
+ };
59
+ };
60
+ type AnyFieldValidators = {
61
+ onMount?: unknown;
62
+ onChange?: unknown;
63
+ onChangeAsync?: unknown;
64
+ onChangeAsyncDebounceMs?: number;
65
+ onBlur?: unknown;
66
+ onBlurAsync?: unknown;
67
+ onBlurAsyncDebounceMs?: number;
68
+ onSubmit?: unknown;
69
+ onSubmitAsync?: unknown;
70
+ onSubmitAsyncDebounceMs?: number;
71
+ onDynamic?: unknown;
72
+ onDynamicAsync?: unknown;
73
+ };
74
+ type AnyFieldListeners = {
75
+ onChange?: unknown;
76
+ onChangeDebounceMs?: number;
77
+ onBlur?: unknown;
78
+ onBlurDebounceMs?: number;
79
+ onMount?: unknown;
80
+ onSubmit?: unknown;
81
+ };
82
+ type WithValidators = {
83
+ validators?: AnyFieldValidators;
84
+ listeners?: AnyFieldListeners;
85
+ };
86
+ type AnyComponent = React.ComponentType<any>;
87
+ type FieldWrappers<FC extends Record<string, AnyComponent>, T> = { [K in keyof FC]: <N extends DeepKeys<T>>(props: ComponentProps<FC[K]> & {
88
+ name: N;
89
+ } & WithValidators) => ReactNode; };
90
+ type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>> = {
91
+ fieldComponents: FC;
92
+ formComponents: FMC;
93
+ };
94
+ /**
95
+ * Build the wired Zeno form API from the caller's field + form components.
96
+ *
97
+ * The shadcn-style field components are dropped into the consumer's repo via the
98
+ * registry and injected here, so `@zeno-lib/forms` (npm) stays UI-free. Returns
99
+ * the `createFormHook` primitives plus the schema-aware `useForm` and the typed
100
+ * `useAppFields` wrappers (prop types are inferred from the injected components).
101
+ */
102
+ declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
103
+ useAppFields: <TForm extends AnyAppForm>(form: TForm) => FieldWrappers<FC, FormDataOf<TForm>>;
104
+ useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
105
+ useForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TSubmitMeta = never>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
106
+ AppField: import("@tanstack/react-form").FieldComponent<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, NoInfer<FC>>;
107
+ AppForm: import("react").ComponentType<import("react").PropsWithChildren<{}>>;
108
+ } & FieldWrappers<FC, FormDataOf<import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>>> & FMC;
109
+ withFieldGroup: <TFieldGroupData, TSubmitMeta, TRenderProps extends object = {}>({ render, props, defaultValues }: import("@tanstack/react-form").WithFieldGroupProps<TFieldGroupData, FC, FMC, TSubmitMeta, TRenderProps>) => <TFormData, TFields extends import("@tanstack/react-form").DeepKeysOfType<TFormData, TFieldGroupData | null | undefined> | import("@tanstack/react-form").FieldsMap<TFormData, TFieldGroupData>, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TFormSubmitMeta>(params: import("react").PropsWithChildren<NoInfer<TRenderProps> & {
110
+ form: import("@tanstack/react-form").AppFieldExtendedReactFieldGroupApi<unknown, TFormData, string | import("@tanstack/react-form").FieldsMap<unknown, TFormData>, any, any, any, any, any, any, any, any, any, any, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC> | import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, FC, FMC>;
111
+ fields: TFields;
112
+ }>) => ReturnType<import("react").FunctionComponent>;
113
+ withForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta, TRenderProps extends object = {}>({ render, props }: import("@tanstack/react-form").WithFormProps<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC, TRenderProps>) => import("react").FunctionComponent<import("react").PropsWithChildren<NoInfer<[unknown] extends [TRenderProps] ? any : TRenderProps> & {
114
+ form: import("@tanstack/react-form").AppFieldExtendedReactFormApi<[unknown] extends [TFormData] ? any : TFormData, [FormValidateOrFn<TFormData> | undefined] extends [TOnMount] ? [TOnMount] extends [TOnMount & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnMount : TOnMount, [FormValidateOrFn<TFormData> | undefined] extends [TOnChange] ? [TOnChange] extends [TOnChange & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChange : TOnChange, [FormValidateOrFn<TFormData> | undefined] extends [TOnChangeAsync] ? [TOnChangeAsync] extends [TOnChangeAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnChangeAsync : TOnChangeAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlur] ? [TOnBlur] extends [TOnBlur & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlur : TOnBlur, [FormValidateOrFn<TFormData> | undefined] extends [TOnBlurAsync] ? [TOnBlurAsync] extends [TOnBlurAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnBlurAsync : TOnBlurAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmit] ? [TOnSubmit] extends [TOnSubmit & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmit : TOnSubmit, [FormValidateOrFn<TFormData> | undefined] extends [TOnSubmitAsync] ? [TOnSubmitAsync] extends [TOnSubmitAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnSubmitAsync : TOnSubmitAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamic] ? [TOnDynamic] extends [TOnDynamic & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamic : TOnDynamic, [FormValidateOrFn<TFormData> | undefined] extends [TOnDynamicAsync] ? [TOnDynamicAsync] extends [TOnDynamicAsync & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnDynamicAsync : TOnDynamicAsync, [FormValidateOrFn<TFormData> | undefined] extends [TOnServer] ? [TOnServer] extends [TOnServer & (FormValidateOrFn<TFormData> | undefined)] ? any : TOnServer : TOnServer, [unknown] extends [TSubmitMeta] ? any : TSubmitMeta, [unknown] extends [FC] ? any : FC, [unknown] extends [FMC] ? any : FMC>;
115
+ }>>;
116
+ };
117
+ //#endregion
118
+ export { createZenoForm };
@@ -0,0 +1,121 @@
1
+ "use client";
2
+ import { applyValidationError } from "./lib/apply-validation-error.mjs";
3
+ import { fieldContext, formContext } from "./lib/contexts.mjs";
4
+ import { deepMergeDefaults, extractZodDefaults } from "./lib/schema-defaults.mjs";
5
+ import { getRequiredPaths } from "./lib/schema-required.mjs";
6
+ import { useRebasedDefaultValues } from "./lib/use-rebased-default-values.mjs";
7
+ import { useUnsavedChangesWarning } from "./lib/use-unsaved-changes-warning.mjs";
8
+ import { ValidationError } from "./lib/validation-error.mjs";
9
+ import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
10
+ import { DEFAULT_VALIDATION_MODE, setFormZenoState } from "./lib/validation-modes.mjs";
11
+ import { createFormHook } from "@tanstack/react-form";
12
+ import { useMemo } from "react";
13
+ import { jsx } from "react/jsx-runtime";
14
+ //#region src/create-zeno-form.tsx
15
+ function buildValidatorsFromSchema(schema, mode) {
16
+ switch (mode) {
17
+ case "blur": return { onBlur: schema };
18
+ case "submit": return { onSubmit: schema };
19
+ default: return { onChange: schema };
20
+ }
21
+ }
22
+ function withMountListener(listeners, onMount) {
23
+ const user = listeners?.onMount;
24
+ return {
25
+ ...listeners,
26
+ onMount: (props) => {
27
+ onMount(props);
28
+ user?.(props);
29
+ }
30
+ };
31
+ }
32
+ /**
33
+ * Build the wired Zeno form API from the caller's field + form components.
34
+ *
35
+ * The shadcn-style field components are dropped into the consumer's repo via the
36
+ * registry and injected here, so `@zeno-lib/forms` (npm) stays UI-free. Returns
37
+ * the `createFormHook` primitives plus the schema-aware `useForm` and the typed
38
+ * `useAppFields` wrappers (prop types are inferred from the injected components).
39
+ */
40
+ function createZenoForm(config) {
41
+ const { useAppForm, withFieldGroup, withForm } = createFormHook({
42
+ fieldComponents: config.fieldComponents,
43
+ fieldContext,
44
+ formComponents: config.formComponents,
45
+ formContext
46
+ });
47
+ function useAppFields(form) {
48
+ return useMemo(() => {
49
+ const Field = form.AppField;
50
+ const wrappers = {};
51
+ for (const key of Object.keys(config.fieldComponents)) {
52
+ const Impl = config.fieldComponents[key];
53
+ wrappers[key] = ({ name, validators, listeners, ...props }) => /* @__PURE__ */ jsx(Field, {
54
+ listeners,
55
+ name,
56
+ validators,
57
+ children: () => /* @__PURE__ */ jsx(Impl, { ...props })
58
+ });
59
+ }
60
+ return wrappers;
61
+ }, [form]);
62
+ }
63
+ function useForm(options) {
64
+ const { schema, validators: validatorsInput, validationLogic: userValidationLogic, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
65
+ let schemaMode;
66
+ let resolvedValidators;
67
+ let resolvedValidationLogic;
68
+ if (schema === void 0) {
69
+ schemaMode = void 0;
70
+ resolvedValidators = typeof validatorsInput === "object" && validatorsInput !== null ? validatorsInput : void 0;
71
+ resolvedValidationLogic = userValidationLogic;
72
+ } else {
73
+ const mode = typeof validatorsInput === "string" ? validatorsInput : DEFAULT_VALIDATION_MODE;
74
+ schemaMode = mode;
75
+ resolvedValidators = buildValidatorsFromSchema(schema, mode);
76
+ resolvedValidationLogic = mode === "blur-then-change" ? blurThenChangeLogic : void 0;
77
+ }
78
+ const wrappedOnSubmit = userOnSubmit ? (async (props) => {
79
+ try {
80
+ await userOnSubmit(props);
81
+ } catch (error) {
82
+ if (error instanceof ValidationError) {
83
+ applyValidationError(props.formApi, error);
84
+ return;
85
+ }
86
+ throw error;
87
+ }
88
+ }) : void 0;
89
+ const requiredFields = useMemo(() => schema && requiredIndicator ? getRequiredPaths(schema) : /* @__PURE__ */ new Set(), [schema, requiredIndicator]);
90
+ const schemaDefaults = useMemo(() => schema ? extractZodDefaults(schema) : void 0, [schema]);
91
+ const mergedDefaultValues = useMemo(() => schemaDefaults ? deepMergeDefaults(schemaDefaults, userDefaultValues) : userDefaultValues, [schemaDefaults, userDefaultValues]);
92
+ const rebased = useRebasedDefaultValues(mergedDefaultValues);
93
+ const listeners = withMountListener(rest.listeners, rebased.onMount);
94
+ const form = useAppForm({
95
+ ...rest,
96
+ listeners,
97
+ ...rebased.defaultValues === void 0 ? {} : { defaultValues: rebased.defaultValues },
98
+ ...wrappedOnSubmit ? { onSubmit: wrappedOnSubmit } : {},
99
+ ...resolvedValidators ? { validators: resolvedValidators } : {},
100
+ ...resolvedValidationLogic ? { validationLogic: resolvedValidationLogic } : {}
101
+ });
102
+ setFormZenoState(form, {
103
+ hideFieldErrors,
104
+ requiredFields,
105
+ requiredIndicator,
106
+ ...schemaMode === void 0 ? {} : { validation: schemaMode }
107
+ });
108
+ useUnsavedChangesWarning(form, unsavedChangesWarning);
109
+ const fields = useAppFields(form);
110
+ return useMemo(() => Object.assign(form, fields, config.formComponents), [form, fields]);
111
+ }
112
+ return {
113
+ useAppFields,
114
+ useAppForm,
115
+ useForm,
116
+ withFieldGroup,
117
+ withForm
118
+ };
119
+ }
120
+ //#endregion
121
+ export { createZenoForm };
@@ -0,0 +1,13 @@
1
+ import { ComponentProps, ReactNode } from "react";
2
+ //#region src/form-element.d.ts
3
+ type FormProviderProps = {
4
+ children: ReactNode;
5
+ form: {
6
+ handleSubmit: () => unknown;
7
+ };
8
+ };
9
+ declare function FormProvider({ children, form }: FormProviderProps): import("react/jsx-runtime").JSX.Element;
10
+ type FormProps = Omit<ComponentProps<"form">, "onSubmit">;
11
+ declare function Form({ children, className, ...props }: FormProps): import("react/jsx-runtime").JSX.Element;
12
+ //#endregion
13
+ export { Form, FormProvider };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { FormProvider as FormProvider$1, useFormContext } from "./lib/contexts.mjs";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/form-element.tsx
5
+ function FormProvider({ children, form }) {
6
+ return /* @__PURE__ */ jsx(FormProvider$1, {
7
+ value: form,
8
+ children
9
+ });
10
+ }
11
+ function Form({ children, className, ...props }) {
12
+ const form = useFormContext();
13
+ return /* @__PURE__ */ jsx("form", {
14
+ className,
15
+ noValidate: true,
16
+ onSubmit: async (event) => {
17
+ event.preventDefault();
18
+ event.stopPropagation();
19
+ const node = event.currentTarget;
20
+ await Promise.resolve(form.handleSubmit()).catch(() => void 0);
21
+ if (!form.state.isValid) {
22
+ const invalid = node.querySelector("[aria-invalid=\"true\"]");
23
+ (invalid ? [invalid, ...invalid.querySelectorAll("*")] : []).find((el) => el.tabIndex >= 0)?.focus();
24
+ }
25
+ },
26
+ ...props,
27
+ children
28
+ });
29
+ }
30
+ //#endregion
31
+ export { Form, FormProvider };
@@ -0,0 +1,11 @@
1
+ import { ValidationMode } from "./lib/validation-modes.mjs";
2
+ import { createZenoForm } from "./create-zeno-form.mjs";
3
+ import { Form, FormProvider } from "./form-element.mjs";
4
+ import { ActionError, ActionIssue, ActionResult, toActionError, toFieldName } from "./lib/action-result.mjs";
5
+ import { FieldMessage, ValidationError } from "./lib/validation-error.mjs";
6
+ import { applyValidationError } from "./lib/apply-validation-error.mjs";
7
+ import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
8
+ import { SubmitActionOptions, SubmitActionSchema, applyActionError, submitAction } from "./lib/submit-action.mjs";
9
+ import { useHideFieldErrors, useIsFieldRequired, useIsInvalid } from "./lib/use-is-invalid.mjs";
10
+ import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
11
+ export { type ActionError, type ActionIssue, type ActionResult, type FieldMessage, Form, FormProvider, type SubmitActionOptions, type SubmitActionSchema, ValidationError, type ValidationMode, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsInvalid };
package/dist/index.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import { applyValidationError } from "./lib/apply-validation-error.mjs";
2
+ import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
3
+ import { ValidationError } from "./lib/validation-error.mjs";
4
+ import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
5
+ import { createZenoForm } from "./create-zeno-form.mjs";
6
+ import { Form, FormProvider } from "./form-element.mjs";
7
+ import { toActionError, toFieldName } from "./lib/action-result.mjs";
8
+ import { applyActionError, submitAction } from "./lib/submit-action.mjs";
9
+ import { useHideFieldErrors, useIsFieldRequired, useIsInvalid } from "./lib/use-is-invalid.mjs";
10
+ export { Form, FormProvider, ValidationError, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsInvalid };
@@ -0,0 +1,22 @@
1
+ //#region src/lib/action-result.d.ts
2
+ type ActionError = {
3
+ readonly fieldErrors: Readonly<Record<string, readonly string[]>>;
4
+ readonly formErrors: readonly string[];
5
+ };
6
+ type ActionResult<TData> = {
7
+ readonly ok: true;
8
+ readonly data: TData;
9
+ } | {
10
+ readonly ok: false;
11
+ readonly error: ActionError;
12
+ };
13
+ type ActionIssue = {
14
+ readonly message: string;
15
+ readonly path?: ReadonlyArray<PropertyKey | {
16
+ readonly key: PropertyKey;
17
+ }> | undefined;
18
+ };
19
+ declare function toFieldName(path: ActionIssue["path"]): string;
20
+ declare function toActionError(issues: readonly ActionIssue[]): ActionError;
21
+ //#endregion
22
+ export { type ActionError, type ActionIssue, type ActionResult, toActionError, toFieldName };
@@ -0,0 +1,32 @@
1
+ //#region src/lib/action-result.ts
2
+ function toFieldName(path) {
3
+ let name = "";
4
+ for (const segment of path ?? []) {
5
+ const key = typeof segment === "object" ? segment.key : segment;
6
+ if (typeof key === "number") name += `[${key}]`;
7
+ else {
8
+ const part = typeof key === "symbol" ? key.description ?? "" : key;
9
+ name += name === "" ? part : `.${part}`;
10
+ }
11
+ }
12
+ return name;
13
+ }
14
+ function toActionError(issues) {
15
+ const fieldErrors = {};
16
+ const formErrors = [];
17
+ for (const issue of issues) {
18
+ const name = toFieldName(issue.path);
19
+ if (name === "") formErrors.push(issue.message);
20
+ else {
21
+ const messages = fieldErrors[name] ?? [];
22
+ messages.push(issue.message);
23
+ fieldErrors[name] = messages;
24
+ }
25
+ }
26
+ return {
27
+ fieldErrors,
28
+ formErrors
29
+ };
30
+ }
31
+ //#endregion
32
+ export { toActionError, toFieldName };
@@ -0,0 +1,6 @@
1
+ import { ValidationError } from "./validation-error.mjs";
2
+ import { AnyFormApi } from "@tanstack/react-form";
3
+ //#region src/lib/apply-validation-error.d.ts
4
+ declare function applyValidationError(formApi: AnyFormApi, error: ValidationError): void;
5
+ //#endregion
6
+ export { applyValidationError };
@@ -0,0 +1,41 @@
1
+ //#region src/lib/apply-validation-error.ts
2
+ function clearWhenEdited(formApi, name, entry) {
3
+ const rejected = formApi.getFieldValue(name);
4
+ const subscription = formApi.store.subscribe(() => {
5
+ if (formApi.getFieldMeta(name)?.errorMap?.onChange !== entry) {
6
+ subscription.unsubscribe();
7
+ return;
8
+ }
9
+ if (Object.is(formApi.getFieldValue(name), rejected)) return;
10
+ subscription.unsubscribe();
11
+ formApi.setFieldMeta(name, (prev) => ({
12
+ ...prev,
13
+ errorMap: {
14
+ ...prev.errorMap,
15
+ onChange: void 0
16
+ }
17
+ }));
18
+ });
19
+ }
20
+ function applyValidationError(formApi, error) {
21
+ for (const [name, message] of Object.entries(error.fields)) {
22
+ const errors = (Array.isArray(message) ? message : [message]).map((m) => ({ message: m }));
23
+ const entry = errors.length === 1 ? errors[0] : errors;
24
+ formApi.setFieldMeta(name, (prev) => ({
25
+ ...prev,
26
+ errorMap: {
27
+ ...prev.errorMap,
28
+ onChange: entry
29
+ },
30
+ errors,
31
+ isValid: false
32
+ }));
33
+ clearWhenEdited(formApi, name, entry);
34
+ }
35
+ if (error.formError) formApi.setErrorMap({ onSubmit: {
36
+ fields: {},
37
+ form: error.formError
38
+ } });
39
+ }
40
+ //#endregion
41
+ export { applyValidationError };
@@ -0,0 +1,4 @@
1
+ //#region src/lib/aria.d.ts
2
+ declare function describedBy(...entries: [unknown, string][]): string | undefined;
3
+ //#endregion
4
+ export { describedBy };
@@ -0,0 +1,7 @@
1
+ //#region src/lib/aria.ts
2
+ function describedBy(...entries) {
3
+ const ids = entries.filter(([show]) => Boolean(show)).map(([, id]) => id);
4
+ return ids.length > 0 ? ids.join(" ") : void 0;
5
+ }
6
+ //#endregion
7
+ export { describedBy };
@@ -0,0 +1,5 @@
1
+ //#region src/lib/contexts.d.ts
2
+ declare const fieldContext: import("react").Context<import("@tanstack/react-form").AnyFieldApi>, formContext: import("react").Context<import("@tanstack/react-form").AnyFormApi>, useFieldContext: <TData>() => import("@tanstack/react-form").FieldApi<any, string, TData, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>, useFormContext: () => import("@tanstack/react-form").ReactFormExtendedApi<Record<string, never>, any, any, any, any, any, any, any, any, any, any, any>;
3
+ declare const FormProvider: import("react").Provider<import("@tanstack/react-form").AnyFormApi>;
4
+ //#endregion
5
+ export { FormProvider, fieldContext, formContext, useFieldContext, useFormContext };
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { createFormHookContexts } from "@tanstack/react-form";
3
+ //#region src/lib/contexts.ts
4
+ const { fieldContext, formContext, useFieldContext, useFormContext } = createFormHookContexts();
5
+ const FormProvider = formContext.Provider;
6
+ //#endregion
7
+ export { FormProvider, fieldContext, formContext, useFieldContext, useFormContext };
@@ -0,0 +1,56 @@
1
+ //#region src/lib/formatted-number.d.ts
2
+ type NumberSeparators = {
3
+ /** Thousands separator, e.g. `’` for `de-CH`, `,` for `en-US`. */
4
+ group: string;
5
+ /** Decimal separator, e.g. `.` for `de-CH`, `,` for `de-DE`. */
6
+ decimal: string;
7
+ };
8
+ type FormatNumberOptions = {
9
+ locale?: string;
10
+ /** Maximum digits after the decimal separator. `0` means integers only. */
11
+ maximumFractionDigits: number;
12
+ /** Minimum digits after the decimal separator (pads with zeros). */
13
+ minimumFractionDigits?: number;
14
+ /** Insert group (thousands) separators. Defaults to `true`. */
15
+ useGrouping?: boolean;
16
+ };
17
+ type TypedNumber = {
18
+ /** The re-formatted text to show in the input. */
19
+ text: string;
20
+ /** The parsed number, or `null` when the text holds no digits. */
21
+ value: number | null;
22
+ };
23
+ type ParseTypedOptions = {
24
+ locale?: string;
25
+ maximumFractionDigits: number;
26
+ useGrouping?: boolean;
27
+ allowNegative?: boolean;
28
+ /** Drop integer digits beyond this count (e.g. `4` for a year). */
29
+ maxIntegerDigits?: number;
30
+ };
31
+ declare function getNumberSeparators(locale?: string): NumberSeparators;
32
+ /** Format a number for display (on blur, on reset, on first render). */
33
+ declare function formatNumber(value: number | null | undefined, { locale, maximumFractionDigits, minimumFractionDigits, useGrouping }: FormatNumberOptions): string;
34
+ /**
35
+ * Re-format what the user is typing. Only the locale's decimal separator
36
+ * starts the fraction; every other non-digit is dropped and group separators
37
+ * are re-inserted. Keeps a trailing decimal separator (`"12."`) so typing
38
+ * isn't fought, and truncates fraction digits beyond `maximumFractionDigits`.
39
+ */
40
+ declare function parseTypedNumber(raw: string, { locale, maximumFractionDigits, useGrouping, allowNegative, maxIntegerDigits }: ParseTypedOptions): TypedNumber;
41
+ /**
42
+ * Parse free-form text (pasted, or a value from another system) into a
43
+ * number. Tolerates currency codes and symbols, any whitespace or apostrophe
44
+ * as grouping, `.` or `,` as the decimal separator, and `−`/`-` for negatives.
45
+ * Returns `null` when no digits are found.
46
+ */
47
+ declare function parseLocaleNumber(text: string, locale?: string): number | null;
48
+ /**
49
+ * Where the caret should land in `next` after the user edited `raw` with the
50
+ * caret at `caret`.
51
+ */
52
+ declare function mapCaret(raw: string, caret: number, next: string, locale?: string): number;
53
+ /** Round to `digits` decimals without the `1.005 → 1.00` float surprise. */
54
+ declare function roundTo(value: number, digits: number): number;
55
+ //#endregion
56
+ export { type FormatNumberOptions, type NumberSeparators, type ParseTypedOptions, type TypedNumber, formatNumber, getNumberSeparators, mapCaret, parseLocaleNumber, parseTypedNumber, roundTo };
@@ -0,0 +1,138 @@
1
+ //#region src/lib/formatted-number.ts
2
+ const separatorCache = /* @__PURE__ */ new Map();
3
+ const MINUS_SIGNS = /[-−–]/;
4
+ const LEADING_MINUS = /^[^\d]*[-−–]/;
5
+ const NON_DIGIT = /\D/g;
6
+ const LEADING_ZEROS = /^0+(?=\d)/;
7
+ const DIGIT = /\d/;
8
+ const DIGITS = /\d/g;
9
+ const PARENTHESIZED = /^\s*\(.*\)\s*$/;
10
+ const NOT_NUMERIC = /[^\d.,]/g;
11
+ const UNICODE_MINUS = /\u2212/g;
12
+ function getNumberSeparators(locale) {
13
+ const key = locale ?? "";
14
+ const cached = separatorCache.get(key);
15
+ if (cached) return cached;
16
+ const parts = new Intl.NumberFormat(locale, { numberingSystem: "latn" }).formatToParts(1234567.5);
17
+ const separators = {
18
+ decimal: parts.find((p) => p.type === "decimal")?.value ?? ".",
19
+ group: parts.find((p) => p.type === "group")?.value ?? ","
20
+ };
21
+ separatorCache.set(key, separators);
22
+ return separators;
23
+ }
24
+ function normalizeMinus(text) {
25
+ return text.replace(UNICODE_MINUS, "-");
26
+ }
27
+ /** Format a number for display (on blur, on reset, on first render). */
28
+ function formatNumber(value, { locale, maximumFractionDigits, minimumFractionDigits = 0, useGrouping = true }) {
29
+ if (value == null || !Number.isFinite(value)) return "";
30
+ return normalizeMinus(new Intl.NumberFormat(locale, {
31
+ maximumFractionDigits,
32
+ minimumFractionDigits: Math.min(minimumFractionDigits, maximumFractionDigits),
33
+ numberingSystem: "latn",
34
+ useGrouping
35
+ }).format(value));
36
+ }
37
+ function groupIntegerDigits(digits, locale, useGrouping) {
38
+ if (!useGrouping || digits.length <= 3) return digits;
39
+ return new Intl.NumberFormat(locale, {
40
+ numberingSystem: "latn",
41
+ useGrouping: true
42
+ }).format(BigInt(digits));
43
+ }
44
+ function toNumber(negative, intDigits, fracDigits) {
45
+ if (intDigits === "" && fracDigits === "") return null;
46
+ const value = Number(`${intDigits || "0"}.${fracDigits || "0"}`);
47
+ return negative && value !== 0 ? -value : value;
48
+ }
49
+ /**
50
+ * Re-format what the user is typing. Only the locale's decimal separator
51
+ * starts the fraction; every other non-digit is dropped and group separators
52
+ * are re-inserted. Keeps a trailing decimal separator (`"12."`) so typing
53
+ * isn't fought, and truncates fraction digits beyond `maximumFractionDigits`.
54
+ */
55
+ function parseTypedNumber(raw, { locale, maximumFractionDigits, useGrouping = true, allowNegative = true, maxIntegerDigits }) {
56
+ const { decimal } = getNumberSeparators(locale);
57
+ const negative = allowNegative && LEADING_MINUS.test(raw);
58
+ const decimalIndex = maximumFractionDigits > 0 ? raw.indexOf(decimal) : -1;
59
+ const intRaw = decimalIndex === -1 ? raw : raw.slice(0, decimalIndex);
60
+ const fracRaw = decimalIndex === -1 ? "" : raw.slice(decimalIndex + 1);
61
+ let intDigits = intRaw.replace(NON_DIGIT, "").replace(LEADING_ZEROS, "");
62
+ if (maxIntegerDigits !== void 0) intDigits = intDigits.slice(0, maxIntegerDigits);
63
+ const fracDigits = fracRaw.replace(NON_DIGIT, "").slice(0, maximumFractionDigits);
64
+ const hasDecimal = decimalIndex !== -1;
65
+ return {
66
+ text: `${negative ? "-" : ""}${intDigits === "" && hasDecimal ? "0" : groupIntegerDigits(intDigits, locale, useGrouping)}${hasDecimal ? `${decimal}${fracDigits}` : ""}`,
67
+ value: toNumber(negative, intDigits, fracDigits)
68
+ };
69
+ }
70
+ function detectDecimalSeparator(text, localeDecimal) {
71
+ const lastDot = text.lastIndexOf(".");
72
+ const lastComma = text.lastIndexOf(",");
73
+ if (lastDot !== -1 && lastComma !== -1) return lastDot > lastComma ? "." : ",";
74
+ let candidate = null;
75
+ if (lastDot !== -1) candidate = ".";
76
+ else if (lastComma !== -1) candidate = ",";
77
+ if (candidate === null) return null;
78
+ if (text.split(candidate).length - 1 > 1) return null;
79
+ if (candidate === localeDecimal) return candidate;
80
+ return (text.slice(text.lastIndexOf(candidate) + 1).match(DIGITS)?.length ?? 0) === 3 ? null : candidate;
81
+ }
82
+ /**
83
+ * Parse free-form text (pasted, or a value from another system) into a
84
+ * number. Tolerates currency codes and symbols, any whitespace or apostrophe
85
+ * as grouping, `.` or `,` as the decimal separator, and `−`/`-` for negatives.
86
+ * Returns `null` when no digits are found.
87
+ */
88
+ function parseLocaleNumber(text, locale) {
89
+ if (!DIGIT.test(text)) return null;
90
+ const { decimal } = getNumberSeparators(locale);
91
+ const negative = LEADING_MINUS.test(text) || PARENTHESIZED.test(text);
92
+ const kept = text.replace(NOT_NUMERIC, "");
93
+ const decimalSeparator = detectDecimalSeparator(kept, decimal);
94
+ let intPart = kept;
95
+ let fracPart = "";
96
+ if (decimalSeparator) {
97
+ const index = kept.lastIndexOf(decimalSeparator);
98
+ intPart = kept.slice(0, index);
99
+ fracPart = kept.slice(index + 1);
100
+ }
101
+ return toNumber(negative, intPart.replace(NON_DIGIT, ""), fracPart.replace(NON_DIGIT, ""));
102
+ }
103
+ function countSignificant(text, end, decimal) {
104
+ let count = 0;
105
+ for (let i = 0; i < end && i < text.length; i++) {
106
+ const char = text[i];
107
+ if (DIGIT.test(char) || char === decimal || MINUS_SIGNS.test(char)) count++;
108
+ }
109
+ return count;
110
+ }
111
+ function caretForSignificant(text, significant, decimal) {
112
+ if (significant <= 0) return 0;
113
+ let count = 0;
114
+ for (let i = 0; i < text.length; i++) {
115
+ const char = text[i];
116
+ if (DIGIT.test(char) || char === decimal || char === "-") {
117
+ count++;
118
+ if (count === significant) return i + 1;
119
+ }
120
+ }
121
+ return text.length;
122
+ }
123
+ /**
124
+ * Where the caret should land in `next` after the user edited `raw` with the
125
+ * caret at `caret`.
126
+ */
127
+ function mapCaret(raw, caret, next, locale) {
128
+ const { decimal } = getNumberSeparators(locale);
129
+ return caretForSignificant(next, countSignificant(raw, caret, decimal), decimal);
130
+ }
131
+ /** Round to `digits` decimals without the `1.005 → 1.00` float surprise. */
132
+ function roundTo(value, digits) {
133
+ const factor = 10 ** digits;
134
+ const rounded = Math.round((Math.abs(value) + Number.EPSILON) * factor);
135
+ return Math.sign(value) * rounded / factor;
136
+ }
137
+ //#endregion
138
+ export { formatNumber, getNumberSeparators, mapCaret, parseLocaleNumber, parseTypedNumber, roundTo };