diff --git a/.changeset/spotty-flies-jump.md b/.changeset/spotty-flies-jump.md new file mode 100644 index 0000000000..3a86369b06 --- /dev/null +++ b/.changeset/spotty-flies-jump.md @@ -0,0 +1,6 @@ +--- +"@nextui-org/input-otp": minor +"@nextui-org/theme": minor +--- + +Adding new input-otp component. diff --git a/apps/docs/config/routes.json b/apps/docs/config/routes.json index 1d5500f469..8f482940f3 100644 --- a/apps/docs/config/routes.json +++ b/apps/docs/config/routes.json @@ -273,6 +273,13 @@ "keywords": "input, text box, form field, data entry", "path": "/docs/components/input.mdx" }, + { + "key": "input-otp", + "title": "Input OTP", + "keywords": "input, otp, auth", + "path": "/docs/components/input-otp.mdx", + "newPost": true + }, { "key": "kbd", "title": "Kbd", diff --git a/apps/docs/content/components/dropdown/variants.raw.jsx b/apps/docs/content/components/dropdown/variants.raw.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/docs/content/components/index.ts b/apps/docs/content/components/index.ts index 45cfacb811..ced4352a6f 100644 --- a/apps/docs/content/components/index.ts +++ b/apps/docs/content/components/index.ts @@ -18,6 +18,7 @@ export * from "./user"; export * from "./skeleton"; export * from "./snippet"; export * from "./input"; +export * from "./input-otp"; export * from "./textarea"; export * from "./image"; export * from "./radio-group"; diff --git a/apps/docs/content/components/input-otp/allowed-keys.raw.jsx b/apps/docs/content/components/input-otp/allowed-keys.raw.jsx new file mode 100644 index 0000000000..a9d2e828aa --- /dev/null +++ b/apps/docs/content/components/input-otp/allowed-keys.raw.jsx @@ -0,0 +1,22 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const exps = [ + { + name: "For below InputOtp, only lower-case alphabets (a to z) are allowed:", + value: "^[a-z]*$", + }, + {name: "For below InputOtp, only upper-case alphabets(A to Z) are allowed:", value: "^[A-Z]*$"}, + ]; + + return ( +
+ {exps.map((exp, idx) => ( +
+
{exp.name}
+ +
+ ))} +
+ ); +} diff --git a/apps/docs/content/components/input-otp/allowed-keys.ts b/apps/docs/content/components/input-otp/allowed-keys.ts new file mode 100644 index 0000000000..4b28d9836f --- /dev/null +++ b/apps/docs/content/components/input-otp/allowed-keys.ts @@ -0,0 +1,9 @@ +import App from "./allowed-keys.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/colors.raw.jsx b/apps/docs/content/components/input-otp/colors.raw.jsx new file mode 100644 index 0000000000..5523609028 --- /dev/null +++ b/apps/docs/content/components/input-otp/colors.raw.jsx @@ -0,0 +1,19 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const colors = ["default", "primary", "secondary", "success", "warning", "danger"]; + + return ( +
+ {colors.map((color) => ( +
+
color: {color}
+ +
+ ))} +
+ ); +} diff --git a/apps/docs/content/components/input-otp/colors.ts b/apps/docs/content/components/input-otp/colors.ts new file mode 100644 index 0000000000..d5bef810aa --- /dev/null +++ b/apps/docs/content/components/input-otp/colors.ts @@ -0,0 +1,9 @@ +import App from "./colors.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/controlled.raw.jsx b/apps/docs/content/components/input-otp/controlled.raw.jsx new file mode 100644 index 0000000000..941963a4f1 --- /dev/null +++ b/apps/docs/content/components/input-otp/controlled.raw.jsx @@ -0,0 +1,13 @@ +import {InputOtp} from "@nextui-org/react"; +import React from "react"; + +export default function App() { + const [value, setValue] = React.useState(""); + + return ( +
+ +

InputOtp value: {value}

