monacousa-portal/components/ui/FloatingInput.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>