├── .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 |
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 |
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 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/colors.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/comments.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/direction.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/flow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/plugin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/repo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/storybook/src/stories/assets/stackalt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
14 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/clerk-wordmark-white-in-black-bg.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/clerk-wordmark-white.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/evomi-wordmark-black.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/evomi-wordmark-white-in-black-bg.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/evomi-wordmark-white.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/resend-wordmark-black.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/resend-wordmark-white-in-black-bg.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/apps/website/public/sponsors/resend-wordmark-white.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
55 | )
56 | }
57 |
58 | // Inspired by Stripe's MFA input.
59 | function FakeDash() {
60 | return (
61 |
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 |
63 | )
64 | }
65 |
66 | // Inspired by Stripe's MFA input.
67 | function FakeDash() {
68 | return (
69 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------