diff --git a/.changeset/famous-dancers-raise.md b/.changeset/green-days-exist.md similarity index 63% rename from .changeset/famous-dancers-raise.md rename to .changeset/green-days-exist.md index bc25439..3fd42ca 100644 --- a/.changeset/famous-dancers-raise.md +++ b/.changeset/green-days-exist.md @@ -2,4 +2,4 @@ "@babylonlabs-io/bbn-core-ui": minor --- -add form control component \ No newline at end of file +add Form widget diff --git a/.gitignore b/.gitignore index d600b6c..0465bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +storybook-static # Editor directories and files .vscode/* diff --git a/CHANGELOG.md b/CHANGELOG.md index cb51423..4b1a3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @babylonlabs-io/bbn-core-ui +## 0.5.0 + +### Minor Changes + +- a8049c4: add form control component + ## 0.4.1 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index b15931f..f5092da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "@babylonlabs-io/bbn-core-ui", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@babylonlabs-io/bbn-core-ui", - "version": "0.4.0", + "version": "0.5.0", "dependencies": { + "@hookform/resolvers": "^3.9.1", "@popperjs/core": "^2.11.8", + "react-hook-form": "^7.54.0", "react-popper": "^2.3.0" }, "devDependencies": { @@ -50,7 +52,8 @@ "peerDependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^2.5.4", + "yup": "^1.5.0" } }, "node_modules/@adobe/css-tools": { @@ -1261,6 +1264,14 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -6869,6 +6880,12 @@ "node": ">= 0.6.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "peer": true + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -6979,6 +6996,21 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "node_modules/react-hook-form": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", + "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", @@ -7839,6 +7871,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "peer": true + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -7888,6 +7926,12 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "peer": true + }, "node_modules/ts-api-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", @@ -7957,7 +8001,6 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, "engines": { "node": ">=12.20" }, @@ -8410,6 +8453,18 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.5.0.tgz", + "integrity": "sha512-NJfBIHnp1QbqZwxcgl6irnDMIsb/7d1prNhFx02f1kp8h+orpi4xs3w90szNpOh68a/iHPdMsYvhZWoDmUvXBQ==", + "peer": true, + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/package.json b/package.json index 57b4153..f7fa917 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@babylonlabs-io/bbn-core-ui", - "version": "0.4.1", + "version": "0.5.0", "type": "module", "types": "dist/index.d.ts", "publishConfig": { @@ -37,7 +37,8 @@ "peerDependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "tailwind-merge": "^2.5.4" + "tailwind-merge": "^2.5.4", + "yup": "^1.5.0" }, "devDependencies": { "@changesets/cli": "^2.27.9", @@ -87,7 +88,9 @@ ] }, "dependencies": { + "@hookform/resolvers": "^3.9.1", "@popperjs/core": "^2.11.8", + "react-hook-form": "^7.54.0", "react-popper": "^2.3.0" } } diff --git a/src/components/Form/FormControl.css b/src/components/Form/FormControl.css new file mode 100644 index 0000000..90d1700 --- /dev/null +++ b/src/components/Form/FormControl.css @@ -0,0 +1,23 @@ +.bbn-form-control { + @apply flex flex-col; + + &-title { + @apply mb-2 flex items-center text-sm text-primary-light; + } + + &-hint { + @apply mt-1 text-sm; + + &.bbn-form-control-hint-error { + @apply text-error-main; + } + + &.bbn-form-control-hint-warning { + @apply text-warning-main; + } + + &.bbn-form-control-hint-success { + @apply text-success-main; + } + } +} diff --git a/src/components/Form/FormControl.stories.tsx b/src/components/Form/FormControl.stories.tsx new file mode 100644 index 0000000..39ca1bf --- /dev/null +++ b/src/components/Form/FormControl.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { FormControl } from "./FormControl"; +import { Input } from "./Input"; + +const meta: Meta = { + component: FormControl, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: "Label", + children: , + hint: "Some random hint", + }, +}; + +export const WithoutLabel: Story = { + args: { + children: , + hint: "Some random error", + state: "error", + }, +}; + +export const WithError: Story = { + args: { + label: "Label", + children: , + hint: "Some random error", + state: "error", + }, +}; diff --git a/src/components/Form/FormControl.tsx b/src/components/Form/FormControl.tsx new file mode 100644 index 0000000..bc5bdd1 --- /dev/null +++ b/src/components/Form/FormControl.tsx @@ -0,0 +1,27 @@ +import { type PropsWithChildren } from "react"; +import { twJoin } from "tailwind-merge"; +import "./FormControl.css"; + +export interface FormControlProps extends PropsWithChildren { + label?: string | JSX.Element; + hint?: string | JSX.Element; + state?: "default" | "error" | "warning" | "success"; + className?: string; +} + +export function FormControl({ children, label, hint, state = "default", className }: FormControlProps) { + return ( +
+ {label ? ( + + ) : ( + children + )} + + {hint &&
{hint}
} +
+ ); +} diff --git a/src/components/Form/Input.css b/src/components/Form/Input.css index dcd2f13..8641d50 100644 --- a/src/components/Form/Input.css +++ b/src/components/Form/Input.css @@ -1,28 +1,24 @@ .bbn-input { - @apply relative flex flex-col; + @apply flex items-center rounded border border-primary-light/20 bg-secondary-contrast px-4 py-2 text-primary-light transition-colors; - &-wrapper { - @apply flex items-center rounded border border-primary-light/20 bg-secondary-contrast px-4 py-2 text-primary-light transition-colors; - - &:focus-within { - @apply border-primary-light; - } + &:focus-within { + @apply border-primary-light; + } - &.bbn-input-error { - @apply border-error-main; - } + &.bbn-input-error { + @apply border-error-main; + } - &.bbn-input-warning { - @apply border-warning-main; - } + &.bbn-input-warning { + @apply border-warning-main; + } - &.bbn-input-success { - @apply border-success-main; - } + &.bbn-input-success { + @apply border-success-main; + } - &.bbn-input-disabled { - @apply opacity-50 pointer-events-none; - } + &.bbn-input-disabled { + @apply pointer-events-none opacity-50; } &-field { @@ -36,4 +32,4 @@ &-prefix { @apply mr-2 flex items-center text-primary-light/50; } -} \ No newline at end of file +} diff --git a/src/components/Form/Input.stories.tsx b/src/components/Form/Input.stories.tsx index 946a4c5..cd11fef 100644 --- a/src/components/Form/Input.stories.tsx +++ b/src/components/Form/Input.stories.tsx @@ -28,14 +28,6 @@ export const Disabled: Story = { }, }; -export const WithError: Story = { - args: { - placeholder: "Input with error", - state: "error", - hint: "This field is required", - }, -}; - export const WithSuffix: Story = { args: { placeholder: "Search...", @@ -63,7 +55,7 @@ export const LoadingWithInteraction: Story = { + } diff --git a/src/components/Form/Input.tsx b/src/components/Form/Input.tsx index 1391358..1517277 100644 --- a/src/components/Form/Input.tsx +++ b/src/components/Form/Input.tsx @@ -1,33 +1,24 @@ import { forwardRef, type DetailedHTMLProps, type InputHTMLAttributes, type ReactNode } from "react"; import { twJoin } from "tailwind-merge"; import "./Input.css"; -import { FormControl } from "./components/FormControl"; export interface InputProps extends Omit, HTMLInputElement>, "prefix" | "suffix"> { className?: string; - wrapperClassName?: string; prefix?: ReactNode; suffix?: ReactNode; disabled?: boolean; state?: "default" | "error" | "warning"; - hint?: string; - label?: string; } export const Input = forwardRef( - ( - { className, wrapperClassName, prefix, suffix, disabled = false, state = "default", hint, label, ...props }, - ref, - ) => { + ({ className, prefix, suffix, disabled = false, state = "default", ...props }, ref) => { return ( - -
- {prefix &&
{prefix}
} - - {suffix &&
{suffix}
} -
-
+
+ {prefix &&
{prefix}
} + + {suffix &&
{suffix}
} +
); }, ); diff --git a/src/components/Form/Select.css b/src/components/Form/Select.css index a04b96c..dd8b13e 100644 --- a/src/components/Form/Select.css +++ b/src/components/Form/Select.css @@ -1,6 +1,5 @@ .bbn-select { @apply relative flex w-full cursor-pointer items-center justify-between rounded border border-primary-light bg-secondary-contrast px-4 py-2 text-sm text-primary-light outline-none transition-all; - width: var(--select-width, auto); &:focus-visible { @apply border-primary-light ring-1 ring-primary-light; diff --git a/src/components/Form/Select.stories.tsx b/src/components/Form/Select.stories.tsx index 56830e0..65d63b1 100644 --- a/src/components/Form/Select.stories.tsx +++ b/src/components/Form/Select.stories.tsx @@ -33,43 +33,6 @@ export const Controlled: Story = { }, }; -export const CenterAligned: Story = { - args: { - options, - placeholder: "Select status", - }, - render: (args) => ( -
- -
- ), -}; - -export const RightAligned: Story = { - args: { - placeholder: "Select status", - options, - }, - render: (args) => ( -
-
Title
- + + ); +}; + +const schema = yup + .object() + .shape({ + test: yup.string().required(), + }) + .required(); + +export const Default: Story = { + args: { + onChange: console.log, + schema, + }, + render: (props) => ( +
+ + + ), +}; diff --git a/src/widgets/Form/Form/Form.tsx b/src/widgets/Form/Form/Form.tsx new file mode 100644 index 0000000..96e7f3d --- /dev/null +++ b/src/widgets/Form/Form/Form.tsx @@ -0,0 +1,66 @@ +import { type PropsWithChildren, useEffect, HTMLProps } from "react"; +import { + type DefaultValues, + type Mode, + type SubmitHandler, + type DeepPartial, + FormProvider, + useForm, + Resolver, +} from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { type ObjectSchema } from "yup"; +import { twJoin } from "tailwind-merge"; + +export interface FormProps extends PropsWithChildren { + className?: string; + name?: string; + mode?: Mode; + reValidateMode?: Exclude; + defaultValues?: DefaultValues; + schema?: ObjectSchema; + formProps?: HTMLProps; + onSubmit?: SubmitHandler; + onChange?: (data: DeepPartial) => void; +} + +export function Form({ + className, + name, + children, + mode = "onBlur", + reValidateMode = "onBlur", + defaultValues, + schema, + formProps, + onSubmit = () => null, + onChange, +}: FormProps) { + const methods = useForm({ + mode, + reValidateMode, + defaultValues, + resolver: schema ? (yupResolver(schema) as unknown as Resolver) : undefined, + }); + + useEffect(() => { + if (!onChange) return; + + const { unsubscribe } = methods.watch(onChange); + + return unsubscribe; + }, [onChange, methods.watch]); + + return ( + +
+ {children} +
+
+ ); +} diff --git a/src/widgets/Form/Form/index.tsx b/src/widgets/Form/Form/index.tsx new file mode 100644 index 0000000..8897ad7 --- /dev/null +++ b/src/widgets/Form/Form/index.tsx @@ -0,0 +1,2 @@ +export { useFormContext, useFormState, useWatch } from "react-hook-form"; +export * from "./Form"; diff --git a/src/widgets/Form/NumberField/NumberField.stories.tsx b/src/widgets/Form/NumberField/NumberField.stories.tsx new file mode 100644 index 0000000..2f22ae0 --- /dev/null +++ b/src/widgets/Form/NumberField/NumberField.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import * as yup from "yup"; + +import { NumberField } from "./NumberField"; +import { Form } from "../Form"; + +const meta: Meta = { + component: NumberField, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const schema = yup + .object() + .shape({ + number_field: yup + .number() + .transform((value) => (Number.isNaN(value) ? null : value)) + .required(), + }) + .required(); + +export const Default: Story = { + args: { + label: "Number Field", + name: "number_field", + placeholder: "Default input", + hint: "Some random and useless hint", + defaultValue: "", + autoFocus: true, + }, + decorators: [ + (Story) => ( +
+ + + ), + ], +}; diff --git a/src/widgets/Form/NumberField/NumberField.tsx b/src/widgets/Form/NumberField/NumberField.tsx new file mode 100644 index 0000000..507781e --- /dev/null +++ b/src/widgets/Form/NumberField/NumberField.tsx @@ -0,0 +1,72 @@ +import { ChangeEventHandler, ReactNode } from "react"; + +import { FormControl, Input } from "@/components/Form"; +import type { FieldProps } from "../types"; +import { useField } from "../hooks"; + +export interface NumberFieldProps extends FieldProps { + type?: "text" | "hidden" | "number" | "password" | "tel" | "url" | "email"; + suffix?: ReactNode; + prefix?: JSX.Element; +} + +const NUMBER_REG_EXP = /^-?\d*\.?\d*$/; + +export function NumberField({ + name, + id = name, + label, + hint, + className, + controlClassName, + disabled, + autoFocus, + defaultValue, + placeholder, + type, + suffix, + prefix, + shouldUnregister, + state, +}: NumberFieldProps) { + const { + ref, + value = "", + error, + invalid, + onChange, + onBlur, + } = useField({ name, defaultValue, autoFocus, shouldUnregister }); + + const fieldState = invalid ? "error" : state; + const fieldHint = invalid ? error : hint; + + const handleChange: ChangeEventHandler = (e) => { + if (NUMBER_REG_EXP.test(e.target.value)) { + onChange(e.target.value); + } + }; + + return ( + + + + ); +} diff --git a/src/widgets/Form/NumberField/index.ts b/src/widgets/Form/NumberField/index.ts new file mode 100644 index 0000000..38ecedc --- /dev/null +++ b/src/widgets/Form/NumberField/index.ts @@ -0,0 +1 @@ +export * from "./NumberField"; diff --git a/src/widgets/Form/RadioField/RadioField.stories.tsx b/src/widgets/Form/RadioField/RadioField.stories.tsx new file mode 100644 index 0000000..fe647e1 --- /dev/null +++ b/src/widgets/Form/RadioField/RadioField.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Form } from "@/widgets/form/Form"; + +import { RadioField } from "./RadioField"; + +const meta: Meta = { + component: RadioField, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + name: "radio_field", + value: "test", + label: "Radio field", + }, + decorators: [ + (Story) => ( +
+ + + ), + ], +}; diff --git a/src/widgets/Form/RadioField/RadioField.tsx b/src/widgets/Form/RadioField/RadioField.tsx new file mode 100644 index 0000000..3912241 --- /dev/null +++ b/src/widgets/Form/RadioField/RadioField.tsx @@ -0,0 +1,38 @@ +import { type RadioProps, Radio } from "@/components/Form/Radio"; +import { useFormContext } from "react-hook-form"; + +export interface RadioFieldProps extends RadioProps { + name: string; + value: string; +} + +export function RadioField({ + name, + id = name, + label, + className, + disabled, + value, + defaultChecked, + labelClassName, + orientation, +}: RadioFieldProps) { + const { register } = useFormContext(); + const { name: inputName, ref, onChange, onBlur } = register(name); + + return ( + + ); +} diff --git a/src/widgets/Form/RadioField/index.ts b/src/widgets/Form/RadioField/index.ts new file mode 100644 index 0000000..119b257 --- /dev/null +++ b/src/widgets/Form/RadioField/index.ts @@ -0,0 +1 @@ +export * from "./RadioField"; diff --git a/src/widgets/Form/SelectField/SelectField.stories.tsx b/src/widgets/Form/SelectField/SelectField.stories.tsx new file mode 100644 index 0000000..dd07d61 --- /dev/null +++ b/src/widgets/Form/SelectField/SelectField.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import * as yup from "yup"; + +import { SelectField } from "./SelectField"; +import { Form } from "../Form"; + +const meta: Meta = { + component: SelectField, + tags: ["autodocs"], +}; + +export default meta; + +type Story = StoryObj; + +const schema = yup + .object() + .shape({ + select_field: yup.string().required(), + }) + .required(); + +export const Default: Story = { + args: { + label: "Select Field", + name: "select_field", + placeholder: "Select value", + hint: "Some random and useless hint", + defaultValue: "pending", + options: [ + { value: "", label: "None" }, + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + { value: "pending", label: "Pending" }, + ], + }, + decorators: [ + (Story) => ( +
+ + + ), + ], +}; diff --git a/src/widgets/Form/SelectField/SelectField.tsx b/src/widgets/Form/SelectField/SelectField.tsx new file mode 100644 index 0000000..28c75c4 --- /dev/null +++ b/src/widgets/Form/SelectField/SelectField.tsx @@ -0,0 +1,81 @@ +import { ReactNode } from "react"; + +import { type Option, FormControl, Select } from "@/components/Form"; +import type { FieldProps } from "../types"; +import { useField } from "../hooks"; + +export interface SelectFieldProps extends FieldProps { + open?: boolean; + defaultOpen?: boolean; + options: Option[]; + optionClassName?: string; + popoverClassName?: string; + onOpen?: () => void; + onClose?: () => void; + renderSelectedOption?: (option: Option) => ReactNode; +} + +export function SelectField({ + open, + defaultOpen, + options, + optionClassName, + popoverClassName, + name, + id = name, + label, + hint, + className, + controlClassName, + disabled, + autoFocus, + defaultValue, + placeholder, + shouldUnregister, + state, + onOpen, + onClose, + renderSelectedOption, +}: SelectFieldProps) { + const { + ref, + value = "", + error, + invalid, + onChange, + onBlur, + } = useField({ name, defaultValue, autoFocus, shouldUnregister }); + + const fieldState = invalid ? "error" : state; + const fieldHint = invalid ? error : hint; + + const handleClose = () => { + onBlur?.(); + onClose?.(); + }; + + return ( + + + + ); +} diff --git a/src/widgets/Form/TextField/index.ts b/src/widgets/Form/TextField/index.ts new file mode 100644 index 0000000..79aaa71 --- /dev/null +++ b/src/widgets/Form/TextField/index.ts @@ -0,0 +1 @@ +export * from "./TextField"; diff --git a/src/widgets/Form/hooks.ts b/src/widgets/Form/hooks.ts new file mode 100644 index 0000000..f860a38 --- /dev/null +++ b/src/widgets/Form/hooks.ts @@ -0,0 +1,35 @@ +import { useEffect } from "react"; +import { useController, useFormContext } from "react-hook-form"; + +interface FieldProps { + name: string; + defaultValue?: V; + disabled?: boolean; + autoFocus?: boolean; + shouldUnregister?: boolean; +} + +export function useField({ + name, + defaultValue, + disabled = false, + autoFocus = false, + shouldUnregister = false, +}: FieldProps) { + const { setFocus } = useFormContext(); + const { field, fieldState } = useController({ name, defaultValue, disabled, shouldUnregister }); + const { invalid, isTouched, error } = fieldState; + + useEffect(() => { + if (autoFocus) { + setFocus(name); + } + }, [name]); + + return { + ...field, + value: field.value as V, + invalid: invalid && isTouched, + error: error?.message ?? "", + }; +} diff --git a/src/widgets/Form/types.ts b/src/widgets/Form/types.ts new file mode 100644 index 0000000..bd50cd0 --- /dev/null +++ b/src/widgets/Form/types.ts @@ -0,0 +1,14 @@ +export interface FieldProps { + id?: string; + label?: string | JSX.Element; + name: string; + autoFocus?: boolean; + className?: string; + controlClassName?: string; + defaultValue?: string; + disabled?: boolean; + placeholder?: string; + hint?: string | JSX.Element; + shouldUnregister?: boolean; + state?: "default" | "error" | "warning"; +} diff --git a/vite.config.ts b/vite.config.ts index 17bcdd7..d1ad8b0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ fileName: (format) => `index.${format}.js`, }, rollupOptions: { - external: ["react", "react-dom", "react/jsx-runtime", "tailwind-merge"], + external: ["react", "react-dom", "react/jsx-runtime", "tailwind-merge", "yup"], output: { sourcemapExcludeSources: true, },