Skip to content

Commit 6ab704a

Browse files
feat: pin input (#325)
* feat: pin input * chore: build registry * chore: build registry, add form example * chore: update demo abit --------- Co-authored-by: zernonia <zernonia@gmail.com>
1 parent b0e1b55 commit 6ab704a

File tree

16 files changed

+406
-0
lines changed

16 files changed

+406
-0
lines changed

apps/www/.vitepress/theme/config/docs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ export const docsConfig: DocsConfig = {
254254
href: '/docs/components/pagination',
255255
items: [],
256256
},
257+
{
258+
title: 'Pin Input',
259+
href: '/docs/components/pin-input',
260+
label: 'New',
261+
items: [],
262+
},
257263
{
258264
title: 'Popover',
259265
href: '/docs/components/popover',

apps/www/__registry__/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,20 @@ export const Index = {
485485
component: () => import('../src/lib/registry/default/example/PaginationDemo.vue').then(m => m.default),
486486
files: ['../src/lib/registry/default/example/PaginationDemo.vue'],
487487
},
488+
PinInputDemo: {
489+
name: 'PinInputDemo',
490+
type: 'components:example',
491+
registryDependencies: ['pin-input'],
492+
component: () => import('../src/lib/registry/default/example/PinInputDemo.vue').then(m => m.default),
493+
files: ['../src/lib/registry/default/example/PinInputDemo.vue'],
494+
},
495+
PinInputFormDemo: {
496+
name: 'PinInputFormDemo',
497+
type: 'components:example',
498+
registryDependencies: ['pin-input', 'button', 'form', 'toast'],
499+
component: () => import('../src/lib/registry/default/example/PinInputFormDemo.vue').then(m => m.default),
500+
files: ['../src/lib/registry/default/example/PinInputFormDemo.vue'],
501+
},
488502
PopoverDemo: {
489503
name: 'PopoverDemo',
490504
type: 'components:example',
@@ -1390,6 +1404,20 @@ export const Index = {
13901404
component: () => import('../src/lib/registry/new-york/example/PaginationDemo.vue').then(m => m.default),
13911405
files: ['../src/lib/registry/new-york/example/PaginationDemo.vue'],
13921406
},
1407+
PinInputDemo: {
1408+
name: 'PinInputDemo',
1409+
type: 'components:example',
1410+
registryDependencies: ['pin-input'],
1411+
component: () => import('../src/lib/registry/new-york/example/PinInputDemo.vue').then(m => m.default),
1412+
files: ['../src/lib/registry/new-york/example/PinInputDemo.vue'],
1413+
},
1414+
PinInputFormDemo: {
1415+
name: 'PinInputFormDemo',
1416+
type: 'components:example',
1417+
registryDependencies: ['pin-input', 'button', 'form', 'toast'],
1418+
component: () => import('../src/lib/registry/new-york/example/PinInputFormDemo.vue').then(m => m.default),
1419+
files: ['../src/lib/registry/new-york/example/PinInputFormDemo.vue'],
1420+
},
13931421
PopoverDemo: {
13941422
name: 'PopoverDemo',
13951423
type: 'components:example',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: PIN Input
3+
description: Allows users to input a sequence of one-character alphanumeric inputs.
4+
source: apps/www/src/lib/registry/default/ui/pin-input
5+
primitive: https://www.radix-vue.com/components/pin-input.html
6+
---
7+
8+
<ComponentPreview name="PinInputDemo" />
9+
10+
11+
## Installation
12+
13+
```bash
14+
npx shadcn-vue@latest add pin-input
15+
```
16+
17+
## Usage
18+
19+
### Form
20+
21+
<ComponentPreview name="PinInputFormDemo" />
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import {
4+
PinInput,
5+
PinInputInput,
6+
} from '@/lib/registry/default/ui/pin-input'
7+
8+
const value = ref<string[]>([])
9+
const handleComplete = (e: string[]) => alert(e.join(''))
10+
</script>
11+
12+
<template>
13+
<div>
14+
<PinInput
15+
id="pin-input"
16+
v-model="value"
17+
placeholder=""
18+
class="flex gap-2 items-center mt-1"
19+
@complete="handleComplete"
20+
>
21+
<PinInputInput
22+
v-for="(id, index) in 5"
23+
:key="id"
24+
:index="index"
25+
/>
26+
</PinInput>
27+
</div>
28+
</template>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<script setup lang="ts">
2+
import { h } from 'vue'
3+
import { useForm } from 'vee-validate'
4+
import { toTypedSchema } from '@vee-validate/zod'
5+
import * as z from 'zod'
6+
import {
7+
PinInput,
8+
PinInputInput,
9+
} from '@/lib/registry/new-york/ui/pin-input'
10+
import { Button } from '@/lib/registry/default/ui/button'
11+
import {
12+
FormControl,
13+
FormDescription,
14+
FormField,
15+
FormItem,
16+
FormLabel,
17+
FormMessage,
18+
} from '@/lib/registry/default/ui/form'
19+
import { toast } from '@/lib/registry/default/ui/toast'
20+
21+
const formSchema = toTypedSchema(z.object({
22+
pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }),
23+
}))
24+
25+
const { handleSubmit, setValues } = useForm({
26+
validationSchema: formSchema,
27+
initialValues: {
28+
pin: [],
29+
},
30+
})
31+
32+
const onSubmit = handleSubmit(({ pin }) => {
33+
toast({
34+
title: 'You submitted the following values:',
35+
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(pin.join(''), null, 2))),
36+
})
37+
})
38+
39+
const handleComplete = (e: string[]) => console.log(e.join(''))
40+
</script>
41+
42+
<template>
43+
<form class="w-2/3 space-y-6 mx-auto" @submit="onSubmit">
44+
<FormField v-slot="{ componentField }" name="pin">
45+
<FormItem>
46+
<FormLabel>OTP</FormLabel>
47+
<FormControl>
48+
<PinInput
49+
id="pin-input"
50+
placeholder=""
51+
class="flex gap-2 items-center mt-1"
52+
otp
53+
type="number"
54+
:name="componentField.name"
55+
@complete="handleComplete"
56+
@update:model-value="(arrStr) => {
57+
setValues({
58+
pin: arrStr.filter(Boolean),
59+
})
60+
}"
61+
>
62+
<PinInputInput
63+
v-for="(id, index) in 5"
64+
:key="id"
65+
:index="index"
66+
/>
67+
</PinInput>
68+
</FormControl>
69+
<FormDescription>
70+
Allows users to input a sequence of one-character alphanumeric inputs.
71+
</FormDescription>
72+
<FormMessage />
73+
</FormItem>
74+
</FormField>
75+
76+
<Button>Submit</Button>
77+
</form>
78+
</template>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { PinInputRoot, type PinInputRootEmits, type PinInputRootProps, useForwardPropsEmits } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<PinInputRootProps & { class?: HTMLAttributes['class'] }>()
7+
const emits = defineEmits<PinInputRootEmits>()
8+
9+
const delegatedProps = computed(() => {
10+
const { class: _, ...delegated } = props
11+
return delegated
12+
})
13+
14+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
15+
</script>
16+
17+
<template>
18+
<PinInputRoot v-bind="forwarded" :class="cn('flex gap-2 items-center', props.class)">
19+
<slot />
20+
</PinInputRoot>
21+
</template>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { PinInputInput, type PinInputInputProps, useForwardProps } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<PinInputInputProps & { class?: HTMLAttributes['class'] }>()
7+
8+
const delegatedProps = computed(() => {
9+
const { class: _, ...delegated } = props
10+
return delegated
11+
})
12+
13+
const forwardedProps = useForwardProps(delegatedProps)
14+
</script>
15+
16+
<template>
17+
<PinInputInput v-bind="forwardedProps" :class="cn('flex w-10 h-10 text-center rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
18+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as PinInput } from './PinInput.vue'
2+
export { default as PinInputInput } from './PinInputInput.vue'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import {
4+
PinInput,
5+
PinInputInput,
6+
} from '@/lib/registry/new-york/ui/pin-input'
7+
8+
const value = ref<string[]>([])
9+
function handleComplete() {
10+
console.log('212121')
11+
}
12+
</script>
13+
14+
<template>
15+
<PinInput
16+
id="pin-input"
17+
v-model="value"
18+
placeholder=""
19+
class="flex gap-2 items-center mt-1"
20+
@complete="handleComplete"
21+
>
22+
<PinInputInput
23+
v-for="(id, index) in 5"
24+
:key="id"
25+
:index="index"
26+
/>
27+
</PinInput>
28+
</template>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<script setup lang="ts">
2+
import { h } from 'vue'
3+
import { useForm } from 'vee-validate'
4+
import { toTypedSchema } from '@vee-validate/zod'
5+
import * as z from 'zod'
6+
import {
7+
PinInput,
8+
PinInputInput,
9+
} from '@/lib/registry/new-york/ui/pin-input'
10+
import { Button } from '@/lib/registry/new-york/ui/button'
11+
import {
12+
FormControl,
13+
FormDescription,
14+
FormField,
15+
FormItem,
16+
FormLabel,
17+
FormMessage,
18+
} from '@/lib/registry/new-york/ui/form'
19+
import { toast } from '@/lib/registry/new-york/ui/toast'
20+
21+
const formSchema = toTypedSchema(z.object({
22+
pin: z.array(z.coerce.string()).length(5, { message: 'Invalid input' }),
23+
}))
24+
25+
const { handleSubmit, setValues } = useForm({
26+
validationSchema: formSchema,
27+
initialValues: {
28+
pin: [],
29+
},
30+
})
31+
32+
const onSubmit = handleSubmit(({ pin }) => {
33+
toast({
34+
title: 'You submitted the following values:',
35+
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(pin.join(''), null, 2))),
36+
})
37+
})
38+
39+
const handleComplete = (e: string[]) => console.log(e.join(''))
40+
</script>
41+
42+
<template>
43+
<form class="w-2/3 space-y-6 mx-auto" @submit="onSubmit">
44+
<FormField v-slot="{ componentField }" name="pin">
45+
<FormItem>
46+
<FormLabel>OTP</FormLabel>
47+
<FormControl>
48+
<PinInput
49+
id="pin-input"
50+
placeholder=""
51+
class="flex gap-2 items-center mt-1"
52+
otp
53+
type="number"
54+
:name="componentField.name"
55+
@complete="handleComplete"
56+
@update:model-value="(arrStr) => {
57+
setValues({
58+
pin: arrStr.filter(Boolean),
59+
})
60+
}"
61+
>
62+
<PinInputInput
63+
v-for="(id, index) in 5"
64+
:key="id"
65+
:index="index"
66+
/>
67+
</PinInput>
68+
</FormControl>
69+
<FormDescription>
70+
Allows users to input a sequence of one-character alphanumeric inputs.
71+
</FormDescription>
72+
<FormMessage />
73+
</FormItem>
74+
</FormField>
75+
76+
<Button>Submit</Button>
77+
</form>
78+
</template>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { PinInputRoot, type PinInputRootEmits, type PinInputRootProps, useForwardPropsEmits } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<PinInputRootProps & { class?: HTMLAttributes['class'] }>()
7+
const emits = defineEmits<PinInputRootEmits>()
8+
9+
const delegatedProps = computed(() => {
10+
const { class: _, ...delegated } = props
11+
return delegated
12+
})
13+
14+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
15+
</script>
16+
17+
<template>
18+
<PinInputRoot v-bind="forwarded" :class="cn('flex gap-2 items-center', props.class)">
19+
<slot />
20+
</PinInputRoot>
21+
</template>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { PinInputInput, type PinInputInputProps, useForwardProps } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<PinInputInputProps & { class?: HTMLAttributes['class'] }>()
7+
8+
const delegatedProps = computed(() => {
9+
const { class: _, ...delegated } = props
10+
return delegated
11+
})
12+
13+
const forwardedProps = useForwardProps(delegatedProps)
14+
</script>
15+
16+
<template>
17+
<PinInputInput v-bind="forwardedProps" :class="cn('flex w-10 h-10 text-center rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
18+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as PinInput } from './PinInput.vue'
2+
export { default as PinInputInput } from './PinInputInput.vue'

0 commit comments

Comments
 (0)