edit login & otp
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mb-4 flex rounded-xl border border-border bg-surface-3 p-1">
|
||||
<div class="flex rounded-xl border border-border bg-surface-3 p-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 rounded-lg py-2 text-sm font-medium transition"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="space-y-1.5">
|
||||
<label v-if="label" class="block text-sm font-medium text-muted">{{ label }}</label>
|
||||
<div class="flex justify-center gap-3" dir="ltr">
|
||||
<input
|
||||
v-for="(_, index) in length"
|
||||
:key="index"
|
||||
ref="inputRefs"
|
||||
:value="cells[index]"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="1"
|
||||
class="h-14 w-14 rounded-xl border border-border bg-surface-2 text-center text-xl font-semibold text-text outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="disabled"
|
||||
:aria-label="`رقم ${index + 1}`"
|
||||
@input="onInput(index, $event)"
|
||||
@keydown="onKeydown(index, $event)"
|
||||
@paste="onPaste"
|
||||
@focus="($event.target as HTMLInputElement).select()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string;
|
||||
label?: string;
|
||||
length?: number;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
length: 4,
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
complete: [value: string];
|
||||
}>();
|
||||
|
||||
const inputRefs = ref<HTMLInputElement[]>([]);
|
||||
|
||||
const cells = computed(() => {
|
||||
const digits = props.modelValue.replace(/\D/g, '').slice(0, props.length);
|
||||
return Array.from({ length: props.length }, (_, index) => digits[index] ?? '');
|
||||
});
|
||||
|
||||
function updateValue(next: string) {
|
||||
const normalized = next.replace(/\D/g, '').slice(0, props.length);
|
||||
emit('update:modelValue', normalized);
|
||||
if (normalized.length === props.length) {
|
||||
emit('complete', normalized);
|
||||
}
|
||||
}
|
||||
|
||||
function focusAt(index: number) {
|
||||
nextTick(() => {
|
||||
inputRefs.value[index]?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function onInput(index: number, event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const digit = input.value.replace(/\D/g, '').slice(-1);
|
||||
const next = cells.value.map((cell, cellIndex) =>
|
||||
cellIndex === index ? digit : cell,
|
||||
);
|
||||
updateValue(next.join(''));
|
||||
|
||||
if (digit && index < props.length - 1) {
|
||||
focusAt(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(index: number, event: KeyboardEvent) {
|
||||
if (event.key !== 'Backspace') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cells.value[index]) {
|
||||
const next = cells.value.map((cell, cellIndex) =>
|
||||
cellIndex === index ? '' : cell,
|
||||
);
|
||||
updateValue(next.join(''));
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
event.preventDefault();
|
||||
const next = cells.value.map((cell, cellIndex) =>
|
||||
cellIndex === index - 1 ? '' : cell,
|
||||
);
|
||||
updateValue(next.join(''));
|
||||
focusAt(index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
event.preventDefault();
|
||||
const pasted = event.clipboardData?.getData('text') ?? '';
|
||||
const digits = pasted.replace(/\D/g, '').slice(0, props.length);
|
||||
if (!digits) {
|
||||
return;
|
||||
}
|
||||
updateValue(digits);
|
||||
focusAt(Math.min(digits.length, props.length) - 1);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
focusAt(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
focus: () => focusAt(0),
|
||||
clear: () => {
|
||||
updateValue('');
|
||||
focusAt(0);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<img
|
||||
v-if="variant === 'mark'"
|
||||
src="/icons/icon-192.png"
|
||||
alt="منتایم"
|
||||
:class="imgClass"
|
||||
/>
|
||||
<template v-else>
|
||||
<img
|
||||
src="/brand/logo-dark.png"
|
||||
alt="منتایم"
|
||||
:class="['dark:hidden', imgClass]"
|
||||
/>
|
||||
<img
|
||||
src="/brand/logo-lite.png"
|
||||
alt="منتایم"
|
||||
:class="['hidden dark:block', imgClass]"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
export type LogoVariant = 'auto' | 'mark' | 'horizontal' | 'vertical';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: LogoVariant;
|
||||
class?: string;
|
||||
}>(),
|
||||
{ variant: 'auto' },
|
||||
);
|
||||
|
||||
const imgClass = computed(() => props.class ?? 'h-8 w-auto object-contain');
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
<input
|
||||
:id="id"
|
||||
v-model="model"
|
||||
class="w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-text outline-none transition placeholder:text-muted focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30"
|
||||
class="w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-text outline-none transition placeholder:text-[var(--color-placeholder)] focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30"
|
||||
:class="ltr ? 'text-left' : ''"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user