Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/www/src/components/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from './list-examples';
export * from './menu-examples';
export * from './menubar-examples';
export * from './number-field-examples';
export * from './otp-field-examples';
export * from './popover-examples';
export * from './preview-card-examples';
export * from './radio-examples';
Expand Down
121 changes: 121 additions & 0 deletions apps/www/src/components/playground/otp-field-examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';

import { Field, Flex, OTPField, Text } from '@raystack/apsara';
import { useState } from 'react';
import PlaygroundLayout from './playground-layout';

const renderSlots = (length: number, offset = 0, totalLength = length) =>
Array.from({ length }, (_, i) => (
<OTPField.Input
key={i + offset}
aria-label={`Character ${i + 1 + offset} of ${totalLength}`}
/>
));

function ControlledOTP() {
const [value, setValue] = useState('');
return (
<Flex direction='column' gap={3} align='start'>
<OTPField length={6} value={value} onValueChange={setValue}>
{renderSlots(6)}
</OTPField>
<Text size='small'>
Current value: <code>{value || '(empty)'}</code>
</Text>
</Flex>
);
}

function CompleteOTP() {
const [submitted, setSubmitted] = useState('');
return (
<Flex direction='column' gap={3} align='start'>
<OTPField length={6} onValueComplete={setSubmitted}>
{renderSlots(6)}
</OTPField>
<Text size='small'>
{submitted ? `Submitted: ${submitted}` : 'Type all 6 digits to submit'}
</Text>
</Flex>
);
}

export function OTPFieldExamples() {
return (
<PlaygroundLayout title='OTPField'>
<Flex direction='column' gap={9}>
<Text>Default (6 digits):</Text>
<OTPField length={6}>{renderSlots(6)}</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>4 digits:</Text>
<OTPField length={4}>{renderSlots(4)}</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>With separator:</Text>
<OTPField length={6}>
{renderSlots(3, 0, 6)}
<OTPField.Separator />
{renderSlots(3, 3, 6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Masked:</Text>
<OTPField length={6} mask>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Alphanumeric:</Text>
<OTPField length={6} validationType='alphanumeric'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Default value:</Text>
<OTPField length={6} defaultValue='123456'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Controlled (value / onValueChange):</Text>
<ControlledOTP />
</Flex>

<Flex direction='column' gap={9}>
<Text>onValueComplete:</Text>
<CompleteOTP />
</Flex>

<Flex direction='column' gap={9}>
<Text>Disabled:</Text>
<OTPField length={6} disabled defaultValue='123'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Read-only:</Text>
<OTPField length={6} readOnly defaultValue='934821'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>With Field:</Text>
<Field
label='Verification code'
description='Enter the 6-digit code we sent to your device.'
>
<OTPField length={6}>{renderSlots(6)}</OTPField>
</Field>
</Flex>
</PlaygroundLayout>
);
}
182 changes: 182 additions & 0 deletions apps/www/src/content/docs/components/otp-field/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client';

import type { ComponentPropsType } from '@/components/demo/types';
import { getPropsString } from '@/lib/utils';

const renderInputs = (
length: number
) => `Array.from({ length: ${length} }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of ${length}\`} />
))`;

export const preview = {
type: 'code',
code: `<OTPField length={6}>
{${renderInputs(6)}}
</OTPField>`
};

const getCode = (props: ComponentPropsType) => {
const { length = 6, ...rest } = props;
const slotCount = Number(length) || 6;
return `<OTPField length={${slotCount}}${getPropsString(rest)}>
{${renderInputs(slotCount)}}
</OTPField>`;
};

export const playground = {
type: 'playground',
controls: {
length: {
type: 'select',
options: ['4', '6', '8'],
defaultValue: '6'
},
validationType: {
type: 'select',
options: ['numeric', 'alpha', 'alphanumeric', 'none'],
defaultValue: 'numeric'
},
mask: {
type: 'checkbox',
defaultValue: false
},
disabled: {
type: 'checkbox',
defaultValue: false
},
readOnly: {
type: 'checkbox',
defaultValue: false
},
autoSubmit: {
type: 'checkbox',
defaultValue: false
}
},
getCode
};

export const separatorDemo = {
type: 'code',
code: `<OTPField length={6}>
{Array.from({ length: 3 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
<OTPField.Separator />
{Array.from({ length: 3 }, (_, i) => (
<OTPField.Input key={i + 3} aria-label={\`Character \${i + 4} of 6\`} />
))}
</OTPField>`
};

export const maskedDemo = {
type: 'code',
code: `<OTPField length={6} mask>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const alphanumericDemo = {
type: 'code',
code: `<OTPField length={6} validationType="alphanumeric">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const disabledDemo = {
type: 'code',
code: `<OTPField length={6} disabled defaultValue="123">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const readOnlyDemo = {
type: 'code',
code: `<OTPField length={6} readOnly defaultValue="934821">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const controlledDemo = {
type: 'code',
code: `function ControlledOTP() {
const [value, setValue] = React.useState('');

return (
<Flex direction="column" gap={4} align="start">
<OTPField length={6} value={value} onValueChange={setValue}>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
<Text size="small">Current value: <code>{value || '(empty)'}</code></Text>
</Flex>
);
}`
};

export const onCompleteDemo = {
type: 'code',
code: `function CompleteOTP() {
const [submitted, setSubmitted] = React.useState('');

return (
<Flex direction="column" gap={4} align="start">
<OTPField
length={6}
onValueComplete={(value) => setSubmitted(value)}
>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
<Text size="small">
{submitted ? \`Submitted: \${submitted}\` : 'Type all 6 digits to submit'}
</Text>
</Flex>
);
}`
};

export const customSanitizeDemo = {
type: 'code',
code: `<OTPField
length={4}
validationType="none"
inputMode="numeric"
sanitizeValue={(val) => val.replace(/[^0-3]/g, '')}
>
{Array.from({ length: 4 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 4\`} />
))}
</OTPField>`
};

export const withFieldDemo = {
type: 'code',
code: `<Flex direction="column" gap={6}>
<Field label="Verification code" description="Enter the 6-digit code we sent to your device.">
<OTPField length={6}>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
</Field>
<Field label="Verification code" description="Enter the 6-digit code we sent to your device." error="Invalid OTP">
<OTPField length={6} defaultValue="123456">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
</Field>
</Flex>`
};
Loading
Loading