├── .editorconfig ├── .eslintignore ├── .github ├── ISSUE_TEMPLATE │ ├── bad-design-choice.md │ ├── bug-report.md │ └── feature_request.md └── workflows │ └── playwright.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── apps ├── storybook │ ├── .gitignore │ ├── .storybook │ │ ├── main.js │ │ └── preview.js │ ├── LICENSE │ ├── README.md │ ├── components.json │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── assets │ │ │ ├── font │ │ │ │ ├── OpenSans-Light-webfont.eot │ │ │ │ ├── OpenSans-Light-webfont.svg │ │ │ │ ├── OpenSans-Light-webfont.ttf │ │ │ │ ├── OpenSans-Light-webfont.woff │ │ │ │ ├── OpenSans-Regular-webfont.eot │ │ │ │ ├── OpenSans-Regular-webfont.svg │ │ │ │ ├── OpenSans-Regular-webfont.ttf │ │ │ │ └── OpenSans-Regular-webfont.woff │ │ │ ├── icon │ │ │ │ ├── percolate.eot │ │ │ │ ├── percolate.svg │ │ │ │ ├── percolate.ttf │ │ │ │ └── percolate.woff │ │ │ └── react.svg │ │ ├── components │ │ │ └── ui │ │ │ │ └── input-otp.tsx │ │ ├── globals.css │ │ ├── lib │ │ │ └── utils.ts │ │ └── stories │ │ │ ├── Button.stories.ts │ │ │ ├── Button.tsx │ │ │ ├── InputOTP.stories.tsx │ │ │ ├── Introduction.mdx │ │ │ ├── assets │ │ │ ├── code-brackets.svg │ │ │ ├── colors.svg │ │ │ ├── comments.svg │ │ │ ├── direction.svg │ │ │ ├── flow.svg │ │ │ ├── plugin.svg │ │ │ ├── repo.svg │ │ │ └── stackalt.svg │ │ │ └── button.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.js ├── test │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next.config.mjs │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ ├── base │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── props │ │ │ │ └── page.tsx │ │ │ ├── with-autofocus │ │ │ │ └── page.tsx │ │ │ └── with-on-complete │ │ │ │ └── page.tsx │ │ ├── components │ │ │ └── base-input.tsx │ │ ├── lib │ │ │ ├── fonts.ts │ │ │ └── utils.ts │ │ └── tests │ │ │ ├── base.delete-word.spec.ts │ │ │ ├── base.props.spec.ts │ │ │ ├── base.render.spec.ts │ │ │ ├── base.selections.spec.ts │ │ │ ├── base.slot.spec.ts │ │ │ ├── base.typing.spec.ts │ │ │ ├── util │ │ │ └── modifier.ts │ │ │ ├── with-autofocus.spec.ts │ │ │ └── with-on-complete.spec.ts │ ├── tailwind.config.ts │ └── tsconfig.json └── website │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── next.config.mjs │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ ├── next.svg │ ├── og.jpg │ ├── site.webmanifest │ ├── sponsors │ │ ├── clerk-wordmark-black.svg │ │ ├── clerk-wordmark-white-in-black-bg.svg │ │ ├── clerk-wordmark-white.svg │ │ ├── evomi-wordmark-black.svg │ │ ├── evomi-wordmark-white-in-black-bg.svg │ │ ├── evomi-wordmark-white.svg │ │ ├── resend-wordmark-black.svg │ │ ├── resend-wordmark-white-in-black-bg.svg │ │ └── resend-wordmark-white.svg │ └── vercel.svg │ ├── src │ ├── app │ │ ├── (local-pages) │ │ │ ├── example-auto-submit │ │ │ │ ├── page.tsx │ │ │ │ └── server │ │ │ │ │ └── form-action.ts │ │ │ ├── example-playground │ │ │ │ ├── code.tsx │ │ │ │ ├── component.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── raw-input │ │ │ │ └── page.tsx │ │ │ └── shadcn │ │ │ │ ├── page.tsx │ │ │ │ ├── pwmb │ │ │ │ └── page.tsx │ │ │ │ └── static │ │ │ │ └── page.tsx │ │ ├── (pages) │ │ │ ├── (home) │ │ │ │ ├── _components │ │ │ │ │ ├── confetti.tsx │ │ │ │ │ └── showcase.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── favicon.ico │ │ └── globals.css │ ├── components │ │ ├── code.tsx │ │ ├── copy-button.tsx │ │ ├── icons.tsx │ │ ├── mode-toggle.tsx │ │ ├── page-header.tsx │ │ ├── provider.tsx │ │ ├── site-footer.tsx │ │ ├── site-header.tsx │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── scroll-area.tsx │ │ │ └── sonner.tsx │ ├── config │ │ └── site.ts │ └── lib │ │ ├── fonts.ts │ │ └── utils.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── package.json ├── packages └── input-otp │ ├── package.json │ ├── src │ ├── index.ts │ ├── input.tsx │ ├── regexp.ts │ ├── sync-timeouts.ts │ ├── types.ts │ ├── use-previous.ts │ └── use-pwm-badge.tsx │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc* 2 | *.config.js 3 | /.jest/* 4 | /coverage/* 5 | /dist/* 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bad-design-choice.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bad design choice 3 | about: Describe a preference/default that was either 1. ignored 2. blocked or 3. wrong 4 | from the API perspective. 5 | title: "[DX/UX]" 6 | labels: invalid 7 | assignees: '' 8 | 9 | --- 10 | 11 | Example: 12 | 13 | I'm unhappy with ___ design/API choice because ___. The ___ could be better if you ___. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Describe your bug. Which version? 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: guilhermerodz 7 | 8 | --- 9 | 10 | Issue tracker is **ONLY** used for reporting bugs. New features should be discussed on our slack channel. Please use [stackoverflow](https://stackoverflow.com) for supporting issues. 11 | 12 | 13 | 14 | ## Version of the library: 1.x 15 | 16 | ## Expected Behavior 17 | 18 | 19 | ## Current Behavior 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 4. 32 | 33 | ## Context (Environment) 34 | 35 | 36 | 37 | 38 | 39 | ## Detailed Description 40 | 41 | 42 | ## Possible Implementation 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [main, master] 5 | pull_request: 6 | branches: [main, master] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18.17.1 16 | - run: npm install -g pnpm@8.7.1 17 | 18 | - run: pnpm install 19 | - run: npx playwright install --with-deps 20 | - run: pnpm test || exit 1 21 | 22 | - uses: actions/upload-artifact@v4 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | dist 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | coverage 11 | test-results/ 12 | playwright-report/ 13 | playwright/.cache/ 14 | 15 | # next.js 16 | .next/ 17 | out/ 18 | build 19 | next-env.d.ts 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # turbo 38 | .turbo 39 | 40 | # post-build 41 | packages/input-otp/README.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.2] 4 | 5 | - chore(input): remove unintentional log within internal pasteListener 6 | 7 | ## [1.4.1] 8 | 9 | - chore(input): add peer dep for react@19-rc 10 | 11 | ## [1.4.0] 12 | 13 | I'm sorry to skip `1.3.0` due to an issue I've had while publishing the NPM package. 14 | 15 | - chore(input): stop enforcing only digits regexp by default 16 | - Before 1.4.0, the input would take `REGEXP_ONLY_DIGITS` as the default pattern behavior, mistaking mobile users when they couldn't type in or even paste alphanumeric entries. 17 | - feat(input): add pasteTransformer prop 18 | - Allows pasting invalid codes and then transforming them into something that the input's regex/pattern would accept. Example: you can now take "XXX-XXX" as pasted input even though you've determined a pattern of 6 numerical digits; just add a prop to your OTPInput: `pasteTransformer={pasted => pasted.replaceAll('-','')}`. 19 | - feat(input): add placeholder 20 | - Input can now render a placeholder, all you should do is adjust your CSS to render it (look at the default example on README)! 21 | - The input's HTML now lives with an attribute `data-input-otp-placeholder-shown` when its content is empty. 22 | - chore(input): remove re-focus feature for password manager badges 23 | - Fixed a bug where the input's `blur` event was triggering even if the user hasn't requested it. The sacrifice was to remove the auto re-focus feature for password manager badges, meaning if the password badge ever disappears, then the user himself has to re-trigger focus by manually clicking or selecting the input. 24 | 25 | ## [1.2.5] 26 | 27 | - chore(input): add peer dep for react@19 28 | 29 | ## [1.2.4] 30 | 31 | - fix(input): prevent single caret selection on deletion/cutting 32 | 33 | ## [1.2.3] 34 | 35 | - fix(input/css): specify `color: transparent !important` for `::selection` modifier 36 | - fix(input/node-env): check for CSS supports api before calling fn 37 | 38 | ## [1.2.2] 39 | 40 | - chore(input): remove experimental flag `pushPasswordManagerStrategy` 41 | 42 | ## [1.2.1] 43 | 44 | - fix(input): use `color` not `text` for autofillStyles 45 | - chore(input): keep support for prop pushPasswordManagerStrategy="experimental-no-flickering" 46 | - fix(input): prevent layout expansion when password managers aren't there and remove "experimental-no-flickering" strategy 47 | 48 | ## [1.2.0] 49 | 50 | - chore(input): don't restrict inputMode typing 51 | 52 | ## [1.2.0-beta.1] 53 | 54 | - fix(input): renderfn typing 55 | 56 | ## [1.2.0-beta.0] 57 | 58 | - feat(input): add context option 59 | - chore(input): remove unused type `SelectionType` 60 | 61 | ## [1.1.0] 62 | 63 | - feat(input/no-js): allow opting out of no-js fallback 64 | - fix(input/no-js): move noscript to the top 65 | - chore(input): optimize use-badge 66 | - fix(input): set no extra width on default noscript css fallback 67 | - fix(input): check window during ssr 68 | - fix(input/ios): add right: 1px to compensate left: -1px 69 | - chore(input/ios): revert paste listener (re-add) 70 | - chore(input): always trigger selection menu on ios 71 | - perf(input): prevent trackPWMBadge when strategy is none 72 | - fix(input): do not skip left slot 73 | - fix(input): do not skip left slot when pressing arrowleft after insert mode 74 | - fix(input): reinforce wrapper to pointerEvents none 75 | - feat(input): add experimental push pwm badge 76 | - chore(input): rename prop to pushPasswordManagerStrategy 77 | - chore(input): move focus logic to _focusListener 78 | - fix(input): reinforce no box shadows 79 | - perf(input): rewrite core in a single event listener 80 | - fix(input): safe insert css rules 81 | - fix(input): prevent layout shift caused by password managers 82 | - feat(input): add pwm badge space detector 83 | - feat(input): add passwordManagerBehavior prop 84 | - fix(input): forcefully remove :autofill 85 | - feat(input): track password managers 86 | 87 | ## [1.0.1] 88 | 89 | - fix(input): immediately update selection after paste 90 | - fix(input): hide selection on iOS webkit 91 | 92 | ## [1.0.0] 93 | 94 | - fix(input/firefox): use setselectionrange direction:backwards 95 | 96 | ## [0.3.31-beta] 97 | 98 | No input scope changes for this version. 99 | 100 | ## [0.3.3-beta] 101 | 102 | No input scope changes for this version. 103 | 104 | ## [0.3.2-beta] 105 | 106 | No input scope changes for this version. 107 | 108 | ## [0.3.11-beta] 109 | 110 | No input scope changes for this version. 111 | 112 | ## [0.3.1-beta] 113 | 114 | No input scope changes for this version. 115 | 116 | ## [0.2.4] 117 | 118 | - chore(input): always focus onContainerClick 119 | 120 | ## [0.2.1] 121 | 122 | - fix(input): do not trigger `onComplete` twice 123 | 124 | ## [0.2] 125 | 126 | No input scope changes for this version. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Guilherme Rodz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/storybook/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /apps/storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/react-vite').StorybookConfig } */ 2 | const config = { 3 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 4 | staticDirs: ['../public'], 5 | addons: [ 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-interactions', 9 | ], 10 | framework: { 11 | name: '@storybook/react-vite', 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: 'tag', 16 | }, 17 | webpackFinal: async (config, { configType }) => { 18 | config.resolve.plugins = [new TsconfigPathsPlugin()] 19 | return config 20 | }, 21 | } 22 | export default config 23 | -------------------------------------------------------------------------------- /apps/storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/globals.css' 2 | 3 | /** @type { import('@storybook/react').Preview } */ 4 | const preview = { 5 | parameters: { 6 | actions: { argTypesRegex: "^on[A-Z].*" }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }, 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /apps/storybook/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Chroma Software Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/storybook/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Chromatic 4 | 5 |

6 | 7 |

8 | Chromatic's Intro to Storybook React template 9 |

10 | 11 | This template ships with the main React and Storybook configuration files you'll need to get up and running fast. 12 | 13 | ## 🚅 Quick start 14 | 15 | 1. **Create the application.** 16 | 17 | Use [degit](https://github.com/Rich-Harris/degit) to get this template. 18 | 19 | ```shell 20 | # Clone the template 21 | npx degit chromaui/intro-storybook-react-template taskbox 22 | ``` 23 | 24 | 1. **Install the dependencies.** 25 | 26 | Navigate into your new site’s directory and install the necessary dependencies. 27 | 28 | ```shell 29 | # Navigate to the directory 30 | cd taskbox/ 31 | 32 | # Install the dependencies 33 | yarn 34 | ``` 35 | 36 | 1. **Open the source code and start editing!** 37 | 38 | Open the `taskbox` directory in your code editor of choice and building your first component! 39 | 40 | 1. **Browse your stories!** 41 | 42 | Run `yarn storybook` to see your component's stories at `http://localhost:6006` 43 | 44 | ## 🔎 What's inside? 45 | 46 | A quick look at the top-level files and directories included with this template. 47 | 48 | . 49 | ├── .storybook 50 | ├── node_modules 51 | ├── public 52 | ├── src 53 | ├── .gitignore 54 | ├── .index.html 55 | ├── LICENSE 56 | ├── package.json 57 | ├── yarn.lock 58 | ├── vite.config.js 59 | └── README.md 60 | 61 | 1. **`.storybook`**: This directory contains Storybook's [configuration](https://storybook.js.org/docs/react/configure/overview) files. 62 | 63 | 2. **`node_modules`**: This directory contains all of the modules of code that your project depends on (npm packages). 64 | 65 | 3. **`public`**: This directory will contain the development and production build of the site. 66 | 67 | 4. **`src`**: This directory will contain all of the code related to what you will see on your application. 68 | 69 | 5. **`.gitignore`**: This file tells git which files it should not track or maintain during the development process of your project. 70 | 71 | 6. **`.index.html`**: This is the HTML page that is served when generating a development or production build. 72 | 73 | 7. **`LICENSE`**: The template is licensed under the MIT licence. 74 | 75 | 8. **`package.json`**: Standard manifest file for Node.js projects, which typically includes project specific metadata (such as the project's name, the author among other information). It's based on this file that npm will know which packages are necessary to the project. 76 | 77 | 9. **`yarn.lock`**: This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(Do not change it manually).** 78 | 79 | 10. **`vite.config.js`**: This is the configuration file for [Vite](https://vitejs.dev/), a build tool that aims to provide a faster and leaner development experience for modern web projects. 80 | 81 | 11. **`README.md`**: A text file containing useful reference information about the project. 82 | 83 | ## Contribute 84 | 85 | If you encounter an issue with the template, we encourage you to open an issue in this template's repository. 86 | 87 | ## Learning Storybook 88 | 89 | 1. Read our introductory tutorial at [Learn Storybook](https://storybook.js.org/tutorials/intro-to-storybook/react/en/get-started/). 90 | 2. Learn how to transform your component libraries into design systems in our [Design Systems for Developers](https://storybook.js.org/tutorials/design-systems-for-developers/) tutorial. 91 | 3. See our official documentation at [Storybook](https://storybook.js.org/). 92 | -------------------------------------------------------------------------------- /apps/storybook/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/storybook/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook", 3 | "private": true, 4 | "version": "0.2.0", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/chromaui/intro-storybook-react-template" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/chromaui/intro-storybook-react-template/issues" 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@radix-ui/react-icons": "^1.3.0", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.0", 18 | "input-otp": "workspace:*", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "tailwind-merge": "^2.2.1", 22 | "tailwindcss-animate": "^1.0.7" 23 | }, 24 | "scripts": { 25 | "storybook": "storybook dev -p 6006", 26 | "build-storybook": "storybook build", 27 | "init-msw": "msw init public/" 28 | }, 29 | "devDependencies": { 30 | "@storybook/addon-essentials": "^7.6.6", 31 | "@storybook/addon-interactions": "^7.6.6", 32 | "@storybook/addon-links": "^7.6.6", 33 | "@storybook/blocks": "^7.6.6", 34 | "@storybook/react": "^7.6.6", 35 | "@storybook/react-vite": "^7.6.6", 36 | "@storybook/test": "^7.6.6", 37 | "@types/react": "^18.0.28", 38 | "@types/react-dom": "^18.0.11", 39 | "@vitejs/plugin-react": "^3.1.0", 40 | "autoprefixer": "^10.0.1", 41 | "msw": "^1.2.1", 42 | "msw-storybook-addon": "^1.10.0", 43 | "postcss": "^8", 44 | "prop-types": "^15.8.1", 45 | "storybook": "^7.6.6", 46 | "tailwindcss": "^3.4.1", 47 | "tsconfig-paths-webpack-plugin": "^4.1.0", 48 | "typescript": "^5.4.2", 49 | "vite": "^4.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/storybook/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Light-webfont.ttf -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Regular-webfont.ttf -------------------------------------------------------------------------------- /apps/storybook/src/assets/font/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/font/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /apps/storybook/src/assets/icon/percolate.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/icon/percolate.eot -------------------------------------------------------------------------------- /apps/storybook/src/assets/icon/percolate.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/icon/percolate.ttf -------------------------------------------------------------------------------- /apps/storybook/src/assets/icon/percolate.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/storybook/src/assets/icon/percolate.woff -------------------------------------------------------------------------------- /apps/storybook/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/storybook/src/components/ui/input-otp.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { DashIcon } from "@radix-ui/react-icons" 5 | import { OTPInput, SlotProps } from "input-otp" 6 | 7 | import { cn } from "../../lib/utils" 8 | 9 | const InputOTP = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | InputOTP.displayName = "InputOTP" 20 | 21 | const InputOTPGroup = React.forwardRef< 22 | React.ElementRef<"div">, 23 | React.ComponentPropsWithoutRef<"div"> 24 | >(({ className, ...props }, ref) => ( 25 |
26 | )) 27 | InputOTPGroup.displayName = "InputOTPGroup" 28 | 29 | const InputOTPSlot = React.forwardRef< 30 | React.ElementRef<"div">, 31 | SlotProps & React.ComponentPropsWithoutRef<"div"> 32 | >(({ char, hasFakeCaret, isActive, className, ...props }, ref) => { 33 | return ( 34 |
43 | {char} 44 | {hasFakeCaret && ( 45 |
46 |
47 |
48 | )} 49 |
50 | ) 51 | }) 52 | InputOTPSlot.displayName = "InputOTPSlot" 53 | 54 | const InputOTPSeparator = React.forwardRef< 55 | React.ElementRef<"div">, 56 | React.ComponentPropsWithoutRef<"div"> 57 | >(({ ...props }, ref) => ( 58 |
59 | 60 |
61 | )) 62 | InputOTPSeparator.displayName = "InputOTPSeparator" 63 | 64 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } 65 | -------------------------------------------------------------------------------- /apps/storybook/src/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 72.22% 50.59%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | /* font-feature-settings: "rlig" 1, "calt" 1; */ 59 | font-synthesis-weight: none; 60 | text-rendering: optimizeLegibility; 61 | } 62 | } 63 | 64 | @layer utilities { 65 | } 66 | -------------------------------------------------------------------------------- /apps/storybook/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/storybook/src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import { Button } from './Button'; 2 | 3 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction 4 | export default { 5 | title: 'Example/Button', 6 | component: Button, 7 | tags: ['autodocs'], 8 | argTypes: { 9 | backgroundColor: { control: 'color' }, 10 | }, 11 | }; 12 | 13 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 14 | export const Primary = { 15 | args: { 16 | primary: true, 17 | label: 'Button', 18 | }, 19 | }; 20 | 21 | export const Secondary = { 22 | args: { 23 | label: 'Button', 24 | }, 25 | }; 26 | 27 | export const Large = { 28 | args: { 29 | size: 'large', 30 | label: 'Button', 31 | }, 32 | }; 33 | 34 | export const Small = { 35 | args: { 36 | size: 'small', 37 | label: 'Button', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /apps/storybook/src/stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './button.css'; 4 | 5 | /** 6 | * Primary UI component for user interaction 7 | */ 8 | export const Button = ({ primary, backgroundColor, size, label, ...props }) => { 9 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | Button.propTypes = { 23 | /** 24 | * Is this the principal call to action on the page? 25 | */ 26 | primary: PropTypes.bool, 27 | /** 28 | * What background color to use 29 | */ 30 | backgroundColor: PropTypes.string, 31 | /** 32 | * How large should the button be? 33 | */ 34 | size: PropTypes.oneOf(['small', 'medium', 'large']), 35 | /** 36 | * Button contents 37 | */ 38 | label: PropTypes.string.isRequired, 39 | /** 40 | * Optional click handler 41 | */ 42 | onClick: PropTypes.func, 43 | }; 44 | 45 | Button.defaultProps = { 46 | backgroundColor: null, 47 | primary: false, 48 | size: 'medium', 49 | onClick: undefined, 50 | }; 51 | -------------------------------------------------------------------------------- /apps/storybook/src/stories/InputOTP.stories.tsx: -------------------------------------------------------------------------------- 1 | import { InputOTP, InputOTPGroup, InputOTPSlot } from "../components/ui/input-otp" 2 | import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp" 3 | 4 | const meta = { 5 | title: "ui/InputOTP", 6 | component: InputOTP, 7 | tags: ["autodocs"], 8 | argTypes: {}, 9 | args: { 10 | maxLength: 6, 11 | pattern: REGEXP_ONLY_DIGITS_AND_CHARS, 12 | render: ({ slots }) => ( 13 | 14 | {slots.map((slot, index) => ( 15 | 16 | ))} 17 | 18 | ), 19 | }, 20 | parameters: { 21 | layout: "centered", 22 | }, 23 | } 24 | 25 | export default meta 26 | 27 | type Story = StoryObj 28 | 29 | export const Default: Story = {} -------------------------------------------------------------------------------- /apps/storybook/src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 118 | 119 | # Welcome to Storybook 120 | 121 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 122 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 123 | 124 | Browse example stories now by navigating to them in the sidebar. 125 | View their code in the `stories` directory to learn how they work. 126 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 127 | 128 |
Configure
129 | 130 | 176 | 177 |
Learn
178 | 179 | 209 | 210 |
211 | TipEdit the Markdown in{' '} 212 | stories/Introduction.stories.mdx 213 |
214 | -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /apps/storybook/src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /apps/storybook/src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /apps/storybook/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const config = { 3 | darkMode: ['class'], 4 | content: [ 5 | './index.html', 6 | './src/stories/**/*.{ts,tsx}', 7 | './src/**/*.{ts,tsx}', 8 | '.storybook/**/*.{js,ts,tsx}', 9 | ], 10 | prefix: '', 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: '2rem', 15 | screens: { 16 | '2xl': '1400px', 17 | }, 18 | }, 19 | extend: { 20 | transitionDelay: { 21 | 1500: '1500ms', 22 | }, 23 | colors: { 24 | border: 'hsl(var(--border))', 25 | input: 'hsl(var(--input))', 26 | ring: 'hsl(var(--ring))', 27 | background: 'hsl(var(--background))', 28 | foreground: 'hsl(var(--foreground))', 29 | primary: { 30 | DEFAULT: 'hsl(var(--primary))', 31 | foreground: 'hsl(var(--primary-foreground))', 32 | }, 33 | secondary: { 34 | DEFAULT: 'hsl(var(--secondary))', 35 | foreground: 'hsl(var(--secondary-foreground))', 36 | }, 37 | destructive: { 38 | DEFAULT: 'hsl(var(--destructive))', 39 | foreground: 'hsl(var(--destructive-foreground))', 40 | }, 41 | muted: { 42 | DEFAULT: 'hsl(var(--muted))', 43 | foreground: 'hsl(var(--muted-foreground))', 44 | }, 45 | accent: { 46 | DEFAULT: 'hsl(var(--accent))', 47 | foreground: 'hsl(var(--accent-foreground))', 48 | }, 49 | popover: { 50 | DEFAULT: 'hsl(var(--popover))', 51 | foreground: 'hsl(var(--popover-foreground))', 52 | }, 53 | card: { 54 | DEFAULT: 'hsl(var(--card))', 55 | foreground: 'hsl(var(--card-foreground))', 56 | }, 57 | }, 58 | borderRadius: { 59 | lg: 'var(--radius)', 60 | md: 'calc(var(--radius) - 2px)', 61 | sm: 'calc(var(--radius) - 4px)', 62 | }, 63 | keyframes: { 64 | 'accordion-down': { 65 | from: { height: '0' }, 66 | to: { height: 'var(--radix-accordion-content-height)' }, 67 | }, 68 | 'accordion-up': { 69 | from: { height: 'var(--radix-accordion-content-height)' }, 70 | to: { height: '0' }, 71 | }, 72 | 73 | 'caret-blink': { 74 | '0%,70%,100%': { 75 | opacity: '1', 76 | }, 77 | '20%,50%': { 78 | opacity: '0', 79 | }, 80 | }, 81 | 82 | 'fade-in': { 83 | from: { 84 | opacity: '0', 85 | }, 86 | to: { opacity: '1' }, 87 | }, 88 | 'fade-up': { 89 | from: { 90 | opacity: '0', 91 | transform: 'translateY(var(--fade-distance, .25rem))', 92 | }, 93 | to: { opacity: '1', transform: 'translateY(0)' }, 94 | }, 95 | }, 96 | animation: { 97 | 'accordion-down': 'accordion-down 0.2s ease-out', 98 | 'accordion-up': 'accordion-up 0.2s ease-out', 99 | 100 | 'caret-blink': 'caret-blink 1.2s ease-out infinite', 101 | 'fade-in': 'fade-in 0.3s ease-out forwards', 102 | 'fade-up': 'fade-up 1s ease-out forwards', 103 | }, 104 | }, 105 | }, 106 | // plugins: [require('tailwindcss-animate')], 107 | } 108 | 109 | module.exports = config -------------------------------------------------------------------------------- /apps/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /apps/storybook/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } -------------------------------------------------------------------------------- /apps/storybook/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /apps/test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@typescript-eslint/no-unused-vars": "warn" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/test/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/test/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /apps/test/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev --port=3039", 6 | "lint": "next lint", 7 | "test": "playwright test --retries=3", 8 | "test:ui": "playwright test --ui" 9 | }, 10 | "dependencies": { 11 | "clsx": "^2.1.0", 12 | "next": "14.1.0", 13 | "react": "^18", 14 | "react-dom": "^18", 15 | "tailwind-merge": "^2.2.1", 16 | "input-otp": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "^1.41.2", 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "autoprefixer": "^10.0.1", 24 | "eslint": "^8", 25 | "eslint-config-next": "14.1.0", 26 | "postcss": "^8", 27 | "tailwindcss": "^3.3.0", 28 | "typescript": "^5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/test/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: '.', 14 | /* Maximum time one test can run for. */ 15 | timeout: 30 * 1000, 16 | expect: { 17 | /** 18 | * Maximum time expect() should wait for the condition to be met. 19 | * For example in `await expect(locator).toHaveText();` 20 | */ 21 | timeout: 5000, 22 | }, 23 | /* Run tests in files in parallel */ 24 | fullyParallel: process.env.WINDOWED_TESTS ? false : true, 25 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 26 | forbidOnly: !!process.env.CI, 27 | retries: 2, 28 | /* Opt out of parallel tests on CI. */ 29 | workers: process.env.CI ? 1 : undefined, 30 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 31 | reporter: 'html', 32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 33 | use: { 34 | trace: 'on-first-retry', 35 | baseURL: 'http://localhost:3039', 36 | headless: process.env.WINDOWED_TESTS ? false : true, 37 | launchOptions: { 38 | slowMo: process.env.WINDOWED_TESTS ? 500 : 0, 39 | }, 40 | }, 41 | webServer: { 42 | command: 'npm run dev', 43 | url: 'http://localhost:3039', 44 | cwd: '.', 45 | reuseExistingServer: !process.env.CI, 46 | }, 47 | /* Configure projects for major browsers */ 48 | projects: [ 49 | { 50 | name: 'chromium', 51 | use: { 52 | ...devices['Desktop Chrome'], 53 | }, 54 | }, 55 | { 56 | name: 'firefox', 57 | use: { 58 | ...devices['Desktop Firefox'], 59 | }, 60 | }, 61 | { 62 | name: 'webkit', 63 | use: { ...devices['Desktop Safari'], contextOptions: {} }, 64 | }, 65 | 66 | /* Test against mobile viewports. */ 67 | { 68 | name: 'Mobile Chrome', 69 | use: { ...devices['Pixel 5'] }, 70 | }, 71 | { 72 | name: 'Mobile Safari', 73 | use: { ...devices['iPhone 12'] }, 74 | }, 75 | 76 | /* Test against branded browsers. */ 77 | { 78 | name: 'Microsoft Edge', 79 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 80 | }, 81 | { 82 | name: 'Google Chrome', 83 | use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 84 | }, 85 | ], 86 | }) 87 | -------------------------------------------------------------------------------- /apps/test/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/test/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/test/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/test/src/app/base/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { BaseOTPInput } from '@/components/base-input' 6 | 7 | export default function Page() { 8 | return ( 9 |
10 | 11 | 12 | {/* Editable for testing copy-pasting into input */} 13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/test/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/test/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/test/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/test/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode 16 | }>) { 17 | return ( 18 | 19 | 25 | {children} 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/test/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | Base behavior 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /apps/test/src/app/props/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { BaseOTPInput } from '@/components/base-input' 6 | 7 | export default function Page() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/test/src/app/with-autofocus/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { BaseOTPInput } from '@/components/base-input' 6 | 7 | export default function Page() { 8 | return ( 9 |
10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/test/src/app/with-on-complete/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { BaseOTPInput } from '@/components/base-input' 6 | 7 | export default function Page() { 8 | const [disabled, setDisabled] = React.useState(false) 9 | 10 | return ( 11 |
12 | setDisabled(true)} disabled={disabled} /> 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/test/src/components/base-input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { OTPInput } from 'input-otp' 4 | import { cn } from '@/lib/utils' 5 | 6 | export function BaseOTPInput( 7 | overrideProps: Partial> = {}, 8 | ) { 9 | const [value, setValue] = React.useState('') 10 | const [disabled, setDisabled] = React.useState(false) 11 | 12 | return ( 13 | ( 23 |
31 | {slots.map((slot, idx) => ( 32 |
44 | {slot.char !== null ? slot.char : ' '} 45 |
46 | ))} 47 |
48 | )} 49 | {...overrideProps} 50 | /> 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/test/src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | // import { JetBrains_Mono as FontMono, Inter as FontSans } from "next/font/google" 2 | import { JetBrains_Mono as FontMono } from 'next/font/google' 3 | // import { GeistMono } from "geist/font/mono" 4 | import { GeistSans } from 'geist/font/sans' 5 | 6 | // export const fontSans = FontSans({ 7 | // subsets: ["latin"], 8 | // variable: "--font-sans", 9 | // }) 10 | export const fontSans = GeistSans 11 | 12 | export const fontMono = FontMono({ 13 | subsets: ['latin'], 14 | variable: '--font-mono', 15 | }) 16 | -------------------------------------------------------------------------------- /apps/test/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.delete-word.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | import { modifier } from './util/modifier' 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/base') 6 | }) 7 | 8 | test.describe('Backspace', () => { 9 | test('should backspace previous word (even if there is not a selected character)', async ({ page }) => { 10 | const input = page.getByRole('textbox') 11 | 12 | await input.pressSequentially('1234') 13 | await expect(input).toHaveValue('1234') 14 | 15 | await input.press(`${modifier}+Backspace`) 16 | await expect(input).toHaveValue('') 17 | }) 18 | test('should backspace selected char', async ({ page }) => { 19 | const input = page.getByRole('textbox') 20 | 21 | await input.pressSequentially('123456') 22 | await expect(input).toHaveValue('123456') 23 | 24 | await input.press('ArrowLeft') 25 | await input.press('ArrowLeft') 26 | await input.press(`${modifier}+Backspace`) 27 | 28 | await expect(input).toHaveValue('12356') 29 | }) 30 | }) 31 | // Allow flaky 32 | test.describe.configure({ retries: 3 }) 33 | test.describe('Delete', () => { 34 | test('should forward-delete character when pressing delete', async ({ page }) => { 35 | const input = page.getByRole('textbox') 36 | 37 | await input.pressSequentially('123456') 38 | await expect(input).toHaveValue('123456') 39 | 40 | await input.press('Delete') 41 | await expect(input).toHaveValue('12345') 42 | await input.press('ArrowLeft') 43 | await input.press('ArrowLeft') 44 | await input.press('ArrowLeft') 45 | await input.press('ArrowLeft') 46 | await input.press('ArrowLeft') 47 | await input.press('Delete') 48 | await expect(input).toHaveValue('2345') 49 | await input.press('ArrowRight') 50 | await input.press('ArrowRight') 51 | await input.press('Delete') 52 | await expect(input).toHaveValue('235') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.props.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('/props') 5 | }) 6 | 7 | test.describe('Props tests', () => { 8 | test('should receive props accordingly', async ({ page }) => { 9 | const input1 = page.getByTestId('input-otp-1') 10 | const input2 = page.getByTestId('input-otp-2') 11 | const input3 = page.getByTestId('input-otp-3') 12 | const container4 = page.locator( 13 | `[data-input-otp-container]:has([data-testid="input-otp-4"])`, 14 | ) 15 | const input5 = page.getByTestId('input-otp-5') 16 | const input6 = page.getByTestId('input-otp-6') 17 | const input7 = page.getByTestId('input-otp-7') 18 | 19 | await expect(input1).toBeDisabled() 20 | 21 | await expect(input2).toHaveAttribute('inputmode', 'numeric') 22 | 23 | await expect(input3).toHaveAttribute('inputmode', 'text') 24 | 25 | await expect(container4).toHaveClass('testclassname') 26 | 27 | await expect(input5).toHaveAttribute('maxLength', '3') 28 | 29 | await expect(input6).toHaveAttribute('id', 'testid') 30 | await expect(input6).toHaveAttribute('name', 'testname') 31 | 32 | await expect(input7).toHaveAttribute('pattern', ' ') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.render.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' 4 | 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/base') 7 | }) 8 | 9 | test.describe('Base tests - Render', () => { 10 | test('should expose focus flags', async ({ page }) => { 11 | const input = page.getByRole('textbox') 12 | const renderer = page.getByTestId('input-otp-renderer') 13 | 14 | await input.focus() 15 | await expect(renderer).toHaveAttribute('data-test-render-is-focused', 'true') 16 | 17 | await input.blur() 18 | await page.waitForTimeout(100) 19 | await expect(renderer).not.toHaveAttribute('data-test-render-is-focused') 20 | }) 21 | test('should expose hover flags', async ({ page }) => { 22 | const renderer = page.getByTestId('input-otp-renderer') 23 | 24 | await expect(renderer).not.toHaveAttribute('data-test-render-is-hovering') 25 | 26 | const _rect = await renderer.boundingBox({ timeout: 2_000 }) 27 | expect(_rect).not.toBeNull() 28 | const rect = _rect! 29 | await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2) 30 | 31 | await expect(renderer).toHaveAttribute('data-test-render-is-hovering', 'true') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.selections.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('/base') 5 | }) 6 | 7 | test.describe('Base tests - Selections', () => { 8 | test.skip( 9 | process.env.CI === 'true', 10 | 'Breaks in CI as it cannot handle Shift key', 11 | ) 12 | 13 | test('should replace selected char if another is pressed', async ({ 14 | page, 15 | }) => { 16 | const input = page.getByRole('textbox') 17 | 18 | await input.pressSequentially('123') 19 | // arrow left on keyboard 20 | await input.press('ArrowLeft') 21 | await input.pressSequentially('1') 22 | await expect(input).toHaveValue('121') 23 | }) 24 | test('should replace multi-selected chars if another is pressed', async ({ 25 | page, 26 | }) => { 27 | const input = page.getByRole('textbox') 28 | 29 | await input.pressSequentially('123456') 30 | await page.waitForTimeout(100) 31 | await input.press('Shift+ArrowLeft') 32 | await input.press('Shift+ArrowLeft') 33 | await page.waitForTimeout(100) 34 | await input.pressSequentially('1') 35 | await expect(input).toHaveValue('1231') 36 | }) 37 | test('should replace last char if another one is pressed', async ({ 38 | page, 39 | }) => { 40 | const input = page.getByRole('textbox') 41 | 42 | await input.pressSequentially('1234567') 43 | await page.waitForTimeout(100) 44 | 45 | await expect(input).toHaveValue('123457') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.slot.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' 4 | 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/base') 7 | }) 8 | 9 | test.describe('Base tests - Slots', () => { 10 | test('should expose the slot value', async ({ page }) => { 11 | const input = page.getByRole('textbox') 12 | 13 | await input.pressSequentially('1') 14 | await expect(input).toHaveValue('1') 15 | 16 | const slot0 = page.getByTestId('slot-0') 17 | await expect(slot0).toHaveAttribute('data-test-char', '1') 18 | 19 | await expect(page.getByTestId('slot-1')).not.toHaveAttribute( 20 | 'data-test-char', 21 | ) 22 | 23 | await input.pressSequentially('23456') 24 | await expect(input).toHaveValue('123456') 25 | 26 | await expect(page.getByTestId('slot-1')).toHaveAttribute( 27 | 'data-test-char', 28 | '2', 29 | ) 30 | await expect(page.getByTestId('slot-2')).toHaveAttribute( 31 | 'data-test-char', 32 | '3', 33 | ) 34 | await expect(page.getByTestId('slot-3')).toHaveAttribute( 35 | 'data-test-char', 36 | '4', 37 | ) 38 | await expect(page.getByTestId('slot-4')).toHaveAttribute( 39 | 'data-test-char', 40 | '5', 41 | ) 42 | await expect(page.getByTestId('slot-5')).toHaveAttribute( 43 | 'data-test-char', 44 | '6', 45 | ) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /apps/test/src/tests/base.typing.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { modifier } from './util/modifier' 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/base') 6 | }) 7 | 8 | test.describe('Base tests - Typing', () => { 9 | test('should start as empty value', async ({ page }) => { 10 | const input = page.getByRole('textbox') 11 | 12 | await expect(input).toHaveValue('') 13 | }) 14 | 15 | test('should change the input value', async ({ page }) => { 16 | const input = page.getByRole('textbox') 17 | 18 | await input.pressSequentially('1') 19 | await expect(input).toHaveValue('1') 20 | 21 | await input.pressSequentially('23456') 22 | await expect(input).toHaveValue('123456') 23 | }) 24 | 25 | test('should prevent typing greater than max length', async ({ page }) => { 26 | const input = page.getByRole('textbox') 27 | 28 | await input.pressSequentially('1234567') 29 | await expect(input).toHaveValue('123457') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /apps/test/src/tests/util/modifier.ts: -------------------------------------------------------------------------------- 1 | export const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' -------------------------------------------------------------------------------- /apps/test/src/tests/with-autofocus.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto('/with-autofocus') 5 | }) 6 | 7 | test.describe('With autofocus tests', () => { 8 | test('should autofocus', async ({ page }) => { 9 | const input = page.getByRole('textbox') 10 | 11 | await expect(input).toBeFocused() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /apps/test/src/tests/with-on-complete.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import { modifier } from './util/modifier' 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/with-on-complete') 6 | }) 7 | 8 | test.describe('With on complete tests', () => { 9 | test('should change the input value', async ({ page }) => { 10 | const input = page.getByRole('textbox') 11 | 12 | await input.pressSequentially('123456') 13 | await expect(input).toHaveValue('123456') 14 | 15 | await expect(input).toBeDisabled() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /apps/test/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /apps/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /apps/website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /apps/website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/website/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /apps/website/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/website/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withHydrationOverlay } from '@builder.io/react-hydration-overlay/next' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextProdConfig = {} 5 | 6 | const nextDevConfig = withHydrationOverlay({ 7 | appRootSelector: 'main', 8 | })(nextProdConfig) 9 | 10 | export default process.env.NODE_ENV === 'development' 11 | ? nextDevConfig 12 | : nextProdConfig 13 | -------------------------------------------------------------------------------- /apps/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3040", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@builder.io/react-hydration-overlay": "^0.0.8", 13 | "@radix-ui/react-dropdown-menu": "^2.0.6", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-scroll-area": "^1.0.5", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.0", 19 | "geist": "^1.2.2", 20 | "input-otp": "workspace:*", 21 | "lucide-react": "^0.330.0", 22 | "next": "14.1.0", 23 | "next-themes": "^0.2.1", 24 | "react": "^18", 25 | "react-canvas-confetti": "^2.0.5", 26 | "react-dom": "^18", 27 | "react-hook-form": "^7.50.1", 28 | "rehype-pretty-code": "^0.13.0", 29 | "rehype-stringify": "^10.0.0", 30 | "remark-parse": "^11.0.0", 31 | "remark-rehype": "^11.1.0", 32 | "sonner": "^1.4.0", 33 | "tailwind-merge": "^2.2.1", 34 | "tailwindcss-animate": "^1.0.7", 35 | "unified": "^11.0.4" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20", 39 | "@types/react": "^18", 40 | "@types/react-dom": "^18", 41 | "autoprefixer": "^10.0.1", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.1.0", 44 | "postcss": "^8", 45 | "tailwindcss": "^3.4.1", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/website/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/website/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/website/public/og.jpg -------------------------------------------------------------------------------- /apps/website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#0D0D0E", 17 | "background_color": "#0D0D0E", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /apps/website/public/sponsors/clerk-wordmark-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/clerk-wordmark-white-in-black-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/clerk-wordmark-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/evomi-wordmark-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/evomi-wordmark-white-in-black-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/evomi-wordmark-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/resend-wordmark-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/resend-wordmark-white-in-black-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/website/public/sponsors/resend-wordmark-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/example-auto-submit/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { formAction } from './server/form-action' 7 | import { ExampleComponent } from '../example-playground/component' 8 | 9 | export default function ExampleAutoSubmit() { 10 | const formRef = React.useRef(null) 11 | 12 | const [value, setValue] = React.useState('12') 13 | 14 | return ( 15 |
16 | formRef.current?.submit()} 21 | /> 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/example-auto-submit/server/form-action.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | export async function formAction(formData: FormData) { 4 | const rawFormData = { 5 | otp: formData.get('otp'), 6 | } 7 | 8 | console.log({ rawFormData }) 9 | } 10 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/example-playground/code.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@/components/code' 2 | import { useTheme } from 'next-themes' 3 | 4 | const tsx = `'use client' 5 | import { OTPInput, SlotProps } from 'input-otp' 6 | 7 | ( 11 | <> 12 |
13 | {slots.slice(0, 3).map((slot, idx) => ( 14 | 15 | ))} 16 |
17 | 18 | 19 | 20 |
21 | {slots.slice(3).map((slot, idx) => ( 22 | 23 | ))} 24 |
25 | 26 | )} 27 | /> 28 | 29 | // Feel free to copy. Uses @shadcn/ui tailwind colors. 30 | function Slot(props: SlotProps) { 31 | return ( 32 |
43 | {props.char !== null &&
{props.char}
} 44 | {props.hasFakeCaret && } 45 |
46 | ) 47 | } 48 | 49 | // You can emulate a fake textbox caret! 50 | function FakeCaret() { 51 | return ( 52 |
53 |
54 |
55 | ) 56 | } 57 | 58 | // Inspired by Stripe's MFA input. 59 | function FakeDash() { 60 | return ( 61 |
62 |
63 |
64 | ) 65 | } 66 | 67 | // tailwind.config.ts for the blinking caret animation. 68 | const config = { 69 | theme: { 70 | extend: { 71 | keyframes: { 72 | 'caret-blink': { 73 | '0%,70%,100%': { opacity: '1' }, 74 | '20%,50%': { opacity: '0' }, 75 | }, 76 | }, 77 | animation: { 78 | 'caret-blink': 'caret-blink 1.2s ease-out infinite', 79 | }, 80 | }, 81 | }, 82 | } 83 | 84 | // Small utility to merge class names. 85 | import { clsx } from "clsx"; 86 | import { twMerge } from "tailwind-merge"; 87 | 88 | import type { ClassValue } from "clsx"; 89 | 90 | export function cn(...inputs: ClassValue[]) { 91 | return twMerge(clsx(inputs)); 92 | } 93 | ` 94 | 95 | const code = `\`\`\`tsx /maxLength={6}/ /render/ /slots/1 /.map((slot, idx)/1 /Slot/2,3,4 /props.char/2 // 96 | ${tsx} 97 | \`\`\`` 98 | 99 | export function ExampleCode() { 100 | return ( 101 |
102 |
103 | 104 | 105 |
106 | 107 |
108 | 109 | {/* Anchor */} 110 |
111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/example-playground/component.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { OTPInput, SlotProps } from 'input-otp' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | export function ExampleComponent( 9 | props: Partial, 'children'>>, 10 | ) { 11 | return ( 12 | ( 17 | <> 18 |
19 | {slots.slice(0, 3).map((slot, idx) => ( 20 | 21 | ))} 22 |
23 | 24 | 25 | 26 |
27 | {slots.slice(3).map((slot, idx) => ( 28 | 29 | ))} 30 |
31 | 32 | )} 33 | /> 34 | ) 35 | } 36 | 37 | // Feel free to copy. Uses @shadcn/ui tailwind colors. 38 | function Slot(props: SlotProps) { 39 | return ( 40 |
51 | {props.char !== null &&
{props.char}
} 52 | {props.hasFakeCaret && } 53 |
54 | ) 55 | } 56 | 57 | // You can emulate a fake textbox caret! 58 | function FakeCaret() { 59 | return ( 60 |
61 |
62 |
63 | ) 64 | } 65 | 66 | // Inspired by Stripe's MFA input. 67 | function FakeDash() { 68 | return ( 69 |
70 |
71 |
72 | ) 73 | } 74 | 75 | // tailwind.config.ts for the blinking caret animation. 76 | const config = { 77 | theme: { 78 | extend: { 79 | keyframes: { 80 | 'caret-blink': { 81 | '0%,70%,100%': { opacity: '1' }, 82 | '20%,50%': { opacity: '0' }, 83 | }, 84 | }, 85 | animation: { 86 | 'caret-blink': 'caret-blink 1.2s ease-out infinite', 87 | }, 88 | }, 89 | }, 90 | } 91 | 92 | // Small utility to merge class names. 93 | // import { clsx } from "clsx"; 94 | // import { twMerge } from "tailwind-merge"; 95 | 96 | // import type { ClassValue } from "clsx"; 97 | 98 | // export function cn(...inputs: ClassValue[]) { 99 | // return twMerge(clsx(inputs)); 100 | // } 101 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/example-playground/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExampleComponent } from './component' 2 | 3 | export default function ExamplePlayground() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Toaster } from '@/components/ui/sonner' 3 | import { AppProvider } from '../../components/provider' 4 | import { fontSans } from '../../lib/fonts' 5 | import { cn } from '../../lib/utils' 6 | import '../globals.css' 7 | export default function RootLayout({ 8 | children, 9 | }: Readonly<{ 10 | children: React.ReactNode 11 | }>) { 12 | return ( 13 | 14 | 20 | 21 |
22 | {/* */} 23 |
{children}
24 | {/*
*/} 25 |
26 |
27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/raw-input/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | 5 | export default function ShadcnPage() { 6 | const [value, setValue] = React.useState('') 7 | 8 | return ( 9 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/shadcn/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | InputOTP, 5 | InputOTPGroup, 6 | InputOTPRenderSlot, 7 | InputOTPSeparator, 8 | InputOTPSlot, 9 | } from '@/components/ui/input-otp' 10 | import React from 'react' 11 | 12 | export default function ShadcnPage() { 13 | const [value, setValue] = React.useState('') 14 | 15 | return ( 16 |
17 | {/* With Render prop */} 18 | ( 23 | <> 24 | 25 | {slots.slice(0, 3).map((slot, index) => ( 26 | 27 | ))} 28 | 29 | 30 | 31 | {slots.slice(3).map((slot, index) => ( 32 | 33 | ))} 34 | 35 | 36 | )} 37 | /> 38 | 39 | {/* With Context API */} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/shadcn/pwmb/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | InputOTP, 5 | InputOTPGroup, 6 | InputOTPRenderSlot, 7 | InputOTPSeparator, 8 | InputOTPSlot, 9 | } from '@/components/ui/input-otp' 10 | import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp' 11 | import React from 'react' 12 | 13 | export default function ShadcnPage() { 14 | const [value, setValue] = React.useState('') 15 | 16 | return ( 17 |
18 | {/* */} 24 | ( 36 | <> 37 | 38 | {slots.slice(0, 3).map((slot, index) => ( 39 | 40 | ))} 41 | 42 | 43 | 44 | {slots.slice(3).map((slot, index) => ( 45 | 46 | ))} 47 | 48 | 49 | )} 50 | /> 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /apps/website/src/app/(local-pages)/shadcn/static/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { default as ShadcnPage } from '../page' 4 | 5 | export default async function StaticPage(pageProps: any) { 6 | return ( 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /apps/website/src/app/(pages)/(home)/_components/confetti.tsx: -------------------------------------------------------------------------------- 1 | import VFX from 'react-canvas-confetti/dist/presets/explosion' 2 | 3 | export function Confetti({ 4 | pageCoords = { x: window.innerWidth / 2, y: window.innerHeight / 2 }, 5 | }: { 6 | pageCoords?: { 7 | x: number 8 | y: number 9 | } 10 | }) { 11 | return ( 12 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /apps/website/src/app/(pages)/(home)/_components/showcase.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useMemo } from 'react' 4 | import { toast } from 'sonner' 5 | import dynamic from 'next/dynamic' 6 | 7 | import { OTPInput, REGEXP_ONLY_DIGITS } from 'input-otp' 8 | import { cn } from '@/lib/utils' 9 | 10 | const DynamicConfetti = dynamic(() => 11 | import('./confetti').then(m => m.Confetti), 12 | ) 13 | 14 | export function Showcase({ className, ...props }: { className?: string }) { 15 | const [value, setValue] = React.useState('12') 16 | const [disabled, setDisabled] = React.useState(false) 17 | 18 | const [preloadConfetti, setPreloadConfetti] = React.useState(0) 19 | const [hasGuessed, setHasGuessed] = React.useState(false) 20 | 21 | const inputRef = React.useRef(null) 22 | 23 | React.useEffect(() => { 24 | const isMobile = window.matchMedia('(max-width: 1023px)').matches 25 | if (!isMobile) { 26 | setDisabled(true) 27 | } 28 | const t1 = setTimeout(() => { 29 | setDisabled(false) 30 | }, 1_900) 31 | const t2 = setTimeout( 32 | () => { 33 | inputRef.current?.focus() 34 | }, 35 | isMobile ? 0 : 2_500, 36 | ) 37 | 38 | return () => { 39 | clearTimeout(t1) 40 | clearTimeout(t2) 41 | } 42 | }, []) 43 | 44 | React.useEffect(() => { 45 | if (value.length > 3) { 46 | setPreloadConfetti(p => p + 1) 47 | } 48 | }, [value.length]) 49 | 50 | async function onSubmit(e?: React.FormEvent) { 51 | e?.preventDefault?.() 52 | 53 | inputRef.current?.select() 54 | await new Promise(r => setTimeout(r, 1_00)) 55 | 56 | if (value === '123456') { 57 | setHasGuessed(true) 58 | 59 | setTimeout(() => { 60 | setHasGuessed(false) 61 | }, 1_000) 62 | } else { 63 | toast('Try guessing the right password 🤔', { position: 'top-right' }) 64 | } 65 | 66 | const anchor = document.querySelector( 67 | '#code-example-anchor', 68 | ) 69 | 70 | window.scrollTo({ 71 | top: anchor?.getBoundingClientRect().top, 72 | behavior: 'smooth', 73 | }) 74 | 75 | setValue('') 76 | setTimeout(() => { 77 | inputRef.current?.blur() 78 | }, 20) 79 | } 80 | 81 | return ( 82 | <> 83 | {preloadConfetti === 1 && ( 84 |
85 | 86 |
87 | )} 88 | {hasGuessed && ( 89 |
90 | 94 |
95 | )} 96 | 97 |
104 | ( 115 | <> 116 |
117 | {slots.slice(0, 3).map((slot, idx) => ( 118 | 124 | ))} 125 |
126 | 127 | {/* Layout inspired by Stripe */} 128 |
129 |
130 |
131 | 132 |
133 | {slots.slice(3).map((slot, idx) => ( 134 | 135 | ))} 136 |
137 | 138 | )} 139 | /> 140 | 141 | 142 | ) 143 | } 144 | 145 | function Slot(props: { 146 | char: string | null 147 | isActive: boolean 148 | isFocused: boolean 149 | animateIdx?: number 150 | }) { 151 | const willAnimateChar = props.animateIdx !== undefined && props.animateIdx < 2 152 | const willAnimateCaret = props.animateIdx === 2 153 | 154 | return ( 155 |
164 |
171 | {props.char &&
{props.char}
} 172 | {props.char === null && ' '} 173 |
174 | 175 | {props.isActive && props.char === null && ( 176 |
181 | 182 |
183 | )} 184 |
185 | ) 186 | } 187 | 188 | function FakeCaret() { 189 | return ( 190 |
191 |
192 |
193 | ) 194 | } 195 | -------------------------------------------------------------------------------- /apps/website/src/app/(pages)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { CopyNpmCommandButton } from '@/components/copy-button' 2 | import { Icons } from '@/components/icons' 3 | import { 4 | PageActions, 5 | PageHeader, 6 | PageHeaderDescription, 7 | PageHeaderHeading, 8 | } from '@/components/page-header' 9 | import { buttonVariants } from '@/components/ui/button' 10 | import { siteConfig } from '@/config/site' 11 | import { cn } from '@/lib/utils' 12 | import Link from 'next/link' 13 | import { Showcase } from './_components/showcase' 14 | import { ExampleCode } from '@/app/(local-pages)/example-playground/code' 15 | import Image from 'next/image' 16 | import { Badge } from '@/components/ui/badge' 17 | import { ChevronRightIcon } from 'lucide-react' 18 | 19 | const fadeUpClassname = 20 | 'lg:motion-safe:opacity-0 lg:motion-safe:animate-fade-up' 21 | 22 | async function getRepoStarCount() { 23 | const res = await fetch( 24 | 'https://api.github.com/repos/guilhermerodz/input-otp', 25 | ) 26 | const data = await res.json() 27 | const starCount = data.stargazers_count 28 | if (starCount > 999) { 29 | return (starCount / 1000).toFixed(1) + 'K' 30 | } 31 | return starCount 32 | } 33 | 34 | export default async function IndexPage() { 35 | const starCount = await getRepoStarCount() 36 | 37 | return ( 38 |
39 | 40 | {/*
*/} 41 | 42 | 43 | Stop wasting time building OTP inputs. 44 | 45 | 46 | 52 | 53 | 59 | One-time password input component for React. Accessible. Unstyled. 60 | Customizable. Open Source. 61 | 62 | 63 | 69 |
70 |
71 | npm install input-otp 72 |
73 | 81 |
82 | 91 | 92 |
93 |
GitHub
94 |
95 |
{starCount}
96 |
97 | 98 | 99 | 100 |
101 | 102 | 103 |
104 |

Hero Sponsors

105 | 139 | 140 |
141 | 142 | 143 | 144 |
145 | ) 146 | } 147 | 148 | export const revalidate = 3600 149 | 150 | const SponsorBadgeClerk = () => { 151 | return ( 152 | 153 | 154 | Looking for an authentication solution? 155 | 156 | 157 | Get Started with Clerk 158 | 159 | 160 | 161 | 162 | ) 163 | } 164 | -------------------------------------------------------------------------------- /apps/website/src/app/(pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | // import { HydrationOverlay } from '@builder.io/react-hydration-overlay' 4 | 5 | import { SiteFooter } from '../../components/site-footer' 6 | import { SiteHeader } from '../../components/site-header' 7 | import { siteConfig } from '../../config/site' 8 | import { fontSans } from '../../lib/fonts' 9 | import { cn } from '../../lib/utils' 10 | import '../globals.css' 11 | import { AppProvider } from '../../components/provider' 12 | import { Toaster } from '@/components/ui/sonner' 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: siteConfig.name, 17 | template: `%s - ${siteConfig.name}`, 18 | }, 19 | metadataBase: new URL(siteConfig.url), 20 | description: siteConfig.description, 21 | keywords: [ 22 | 'React', 23 | 'one-time-code', 24 | 'Input', 25 | 'Next.js', 26 | 'Tailwind CSS', 27 | 'Server Components', 28 | 'Accessible', 29 | ], 30 | authors: [ 31 | { 32 | name: 'guilhermerodz', 33 | url: 'https://rodz.dev', 34 | }, 35 | ], 36 | creator: 'guilhermerodz', 37 | openGraph: { 38 | type: 'website', 39 | locale: 'en_US', 40 | url: siteConfig.url, 41 | title: siteConfig.name, 42 | description: siteConfig.description, 43 | siteName: siteConfig.name, 44 | images: [ 45 | { 46 | url: siteConfig.ogImage, 47 | width: 1200, 48 | height: 630, 49 | alt: siteConfig.name, 50 | }, 51 | ], 52 | }, 53 | twitter: { 54 | card: 'summary_large_image', 55 | title: siteConfig.name, 56 | description: siteConfig.description, 57 | images: [siteConfig.ogImage], 58 | creator: '@guilherme_rodz', 59 | }, 60 | icons: { 61 | icon: '/favicon.ico', 62 | shortcut: '/favicon-16x16.png', 63 | apple: '/apple-touch-icon.png', 64 | }, 65 | manifest: `${siteConfig.url}/site.webmanifest`, 66 | } 67 | 68 | export default function RootLayout({ 69 | children, 70 | }: Readonly<{ 71 | children: React.ReactNode 72 | }>) { 73 | return ( 74 | 75 | 81 | 82 |
83 | 84 | 85 | {/* */} 86 |
{children}
87 | {/*
*/} 88 | 89 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /apps/website/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guilhermerodz/input-otp/d4aced995b40292cc45bf810ca31d69d23366ca6/apps/website/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/website/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 72.22% 50.59%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | /* @apply bg-background text-foreground selection:bg-[#6B2BF4] selection:text-foreground; */ 58 | @apply bg-background text-foreground; 59 | /* font-feature-settings: "rlig" 1, "calt" 1; */ 60 | font-synthesis-weight: none; 61 | text-rendering: optimizeLegibility; 62 | } 63 | } 64 | 65 | @layer utilities { 66 | } 67 | 68 | [data-highlighted-chars] { 69 | @apply bg-zinc-900 rounded; 70 | box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); 71 | } 72 | [data-highlighted-chars] .dark { 73 | @apply bg-zinc-700/50 rounded; 74 | box-shadow: 2px 2px 0 2px rgba(139, 139, 148, 0.5); 75 | } 76 | [data-highlighted-chars] * { 77 | @apply !text-white; 78 | } 79 | [data-rehype-pretty-code-figure] pre { 80 | @apply pb-4 pt-6 max-h-[650px] overflow-x-auto rounded-lg border !bg-transparent; 81 | } 82 | [data-rehype-pretty-code-figure] [data-line] { 83 | @apply inline-block min-h-4 w-full py-0.5 px-4; 84 | } 85 | 86 | html[class='light'] .code-example-light { 87 | display: unset; 88 | } 89 | html[class='light'] .code-example-dark { 90 | display: none; 91 | } 92 | html[class='dark'] .code-example-dark { 93 | display: unset; 94 | } 95 | html[class='dark'] .code-example-light { 96 | display: none; 97 | } 98 | -------------------------------------------------------------------------------- /apps/website/src/components/code.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { unified } from 'unified' 3 | import remarkParse from 'remark-parse' 4 | import remarkRehype from 'remark-rehype' 5 | import rehypeStringify from 'rehype-stringify' 6 | import rehypePrettyCode from 'rehype-pretty-code' 7 | import { CopyButton } from './copy-button' 8 | 9 | export async function Code({ 10 | code, 11 | toCopy, 12 | dark = true, 13 | }: { 14 | code: string 15 | toCopy?: string 16 | dark?: boolean 17 | }) { 18 | const highlightedCode = await highlightCode(code, dark) 19 | return ( 20 |
21 |
26 | 
27 |       {toCopy && (
28 |         
29 | 30 |
31 | )} 32 |
33 | ) 34 | } 35 | 36 | async function highlightCode(code: string, dark: boolean) { 37 | const file = await unified() 38 | .use(remarkParse) 39 | .use(remarkRehype) 40 | .use(rehypePrettyCode, { 41 | keepBackground: false, 42 | theme: dark ? 'vesper' : 'github-light', 43 | }) 44 | .use(rehypeStringify) 45 | .process(code) 46 | 47 | return String(file) 48 | } 49 | -------------------------------------------------------------------------------- /apps/website/src/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | // Stolen from @shadcn/ui the man the machine!! 2 | 'use client' 3 | 4 | import * as React from 'react' 5 | import { CheckIcon, CopyIcon } from '@radix-ui/react-icons' 6 | import type { DropdownMenuTriggerProps } from '@radix-ui/react-dropdown-menu' 7 | 8 | import { cn } from '@/lib/utils' 9 | import { Button } from './ui/button' 10 | import { 11 | DropdownMenu, 12 | DropdownMenuContent, 13 | DropdownMenuItem, 14 | DropdownMenuTrigger, 15 | } from './ui/dropdown-menu' 16 | 17 | interface CopyButtonProps extends React.HTMLAttributes { 18 | value: string 19 | src?: string 20 | } 21 | 22 | export async function copyToClipboardWithMeta(value: string) { 23 | window && window.isSecureContext && navigator.clipboard.writeText(value) 24 | } 25 | 26 | export function CopyButton({ 27 | value, 28 | className, 29 | src, 30 | ...props 31 | }: CopyButtonProps) { 32 | const [hasCopied, setHasCopied] = React.useState(false) 33 | 34 | React.useEffect(() => { 35 | setTimeout(() => { 36 | setHasCopied(false) 37 | }, 2000) 38 | }, [hasCopied]) 39 | 40 | return ( 41 | 61 | ) 62 | } 63 | 64 | interface CopyWithClassNamesProps extends DropdownMenuTriggerProps { 65 | value: string 66 | classNames: string 67 | className?: string 68 | } 69 | 70 | export function CopyWithClassNames({ 71 | value, 72 | classNames, 73 | className, 74 | ...props 75 | }: CopyWithClassNamesProps) { 76 | const [hasCopied, setHasCopied] = React.useState(false) 77 | 78 | React.useEffect(() => { 79 | setTimeout(() => { 80 | setHasCopied(false) 81 | }, 2000) 82 | }, [hasCopied]) 83 | 84 | const copyToClipboard = React.useCallback((value: string) => { 85 | copyToClipboardWithMeta(value) 86 | setHasCopied(true) 87 | }, []) 88 | 89 | return ( 90 | 91 | 92 | 107 | 108 | 109 | copyToClipboard(value)}> 110 | Component 111 | 112 | copyToClipboard(classNames)}> 113 | Classname 114 | 115 | 116 | 117 | ) 118 | } 119 | 120 | interface CopyNpmCommandButtonProps extends DropdownMenuTriggerProps { 121 | commands: { 122 | __npmCommand__: string 123 | __yarnCommand__: string 124 | __pnpmCommand__: string 125 | __bunCommand__: string 126 | } 127 | } 128 | 129 | export function CopyNpmCommandButton({ 130 | commands, 131 | className, 132 | ...props 133 | }: CopyNpmCommandButtonProps) { 134 | const [hasCopied, setHasCopied] = React.useState(false) 135 | 136 | React.useEffect(() => { 137 | setTimeout(() => { 138 | setHasCopied(false) 139 | }, 2000) 140 | }, [hasCopied]) 141 | 142 | const copyCommand = React.useCallback( 143 | (value: string, pm: 'npm' | 'pnpm' | 'yarn' | 'bun') => { 144 | copyToClipboardWithMeta(value) 145 | setHasCopied(true) 146 | }, 147 | [], 148 | ) 149 | 150 | return ( 151 | 152 | 153 | 168 | 169 | 170 | copyCommand(commands.__npmCommand__, 'npm')} 172 | > 173 | npm 174 | 175 | copyCommand(commands.__yarnCommand__, 'yarn')} 177 | > 178 | yarn 179 | 180 | copyCommand(commands.__pnpmCommand__, 'pnpm')} 182 | > 183 | pnpm 184 | 185 | copyCommand(commands.__bunCommand__, 'bun')} 187 | > 188 | bun 189 | 190 | 191 | 192 | ) 193 | } 194 | -------------------------------------------------------------------------------- /apps/website/src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme() 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/website/src/components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../lib/utils" 2 | 3 | function PageHeader({ 4 | className, 5 | children, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
16 | {children} 17 |
18 | ) 19 | } 20 | 21 | function PageHeaderHeading({ 22 | className, 23 | ...props 24 | }: React.HTMLAttributes) { 25 | return ( 26 |

33 | ) 34 | } 35 | 36 | function PageHeaderDescription({ 37 | className, 38 | ...props 39 | }: React.HTMLAttributes) { 40 | return ( 41 |

48 | ) 49 | } 50 | 51 | function PageActions({ 52 | className, 53 | ...props 54 | }: React.HTMLAttributes) { 55 | return ( 56 |

63 | ) 64 | } 65 | 66 | export { PageHeader, PageHeaderHeading, PageHeaderDescription, PageActions } -------------------------------------------------------------------------------- /apps/website/src/components/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider } from 'next-themes' 4 | 5 | export function AppProvider({ 6 | children, 7 | ...props 8 | }: React.PropsWithChildren<{}>) { 9 | return ( 10 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/website/src/components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "../config/site" 2 | 3 | export function SiteFooter() { 4 | return ( 5 | 30 | ) 31 | } -------------------------------------------------------------------------------- /apps/website/src/components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { siteConfig } from "../config/site" 4 | import { cn } from "../lib/utils" 5 | import { buttonVariants } from "./ui/button" 6 | import { Icons } from "./icons" 7 | import { ModeToggle } from "./mode-toggle" 8 | 9 | export function SiteHeader() { 10 | return ( 11 |
12 |
13 | {/* 14 | */} 15 |
16 | {/*
17 | 18 |
*/} 19 | 55 |
56 | 57 |
58 |
59 | ) 60 | } -------------------------------------------------------------------------------- /apps/website/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /apps/website/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /apps/website/src/components/ui/input-otp.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { OTPInput, OTPInputContext, SlotProps } from 'input-otp' 5 | import { Dot } from 'lucide-react' 6 | 7 | import { cn } from '@/lib/utils' 8 | 9 | const InputOTP = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ containerClassName, className, ...props }, ref) => ( 13 | 22 | )) 23 | InputOTP.displayName = 'InputOTP' 24 | 25 | const InputOTPGroup = React.forwardRef< 26 | React.ElementRef<'div'>, 27 | React.ComponentPropsWithoutRef<'div'> 28 | >(({ className, ...props }, ref) => ( 29 |
30 | )) 31 | InputOTPGroup.displayName = 'InputOTPGroup' 32 | 33 | type InputOTPSlotProps = { index: number } 34 | const InputOTPSlot = React.forwardRef< 35 | React.ElementRef<'div'>, 36 | React.ComponentPropsWithoutRef<'div'> & InputOTPSlotProps 37 | >(({ index, className, ...props }, ref) => { 38 | const inputContext = React.useContext(OTPInputContext) 39 | const slotProps = inputContext.slots[index] 40 | return ( 41 |
50 |
51 | {slotProps.char ?? slotProps.placeholderChar} 52 |
53 | {slotProps.hasFakeCaret && ( 54 |
55 |
56 |
57 | )} 58 |
59 | ) 60 | }) 61 | InputOTPSlot.displayName = 'InputOTPSlot' 62 | 63 | const InputOTPRenderSlot = React.forwardRef< 64 | React.ElementRef<'div'>, 65 | SlotProps & React.ComponentPropsWithoutRef<'div'> 66 | >(({ char, placeholderChar, hasFakeCaret, isActive, className, ...props }, ref) => { 67 | return ( 68 |
77 |
78 | {char ?? placeholderChar} 79 |
80 | {hasFakeCaret && ( 81 |
82 |
83 |
84 | )} 85 |
86 | ) 87 | }) 88 | InputOTPRenderSlot.displayName = 'InputOTPRenderSlot' 89 | 90 | const InputOTPSeparator = React.forwardRef< 91 | React.ElementRef<'div'>, 92 | React.ComponentPropsWithoutRef<'div'> 93 | >(({ ...props }, ref) => ( 94 |
95 | 96 |
97 | )) 98 | InputOTPSeparator.displayName = 'InputOTPSeparator' 99 | 100 | export { 101 | InputOTP, 102 | InputOTPGroup, 103 | InputOTPRenderSlot, 104 | InputOTPSlot, 105 | InputOTPSeparator, 106 | } 107 | -------------------------------------------------------------------------------- /apps/website/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /apps/website/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTheme } from 'next-themes' 4 | import { Toaster as Sonner } from 'sonner' 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = 'system' } = useTheme() 10 | 11 | return ( 12 | 30 | ) 31 | } 32 | 33 | export { Toaster } 34 | -------------------------------------------------------------------------------- /apps/website/src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: 'rodz/input-otp', 3 | url: 'https://input-otp.rodz.dev', 4 | ogImage: 'https://input-otp.rodz.dev/og.jpg', 5 | description: 6 | 'One-time password input component for React. Accessible. Unstyled. Customizable. Open Source. Build your own OTP form effortlessly.', 7 | links: { 8 | twitter: 'https://twitter.com/guilherme_rodz', 9 | github: 'https://github.com/guilhermerodz/input-otp', 10 | }, 11 | } 12 | 13 | export type SiteConfig = typeof siteConfig 14 | -------------------------------------------------------------------------------- /apps/website/src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | // import { JetBrains_Mono as FontMono, Inter as FontSans } from "next/font/google" 2 | import { JetBrains_Mono as FontMono } from 'next/font/google' 3 | // import { GeistMono } from "geist/font/mono" 4 | import { GeistSans } from 'geist/font/sans' 5 | 6 | // export const fontSans = FontSans({ 7 | // subsets: ["latin"], 8 | // variable: "--font-sans", 9 | // }) 10 | export const fontSans = GeistSans 11 | 12 | export const fontMono = FontMono({ 13 | subsets: ['latin'], 14 | variable: '--font-mono', 15 | }) 16 | -------------------------------------------------------------------------------- /apps/website/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/website/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: '', 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: '2rem', 16 | screens: { 17 | '2xl': '1400px', 18 | }, 19 | }, 20 | extend: { 21 | animation: { 22 | 'accordion-down': 'accordion-down 0.2s ease-out', 23 | 'accordion-up': 'accordion-up 0.2s ease-out', 24 | 25 | 'caret-blink': 'caret-blink 1.2s ease-out infinite', 26 | 27 | 'fade-in': 'fade-in 0.3s ease-out forwards', 28 | 'fade-up': 'fade-up 1s ease-out forwards', 29 | }, 30 | borderRadius: { 31 | lg: 'var(--radius)', 32 | md: 'calc(var(--radius) - 2px)', 33 | sm: 'calc(var(--radius) - 4px)', 34 | }, 35 | colors: { 36 | border: 'hsl(var(--border))', 37 | input: 'hsl(var(--input))', 38 | ring: 'hsl(var(--ring))', 39 | background: 'hsl(var(--background))', 40 | foreground: 'hsl(var(--foreground))', 41 | primary: { 42 | DEFAULT: 'hsl(var(--primary))', 43 | foreground: 'hsl(var(--primary-foreground))', 44 | }, 45 | secondary: { 46 | DEFAULT: 'hsl(var(--secondary))', 47 | foreground: 'hsl(var(--secondary-foreground))', 48 | }, 49 | destructive: { 50 | DEFAULT: 'hsl(var(--destructive))', 51 | foreground: 'hsl(var(--destructive-foreground))', 52 | }, 53 | muted: { 54 | DEFAULT: 'hsl(var(--muted))', 55 | foreground: 'hsl(var(--muted-foreground))', 56 | }, 57 | accent: { 58 | DEFAULT: 'hsl(var(--accent))', 59 | foreground: 'hsl(var(--accent-foreground))', 60 | }, 61 | popover: { 62 | DEFAULT: 'hsl(var(--popover))', 63 | foreground: 'hsl(var(--popover-foreground))', 64 | }, 65 | card: { 66 | DEFAULT: 'hsl(var(--card))', 67 | foreground: 'hsl(var(--card-foreground))', 68 | }, 69 | }, 70 | keyframes: { 71 | 'accordion-down': { 72 | from: { height: '0' }, 73 | to: { height: 'var(--radix-accordion-content-height)' }, 74 | }, 75 | 'accordion-up': { 76 | from: { height: 'var(--radix-accordion-content-height)' }, 77 | to: { height: '0' }, 78 | }, 79 | 80 | 'caret-blink': { 81 | '0%,70%,100%': { 82 | opacity: '1', 83 | }, 84 | '20%,50%': { 85 | opacity: '0', 86 | }, 87 | }, 88 | 89 | 'fade-in': { 90 | from: { 91 | opacity: '0', 92 | }, 93 | to: { opacity: '1' }, 94 | }, 95 | 'fade-up': { 96 | from: { 97 | opacity: '0', 98 | transform: 'translateY(var(--fade-distance, .25rem))', 99 | }, 100 | to: { opacity: '1', transform: 'translateY(0)' }, 101 | }, 102 | }, 103 | transitionDelay: { 104 | 1500: '1500ms', 105 | }, 106 | transitionTimingFunction: { 107 | 'default': 'ease-out', 108 | } 109 | }, 110 | }, 111 | plugins: [require('tailwindcss-animate')], 112 | } satisfies Config 113 | 114 | export default config 115 | -------------------------------------------------------------------------------- /apps/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | "website/.next/types/**/*.ts", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "author": "Guilherme Rodz ", 5 | "description": "One-time password input component for React.", 6 | "homepage": "https://input-otp.rodz.dev/", 7 | "workspaces": [ 8 | "packages/*", 9 | "apps/*" 10 | ], 11 | "scripts": { 12 | "build": "turbo run build --filter=*", 13 | "build:lib": "turbo run build --filter=input-otp...", 14 | "build:website": "turbo run build --filter=website...", 15 | "clean": "turbo run clean", 16 | "dev": "turbo run dev --filter=*", 17 | "dev:website": "turbo run dev --filter=website...", 18 | "dev:test": "turbo run dev --filter=test...", 19 | "start:website": "pnpm --filter=website start", 20 | "storybook": "turbo run storybook", 21 | "test": "turbo run test --filter=test...", 22 | "test:ui": "turbo run test:ui --filter=test...", 23 | "type-check": "turbo run type-check", 24 | "lint:lib": "turbo run lint --filter=input-otp", 25 | "format": "prettier --write .", 26 | "release": "run-s test build:lib && cd ./packages/input-otp && cp ../../README.md . && pnpm release && rimraf ./README.md && cd ../..", 27 | "release:bypass": "run-s build:lib && cd ./packages/input-otp && cp ../../README.md . && pnpm release && rimraf ./README.md && cd ../..", 28 | "release:beta": "run-s test build:lib && cd ./packages/input-otp && cp ../../README.md . && pnpm release:beta && rimraf ./README.md && cd ../..", 29 | "release:beta:bypass": "run-s build:lib && cd ./packages/input-otp && cp ../../README.md . && pnpm release:beta && rimraf ./README.md && cd ../.." 30 | }, 31 | "prettier": { 32 | "tabWidth": 2, 33 | "printWidth": 80, 34 | "useTabs": false, 35 | "semi": false, 36 | "singleQuote": true, 37 | "trailingComma": "all", 38 | "arrowParens": "avoid", 39 | "endOfLine": "lf" 40 | }, 41 | "trustedDependencies": [ 42 | "npm-run-all" 43 | ], 44 | "devDependencies": { 45 | "@playwright/test": "^1.41.2", 46 | "@types/node": "^20", 47 | "@typescript-eslint/eslint-plugin": "^7.0.2", 48 | "@typescript-eslint/parser": "^7.0.2", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-plugin-import": "^2.29.1", 51 | "eslint-plugin-react": "^7.33.2", 52 | "eslint-plugin-react-hooks": "^4.6.0", 53 | "npm-run-all": "^4.1.5", 54 | "prettier": "^3.2.5", 55 | "rimraf": "^5.0.5", 56 | "tsup": "^8.0.2", 57 | "turbo": "^1.12.4", 58 | "typescript": "^5.3.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/input-otp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "input-otp", 3 | "version": "1.4.2", 4 | "author": "Guilherme Rodz ", 5 | "description": "One-time password input component for React.", 6 | "license": "MIT", 7 | "homepage": "https://input-otp.rodz.dev/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/guilhermerodz/input-otp.git", 11 | "directory": "packages/input-otp" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/guilhermerodz/input-otp/issues" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "otp", 19 | "input", 20 | "accessible" 21 | ], 22 | "main": "./dist/index.js", 23 | "module": "./dist/index.mjs", 24 | "types": "./dist/index.d.ts", 25 | "files": [ 26 | "dist" 27 | ], 28 | "exports": { 29 | ".": { 30 | "types": "./dist/index.d.ts", 31 | "module": "./dist/index.mjs", 32 | "import": "./dist/index.mjs", 33 | "require": "./dist/index.js", 34 | "default": "./dist/index.mjs" 35 | }, 36 | "./package.json": "./package.json" 37 | }, 38 | "scripts": { 39 | "type-check": "tsc --noEmit", 40 | "copy-readme": "cp ../../README.md ./README.md", 41 | "build": "run-s build:* copy-readme", 42 | "build:tsup": "tsup --dts --minify", 43 | "clean": "rimraf dist", 44 | "dev": "tsup --watch --dts", 45 | "lint": "run-p lint:*", 46 | "lint:eslint": "eslint src --ext .ts", 47 | "lint:eslint:fix": "eslint src --ext .ts --fix", 48 | "lint:format": "prettier --check \"src/**/*.ts\"", 49 | "lint:format:fix": "prettier --check \"src/**/*.ts\" --write", 50 | "lint:tsc": "tsc --project tsconfig.json --noEmit", 51 | "format": "prettier --write .", 52 | "release": "npm publish", 53 | "release:beta": "npm publish --tag beta" 54 | }, 55 | "eslintConfig": { 56 | "root": true, 57 | "reportUnusedDisableDirectives": true, 58 | "ignorePatterns": [ 59 | "**/build", 60 | "**/coverage", 61 | "**/dist" 62 | ], 63 | "parser": "@typescript-eslint/parser", 64 | "parserOptions": { 65 | "sourceType": "module", 66 | "ecmaVersion": 2020 67 | }, 68 | "settings": { 69 | "import/parsers": { 70 | "@typescript-eslint/parser": [ 71 | ".ts", 72 | ".tsx" 73 | ] 74 | }, 75 | "import/resolver": { 76 | "typescript": true 77 | }, 78 | "react": { 79 | "version": "detect" 80 | } 81 | }, 82 | "plugins": [ 83 | "@typescript-eslint", 84 | "import" 85 | ], 86 | "extends": [ 87 | "plugin:react/jsx-runtime", 88 | "plugin:react-hooks/recommended", 89 | "eslint:recommended", 90 | "plugin:@typescript-eslint/recommended", 91 | "plugin:@typescript-eslint/stylistic", 92 | "plugin:import/recommended", 93 | "plugin:import/typescript", 94 | "prettier" 95 | ], 96 | "env": { 97 | "browser": true, 98 | "es2020": true 99 | }, 100 | "rules": { 101 | "react/jsx-key": [ 102 | "error", 103 | { 104 | "checkFragmentShorthand": true 105 | } 106 | ], 107 | "react-hooks/exhaustive-deps": "error", 108 | "@typescript-eslint/no-explicit-any": "off" 109 | } 110 | }, 111 | "devDependencies": { 112 | "@types/react": "^18.2.55", 113 | "@types/react-dom": "^18.2.19", 114 | "react": "^18.2.0", 115 | "react-dom": "^18.2.0", 116 | "typescript": "^5.3.3" 117 | }, 118 | "peerDependencies": { 119 | "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", 120 | "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/input-otp/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input' 2 | export * from './regexp' 3 | export { OTPInputProps, SlotProps, RenderProps } from './types' 4 | -------------------------------------------------------------------------------- /packages/input-otp/src/regexp.ts: -------------------------------------------------------------------------------- 1 | export const REGEXP_ONLY_DIGITS = '^\\d+$' 2 | export const REGEXP_ONLY_CHARS = '^[a-zA-Z]+$' 3 | export const REGEXP_ONLY_DIGITS_AND_CHARS = '^[a-zA-Z0-9]+$' 4 | -------------------------------------------------------------------------------- /packages/input-otp/src/sync-timeouts.ts: -------------------------------------------------------------------------------- 1 | export function syncTimeouts(cb: (...args: any[]) => unknown): number[] { 2 | const t1 = setTimeout(cb, 0) // For faster machines 3 | const t2 = setTimeout(cb, 1_0) 4 | const t3 = setTimeout(cb, 5_0) 5 | return [t1, t2, t3] 6 | } 7 | -------------------------------------------------------------------------------- /packages/input-otp/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface SlotProps { 2 | isActive: boolean 3 | char: string | null 4 | placeholderChar: string | null 5 | hasFakeCaret: boolean 6 | } 7 | export interface RenderProps { 8 | slots: SlotProps[] 9 | isFocused: boolean 10 | isHovering: boolean 11 | } 12 | type OverrideProps = Omit & R 13 | type OTPInputBaseProps = OverrideProps< 14 | React.InputHTMLAttributes, 15 | { 16 | value?: string 17 | onChange?: (newValue: string) => unknown 18 | 19 | maxLength: number 20 | 21 | textAlign?: 'left' | 'center' | 'right' 22 | 23 | onComplete?: (...args: any[]) => unknown 24 | pushPasswordManagerStrategy?: 'increase-width' | 'none' 25 | pasteTransformer?: (pasted: string) => string 26 | 27 | containerClassName?: string 28 | 29 | noScriptCSSFallback?: string | null 30 | } 31 | > 32 | type InputOTPRenderFn = (props: RenderProps) => React.ReactNode 33 | export type OTPInputProps = OTPInputBaseProps & 34 | ( 35 | | { 36 | render: InputOTPRenderFn 37 | children?: never 38 | } 39 | | { 40 | render?: never 41 | children: React.ReactNode 42 | } 43 | ) 44 | -------------------------------------------------------------------------------- /packages/input-otp/src/use-previous.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function usePrevious(value: T) { 4 | const ref = React.useRef() 5 | React.useEffect(() => { 6 | ref.current = value 7 | }) 8 | return ref.current 9 | } 10 | -------------------------------------------------------------------------------- /packages/input-otp/src/use-pwm-badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { OTPInputProps } from './types' 3 | 4 | const PWM_BADGE_MARGIN_RIGHT = 18 5 | const PWM_BADGE_SPACE_WIDTH_PX = 40 6 | const PWM_BADGE_SPACE_WIDTH = `${PWM_BADGE_SPACE_WIDTH_PX}px` as const 7 | 8 | const PASSWORD_MANAGERS_SELECTORS = [ 9 | '[data-lastpass-icon-root]', // LastPass 10 | 'com-1password-button', // 1Password 11 | '[data-dashlanecreated]', // Dashlane 12 | '[style$="2147483647 !important;"]', // Bitwarden 13 | ].join(',') 14 | 15 | export function usePasswordManagerBadge({ 16 | containerRef, 17 | inputRef, 18 | pushPasswordManagerStrategy, 19 | isFocused, 20 | }: { 21 | containerRef: React.RefObject 22 | inputRef: React.RefObject 23 | pushPasswordManagerStrategy: OTPInputProps['pushPasswordManagerStrategy'] 24 | isFocused: boolean 25 | }) { 26 | /** Password managers have a badge 27 | * and I'll use this state to push them 28 | * outside the input */ 29 | const [hasPWMBadge, setHasPWMBadge] = React.useState(false) 30 | const [hasPWMBadgeSpace, setHasPWMBadgeSpace] = React.useState(false) 31 | const [done, setDone] = React.useState(false) 32 | 33 | const willPushPWMBadge = React.useMemo(() => { 34 | if (pushPasswordManagerStrategy === 'none') { 35 | return false 36 | } 37 | 38 | const increaseWidthCase = 39 | (pushPasswordManagerStrategy === 'increase-width' || 40 | // TODO: remove 'experimental-no-flickering' support in 2.0.0 41 | pushPasswordManagerStrategy === 'experimental-no-flickering') && 42 | hasPWMBadge && 43 | hasPWMBadgeSpace 44 | 45 | return increaseWidthCase 46 | }, [hasPWMBadge, hasPWMBadgeSpace, pushPasswordManagerStrategy]) 47 | 48 | const trackPWMBadge = React.useCallback(() => { 49 | const container = containerRef.current 50 | const input = inputRef.current 51 | if ( 52 | !container || 53 | !input || 54 | done || 55 | pushPasswordManagerStrategy === 'none' 56 | ) { 57 | return 58 | } 59 | 60 | const elementToCompare = container 61 | 62 | // Get the top right-center point of the container. 63 | // That is usually where most password managers place their badge. 64 | const rightCornerX = 65 | elementToCompare.getBoundingClientRect().left + 66 | elementToCompare.offsetWidth 67 | const centereredY = 68 | elementToCompare.getBoundingClientRect().top + 69 | elementToCompare.offsetHeight / 2 70 | const x = rightCornerX - PWM_BADGE_MARGIN_RIGHT 71 | const y = centereredY 72 | 73 | // Do an extra search to check for famous password managers 74 | const pmws = document.querySelectorAll(PASSWORD_MANAGERS_SELECTORS) 75 | 76 | // If no password manager is automatically detect, 77 | // we'll try to dispatch document.elementFromPoint 78 | // to identify badges 79 | if (pmws.length === 0) { 80 | const maybeBadgeEl = document.elementFromPoint(x, y) 81 | 82 | // If the found element is the input itself, 83 | // then we assume it's not a password manager badge. 84 | // We are not sure. Most times that means there isn't a badge. 85 | if (maybeBadgeEl === container) { 86 | return 87 | } 88 | } 89 | 90 | setHasPWMBadge(true) 91 | setDone(true) 92 | }, [containerRef, inputRef, done, pushPasswordManagerStrategy]) 93 | 94 | React.useEffect(() => { 95 | const container = containerRef.current 96 | if (!container || pushPasswordManagerStrategy === 'none') { 97 | return 98 | } 99 | 100 | // Check if the PWM area is 100% visible 101 | function checkHasSpace() { 102 | const viewportWidth = window.innerWidth 103 | const distanceToRightEdge = 104 | viewportWidth - container.getBoundingClientRect().right 105 | setHasPWMBadgeSpace(distanceToRightEdge >= PWM_BADGE_SPACE_WIDTH_PX) 106 | } 107 | 108 | checkHasSpace() 109 | const interval = setInterval(checkHasSpace, 1000) 110 | 111 | return () => { 112 | clearInterval(interval) 113 | } 114 | }, [containerRef, pushPasswordManagerStrategy]) 115 | 116 | React.useEffect(() => { 117 | const _isFocused = isFocused || document.activeElement === inputRef.current 118 | 119 | if (pushPasswordManagerStrategy === 'none' || !_isFocused) { 120 | return 121 | } 122 | const t1 = setTimeout(trackPWMBadge, 0) 123 | const t2 = setTimeout(trackPWMBadge, 2000) 124 | const t3 = setTimeout(trackPWMBadge, 5000) 125 | const t4 = setTimeout(() => { 126 | setDone(true) 127 | }, 6000) 128 | return () => { 129 | clearTimeout(t1) 130 | clearTimeout(t2) 131 | clearTimeout(t3) 132 | clearTimeout(t4) 133 | } 134 | }, [inputRef, isFocused, pushPasswordManagerStrategy, trackPWMBadge]) 135 | 136 | return { hasPWMBadge, willPushPWMBadge, PWM_BADGE_SPACE_WIDTH } 137 | } 138 | -------------------------------------------------------------------------------- /packages/input-otp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "lib": ["es2015", "dom"], 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "removeComments": false 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/input-otp/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | name: 'save input-otp', 5 | entry: ['src/index.ts'], 6 | format: ['cjs', 'esm'], 7 | outDir: 'dist', 8 | clean: true, 9 | sourcemap: true, 10 | }); 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["//"], 3 | "$schema": "https://turbo.build/schema.json", 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**"] 8 | }, 9 | "clean": { 10 | "cache": false 11 | }, 12 | "dev": { 13 | "cache": false 14 | }, 15 | "storybook": { 16 | "cache": false 17 | }, 18 | "test#test": { 19 | "dependsOn": ["input-otp#build"], 20 | "cache": false 21 | }, 22 | "test:ui": { 23 | "dependsOn": ["input-otp#build"], 24 | "cache": false 25 | }, 26 | "type-check": { 27 | "cache": false 28 | }, 29 | "lint": { 30 | "cache": false 31 | }, 32 | "format": { 33 | "cache": false 34 | } 35 | } 36 | } 37 | --------------------------------------------------------------------------------