+
+ ); +} diff --git a/apps/docs/content/components/input-otp/controlled.ts b/apps/docs/content/components/input-otp/controlled.ts new file mode 100644 index 0000000000..2c3f0cacb4 --- /dev/null +++ b/apps/docs/content/components/input-otp/controlled.ts @@ -0,0 +1,9 @@ +import App from "./controlled.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/description.raw.jsx b/apps/docs/content/components/input-otp/description.raw.jsx new file mode 100644 index 0000000000..54ac7feb36 --- /dev/null +++ b/apps/docs/content/components/input-otp/description.raw.jsx @@ -0,0 +1,9 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/description.ts b/apps/docs/content/components/input-otp/description.ts new file mode 100644 index 0000000000..aeb6340b6b --- /dev/null +++ b/apps/docs/content/components/input-otp/description.ts @@ -0,0 +1,9 @@ +import App from "./description.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/disabled.raw.jsx b/apps/docs/content/components/input-otp/disabled.raw.jsx new file mode 100644 index 0000000000..6af63e7e21 --- /dev/null +++ b/apps/docs/content/components/input-otp/disabled.raw.jsx @@ -0,0 +1,9 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/disabled.ts b/apps/docs/content/components/input-otp/disabled.ts new file mode 100644 index 0000000000..1a215cc91f --- /dev/null +++ b/apps/docs/content/components/input-otp/disabled.ts @@ -0,0 +1,9 @@ +import App from "./disabled.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/error-message.raw.jsx b/apps/docs/content/components/input-otp/error-message.raw.jsx new file mode 100644 index 0000000000..7dcfffbf1b --- /dev/null +++ b/apps/docs/content/components/input-otp/error-message.raw.jsx @@ -0,0 +1,13 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/error-message.ts b/apps/docs/content/components/input-otp/error-message.ts new file mode 100644 index 0000000000..fb8101b132 --- /dev/null +++ b/apps/docs/content/components/input-otp/error-message.ts @@ -0,0 +1,9 @@ +import App from "./error-message.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/index.ts b/apps/docs/content/components/input-otp/index.ts new file mode 100644 index 0000000000..2f9f87ce61 --- /dev/null +++ b/apps/docs/content/components/input-otp/index.ts @@ -0,0 +1,29 @@ +import usage from "./usage"; +import disabled from "./disabled"; +import readonly from "./readonly"; +import required from "./required"; +import sizes from "./sizes"; +import colors from "./colors"; +import variants from "./variants"; +import radius from "./radius"; +import description from "./description"; +import errorMessage from "./error-message"; +import allowedKeys from "./allowed-keys"; +import controlled from "./controlled"; +import password from "./password"; + +export const inputOtpContent = { + usage, + disabled, + readonly, + required, + sizes, + colors, + variants, + radius, + description, + errorMessage, + allowedKeys, + controlled, + password, +}; diff --git a/apps/docs/content/components/input-otp/password.raw.jsx b/apps/docs/content/components/input-otp/password.raw.jsx new file mode 100644 index 0000000000..223d1ec6f0 --- /dev/null +++ b/apps/docs/content/components/input-otp/password.raw.jsx @@ -0,0 +1,9 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/password.ts b/apps/docs/content/components/input-otp/password.ts new file mode 100644 index 0000000000..7751eaf935 --- /dev/null +++ b/apps/docs/content/components/input-otp/password.ts @@ -0,0 +1,9 @@ +import App from "./password.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/radius.raw.jsx b/apps/docs/content/components/input-otp/radius.raw.jsx new file mode 100644 index 0000000000..18ed4174f4 --- /dev/null +++ b/apps/docs/content/components/input-otp/radius.raw.jsx @@ -0,0 +1,19 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const radiusValues = ["none", "sm", "md", "lg", "full"]; + + return ( +
+ {radiusValues.map((radius) => ( +
+
radius: {radius}
+ +
+ ))} +
+ ); +} diff --git a/apps/docs/content/components/input-otp/radius.ts b/apps/docs/content/components/input-otp/radius.ts new file mode 100644 index 0000000000..7b78db1ce0 --- /dev/null +++ b/apps/docs/content/components/input-otp/radius.ts @@ -0,0 +1,9 @@ +import App from "./radius.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/readonly.raw.jsx b/apps/docs/content/components/input-otp/readonly.raw.jsx new file mode 100644 index 0000000000..2d70284f38 --- /dev/null +++ b/apps/docs/content/components/input-otp/readonly.raw.jsx @@ -0,0 +1,9 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/readonly.ts b/apps/docs/content/components/input-otp/readonly.ts new file mode 100644 index 0000000000..fabd05ba36 --- /dev/null +++ b/apps/docs/content/components/input-otp/readonly.ts @@ -0,0 +1,9 @@ +import App from "./readonly.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/required.raw.jsx b/apps/docs/content/components/input-otp/required.raw.jsx new file mode 100644 index 0000000000..30c521e995 --- /dev/null +++ b/apps/docs/content/components/input-otp/required.raw.jsx @@ -0,0 +1,9 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/content/components/input-otp/required.ts b/apps/docs/content/components/input-otp/required.ts new file mode 100644 index 0000000000..b50b781e6f --- /dev/null +++ b/apps/docs/content/components/input-otp/required.ts @@ -0,0 +1,9 @@ +import App from "./required.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/sizes.raw.jsx b/apps/docs/content/components/input-otp/sizes.raw.jsx new file mode 100644 index 0000000000..b3a3938afe --- /dev/null +++ b/apps/docs/content/components/input-otp/sizes.raw.jsx @@ -0,0 +1,19 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const sizes = ["sm", "md", "lg"]; + + return ( +
+ {sizes.map((size) => ( +
+
size: {size}
+ +
+ ))} +
+ ); +} diff --git a/apps/docs/content/components/input-otp/sizes.ts b/apps/docs/content/components/input-otp/sizes.ts new file mode 100644 index 0000000000..85a2f5b30b --- /dev/null +++ b/apps/docs/content/components/input-otp/sizes.ts @@ -0,0 +1,9 @@ +import App from "./sizes.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/usage.raw.jsx b/apps/docs/content/components/input-otp/usage.raw.jsx new file mode 100644 index 0000000000..c94601370f --- /dev/null +++ b/apps/docs/content/components/input-otp/usage.raw.jsx @@ -0,0 +1,14 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const [value, setValue] = React.useState(""); + + return ( +
+ +
+ OTP value: {value} +
+
+ ); +} diff --git a/apps/docs/content/components/input-otp/usage.ts b/apps/docs/content/components/input-otp/usage.ts new file mode 100644 index 0000000000..1118304c37 --- /dev/null +++ b/apps/docs/content/components/input-otp/usage.ts @@ -0,0 +1,9 @@ +import App from "./usage.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/components/input-otp/variants.raw.jsx b/apps/docs/content/components/input-otp/variants.raw.jsx new file mode 100644 index 0000000000..b61cb9f2c8 --- /dev/null +++ b/apps/docs/content/components/input-otp/variants.raw.jsx @@ -0,0 +1,19 @@ +import {InputOtp} from "@nextui-org/react"; + +export default function App() { + const variants = ["flat", "bordered", "underlined", "faded"]; + + return ( +
+ {variants.map((variant) => ( +
+
variant: {variant}
+ +
+ ))} +
+ ); +} diff --git a/apps/docs/content/components/input-otp/variants.ts b/apps/docs/content/components/input-otp/variants.ts new file mode 100644 index 0000000000..ddea95fb2e --- /dev/null +++ b/apps/docs/content/components/input-otp/variants.ts @@ -0,0 +1,9 @@ +import App from "./variants.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/docs/components/input-otp.mdx b/apps/docs/content/docs/components/input-otp.mdx new file mode 100644 index 0000000000..e1478727a7 --- /dev/null +++ b/apps/docs/content/docs/components/input-otp.mdx @@ -0,0 +1,186 @@ +--- +title: "Input OTP" +description: "InputOtp is a component that allows users to enter OTP." +--- + +import {inputOtpContent} from "@/content/components/input-otp"; + +# Input OTP + +InputOtp is a component that allows users to enter OTP. + +--- + + + +## Installation + + + + +## Import + + + +## Usage + + + +## Disabled + +Passing `isDisabled` property will make `input-otp` disabled. + + + +## Read Only + +Passing `isReadOnly` property will make `input-otp` read only. + + + +### Required + +Passing `isRequired` property will make `input-otp` required. + + + +### Sizes + +Size of the `input-otp` can be changed by `size` property. By default, `size` property is set to `md`. + + + +### Colors + +Color of the `input-otp` can be changed by `color` property. + + + +### Variants + +Styling/Variant of the `input-otp` can be changed by `variant` property. By default, `variant` property is set to `flat`. + + + +### Radius + +Radius of the `input-otp` can be changed by `radius` property. By default, `radius` property is set to `md`. + + + +### Password + +InputOtp can be used as password/secured-pin input by setting `type` as `password`. + + + +### Description + +Description of the `input-otp` can be set by `description` property. + + + +### Error Message + +Custom error message of the `input-otp` can be set by `errorMessage` property. + + + +### Allowed Keys + +* Users are only allowed to type certain keys. Any input other than allowed keys is simply ignored. +* Allowed Keys can be modified by `allowedKeys` property which accepts the regex of the keys which are allowed. +* By default, the value of `allowedKeys` is `^[0-9]*$` (i.e. only numerical digits are allowed). + + + +### Controlled + + + +## Slots + +- **base**: InputOtp wrapper, it handles alignment, placement, and general appearance. +- **wrapper**: Wraps the underlying input-otp component. Sent as `containerClassName` prop to underlying input-otp component. +- **input**: The input element. +- **segmentWrapper**: Wraps all the segment elements. +- **segment**: The segment element. +- **caret**: The caret represents the typing indicator of the InputOtp component. +- **passwordChar**: The passwordChar represents the text styling when input-type is password. +- **helperWrapper**: Wraps the `description` and the `errorMessage`. +- **description**: The description of the InputOtp. +- **errorMessage**: The error message of the InputOtp. + + + +## Data Attributes + +`InputOtp` has the following attributes on the `base` element: + +- **data-invalid**: + When the input-otp is invalid. Based on `isInvalid` prop. +- **data-required**: + When the input-otp is required. Based on `isRequired` prop. +- **data-readonly**: + When the input-otp is readonly. Based on `isReadOnly` prop. +- **data-filled**: + When the input-otp is completely filled. +- **data-disabled**: + When the input is disabled. Based on `isDisabled` prop. + + + +## Accessibility + +- Built on top of [input-otp](https://github.com/guilhermerodz/input-otp). +- Required and invalid states exposed to assistive technology via ARIA. +- Support for description and error message help text linked to the input-otp via ARIA. + + + +## API + +### InputOtp Props + +| Attribute | Type | Description | Default | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| length | `number` | The length of the input-otp. | `4` | +| allowedKeys | `regEx string` | The allowed keys for the input-otp. | `^[0-9]*$`| +| variant | `flat` \| `bordered` \| `faded` \| `underlined` | The variant of the input-otp. | `flat` | +| color | `default` \| `primary` \| `secondary` \| `success` \| `warning` \| `danger` | The color of the input-otp. | `default` | +| size | `sm` \| `md` \| `lg` | The size of the input-otp. | `md` | +| radius | `none` \| `sm` \| `md` \| `lg` \| `full` | The radius of the input-otp. | - | +| value | `string` | The current value of the input-otp (controlled). | - | +| defaultValue | `string` | The default value of the input-otp (uncontrolled). | - | +| description | `ReactNode` | A description for the input. Provides a hint such as specific requirements for what to choose. | - | +| errorMessage | `ReactNode` \| `((v: ValidationResult) => ReactNode)` | An error message for the input-otp. It is only shown when `isInvalid` is set to `true` | - | | The position of the label. | `inside` | +| fullWidth | `boolean` | Whether the input-otp should take up the width of its parent. | `false` | +| isRequired | `boolean` | Whether user input-otp is required on the input before form submission. | `false` | +| isReadOnly | `boolean` | Whether the input-otp can be selected but not changed by the user. | | +| isDisabled | `boolean` | Whether the input-otp is disabled. | `false` | +| isInvalid | `boolean` | Whether the input-otp is invalid. | `false` | +| baseRef | `RefObject` | The ref to the base element. | - | +| disableAnimation | `boolean` | Whether the input-otp should be animated. | `false` | +| classNames | `Record<"base"| "inputWrapper"| "input"| "segmentWrapper"| "segment" | "caret" | "passwordChar" | "helperWrapper" | "description" | "errorMessage", string>` | Allows to set custom class names for the Input slots. | - | + +### InputOtp Events + +| Attribute | Type | Description | +| ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| onChange | `React.ChangeEvent` | Handler that is called when the element's value changes. You can pull out the new value by accessing `event.target.value` (string). | +| onValueChange | `(value: string) => void` | Handler that is called when the element's value changes. | +| onComplete | `(value: string) => void` | Handler that is called when the element's value is completely filled. | diff --git a/apps/docs/package.json b/apps/docs/package.json index 01e4bc4aba..b135d859ac 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -26,6 +26,7 @@ "@nextui-org/divider": "workspace:*", "@nextui-org/kbd": "workspace:*", "@nextui-org/listbox": "workspace:*", + "@nextui-org/input-otp": "workspace:*", "@nextui-org/react": "workspace:*", "@nextui-org/shared-icons": "workspace:*", "@nextui-org/shared-utils": "workspace:*", diff --git a/apps/docs/public/sitemap-0.xml b/apps/docs/public/sitemap-0.xml index 09725255fe..4d60321842 100644 --- a/apps/docs/public/sitemap-0.xml +++ b/apps/docs/public/sitemap-0.xml @@ -1,65 +1,77 @@ -https://nextui.org/feed.xml2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/figma2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/accordion2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/autocomplete2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/avatar2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/badge2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/breadcrumbs2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/button2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/card2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/checkbox-group2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/checkbox2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/chip2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/circular-progress2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/code2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/divider2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/dropdown2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/image2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/input2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/kbd2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/link2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/listbox2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/modal2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/navbar2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/pagination2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/popover2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/progress2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/radio-group2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/scroll-shadow2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/select2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/skeleton2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/slider2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/snippet2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/spacer2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/spinner2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/switch2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/table2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/tabs2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/textarea2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/tooltip2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/components/user2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/frameworks/astro2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/frameworks/nextjs2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/frameworks/remix2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/frameworks/vite2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/guide/design-principles2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/guide/installation2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/guide/introduction2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/guide/routing2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/guide/upgrade-to-v22024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/colors2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/create-theme2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/custom-variants2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/customize-theme2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/dark-mode2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/layout2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/override-styles2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/docs/customization/theme2024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/blog/nextui-v22024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/blog/v2.1.02024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/blog/v2.2.02024-02-02T20:24:45.107Zdaily0.7 -https://nextui.org/blog2024-02-02T20:24:45.107Zdaily0.7 +https://nextui.org/feed.xml2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/figma2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog/nextui-v22024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog/v2.1.02024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog/v2.2.02024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog/v2.3.02024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog/v2.4.02024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/api-references/cli-api2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/api-references/nextui-provider2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/colors2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/create-theme2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/custom-variants2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/customize-theme2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/dark-mode2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/layout2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/override-styles2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/customization/theme2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/accordion2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/autocomplete2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/avatar2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/badge2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/breadcrumbs2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/button2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/calendar2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/card2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/checkbox-group2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/checkbox2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/chip2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/circular-progress2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/code2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/date-input2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/date-picker2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/date-range-picker2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/divider2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/dropdown2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/image2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/input-otp2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/input2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/kbd2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/link2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/listbox2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/modal2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/navbar2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/pagination2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/popover2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/progress2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/radio-group2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/range-calendar2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/scroll-shadow2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/select2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/skeleton2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/slider2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/snippet2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/spacer2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/spinner2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/switch2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/table2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/tabs2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/textarea2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/time-input2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/tooltip2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/components/user2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/frameworks/astro2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/frameworks/nextjs2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/frameworks/remix2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/frameworks/vite2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/cli2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/design-principles2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/installation2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/introduction2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/routing2024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/docs/guide/upgrade-to-v22024-10-23T19:45:31.239Zdaily0.7 +https://nextui.org/blog2024-10-23T19:45:31.239Zdaily0.7 \ No newline at end of file diff --git a/packages/components/input-otp/README.md b/packages/components/input-otp/README.md new file mode 100644 index 0000000000..61442f0d22 --- /dev/null +++ b/packages/components/input-otp/README.md @@ -0,0 +1,26 @@ +# @nextui-org/input-otp + +InputOTP is a component that allows users to enter otp input. It can be used to get user otp in forms. + +This package contains the InputOTPcomponent. + +Please refer to the [documentation](https://nextui.org/docs/components/input-otp) for more information. + +## Installation + +```sh +yarn add @nextui-org/input-otp +# or +npm i @nextui-org/input-otp +``` + +## Contribution + +Yes please! See the +[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md) +for details. + +## License + +This project is licensed under the terms of the +[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE). diff --git a/packages/components/input-otp/__tests__/input-otp.test.tsx b/packages/components/input-otp/__tests__/input-otp.test.tsx new file mode 100644 index 0000000000..fdde22f4d6 --- /dev/null +++ b/packages/components/input-otp/__tests__/input-otp.test.tsx @@ -0,0 +1,195 @@ +import * as React from "react"; +import {render, renderHook, screen} from "@testing-library/react"; +import {useForm} from "react-hook-form"; +import userEvent, {UserEvent} from "@testing-library/user-event"; + +import {InputOtp} from "../src"; + +// Mock document.elementFromPoint to avoid test environment errors +beforeAll(() => { + document.elementFromPoint = jest.fn(() => { + const mockElement = document.createElement("div"); + + return mockElement; + }); +}); + +describe("InputOtp Component", () => { + let user: UserEvent; + + beforeAll(() => { + user = userEvent.setup(); + }); + + it("should render correctly", () => { + const wrapper = render(); + + expect(() => wrapper.unmount()).not.toThrow(); + }); + + it("should forward ref correctly", () => { + const ref = React.createRef(); + + render(); + expect(ref.current).not.toBeNull(); + }); + + it("should create segments according to length prop", () => { + render(); + const segments = screen.getAllByRole("presentation"); + + expect(segments.length).toBe(5); + }); + + it("should display error message when isInvalid is true", () => { + const errorMessage = "custom error message"; + + render(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it("should display description message", () => { + const descriptionMessage = "custom description message"; + + render(); + expect(screen.getByText(descriptionMessage)).toBeInTheDocument(); + }); + + it("should not focus when disabled", async () => { + render(); + const input = screen.getByRole("textbox"); + + await user.click(input); + expect(input).not.toHaveAttribute("data-focus", "true"); + }); + + it("should activate the first segment on click", async () => { + render(); + const input = screen.getByRole("textbox"); + const segments = screen.getAllByRole("presentation"); + + await user.click(input); + expect(segments[0]).toHaveAttribute("data-active", "true"); + expect(segments[1]).not.toHaveAttribute("data-active"); + }); + + it("should move focus to the next segment on valid input", async () => { + render(); + const input = screen.getByRole("textbox"); + const segments = screen.getAllByRole("presentation"); + + await user.click(input); + expect(segments[1]).not.toHaveAttribute("data-active"); + + await user.keyboard("1"); + expect(segments[1]).toHaveAttribute("data-active", "true"); + expect(input).toHaveAttribute("value", "1"); + }); + + it("should clear input on backspace", async () => { + render(); + const input = screen.getByRole("textbox"); + const segments = screen.getAllByRole("presentation"); + + await user.click(input); + await user.keyboard("12"); + expect(input).toHaveAttribute("value", "12"); + expect(segments[2]).toHaveAttribute("data-active", "true"); + + await user.keyboard("[Backspace]"); + expect(input).toHaveAttribute("value", "1"); + expect(segments[1]).toHaveAttribute("data-active", "true"); + }); + + it("should paste values", async () => { + render(); + const input = screen.getByRole("textbox"); + + await user.click(input); + await user.paste("1234"); + expect(input).toHaveAttribute("value", "1234"); + }); + + it("should restrict non-allowed inputs", async () => { + render(); + const input = screen.getByRole("textbox"); + const segments = screen.getAllByRole("presentation"); + + await user.click(input); + await user.keyboard("a"); + expect(input).toHaveAttribute("value", ""); + expect(segments[0]).toHaveAttribute("data-active", "true"); + }); + + it("should allow inputs based on custom regex", async () => { + const regEx = "^[a-z]*$"; + + render(); + const input = screen.getByRole("textbox"); + + await user.click(input); + await user.keyboard("a"); + expect(input).toHaveAttribute("value", "a"); + }); + + it("should call onComplete when all segments are filled", async () => { + const onComplete = jest.fn(); + + render(); + const input = screen.getByRole("textbox"); + + await user.click(input); + await user.paste("1234"); + expect(onComplete).toHaveBeenCalledTimes(1); + }); +}); + +describe("InputOtp with react-hook-form", () => { + let user: UserEvent; + + beforeAll(() => { + user = userEvent.setup(); + }); + + it("should integrate with react-hook-form correctly", async () => { + const {result} = renderHook(() => + useForm({ + defaultValues: { + defaultValue: "1234", + withoutDefaultValue: "", + requiredField: "", + }, + }), + ); + + const { + handleSubmit, + register, + formState: {errors}, + } = result.current; + const onSubmit = jest.fn(); + + render( +
+ + + + {errors.requiredField && This field is required} + + , + ); + + await user.click(screen.getByText(/Submit/i)); + expect(onSubmit).toHaveBeenCalledTimes(0); + + const inputOtp3 = screen.getAllByRole("textbox")[2]; + + await user.type(inputOtp3, "1234"); + await user.click(screen.getByText(/Submit/i)); + expect(onSubmit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/components/input-otp/package.json b/packages/components/input-otp/package.json new file mode 100644 index 0000000000..0008d78683 --- /dev/null +++ b/packages/components/input-otp/package.json @@ -0,0 +1,61 @@ +{ + "name": "@nextui-org/input-otp", + "version": "2.0.0", + "description": "", + "keywords": [ + "input-otp" + ], + "author": "Junior Garcia ", + "homepage": "https://nextui.org", + "license": "MIT", + "main": "src/index.ts", + "sideEffects": false, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nextui-org/nextui.git", + "directory": "packages/components/input-otp" + }, + "bugs": { + "url": "https://github.com/nextui-org/nextui/issues" + }, + "scripts": { + "build": "tsup src --dts", + "build:fast": "tsup src", + "dev": "pnpm build:fast --watch", + "clean": "rimraf dist .turbo", + "typecheck": "tsc --noEmit", + "prepack": "clean-package", + "postpack": "clean-package restore" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "@nextui-org/theme": ">=2.0.0", + "@nextui-org/system": ">=2.0.0" + }, + "dependencies": { + "@nextui-org/shared-utils": "workspace:*", + "@nextui-org/react-utils": "workspace:*", + "@react-aria/utils": "3.24.1", + "@react-aria/form": "3.0.8", + "@react-stately/utils": "3.10.1", + "@react-stately/form": "3.0.5", + "@react-types/textfield": "3.9.3", + "input-otp": "1.4.1" + }, + "devDependencies": { + "@nextui-org/theme": "workspace:*", + "@nextui-org/system": "workspace:*", + "clean-package": "2.2.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-hook-form": "^7.51.3" + }, + "clean-package": "../../../clean-package.config.json" +} diff --git a/packages/components/input-otp/src/index.ts b/packages/components/input-otp/src/index.ts new file mode 100644 index 0000000000..3d5502027c --- /dev/null +++ b/packages/components/input-otp/src/index.ts @@ -0,0 +1,10 @@ +import InputOtp from "./input-otp"; + +// export types +export type {InputOtpProps} from "./input-otp"; + +// export hooks +export {useInputOtp} from "./use-input-otp"; + +// export component +export {InputOtp}; diff --git a/packages/components/input-otp/src/input-otp-context.ts b/packages/components/input-otp/src/input-otp-context.ts new file mode 100644 index 0000000000..18779ac3d4 --- /dev/null +++ b/packages/components/input-otp/src/input-otp-context.ts @@ -0,0 +1,9 @@ +import {createContext} from "@nextui-org/react-utils"; + +import {UseInputOtpReturn} from "./use-input-otp"; + +export const [InputOtpProvider, useInputOtpContext] = createContext({ + name: "InputOtpContext", + errorMessage: + "useInputOtpContext: `context` is undefined. Seems like you forgot to wrap all input-otp components within ``", +}); diff --git a/packages/components/input-otp/src/input-otp-segment.tsx b/packages/components/input-otp/src/input-otp-segment.tsx new file mode 100644 index 0000000000..9a106f63b4 --- /dev/null +++ b/packages/components/input-otp/src/input-otp-segment.tsx @@ -0,0 +1,40 @@ +import {SlotProps} from "input-otp"; +import {useMemo} from "react"; +import {clsx, dataAttr} from "@nextui-org/shared-utils"; + +import {useInputOtpContext} from "./input-otp-context"; + +export const InputOtpSegment = (props: SlotProps) => { + const {classNames, slots, type} = useInputOtpContext(); + + const passwordCharStyles = clsx(classNames?.passwordChar); + const caretStyles = clsx(classNames?.caret); + const segmentStyles = clsx(classNames?.segment); + + const displayValue = useMemo(() => { + if (props.isActive && !props.char) { + return
; + } + if (props.char) { + return type === "password" ? ( +
+ ) : ( +
{props.char}
+ ); + } + + return
{props.placeholderChar}
; + }, [props.char, props.isActive, type]); + + return ( +
+ {displayValue} +
+ ); +}; diff --git a/packages/components/input-otp/src/input-otp.tsx b/packages/components/input-otp/src/input-otp.tsx new file mode 100644 index 0000000000..8e8f0cf351 --- /dev/null +++ b/packages/components/input-otp/src/input-otp.tsx @@ -0,0 +1,84 @@ +import {forwardRef} from "@nextui-org/system"; +import {useMemo} from "react"; +import {OTPInput} from "input-otp"; +import {clsx} from "@nextui-org/shared-utils"; + +import {UseInputOtpProps, useInputOtp} from "./use-input-otp"; +import {InputOtpProvider} from "./input-otp-context"; +import {InputOtpSegment} from "./input-otp-segment"; + +export interface InputOtpProps extends UseInputOtpProps {} + +const InputOtp = forwardRef<"div", InputOtpProps>((props, ref) => { + const context = useInputOtp({...props, ref}); + + const { + Component, + length, + hasHelper, + isInvalid, + errorMessage, + description, + slots, + classNames, + getBaseProps, + getInputOtpProps, + getSegmentWrapperProps, + getHelperWrapperProps, + getErrorMessageProps, + getDescriptionProps, + } = context; + + const helperSection = useMemo(() => { + if (!hasHelper) { + return null; + } + + return ( +
+ {isInvalid && errorMessage ? ( +
{errorMessage}
+ ) : ( +
{description}
+ )} +
+ ); + }, [ + hasHelper, + isInvalid, + errorMessage, + description, + getHelperWrapperProps, + getErrorMessageProps, + getDescriptionProps, + ]); + + const wrapperStyles = clsx(classNames?.wrapper); + + return ( + + + ( +
+ {slots.map((slot, idx) => ( + + ))} +
+ )} + {...getInputOtpProps()} + data-slot="input" + role="textbox" + /> + {helperSection} +
+
+ ); +}); + +InputOtp.displayName = "NextUI.InputOtp"; + +export default InputOtp; diff --git a/packages/components/input-otp/src/use-input-otp.ts b/packages/components/input-otp/src/use-input-otp.ts new file mode 100644 index 0000000000..f437d67622 --- /dev/null +++ b/packages/components/input-otp/src/use-input-otp.ts @@ -0,0 +1,284 @@ +import type { + InputOtpReturnType, + InputOtpSlots, + InputOtpVariantProps, + SlotsToClasses, +} from "@nextui-org/theme"; + +import { + HTMLNextUIProps, + mapPropsVariants, + PropGetter, + useProviderContext, +} from "@nextui-org/system"; +import {inputOtp} from "@nextui-org/theme"; +import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; +import {clsx, dataAttr, objectToDeps} from "@nextui-org/shared-utils"; +import {useCallback, useMemo} from "react"; +import {chain, mergeProps} from "@react-aria/utils"; +import {AriaTextFieldProps} from "@react-types/textfield"; +import {useControlledState} from "@react-stately/utils"; +import {useFormValidationState} from "@react-stately/form"; +import {useFormValidation} from "@react-aria/form"; + +interface Props extends HTMLNextUIProps<"div"> { + /** + * Ref to the DOM node. + */ + ref?: ReactRef; + /** + * Ref to the container DOM node. + */ + baseRef?: ReactRef; + /** + * Length required for the otp. + */ + length: number; + /** + * Regex string for the allowed keys. + */ + allowedKeys?: string; + /** + * Callback that will be fired when the value has length equal to otp length + */ + onComplete?: (v?: string) => void; + /** + * Boolean to disable the input-otp component. + */ + isDisabled?: boolean; + /** + * Boolean to disable the animation in input-otp component. + */ + disableAnimation?: boolean; + /** + * Classname or List of classes to change the classNames of the element. + * if `className` is passed, it will be added to the base slot. + * + * @example + * ```ts + * + * ``` + */ + classNames?: SlotsToClasses; + /** + * React aria onChange event. + */ + onValueChange?: (value: string) => void; +} + +export type ValueTypes = { + slots: InputOtpReturnType; + classNames: SlotsToClasses; +}; + +export type UseInputOtpProps = Props & InputOtpVariantProps & Omit; + +export function useInputOtp(originalProps: UseInputOtpProps) { + const globalContext = useProviderContext(); + const [props, variantProps] = mapPropsVariants(originalProps, inputOtp.variantKeys); + + const { + ref, + baseRef, + as, + className, + classNames, + length = 4, + onComplete = () => {}, + onValueChange = () => {}, + allowedKeys = "^[0-9]*$", + validationBehavior = globalContext?.validationBehavior ?? "aria", + type, + name, + } = props; + + const Component = as || "div"; + + const inputRef = useDOMRef(ref); + const baseDomRef = useDOMRef(baseRef); + + const handleValueChange = useCallback( + (value: string | undefined) => { + onValueChange(value ?? ""); + }, + [onValueChange], + ); + + const [value, setValue] = useControlledState( + props.value, + props.defaultValue ?? "", + handleValueChange, + ); + + const disableAnimation = + originalProps.disableAnimation ?? globalContext?.disableAnimation ?? false; + const isDisabled = originalProps.isDisabled; + const baseStyles = clsx(classNames?.base, className); + + const validationState = useFormValidationState({ + ...props, + validationBehavior, + value, + }); + + useFormValidation(props, validationState, inputRef); + + const { + isInvalid: isAriaInvalid, + validationErrors, + validationDetails, + } = validationState.displayValidation; + const isReadOnly = originalProps.isReadOnly; + const isRequired = originalProps.isRequired; + const isInvalid = originalProps.isInvalid || isAriaInvalid; + const errorMessage = + typeof props.errorMessage === "function" + ? props.errorMessage({isInvalid, validationErrors, validationDetails}) + : props.errorMessage || validationErrors?.join(" "); + + const description = props.description; + const hasHelper = !!description || !!errorMessage; + + const slots = useMemo( + () => + inputOtp({ + ...variantProps, + disableAnimation, + isInvalid, + isReadOnly, + }), + [objectToDeps(variantProps), disableAnimation, isInvalid], + ); + + const getBaseProps: PropGetter = useCallback( + (props = {}) => { + return { + ref: baseDomRef, + className: slots.base({ + class: baseStyles, + }), + "data-slot": "base", + "data-disabled": dataAttr(isDisabled), + "data-invalid": dataAttr(isInvalid), + "data-required": dataAttr(originalProps?.isRequired), + "data-readonly": dataAttr(originalProps?.isReadOnly), + "data-filled": dataAttr(value.length === length), + role: "base", + ...props, + }; + }, + [baseDomRef, slots, baseStyles, isDisabled], + ); + + const getInputOtpProps = useCallback( + () => ({ + required: isRequired, + disabled: isDisabled, + readOnly: isReadOnly, + pattern: allowedKeys, + ref: inputRef, + name: name, + value: value, + onChange: chain(setValue), + onBlur: props.onBlur, + onComplete: onComplete, + }), + [ + isRequired, + isDisabled, + isReadOnly, + allowedKeys, + inputRef, + name, + length, + props.onChange, + setValue, + props.onBlur, + onComplete, + ], + ); + + const getSegmentWrapperProps: PropGetter = useCallback( + (props = {}) => { + return { + className: slots.segmentWrapper({ + class: clsx(classNames?.segmentWrapper, props?.className), + }), + "data-slot": "segment-wrapper", + "data-disabled": dataAttr(isDisabled), + ...props, + }; + }, + [slots, classNames?.segmentWrapper, isDisabled], + ); + + const getHelperWrapperProps: PropGetter = useCallback( + (props = {}) => { + return { + className: slots.helperWrapper({ + class: clsx(classNames?.helperWrapper, props?.className), + }), + "data-slot": "helper-wrapper", + ...props, + }; + }, + [slots, classNames?.helperWrapper], + ); + + const getErrorMessageProps: PropGetter = useCallback( + (props = {}) => { + return { + className: slots.errorMessage({ + class: clsx(classNames?.errorMessage, props?.className), + }), + "data-slot": "error-message", + ...mergeProps(props), + }; + }, + [slots, classNames?.errorMessage], + ); + + const getDescriptionProps: PropGetter = useCallback( + (props = {}) => { + return { + className: slots.description({ + class: clsx(classNames?.description, props?.className), + }), + "data-slot": "description", + ...mergeProps(props), + }; + }, + [slots, classNames?.description], + ); + + return { + Component, + inputRef, + length, + value, + classNames, + slots, + hasHelper, + isInvalid, + description, + errorMessage, + type, + getBaseProps, + getInputOtpProps, + getSegmentWrapperProps, + getHelperWrapperProps, + getErrorMessageProps, + getDescriptionProps, + }; +} + +export type UseInputOtpReturn = ReturnType; diff --git a/packages/components/input-otp/stories/input-otp.stories.tsx b/packages/components/input-otp/stories/input-otp.stories.tsx new file mode 100644 index 0000000000..9eedd6669d --- /dev/null +++ b/packages/components/input-otp/stories/input-otp.stories.tsx @@ -0,0 +1,271 @@ +import React from "react"; +import {Meta} from "@storybook/react"; +import {button, inputOtp} from "@nextui-org/theme"; +import {useForm} from "react-hook-form"; +import {ValidationResult} from "@react-types/shared"; + +import {InputOtp} from "../src"; + +export default { + title: "Components/InputOtp", + component: InputOtp, + argTypes: { + variant: { + control: {type: "select"}, + options: ["flat", "faded", "bordered", "underlined"], + }, + color: { + control: {type: "select"}, + options: ["default", "primary", "secondary", "success", "warning", "danger"], + }, + radius: { + control: {type: "select"}, + options: ["none", "sm", "md", "lg", "full"], + }, + size: { + control: {type: "select"}, + options: ["sm", "md", "lg"], + }, + isDisabled: { + control: { + type: "boolean", + }, + }, + validationBehavior: { + control: { + type: "select", + }, + options: ["aria", "native"], + }, + }, +} as Meta; + +const defaultProps = { + ...inputOtp.defaultVariants, +}; + +const Template = (args) => ( +
+ +
+); + +const RequiredTemplate = (args) => { + const { + register, + formState: {errors}, + handleSubmit, + } = useForm({ + defaultValues: { + otp: "", + }, + }); + + const onSubmit = (data: any) => { + // eslint-disable-next-line no-console + alert("Submitted value: " + JSON.stringify(data)); + }; + + return ( +
+
+ + {errors.otp &&
This field is required
} + + +
+ ); +}; + +const ErrorMessageFunctionTemplate = (args) => { + const {register, handleSubmit} = useForm({ + defaultValues: { + otp: "", + }, + }); + + const onSubmit = (data: any) => { + // eslint-disable-next-line no-console + alert("Submitted value: " + JSON.stringify(data)); + }; + + return ( +
+
+ + + +
+ ); +}; + +const ControlledTemplate = (args) => { + const [value, setValue] = React.useState(""); + + return ( +
+ +

Input value: {value}

+
+ ); +}; + +const WithReactHookFormTemplate = (args) => { + const { + register, + formState: {errors}, + handleSubmit, + } = useForm({ + defaultValues: { + withDefaultValue: "12", + requiredField: "", + }, + }); + + const onSubmit = (data: any) => { + // eslint-disable-next-line no-console + console.log(data); + alert("Submitted value: " + JSON.stringify(data)); + }; + + return ( +
+
+
Default value:
+ +
+
+
Required value:
+ + {errors.requiredField && ( + This field is required + )} +
+ +
+ ); +}; + +export const Default = { + render: Template, + args: { + ...defaultProps, + length: 4, + }, +}; + +export const Required = { + render: RequiredTemplate, + args: { + ...defaultProps, + length: 4, + }, +}; + +export const Disabled = { + render: Template, + args: { + ...defaultProps, + length: 4, + defaultValue: "123", + isDisabled: true, + }, +}; + +export const ReadOnly = { + render: Template, + args: { + ...defaultProps, + length: 4, + value: "12", + isReadOnly: true, + }, +}; + +export const WithDescription = { + render: Template, + args: { + ...defaultProps, + length: 4, + description: "description for the otp component", + }, +}; + +export const WithErrorMessage = { + render: Template, + args: { + ...defaultProps, + length: 4, + isInvalid: true, + errorMessage: "Please enter a valid OTP.", + }, +}; + +export const WithErrorMessageFunction = { + render: ErrorMessageFunctionTemplate, + args: { + ...defaultProps, + length: 4, + isRequired: true, + minLength: 4, + errorMessage: (value: ValidationResult) => { + if (value.validationDetails.tooShort) { + return "Value is too short"; + } + }, + }, +}; + +export const Password = { + render: RequiredTemplate, + args: { + ...defaultProps, + type: "password", + }, +}; + +export const isInvalid = { + render: Template, + args: { + ...defaultProps, + length: 4, + isInvalid: true, + errorMessage: "Invalid OTP", + }, +}; + +export const Controlled = { + render: ControlledTemplate, + args: { + ...defaultProps, + }, +}; + +export const WithReactHookForm = { + render: WithReactHookFormTemplate, + args: { + ...defaultProps, + length: 4, + }, +}; + +export const CustomWithClassNames = { + render: Template, + + args: { + ...defaultProps, + length: 4, + classNames: { + segment: "bg-gradient-to-tr from-pink-500 to-yellow-500", + caret: "bg-red-700", + }, + radius: "md", + description: "custom otp component", + }, +}; diff --git a/packages/components/input-otp/tsconfig.json b/packages/components/input-otp/tsconfig.json new file mode 100644 index 0000000000..5d012f6e61 --- /dev/null +++ b/packages/components/input-otp/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "tailwind-variants": ["../../../node_modules/tailwind-variants"] + }, + }, + "include": ["src", "index.ts"] +} diff --git a/packages/components/input-otp/tsup.config.ts b/packages/components/input-otp/tsup.config.ts new file mode 100644 index 0000000000..3e2bcff6cc --- /dev/null +++ b/packages/components/input-otp/tsup.config.ts @@ -0,0 +1,8 @@ +import {defineConfig} from "tsup"; + +export default defineConfig({ + clean: true, + target: "es2019", + format: ["cjs", "esm"], + banner: {js: '"use client";'}, +}); diff --git a/packages/core/react/package.json b/packages/core/react/package.json index 5513fa10f0..9d2cc0b0d0 100644 --- a/packages/core/react/package.json +++ b/packages/core/react/package.json @@ -62,6 +62,7 @@ "@nextui-org/user": "workspace:*", "@nextui-org/progress": "workspace:*", "@nextui-org/input": "workspace:*", + "@nextui-org/input-otp": "workspace:*", "@nextui-org/popover": "workspace:*", "@nextui-org/dropdown": "workspace:*", "@nextui-org/image": "workspace:*", diff --git a/packages/core/react/src/index.ts b/packages/core/react/src/index.ts index cdaab4d346..038933be97 100644 --- a/packages/core/react/src/index.ts +++ b/packages/core/react/src/index.ts @@ -45,6 +45,7 @@ export * from "@nextui-org/date-input"; export * from "@nextui-org/date-picker"; export * from "@nextui-org/alert"; export * from "@nextui-org/drawer"; +export * from "@nextui-org/input-otp"; /** * React Aria - Exports diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts index bd7aa182b2..20be0fb2db 100644 --- a/packages/core/theme/src/components/index.ts +++ b/packages/core/theme/src/components/index.ts @@ -17,6 +17,7 @@ export * from "./toggle"; export * from "./accordion"; export * from "./progress"; export * from "./circular-progress"; +export * from "./input-otp"; export * from "./input"; export * from "./dropdown"; export * from "./image"; diff --git a/packages/core/theme/src/components/input-otp.ts b/packages/core/theme/src/components/input-otp.ts new file mode 100644 index 0000000000..db359587e3 --- /dev/null +++ b/packages/core/theme/src/components/input-otp.ts @@ -0,0 +1,458 @@ +import type {VariantProps} from "tailwind-variants"; + +import {tv} from "../utils/tv"; + +const inputOtp = tv({ + slots: { + base: ["relative", "flex", "flex-col", "w-fit"], + wrapper: ["group", "flex items-center", "has-[:disabled]:opacity-60"], + input: [ + "absolute", + "inset-0", + "border-none", + "outline-none", + "bg-transparent", + "text-transparent", + ], + segmentWrapper: ["inline-flex", "gap-x-1", "py-2"], + segment: [ + "h-10", + "w-10", + "font-semibold", + "flex", + "justify-center", + "items-center", + "border-default-200", + "data-[active=true]:border-default-400", + "data-[active=true]:scale-110", + ], + passwordChar: ["w-1", "h-1", "bg-default-800", "rounded-full"], + caret: [ + "animate-[appearance-in_1s_infinite]", + "font-extralight", + "h-full", + "w-full", + "flex", + "justify-center", + "items-center", + "text-2xl", + "h-[50%]", + "w-px", + "bg-foreground", + ], + helperWrapper: ["text-xs", "mt-0.5", "font-extralight", ""], + errorMessage: ["text-xs text-danger w-full"], + description: ["text-xs text-foreground-400"], + }, + variants: { + variant: { + flat: { + segment: ["border-none", "bg-default-100", "data-[active=true]:bg-default-200"], + }, + faded: { + segment: ["bg-default-100", "border-1", "data-[active=true]:border-2"], + }, + bordered: { + segment: ["border-1", "data-[active=true]:border-2"], + }, + underlined: { + segment: ["border-b-1", "data-[active=true]:border-b-2", "data-[active=true]:scale-100"], + }, + }, + disableAnimation: { + true: { + segment: "transition-none", + }, + false: { + segment: "transition duration-150", + }, + }, + isDisabled: { + true: { + segment: "opacity-disabled pointer-events-none", + input: "pointer-events-none", + }, + }, + isInvalid: { + true: {}, + }, + isReadOnly: { + true: { + caret: "bg-transparent", + segment: "transition-none data-[active=true]:scale-100", + }, + }, + fullWidth: { + true: { + base: "w-full", + }, + }, + radius: { + none: { + segment: "rounded-none", + }, + sm: { + segment: "rounded-sm", + }, + md: { + segment: "rounded-md", + }, + lg: { + segment: "rounded-lg", + }, + full: { + segment: "rounded-full", + }, + }, + color: { + default: {}, + primary: {}, + secondary: {}, + success: {}, + warning: {}, + danger: {}, + }, + size: { + sm: { + segment: "h-8 min-h-8 w-8 min-w-8 text-small", + }, + md: { + segment: "h-10 min-h-10 w-10 min-w-10 text-small", + }, + lg: { + segment: "h-12 min-h-12 w-12 min-w-12 text-medium", + }, + }, + }, + defaultVariants: { + variant: "flat", + color: "default", + }, + compoundVariants: [ + // flat & color + { + variant: "flat", + color: "default", + class: { + segment: ["bg-default-200", "data-[active=true]:bg-default-400"], + }, + }, + { + variant: "flat", + color: "primary", + class: { + segment: ["bg-primary-100", "data-[active=true]:bg-primary-200", "text-primary"], + caret: ["bg-primary"], + passwordChar: ["bg-primary"], + }, + }, + { + variant: "flat", + color: "secondary", + class: { + segment: ["bg-secondary-100", "data-[active=true]:bg-secondary-200", "text-secondary"], + caret: ["bg-secondary"], + passwordChar: ["bg-secondary"], + }, + }, + { + variant: "flat", + color: "success", + class: { + segment: ["bg-success-100", "data-[active=true]:bg-success-200", "text-success"], + caret: ["bg-success"], + passwordChar: ["bg-success"], + }, + }, + { + variant: "flat", + color: "warning", + class: { + segment: ["bg-warning-100", "data-[active=true]:bg-warning-200", "text-warning"], + caret: ["bg-warning"], + passwordChar: ["bg-warning"], + }, + }, + { + variant: "flat", + color: "danger", + class: { + segment: ["bg-danger-100", "data-[active=true]:bg-danger-200", "text-danger"], + caret: ["bg-danger"], + passwordChar: ["bg-danger"], + }, + }, + // faded & color + { + variant: "faded", + color: "default", + class: { + segment: "bg-default-200", + }, + }, + { + variant: "faded", + color: "primary", + class: { + segment: [ + "bg-primary-100", + "text-primary", + "border-1", + "border-primary-200", + "data-[active=true]:border-2", + "data-[active=true]:border-primary-400", + ], + caret: ["bg-primary"], + passwordChar: ["bg-primary"], + }, + }, + { + variant: "faded", + color: "secondary", + class: { + segment: [ + "bg-secondary-100", + "text-secondary", + "border-1", + "border-secondary-200", + "data-[active=true]:border-2", + "data-[active=true]:border-secondary-400", + ], + caret: ["bg-secondary"], + passwordChar: ["bg-secondary"], + }, + }, + { + variant: "faded", + color: "success", + class: { + segment: [ + "bg-success-100", + "text-success", + "border-1", + "border-success-200", + "data-[active=true]:border-2", + "data-[active=true]:border-success-400", + ], + caret: ["bg-success"], + passwordChar: ["bg-success"], + }, + }, + { + variant: "faded", + color: "warning", + class: { + segment: [ + "bg-warning-100", + "text-warning", + "border-1", + "border-warning-200", + "data-[active=true]:border-2", + "data-[active=true]:border-warning-400", + ], + caret: ["bg-warning"], + passwordChar: ["bg-warning"], + }, + }, + { + variant: "faded", + color: "danger", + class: { + segment: [ + "bg-danger-100", + "text-danger", + "border-1", + "border-danger-200", + "data-[active=true]:border-2", + "data-[active=true]:border-danger-400", + ], + caret: ["bg-danger"], + passwordChar: ["bg-danger"], + }, + }, + // bordered & color + { + variant: "bordered", + color: "default", + class: { + segment: "data-[has-value=true]:text-default-foreground", + }, + }, + { + variant: "bordered", + color: "primary", + class: { + segment: ["border-primary-200", "text-primary", "data-[active=true]:border-primary-400"], + caret: ["bg-primary"], + passwordChar: ["bg-primary"], + }, + }, + { + variant: "bordered", + color: "secondary", + class: { + segment: [ + "border-secondary-200", + "text-secondary", + "data-[active=true]:border-secondary-400", + ], + caret: ["bg-secondary"], + passwordChar: ["bg-secondary"], + }, + }, + { + variant: "bordered", + color: "success", + class: { + segment: ["border-success-200", "text-success", "data-[active=true]:border-success-400"], + caret: ["bg-success"], + passwordChar: ["bg-success"], + }, + }, + { + variant: "bordered", + color: "warning", + class: { + segment: ["border-warning-200", "text-warning", "data-[active=true]:border-warning-400"], + caret: ["bg-warning"], + passwordChar: ["bg-warning"], + }, + }, + { + variant: "bordered", + color: "danger", + class: { + segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"], + caret: ["bg-danger"], + passwordChar: ["bg-danger"], + }, + }, + // underlined & color + { + variant: "underlined", + color: "default", + class: { + segment: "data-[has-value=true]:text-default-foreground rounded-none", + }, + }, + { + variant: "underlined", + color: "primary", + class: { + segment: [ + "border-primary-200", + "text-primary", + "data-[active=true]:border-primary-400", + "rounded-none", + ], + caret: ["bg-primary"], + passwordChar: ["bg-primary"], + }, + }, + { + variant: "underlined", + color: "secondary", + class: { + segment: [ + "border-secondary-200", + "text-secondary", + "data-[active=true]:border-secondary-400", + "rounded-none", + ], + caret: ["bg-secondary"], + passwordChar: ["bg-secondary"], + }, + }, + { + variant: "underlined", + color: "success", + class: { + segment: [ + "border-success-200", + "text-success", + "data-[active=true]:border-success-400", + "rounded-none", + ], + caret: ["bg-success"], + passwordChar: ["bg-success"], + }, + }, + { + variant: "underlined", + color: "warning", + class: { + segment: [ + "border-warning-200", + "text-warning", + "data-[active=true]:border-warning-400", + "rounded-none", + ], + caret: ["bg-warning"], + passwordChar: ["bg-warning"], + }, + }, + { + variant: "underlined", + color: "danger", + class: { + segment: [ + "border-danger-200", + "text-danger", + "data-[active=true]:border-danger-400", + "rounded-none", + ], + caret: ["bg-danger"], + passwordChar: ["bg-danger"], + }, + }, + // isInvalid and flat + { + variant: "flat", + isInvalid: true, + class: { + segment: ["bg-danger-50", "data-[active=true]:bg-danger-100", "text-danger"], + caret: ["bg-danger"], + }, + }, + // isInvalid and faded + { + variant: "faded", + isInvalid: true, + class: { + segment: [ + "bg-danger-50", + "text-danger", + "border-1", + "border-danger-200", + "data-[active=true]:border-2", + "data-[active=true]:border-danger-400", + ], + caret: ["bg-danger"], + }, + }, + // isInvalid and bordered + { + variant: "bordered", + isInvalid: true, + class: { + segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"], + caret: ["bg-danger"], + }, + }, + // isInvalid anf underlined + { + variant: "underlined", + isInvalid: true, + class: { + segment: ["border-danger-200", "text-danger", "data-[active=true]:border-danger-400"], + caret: ["bg-danger"], + }, + }, + ], +}); + +export type InputOtpVariantProps = VariantProps; +export type InputOtpSlots = keyof ReturnType; +export type InputOtpReturnType = ReturnType; + +export {inputOtp}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25fe5137e6..226b775b01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,6 +291,9 @@ importers: '@nextui-org/divider': specifier: workspace:* version: link:../../packages/components/divider + '@nextui-org/input-otp': + specifier: workspace:* + version: link:../../packages/components/input-otp '@nextui-org/kbd': specifier: workspace:* version: link:../../packages/components/kbd @@ -1662,6 +1665,52 @@ importers: specifier: ^7.51.3 version: 7.53.1(react@18.3.1) + packages/components/input-otp: + dependencies: + '@nextui-org/react-utils': + specifier: workspace:* + version: link:../../utilities/react-utils + '@nextui-org/shared-utils': + specifier: workspace:* + version: link:../../utilities/shared-utils + '@react-aria/form': + specifier: 3.0.8 + version: 3.0.8(react@18.3.1) + '@react-aria/utils': + specifier: 3.24.1 + version: 3.24.1(react@18.3.1) + '@react-stately/form': + specifier: 3.0.5 + version: 3.0.5(react@18.3.1) + '@react-stately/utils': + specifier: 3.10.1 + version: 3.10.1(react@18.3.1) + '@react-types/textfield': + specifier: 3.9.3 + version: 3.9.3(react@18.3.1) + input-otp: + specifier: 1.4.1 + version: 1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@nextui-org/system': + specifier: workspace:* + version: link:../../core/system + '@nextui-org/theme': + specifier: workspace:* + version: link:../../core/theme + clean-package: + specifier: 2.2.0 + version: 2.2.0 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.51.3 + version: 7.53.1(react@18.3.1) + packages/components/kbd: dependencies: '@nextui-org/react-utils': @@ -2995,6 +3044,9 @@ importers: '@nextui-org/input': specifier: workspace:* version: link:../../components/input + '@nextui-org/input-otp': + specifier: workspace:* + version: link:../../components/input-otp '@nextui-org/kbd': specifier: workspace:* version: link:../../components/kbd @@ -7105,6 +7157,11 @@ packages: peerDependencies: react: ^18.2.0 + '@react-types/textfield@3.9.3': + resolution: {integrity: sha512-DoAY6cYOL0pJhgNGI1Rosni7g72GAt4OVr2ltEx2S9ARmFZ0DBvdhA9lL2nywcnKMf27PEJcKMXzXc10qaHsJw==} + peerDependencies: + react: ^18.2.0 + '@react-types/textfield@3.9.6': resolution: {integrity: sha512-0uPqjJh4lYp1aL1HL9IlV8Cgp8eT0PcsNfdoCktfkLytvvBPmox2Pfm57W/d0xTtzZu2CjxhYNTob+JtGAOeXA==} peerDependencies: @@ -11054,6 +11111,12 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + input-otp@1.4.1: + resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + inquirer@6.5.2: resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} engines: {node: '>=6.0.0'} @@ -20334,6 +20397,11 @@ snapshots: '@react-types/shared': 3.24.1(react@18.3.1) react: 18.3.1 + '@react-types/textfield@3.9.3(react@18.3.1)': + dependencies: + '@react-types/shared': 3.24.1(react@18.3.1) + react: 18.3.1 + '@react-types/textfield@3.9.6(react@18.3.1)': dependencies: '@react-types/shared': 3.24.1(react@18.3.1) @@ -23833,7 +23901,7 @@ snapshots: eslint: 7.32.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@7.32.0) eslint-plugin-react: 7.37.2(eslint@7.32.0) eslint-plugin-react-hooks: 4.6.2(eslint@7.32.0) @@ -23903,7 +23971,7 @@ snapshots: is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -23954,35 +24022,6 @@ snapshots: lodash: 4.17.21 string-natural-compare: 3.0.1 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 7.32.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0) - hasown: 2.0.2 - is-core-module: 2.15.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - string.prototype.trimend: 1.0.8 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@7.32.0)(typescript@4.9.5) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0): dependencies: '@rtsao/scc': 1.1.0 @@ -25351,6 +25390,11 @@ snapshots: inline-style-parser@0.2.4: {} + input-otp@1.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + inquirer@6.5.2: dependencies: ansi-escapes: 3.2.0