first commit
This commit is contained in:
commit
e687b4437b
418 files changed
+33577
No files matched your search
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { setInputOTPContext } from './context';
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
maxlength = 6,
|
||||
disabled = false,
|
||||
id,
|
||||
class: className = '',
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
value?: string;
|
||||
maxlength?: number;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
||||
let inputRef: HTMLInputElement;
|
||||
let isFocused = $state(false);
|
||||
const activeIndex = $derived(Math.min(value.length, maxlength - 1));
|
||||
|
||||
setInputOTPContext({
|
||||
value: () => value,
|
||||
activeIndex: () => (isFocused ? activeIndex : -1),
|
||||
isFocused: () => isFocused,
|
||||
focus: () => inputRef?.focus(),
|
||||
});
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
value = target.value.replace(/[^0-9]/g, '').slice(0, maxlength);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Backspace' && value.length > 0) {
|
||||
e.preventDefault();
|
||||
value = value.slice(0, -1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-slot="input-otp"
|
||||
class={cn('relative flex items-center gap-2 has-disabled:opacity-50', className)}
|
||||
role="group"
|
||||
{...rest}
|
||||
>
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
{id}
|
||||
{disabled}
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
{value}
|
||||
oninput={handleInput}
|
||||
onkeydown={handleKeyDown}
|
||||
onfocus={() => (isFocused = true)}
|
||||
onblur={() => (isFocused = false)}
|
||||
class="absolute inset-0 z-10 h-full w-full cursor-text opacity-0 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<div class="pointer-events-none">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
children,
|
||||
}: {
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="input-otp-group" class={cn('flex items-center', className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Minus from 'lucide-svelte/icons/minus';
|
||||
</script>
|
||||
|
||||
<div data-slot="input-otp-separator" role="separator">
|
||||
<Minus />
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getInputOTPContext } from './context';
|
||||
|
||||
let {
|
||||
index,
|
||||
class: className = '',
|
||||
}: {
|
||||
index: number;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const ctx = getInputOTPContext();
|
||||
|
||||
const char = $derived(ctx.value()[index]);
|
||||
const isActive = $derived(ctx.isFocused() && ctx.activeIndex() === index);
|
||||
const hasFakeCaret = $derived(isActive && !char);
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
class={cn(
|
||||
'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{char ?? ''}
|
||||
{#if hasFakeCaret}
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
export const INPUT_OTP_CONTEXT = Symbol('input-otp');
|
||||
|
||||
export type InputOTPContext = {
|
||||
value: () => string;
|
||||
activeIndex: () => number;
|
||||
isFocused: () => boolean;
|
||||
focus: () => void;
|
||||
};
|
||||
|
||||
export function setInputOTPContext(ctx: InputOTPContext): void {
|
||||
setContext(INPUT_OTP_CONTEXT, ctx);
|
||||
}
|
||||
|
||||
export function getInputOTPContext(): InputOTPContext {
|
||||
return getContext<InputOTPContext>(INPUT_OTP_CONTEXT);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as InputOTP } from './InputOTP.svelte';
|
||||
export { default as InputOTPGroup } from './InputOTPGroup.svelte';
|
||||
export { default as InputOTPSeparator } from './InputOTPSeparator.svelte';
|
||||
export { default as InputOTPSlot } from './InputOTPSlot.svelte';
|
||||
Reference in new issue
Block a user