@gntik-ai/ui 0.0.0-stage → 0.7.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.
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+ import { clsx } from "clsx";
3
+ import { extendTailwindMerge } from "tailwind-merge";
4
+ import { createTV } from "tailwind-variants";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { CircleAlert } from "lucide-react";
7
+ import { Field } from "@base-ui/react/field";
8
+ import { Fieldset } from "@base-ui/react/fieldset";
9
+ //#region src/utils/density-merge.ts
10
+ /**
11
+ * Teaches tailwind-merge the density utilities from @gntik-ai/tokens (density.css), so
12
+ * `cn('h-control', 'h-10')` and `px-control … px-0` resolve like any other Tailwind conflict.
13
+ * Shared by `cn()` and the `tv()` instance.
14
+ */
15
+ const densityMergeConfig = { extend: { classGroups: {
16
+ h: [{ h: [
17
+ "control-sm",
18
+ "control",
19
+ "control-lg",
20
+ "item",
21
+ "tab",
22
+ "chip",
23
+ "token",
24
+ "row"
25
+ ] }],
26
+ w: [{ w: [
27
+ "control-sm",
28
+ "control",
29
+ "control-lg"
30
+ ] }],
31
+ size: [{ size: [
32
+ "control-sm",
33
+ "control",
34
+ "control-lg"
35
+ ] }],
36
+ px: [{ px: [
37
+ "control-sm",
38
+ "control",
39
+ "control-lg",
40
+ "field"
41
+ ] }],
42
+ ps: [{ ps: ["field"] }],
43
+ py: [{ py: ["cell", "bar"] }],
44
+ gap: [{ gap: ["tight"] }]
45
+ } } };
46
+ //#endregion
47
+ //#region src/utils/cn.ts
48
+ const twMerge = extendTailwindMerge(densityMergeConfig);
49
+ /** Joins class names and resolves Tailwind conflicts (last one wins), density utilities included. */
50
+ function cn(...inputs) {
51
+ return twMerge(clsx(inputs));
52
+ }
53
+ //#endregion
54
+ //#region src/utils/tv.ts
55
+ /** `tv()` with tailwind-merge aware of the density utilities (h-control, px-control…). */
56
+ const tv = createTV({ twMergeConfig: densityMergeConfig });
57
+ //#endregion
58
+ //#region src/components/Field/field.variants.ts
59
+ const fieldVariants = tv({ slots: {
60
+ root: "flex min-w-0 flex-col gap-2 data-disabled:opacity-70",
61
+ label: "inline-flex items-center gap-0.5 text-[13px] font-medium text-foreground data-disabled:cursor-not-allowed",
62
+ required: "text-destructive-text",
63
+ description: "text-[12px] leading-5 text-muted-foreground text-pretty",
64
+ error: "flex items-start gap-1.5 text-[12px] leading-5 text-destructive-text",
65
+ item: "flex",
66
+ fieldset: "m-0 flex min-w-0 flex-col gap-3 border-0 p-0 data-disabled:opacity-70",
67
+ legend: "p-0 text-[13px] font-semibold text-foreground",
68
+ legendDescription: "-mt-2 text-[13px] leading-6 text-muted-foreground text-pretty"
69
+ } });
70
+ //#endregion
71
+ //#region src/components/Field/Field.tsx
72
+ const s = fieldVariants();
73
+ /**
74
+ * Groups a label, a control, a description and an error. Base UI wires the ids: the label
75
+ * targets the control, and the description and error are linked with aria-describedby.
76
+ * Pass `invalid` to drive the error from outside (e.g. a form library).
77
+ */
78
+ function Field$1({ className, ...props }) {
79
+ return /* @__PURE__ */ jsx(Field.Root, {
80
+ className: cn(s.root(), className),
81
+ ...props
82
+ });
83
+ }
84
+ /** Visible label; clicking it focuses the field's control. */
85
+ function FieldLabel({ className, required, children, ...props }) {
86
+ return /* @__PURE__ */ jsxs(Field.Label, {
87
+ className: cn(s.label(), className),
88
+ ...props,
89
+ children: [children, required && /* @__PURE__ */ jsx("span", {
90
+ "aria-hidden": true,
91
+ className: s.required(),
92
+ children: "*"
93
+ })]
94
+ });
95
+ }
96
+ /** Helper text under the control, announced as part of its description. */
97
+ function FieldDescription({ className, ...props }) {
98
+ return /* @__PURE__ */ jsx(Field.Description, {
99
+ className: cn(s.description(), className),
100
+ ...props
101
+ });
102
+ }
103
+ /**
104
+ * Validation message. Rendered only while the field is invalid (or when `match` says so)
105
+ * and linked to the control through aria-describedby.
106
+ */
107
+ function FieldError({ className, hideIcon, children, ...props }) {
108
+ return /* @__PURE__ */ jsxs(Field.Error, {
109
+ className: cn(s.error(), className),
110
+ ...props,
111
+ children: [!hideIcon && /* @__PURE__ */ jsx(CircleAlert, {
112
+ size: 13,
113
+ "aria-hidden": true,
114
+ className: "mt-[3px] shrink-0"
115
+ }), /* @__PURE__ */ jsx("span", { children })]
116
+ });
117
+ }
118
+ /** Wraps one checkbox or radio inside a group that lives in a Field. */
119
+ function FieldItem({ className, ...props }) {
120
+ return /* @__PURE__ */ jsx(Field.Item, {
121
+ className: cn(s.item(), className),
122
+ ...props
123
+ });
124
+ }
125
+ /** Groups related fields (or a checkbox/radio group) under a shared legend. */
126
+ function Fieldset$1({ className, ...props }) {
127
+ return /* @__PURE__ */ jsx(Fieldset.Root, {
128
+ className: cn(s.fieldset(), className),
129
+ ...props
130
+ });
131
+ }
132
+ /** The fieldset's accessible name. */
133
+ function FieldsetLegend({ className, ...props }) {
134
+ return /* @__PURE__ */ jsx(Fieldset.Legend, {
135
+ className: cn(s.legend(), className),
136
+ ...props
137
+ });
138
+ }
139
+ /** Supporting text under a legend. */
140
+ function FieldsetDescription({ className, ...props }) {
141
+ return /* @__PURE__ */ jsx("p", {
142
+ className: cn(s.legendDescription(), className),
143
+ ...props
144
+ });
145
+ }
146
+ //#endregion
147
+ export { FieldLabel as a, FieldsetLegend as c, cn as d, FieldItem as i, fieldVariants as l, FieldDescription as n, Fieldset$1 as o, FieldError as r, FieldsetDescription as s, Field$1 as t, tv as u };
148
+
149
+ //# sourceMappingURL=Field-neBukiNh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Field-neBukiNh.js","names":["Field","BaseField","Fieldset","BaseFieldset"],"sources":["../src/utils/density-merge.ts","../src/utils/cn.ts","../src/utils/tv.ts","../src/components/Field/field.variants.ts","../src/components/Field/Field.tsx"],"sourcesContent":["/**\n * Teaches tailwind-merge the density utilities from @gntik-ai/tokens (density.css), so\n * `cn('h-control', 'h-10')` and `px-control … px-0` resolve like any other Tailwind conflict.\n * Shared by `cn()` and the `tv()` instance.\n */\nexport const densityMergeConfig = {\n extend: {\n classGroups: {\n h: [{ h: ['control-sm', 'control', 'control-lg', 'item', 'tab', 'chip', 'token', 'row'] }],\n w: [{ w: ['control-sm', 'control', 'control-lg'] }],\n size: [{ size: ['control-sm', 'control', 'control-lg'] }],\n px: [{ px: ['control-sm', 'control', 'control-lg', 'field'] }],\n ps: [{ ps: ['field'] }],\n py: [{ py: ['cell', 'bar'] }],\n gap: [{ gap: ['tight'] }],\n },\n },\n};\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\nimport { densityMergeConfig } from './density-merge';\n\nconst twMerge = extendTailwindMerge(densityMergeConfig);\n\n/** Joins class names and resolves Tailwind conflicts (last one wins), density utilities included. */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import { createTV } from 'tailwind-variants';\nimport { densityMergeConfig } from './density-merge';\n\nexport { type VariantProps } from 'tailwind-variants';\n\n/** `tv()` with tailwind-merge aware of the density utilities (h-control, px-control…). */\nexport const tv = createTV({ twMergeConfig: densityMergeConfig });\n","import { tv, type VariantProps } from '../../utils/tv';\n\nexport const fieldVariants = tv({\n slots: {\n root: 'flex min-w-0 flex-col gap-2 data-disabled:opacity-70',\n label: 'inline-flex items-center gap-0.5 text-[13px] font-medium text-foreground data-disabled:cursor-not-allowed',\n required: 'text-destructive-text',\n description: 'text-[12px] leading-5 text-muted-foreground text-pretty',\n error: 'flex items-start gap-1.5 text-[12px] leading-5 text-destructive-text',\n item: 'flex',\n fieldset: 'm-0 flex min-w-0 flex-col gap-3 border-0 p-0 data-disabled:opacity-70',\n legend: 'p-0 text-[13px] font-semibold text-foreground',\n legendDescription: '-mt-2 text-[13px] leading-6 text-muted-foreground text-pretty',\n },\n});\n\nexport type FieldVariantProps = VariantProps<typeof fieldVariants>;\n","import { Field as BaseField } from '@base-ui/react/field';\nimport { Fieldset as BaseFieldset } from '@base-ui/react/fieldset';\nimport { CircleAlert } from 'lucide-react';\nimport type { HTMLAttributes, ReactNode, Ref } from 'react';\nimport { cn } from '../../utils/cn';\nimport { fieldVariants } from './field.variants';\n\nconst s = fieldVariants();\n\nexport interface FieldProps extends Omit<BaseField.Root.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * Groups a label, a control, a description and an error. Base UI wires the ids: the label\n * targets the control, and the description and error are linked with aria-describedby.\n * Pass `invalid` to drive the error from outside (e.g. a form library).\n */\nexport function Field({ className, ...props }: FieldProps) {\n return <BaseField.Root className={cn(s.root(), className)} {...props} />;\n}\n\nexport interface FieldLabelProps extends Omit<BaseField.Label.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLLabelElement>;\n /** Shows a required marker after the label text (also set `required` on the control). */\n required?: boolean;\n}\n\n/** Visible label; clicking it focuses the field's control. */\nexport function FieldLabel({ className, required, children, ...props }: FieldLabelProps) {\n return (\n <BaseField.Label className={cn(s.label(), className)} {...props}>\n {children}\n {required && (\n <span aria-hidden className={s.required()}>\n *\n </span>\n )}\n </BaseField.Label>\n );\n}\n\nexport interface FieldDescriptionProps extends Omit<BaseField.Description.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLParagraphElement>;\n}\n\n/** Helper text under the control, announced as part of its description. */\nexport function FieldDescription({ className, ...props }: FieldDescriptionProps) {\n return <BaseField.Description className={cn(s.description(), className)} {...props} />;\n}\n\nexport interface FieldErrorProps extends Omit<BaseField.Error.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLDivElement>;\n /** Hide the leading alert icon. */\n hideIcon?: boolean;\n}\n\n/**\n * Validation message. Rendered only while the field is invalid (or when `match` says so)\n * and linked to the control through aria-describedby.\n */\nexport function FieldError({ className, hideIcon, children, ...props }: FieldErrorProps) {\n return (\n <BaseField.Error className={cn(s.error(), className)} {...props}>\n {!hideIcon && <CircleAlert size={13} aria-hidden className=\"mt-[3px] shrink-0\" />}\n <span>{children}</span>\n </BaseField.Error>\n );\n}\n\nexport interface FieldItemProps extends Omit<BaseField.Item.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/** Wraps one checkbox or radio inside a group that lives in a Field. */\nexport function FieldItem({ className, ...props }: FieldItemProps) {\n return <BaseField.Item className={cn(s.item(), className)} {...props} />;\n}\n\nexport interface FieldsetProps extends Omit<BaseFieldset.Root.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLFieldSetElement>;\n}\n\n/** Groups related fields (or a checkbox/radio group) under a shared legend. */\nexport function Fieldset({ className, ...props }: FieldsetProps) {\n return <BaseFieldset.Root className={cn(s.fieldset(), className)} {...props} />;\n}\n\nexport interface FieldsetLegendProps extends Omit<BaseFieldset.Legend.Props, 'className'> {\n className?: string;\n ref?: Ref<HTMLDivElement>;\n}\n\n/** The fieldset's accessible name. */\nexport function FieldsetLegend({ className, ...props }: FieldsetLegendProps) {\n return <BaseFieldset.Legend className={cn(s.legend(), className)} {...props} />;\n}\n\n/** Supporting text under a legend. */\nexport function FieldsetDescription({\n className,\n ...props\n}: HTMLAttributes<HTMLParagraphElement> & { children?: ReactNode }) {\n return <p className={cn(s.legendDescription(), className)} {...props} />;\n}\n"],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+
2
+ import { FormHTMLAttributes, ReactNode } from "react";
3
+ import { Control, ControllerFieldState, ControllerRenderProps, FieldErrors, FieldPath, FieldValues, RegisterOptions, Resolver, SubmitErrorHandler, SubmitHandler, UseFormReturn } from "react-hook-form";
4
+ //#region src/forms/useFieldProps.d.ts
5
+ interface UseFieldPropsOptions<TValues extends FieldValues, TName extends FieldPath<TValues>> {
6
+ /** The form's `control`. Defaults to the nearest `<FormProvider>` / `<Form>`. */
7
+ control?: Control<TValues>;
8
+ /** Field-level rules (`required`, `minLength`, `validate`…). Prefer a schema resolver for whole forms. */
9
+ rules?: Omit<RegisterOptions<TValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
10
+ /** Disables the control (and skips its validation). */
11
+ disabled?: boolean;
12
+ }
13
+ /** Props for the kit's `<Field>`: drive its invalid / touched / dirty state from react-hook-form. */
14
+ interface FormFieldStateProps {
15
+ name: string;
16
+ invalid: boolean;
17
+ touched: boolean;
18
+ dirty: boolean;
19
+ }
20
+ interface UseFieldPropsResult<TValues extends FieldValues, TName extends FieldPath<TValues>> {
21
+ /** Spread on `<Field>`. Base UI then links the label, description and error to the control. */
22
+ fieldProps: FormFieldStateProps;
23
+ /**
24
+ * Spread on the control (`<Input {...controlProps} />`), or map it (`value` + `onValueChange={controlProps.onChange}`).
25
+ * `ref` must reach a focusable element so a failed submit can focus the first invalid field.
26
+ */
27
+ controlProps: ControllerRenderProps<TValues, TName> & {
28
+ 'aria-invalid'?: true;
29
+ };
30
+ /** The current error message, for `<FieldError match={!!error}>{error}</FieldError>`. */
31
+ error: string | undefined;
32
+ fieldState: ControllerFieldState;
33
+ }
34
+ /**
35
+ * Wires one react-hook-form field (through `useController`) into the kit's Field parts.
36
+ * Works with uncontrolled-looking inputs too: `Input` gets `value` / `onChange` / `onBlur` / `ref`.
37
+ */
38
+ export declare function useFieldProps<TValues extends FieldValues = FieldValues, TName extends FieldPath<TValues> = FieldPath<TValues>>(name: TName, { control, rules, disabled }?: UseFieldPropsOptions<TValues, TName>): UseFieldPropsResult<TValues, TName>;
39
+ //#endregion
40
+ //#region src/forms/FormField.d.ts
41
+ interface FormFieldProps<TValues extends FieldValues, TName extends FieldPath<TValues>> extends UseFieldPropsOptions<TValues, TName> {
42
+ name: TName;
43
+ label?: ReactNode;
44
+ /** Shows the required marker on the label (validation itself comes from `rules` or the schema). */
45
+ required?: boolean;
46
+ description?: ReactNode;
47
+ className?: string;
48
+ /** Renders the control: `{(field) => <Input {...field} />}`. Gets the field's state as the second argument. */
49
+ children: (control: UseFieldPropsResult<TValues, TName>['controlProps'], state: UseFieldPropsResult<TValues, TName>) => ReactNode;
50
+ }
51
+ /**
52
+ * A kit `Field` bound to a react-hook-form field: label, control, description and the field's
53
+ * error message, with `aria-invalid` and `aria-describedby` wired by Base UI's Field.
54
+ */
55
+ export declare function FormField<TValues extends FieldValues = FieldValues, TName extends FieldPath<TValues> = FieldPath<TValues>>({ name, label, required, description, className, children, ...options }: FormFieldProps<TValues, TName>): import("react").JSX.Element;
56
+ interface FormProps<TValues extends FieldValues, TTransformed = TValues> extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'onInvalid'> {
57
+ /** The object returned by `useForm()`. */
58
+ form: UseFormReturn<TValues, unknown, TTransformed>;
59
+ onSubmit: SubmitHandler<TTransformed>;
60
+ onInvalid?: SubmitErrorHandler<TValues>;
61
+ children?: ReactNode;
62
+ }
63
+ /**
64
+ * `<form noValidate>` + `FormProvider`: submits through `handleSubmit`, so a failed submit shows
65
+ * every message and focuses the first invalid field (react-hook-form's `shouldFocusError`).
66
+ */
67
+ export declare function Form<TValues extends FieldValues, TTransformed = TValues>({ form, onSubmit, onInvalid, children, ...props }: FormProps<TValues, TTransformed>): import("react").JSX.Element;
68
+ //#endregion
69
+ //#region src/forms/schemaResolver.d.ts
70
+ /** One validation issue, as zod (v3 and v4) and other Standard-Schema-style libraries report it. */
71
+ interface SchemaIssue {
72
+ path: ReadonlyArray<PropertyKey | {
73
+ key: PropertyKey;
74
+ }>;
75
+ message: string;
76
+ code?: string;
77
+ }
78
+ type SafeParseResult<Out> = {
79
+ success: true;
80
+ data: Out;
81
+ } | {
82
+ success: false;
83
+ error: {
84
+ issues: readonly SchemaIssue[];
85
+ };
86
+ };
87
+ /** Anything with zod's `safeParseAsync` / `safeParse` shape (a zod schema, or an adapter). */
88
+ interface ParseableSchema<Out> {
89
+ safeParseAsync?: (values: unknown) => Promise<SafeParseResult<Out>>;
90
+ safeParse?: (values: unknown) => SafeParseResult<Out>;
91
+ }
92
+ /** Converts schema issues into react-hook-form's nested `errors` object. */
93
+ export declare function issuesToErrors<TValues extends FieldValues>(issues: readonly SchemaIssue[]): FieldErrors<TValues>;
94
+ /**
95
+ * A `zodResolver`-compatible resolver with no extra dependency: `useForm({ resolver: schemaResolver(schema) })`.
96
+ * Accepts a zod 3/4 schema (or any object with `safeParseAsync` / `safeParse`). On success the
97
+ * parsed (transformed) data reaches `onSubmit`. `@hookform/resolvers/zod` works the same way with `FormField`.
98
+ */
99
+ export declare function schemaResolver<TValues extends FieldValues, TOutput = TValues>(schema: ParseableSchema<TOutput>): Resolver<TValues, unknown, TOutput>;
100
+ //#endregion
101
+ export type { FormFieldProps, FormFieldStateProps, FormProps, ParseableSchema, SchemaIssue, UseFieldPropsOptions, UseFieldPropsResult };
102
+ //# sourceMappingURL=forms.d.ts.map
package/dist/forms.js ADDED
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+ import { a as FieldLabel, n as FieldDescription, r as FieldError, t as Field } from "./Field-neBukiNh.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { FormProvider, useController, useFormContext } from "react-hook-form";
5
+ //#region src/forms/useFieldProps.ts
6
+ /**
7
+ * Wires one react-hook-form field (through `useController`) into the kit's Field parts.
8
+ * Works with uncontrolled-looking inputs too: `Input` gets `value` / `onChange` / `onBlur` / `ref`.
9
+ */
10
+ function useFieldProps(name, { control, rules, disabled } = {}) {
11
+ const context = useFormContext();
12
+ const { field, fieldState } = useController({
13
+ name,
14
+ control: control ?? context?.control,
15
+ rules,
16
+ disabled
17
+ });
18
+ const invalid = !!fieldState.error;
19
+ return {
20
+ fieldProps: {
21
+ name,
22
+ invalid,
23
+ touched: fieldState.isTouched,
24
+ dirty: fieldState.isDirty
25
+ },
26
+ controlProps: {
27
+ ...field,
28
+ "aria-invalid": invalid || void 0
29
+ },
30
+ error: errorMessage(fieldState.error),
31
+ fieldState
32
+ };
33
+ }
34
+ /** First message of a field error (also for array / object errors that carry `root`). */
35
+ function errorMessage(error) {
36
+ if (!error) return void 0;
37
+ if (typeof error.message === "string" && error.message) return error.message;
38
+ const root = error.root;
39
+ return typeof root?.message === "string" ? root.message : void 0;
40
+ }
41
+ //#endregion
42
+ //#region src/forms/FormField.tsx
43
+ /**
44
+ * A kit `Field` bound to a react-hook-form field: label, control, description and the field's
45
+ * error message, with `aria-invalid` and `aria-describedby` wired by Base UI's Field.
46
+ */
47
+ function FormField({ name, label, required, description, className, children, ...options }) {
48
+ const state = useFieldProps(name, options);
49
+ return /* @__PURE__ */ jsxs(Field, {
50
+ ...state.fieldProps,
51
+ disabled: options.disabled,
52
+ className,
53
+ children: [
54
+ label && /* @__PURE__ */ jsx(FieldLabel, {
55
+ required,
56
+ children: label
57
+ }),
58
+ children(state.controlProps, state),
59
+ description && /* @__PURE__ */ jsx(FieldDescription, { children: description }),
60
+ /* @__PURE__ */ jsx(FieldError, {
61
+ match: !!state.error,
62
+ children: state.error
63
+ })
64
+ ]
65
+ });
66
+ }
67
+ /**
68
+ * `<form noValidate>` + `FormProvider`: submits through `handleSubmit`, so a failed submit shows
69
+ * every message and focuses the first invalid field (react-hook-form's `shouldFocusError`).
70
+ */
71
+ function Form({ form, onSubmit, onInvalid, children, ...props }) {
72
+ return /* @__PURE__ */ jsx(FormProvider, {
73
+ ...form,
74
+ children: /* @__PURE__ */ jsx("form", {
75
+ noValidate: true,
76
+ ...props,
77
+ onSubmit: form.handleSubmit(onSubmit, onInvalid),
78
+ children
79
+ })
80
+ });
81
+ }
82
+ //#endregion
83
+ //#region src/forms/schemaResolver.ts
84
+ function setPath(target, path, value) {
85
+ let node = target;
86
+ path.forEach((key, i) => {
87
+ if (i === path.length - 1) {
88
+ if (node[key] === void 0) node[key] = value;
89
+ return;
90
+ }
91
+ const next = node[key];
92
+ node[key] = next && typeof next === "object" ? next : {};
93
+ node = node[key];
94
+ });
95
+ }
96
+ /** Converts schema issues into react-hook-form's nested `errors` object. */
97
+ function issuesToErrors(issues) {
98
+ const errors = {};
99
+ for (const issue of issues) {
100
+ const path = issue.path.map((p) => String(typeof p === "object" && p !== null ? p.key : p));
101
+ setPath(errors, path.length ? path : ["root"], {
102
+ type: issue.code ?? "validate",
103
+ message: issue.message
104
+ });
105
+ }
106
+ return errors;
107
+ }
108
+ /**
109
+ * A `zodResolver`-compatible resolver with no extra dependency: `useForm({ resolver: schemaResolver(schema) })`.
110
+ * Accepts a zod 3/4 schema (or any object with `safeParseAsync` / `safeParse`). On success the
111
+ * parsed (transformed) data reaches `onSubmit`. `@hookform/resolvers/zod` works the same way with `FormField`.
112
+ */
113
+ function schemaResolver(schema) {
114
+ return async (values) => {
115
+ const result = schema.safeParseAsync ? await schema.safeParseAsync(values) : schema.safeParse?.(values);
116
+ if (!result) throw new Error("schemaResolver: the schema has neither safeParseAsync nor safeParse");
117
+ if (result.success) return {
118
+ values: result.data,
119
+ errors: {}
120
+ };
121
+ return {
122
+ values: {},
123
+ errors: issuesToErrors(result.error.issues)
124
+ };
125
+ };
126
+ }
127
+ //#endregion
128
+ export { Form, FormField, issuesToErrors, schemaResolver, useFieldProps };
129
+
130
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms.js","names":["provider"],"sources":["../src/forms/useFieldProps.ts","../src/forms/FormField.tsx","../src/forms/schemaResolver.ts"],"sourcesContent":["import {\n useController,\n useFormContext,\n type Control,\n type ControllerFieldState,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n type RegisterOptions,\n} from 'react-hook-form';\n\nexport interface UseFieldPropsOptions<TValues extends FieldValues, TName extends FieldPath<TValues>> {\n /** The form's `control`. Defaults to the nearest `<FormProvider>` / `<Form>`. */\n control?: Control<TValues>;\n /** Field-level rules (`required`, `minLength`, `validate`…). Prefer a schema resolver for whole forms. */\n rules?: Omit<RegisterOptions<TValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;\n /** Disables the control (and skips its validation). */\n disabled?: boolean;\n}\n\n/** Props for the kit's `<Field>`: drive its invalid / touched / dirty state from react-hook-form. */\nexport interface FormFieldStateProps {\n name: string;\n invalid: boolean;\n touched: boolean;\n dirty: boolean;\n}\n\nexport interface UseFieldPropsResult<TValues extends FieldValues, TName extends FieldPath<TValues>> {\n /** Spread on `<Field>`. Base UI then links the label, description and error to the control. */\n fieldProps: FormFieldStateProps;\n /**\n * Spread on the control (`<Input {...controlProps} />`), or map it (`value` + `onValueChange={controlProps.onChange}`).\n * `ref` must reach a focusable element so a failed submit can focus the first invalid field.\n */\n controlProps: ControllerRenderProps<TValues, TName> & { 'aria-invalid'?: true };\n /** The current error message, for `<FieldError match={!!error}>{error}</FieldError>`. */\n error: string | undefined;\n fieldState: ControllerFieldState;\n}\n\n/**\n * Wires one react-hook-form field (through `useController`) into the kit's Field parts.\n * Works with uncontrolled-looking inputs too: `Input` gets `value` / `onChange` / `onBlur` / `ref`.\n */\nexport function useFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>(\n name: TName,\n { control, rules, disabled }: UseFieldPropsOptions<TValues, TName> = {},\n): UseFieldPropsResult<TValues, TName> {\n const context = useFormContext<TValues>();\n const { field, fieldState } = useController<TValues, TName>({\n name,\n control: control ?? context?.control,\n rules,\n disabled,\n });\n const invalid = !!fieldState.error;\n return {\n fieldProps: { name, invalid, touched: fieldState.isTouched, dirty: fieldState.isDirty },\n controlProps: { ...field, 'aria-invalid': invalid || undefined },\n error: errorMessage(fieldState.error),\n fieldState,\n };\n}\n\n/** First message of a field error (also for array / object errors that carry `root`). */\nfunction errorMessage(error: ControllerFieldState['error']): string | undefined {\n if (!error) return undefined;\n if (typeof error.message === 'string' && error.message) return error.message;\n const root = (error as { root?: { message?: unknown } }).root;\n return typeof root?.message === 'string' ? root.message : undefined;\n}\n","import type { FormHTMLAttributes, ReactNode } from 'react';\nimport type {\n FieldPath,\n FieldValues,\n FormProviderProps,\n SubmitErrorHandler,\n SubmitHandler,\n UseFormReturn,\n} from 'react-hook-form';\nimport { FormProvider } from 'react-hook-form';\nimport { Field, FieldDescription, FieldError, FieldLabel } from '../components/Field';\nimport { useFieldProps, type UseFieldPropsOptions, type UseFieldPropsResult } from './useFieldProps';\n\nexport interface FormFieldProps<\n TValues extends FieldValues,\n TName extends FieldPath<TValues>,\n> extends UseFieldPropsOptions<TValues, TName> {\n name: TName;\n label?: ReactNode;\n /** Shows the required marker on the label (validation itself comes from `rules` or the schema). */\n required?: boolean;\n description?: ReactNode;\n className?: string;\n /** Renders the control: `{(field) => <Input {...field} />}`. Gets the field's state as the second argument. */\n children: (\n control: UseFieldPropsResult<TValues, TName>['controlProps'],\n state: UseFieldPropsResult<TValues, TName>,\n ) => ReactNode;\n}\n\n/**\n * A kit `Field` bound to a react-hook-form field: label, control, description and the field's\n * error message, with `aria-invalid` and `aria-describedby` wired by Base UI's Field.\n */\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, required, description, className, children, ...options }: FormFieldProps<TValues, TName>) {\n const state = useFieldProps<TValues, TName>(name, options);\n return (\n <Field {...state.fieldProps} disabled={options.disabled} className={className}>\n {label && <FieldLabel required={required}>{label}</FieldLabel>}\n {children(state.controlProps, state)}\n {description && <FieldDescription>{description}</FieldDescription>}\n <FieldError match={!!state.error}>{state.error}</FieldError>\n </Field>\n );\n}\n\nexport interface FormProps<TValues extends FieldValues, TTransformed = TValues> extends Omit<\n FormHTMLAttributes<HTMLFormElement>,\n 'onSubmit' | 'onInvalid'\n> {\n /** The object returned by `useForm()`. */\n form: UseFormReturn<TValues, unknown, TTransformed>;\n onSubmit: SubmitHandler<TTransformed>;\n onInvalid?: SubmitErrorHandler<TValues>;\n children?: ReactNode;\n}\n\n/**\n * `<form noValidate>` + `FormProvider`: submits through `handleSubmit`, so a failed submit shows\n * every message and focuses the first invalid field (react-hook-form's `shouldFocusError`).\n */\nexport function Form<TValues extends FieldValues, TTransformed = TValues>({\n form,\n onSubmit,\n onInvalid,\n children,\n ...props\n}: FormProps<TValues, TTransformed>) {\n const provider = form as unknown as FormProviderProps<TValues, unknown, TTransformed>;\n return (\n <FormProvider {...provider}>\n <form noValidate {...props} onSubmit={form.handleSubmit(onSubmit, onInvalid)}>\n {children}\n </form>\n </FormProvider>\n );\n}\n","import type { FieldErrors, FieldValues, Resolver } from 'react-hook-form';\n\n/** One validation issue, as zod (v3 and v4) and other Standard-Schema-style libraries report it. */\nexport interface SchemaIssue {\n path: ReadonlyArray<PropertyKey | { key: PropertyKey }>;\n message: string;\n code?: string;\n}\n\ntype SafeParseResult<Out> =\n { success: true; data: Out } | { success: false; error: { issues: readonly SchemaIssue[] } };\n\n/** Anything with zod's `safeParseAsync` / `safeParse` shape (a zod schema, or an adapter). */\nexport interface ParseableSchema<Out> {\n safeParseAsync?: (values: unknown) => Promise<SafeParseResult<Out>>;\n safeParse?: (values: unknown) => SafeParseResult<Out>;\n}\n\nfunction setPath(target: Record<string, unknown>, path: readonly string[], value: unknown) {\n let node = target;\n path.forEach((key, i) => {\n if (i === path.length - 1) {\n if (node[key] === undefined) node[key] = value; // first issue per field wins\n return;\n }\n const next = node[key];\n node[key] = next && typeof next === 'object' ? next : {};\n node = node[key] as Record<string, unknown>;\n });\n}\n\n/** Converts schema issues into react-hook-form's nested `errors` object. */\nexport function issuesToErrors<TValues extends FieldValues>(issues: readonly SchemaIssue[]): FieldErrors<TValues> {\n const errors: Record<string, unknown> = {};\n for (const issue of issues) {\n const path = issue.path.map((p) => String(typeof p === 'object' && p !== null ? p.key : p));\n setPath(errors, path.length ? path : ['root'], { type: issue.code ?? 'validate', message: issue.message });\n }\n return errors as FieldErrors<TValues>;\n}\n\n/**\n * A `zodResolver`-compatible resolver with no extra dependency: `useForm({ resolver: schemaResolver(schema) })`.\n * Accepts a zod 3/4 schema (or any object with `safeParseAsync` / `safeParse`). On success the\n * parsed (transformed) data reaches `onSubmit`. `@hookform/resolvers/zod` works the same way with `FormField`.\n */\nexport function schemaResolver<TValues extends FieldValues, TOutput = TValues>(\n schema: ParseableSchema<TOutput>,\n): Resolver<TValues, unknown, TOutput> {\n return async (values) => {\n const result = schema.safeParseAsync ? await schema.safeParseAsync(values) : schema.safeParse?.(values);\n if (!result) throw new Error('schemaResolver: the schema has neither safeParseAsync nor safeParse');\n if (result.success) return { values: result.data, errors: {} };\n return { values: {}, errors: issuesToErrors<TValues>(result.error.issues) };\n };\n}\n"],"mappings":";;;;;;;;;AA6CA,SAAgB,cAId,MACA,EAAE,SAAS,OAAO,aAAmD,CAAC,GACjC;CACrC,MAAM,UAAU,eAAwB;CACxC,MAAM,EAAE,OAAO,eAAe,cAA8B;EAC1D;EACA,SAAS,WAAW,SAAS;EAC7B;EACA;CACF,CAAC;CACD,MAAM,UAAU,CAAC,CAAC,WAAW;CAC7B,OAAO;EACL,YAAY;GAAE;GAAM;GAAS,SAAS,WAAW;GAAW,OAAO,WAAW;EAAQ;EACtF,cAAc;GAAE,GAAG;GAAO,gBAAgB,WAAW,KAAA;EAAU;EAC/D,OAAO,aAAa,WAAW,KAAK;EACpC;CACF;AACF;;AAGA,SAAS,aAAa,OAA0D;CAC9E,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,IAAI,OAAO,MAAM,YAAY,YAAY,MAAM,SAAS,OAAO,MAAM;CACrE,MAAM,OAAQ,MAA2C;CACzD,OAAO,OAAO,MAAM,YAAY,WAAW,KAAK,UAAU,KAAA;AAC5D;;;;;;;ACxCA,SAAgB,UAGd,EAAE,MAAM,OAAO,UAAU,aAAa,WAAW,UAAU,GAAG,WAA2C;CACzG,MAAM,QAAQ,cAA8B,MAAM,OAAO;CACzD,OACE,qBAAC,OAAD;EAAO,GAAI,MAAM;EAAY,UAAU,QAAQ;EAAqB;EAApE,UAAA;GACG,SAAS,oBAAC,YAAD;IAAsB;IAAW,UAAA;GAAkB,CAAA;GAC5D,SAAS,MAAM,cAAc,KAAK;GAClC,eAAe,oBAAC,kBAAD,EAAA,UAAmB,YAA8B,CAAA;GACjE,oBAAC,YAAD;IAAY,OAAO,CAAC,CAAC,MAAM;IAAQ,UAAA,MAAM;GAAkB,CAAA;EACtD;;AAEX;;;;;AAiBA,SAAgB,KAA0D,EACxE,MACA,UACA,WACA,UACA,GAAG,SACgC;CAEnC,OACE,oBAAC,cAAD;EAAc,GAAIA;EAChB,UAAA,oBAAC,QAAD;GAAM,YAAA;GAAW,GAAI;GAAO,UAAU,KAAK,aAAa,UAAU,SAAS;GACxE;EACG,CAAA;CACM,CAAA;AAElB;;;AC7DA,SAAS,QAAQ,QAAiC,MAAyB,OAAgB;CACzF,IAAI,OAAO;CACX,KAAK,SAAS,KAAK,MAAM;EACvB,IAAI,MAAM,KAAK,SAAS,GAAG;GACzB,IAAI,KAAK,SAAS,KAAA,GAAW,KAAK,OAAO;GACzC;EACF;EACA,MAAM,OAAO,KAAK;EAClB,KAAK,OAAO,QAAQ,OAAO,SAAS,WAAW,OAAO,CAAC;EACvD,OAAO,KAAK;CACd,CAAC;AACH;;AAGA,SAAgB,eAA4C,QAAsD;CAChH,MAAM,SAAkC,CAAC;CACzC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,MAAM,KAAK,KAAK,MAAM,OAAO,OAAO,MAAM,YAAY,MAAM,OAAO,EAAE,MAAM,CAAC,CAAC;EAC1F,QAAQ,QAAQ,KAAK,SAAS,OAAO,CAAC,MAAM,GAAG;GAAE,MAAM,MAAM,QAAQ;GAAY,SAAS,MAAM;EAAQ,CAAC;CAC3G;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,eACd,QACqC;CACrC,OAAO,OAAO,WAAW;EACvB,MAAM,SAAS,OAAO,iBAAiB,MAAM,OAAO,eAAe,MAAM,IAAI,OAAO,YAAY,MAAM;EACtG,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,qEAAqE;EAClG,IAAI,OAAO,SAAS,OAAO;GAAE,QAAQ,OAAO;GAAM,QAAQ,CAAC;EAAE;EAC7D,OAAO;GAAE,QAAQ,CAAC;GAAG,QAAQ,eAAwB,OAAO,MAAM,MAAM;EAAE;CAC5E;AACF"}