417 lines
8.7 KiB
Vue
417 lines
8.7 KiB
Vue
<template>
|
|
<div
|
|
class="floating-input"
|
|
:class="[
|
|
`floating-input--${variant}`,
|
|
{
|
|
'floating-input--focused': isFocused || modelValue,
|
|
'floating-input--error': error,
|
|
'floating-input--disabled': disabled
|
|
}
|
|
]"
|
|
>
|
|
<div class="floating-input__wrapper">
|
|
<Icon
|
|
v-if="leftIcon"
|
|
:name="leftIcon"
|
|
class="floating-input__icon floating-input__icon--left"
|
|
/>
|
|
|
|
<input
|
|
:id="inputId"
|
|
v-model="modelValue"
|
|
:type="type"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:autocomplete="autocomplete"
|
|
class="floating-input__field"
|
|
:class="{
|
|
'floating-input__field--with-left-icon': leftIcon,
|
|
'floating-input__field--with-right-icon': rightIcon || clearable
|
|
}"
|
|
@focus="handleFocus"
|
|
@blur="handleBlur"
|
|
@input="$emit('update:modelValue', $event.target.value)"
|
|
/>
|
|
|
|
<label
|
|
:for="inputId"
|
|
class="floating-input__label"
|
|
:class="{
|
|
'floating-input__label--floating': isFocused || modelValue,
|
|
'floating-input__label--with-icon': leftIcon
|
|
}"
|
|
>
|
|
{{ label }}
|
|
<span v-if="required" class="floating-input__required">*</span>
|
|
</label>
|
|
|
|
<button
|
|
v-if="clearable && modelValue"
|
|
type="button"
|
|
class="floating-input__clear"
|
|
@click="clearInput"
|
|
>
|
|
<Icon name="x" />
|
|
</button>
|
|
|
|
<Icon
|
|
v-if="rightIcon && !clearable"
|
|
:name="rightIcon"
|
|
class="floating-input__icon floating-input__icon--right"
|
|
/>
|
|
</div>
|
|
|
|
<Transition name="message">
|
|
<div v-if="error || helperText" class="floating-input__message">
|
|
<Icon
|
|
v-if="error"
|
|
name="alert-circle"
|
|
class="floating-input__message-icon"
|
|
/>
|
|
<span>{{ error || helperText }}</span>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import Icon from '~/components/ui/Icon.vue'
|
|
|
|
interface Props {
|
|
modelValue?: string
|
|
label: string
|
|
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'number'
|
|
variant?: 'glass' | 'solid' | 'outline'
|
|
leftIcon?: string
|
|
rightIcon?: string
|
|
error?: string
|
|
helperText?: string
|
|
required?: boolean
|
|
disabled?: boolean
|
|
readonly?: boolean
|
|
clearable?: boolean
|
|
autocomplete?: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
type: 'text',
|
|
variant: 'glass',
|
|
required: false,
|
|
disabled: false,
|
|
readonly: false,
|
|
clearable: false,
|
|
autocomplete: 'off'
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
'focus': []
|
|
'blur': []
|
|
'clear': []
|
|
}>()
|
|
|
|
const isFocused = ref(false)
|
|
const inputId = computed(() => `input-${Math.random().toString(36).substr(2, 9)}`)
|
|
|
|
const handleFocus = () => {
|
|
isFocused.value = true
|
|
emit('focus')
|
|
}
|
|
|
|
const handleBlur = () => {
|
|
isFocused.value = false
|
|
emit('blur')
|
|
}
|
|
|
|
const clearInput = () => {
|
|
emit('update:modelValue', '')
|
|
emit('clear')
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.floating-input {
|
|
position: relative;
|
|
width: 100%;
|
|
|
|
&__wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
border-radius: 12px;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
// Base styles
|
|
.floating-input--glass & {
|
|
background: rgba(255, 255, 255, 0.7);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
|
|
|
&:hover:not(.floating-input--disabled &) {
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-color: rgba(220, 38, 38, 0.2);
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
|
}
|
|
}
|
|
|
|
.floating-input--solid & {
|
|
background: white;
|
|
border: 2px solid #e5e5e5;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
&:hover:not(.floating-input--disabled &) {
|
|
border-color: #d4d4d4;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
}
|
|
|
|
.floating-input--outline & {
|
|
background: transparent;
|
|
border: 2px solid #d4d4d4;
|
|
|
|
&:hover:not(.floating-input--disabled &) {
|
|
border-color: #a3a3a3;
|
|
}
|
|
}
|
|
|
|
// Focus state
|
|
.floating-input--focused & {
|
|
border-color: #dc2626;
|
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
|
transform: translateY(-1px);
|
|
|
|
.floating-input--glass& {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
}
|
|
|
|
// Error state
|
|
.floating-input--error & {
|
|
border-color: #ef4444;
|
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
// Disabled state
|
|
.floating-input--disabled & {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
|
|
&__field {
|
|
flex: 1;
|
|
padding: 1.25rem 1rem 0.5rem;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
font-size: 1rem;
|
|
color: #27272a;
|
|
transition: padding 0.2s ease;
|
|
|
|
&--with-left-icon {
|
|
padding-left: 3rem;
|
|
}
|
|
|
|
&--with-right-icon {
|
|
padding-right: 3rem;
|
|
}
|
|
|
|
&:disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
// Remove autofill background
|
|
&:-webkit-autofill {
|
|
-webkit-box-shadow: 0 0 0 1000px transparent inset;
|
|
-webkit-text-fill-color: #27272a;
|
|
transition: background-color 5000s ease-in-out 0s;
|
|
}
|
|
}
|
|
|
|
&__label {
|
|
position: absolute;
|
|
left: 1rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 1rem;
|
|
color: #71717a;
|
|
pointer-events: none;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
background: transparent;
|
|
padding: 0 0.25rem;
|
|
|
|
&--with-icon {
|
|
left: 3rem;
|
|
}
|
|
|
|
&--floating {
|
|
top: 0.75rem;
|
|
transform: translateY(0);
|
|
font-size: 0.75rem;
|
|
color: #dc2626;
|
|
font-weight: 500;
|
|
|
|
.floating-input--glass & {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
rgba(255, 255, 255, 0.9) 0%,
|
|
rgba(255, 255, 255, 0.9) 50%,
|
|
transparent 50%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
|
|
.floating-input--solid & {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
white 0%,
|
|
white 50%,
|
|
transparent 50%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
}
|
|
|
|
.floating-input--error &--floating {
|
|
color: #ef4444;
|
|
}
|
|
}
|
|
|
|
&__required {
|
|
color: #ef4444;
|
|
margin-left: 0.125rem;
|
|
}
|
|
|
|
&__icon {
|
|
position: absolute;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
color: #dc2626;
|
|
|
|
&--left {
|
|
left: 1rem;
|
|
}
|
|
|
|
&--right {
|
|
right: 1rem;
|
|
}
|
|
}
|
|
|
|
&__clear {
|
|
position: absolute;
|
|
right: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
padding: 0;
|
|
background: rgba(220, 38, 38, 0.1);
|
|
border: none;
|
|
border-radius: 50%;
|
|
color: #dc2626;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
background: rgba(220, 38, 38, 0.2);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
svg {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
}
|
|
}
|
|
|
|
&__message {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: #71717a;
|
|
|
|
.floating-input--error & {
|
|
color: #ef4444;
|
|
}
|
|
}
|
|
|
|
&__message-icon {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
|
|
// Animations
|
|
.message-enter-active,
|
|
.message-leave-active {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.message-enter-from {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.message-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
// Dark mode support
|
|
@media (prefers-color-scheme: dark) {
|
|
.floating-input {
|
|
&__field {
|
|
color: white;
|
|
|
|
&:-webkit-autofill {
|
|
-webkit-text-fill-color: white;
|
|
}
|
|
}
|
|
|
|
&__wrapper {
|
|
.floating-input--glass & {
|
|
background: rgba(30, 30, 30, 0.7);
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.floating-input--solid & {
|
|
background: #27272a;
|
|
border-color: #3f3f46;
|
|
}
|
|
}
|
|
|
|
&__label {
|
|
color: #a3a3a3;
|
|
|
|
&--floating {
|
|
.floating-input--glass & {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
rgba(30, 30, 30, 0.9) 0%,
|
|
rgba(30, 30, 30, 0.9) 50%,
|
|
transparent 50%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
|
|
.floating-input--solid & {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
#27272a 0%,
|
|
#27272a 50%,
|
|
transparent 50%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style> |