import { computed, ref, type Ref } from 'vue' interface FloatingLabelOptions { /** * Whether the input has a value */ hasValue: Ref /** * Whether the input is focused */ isFocused: Ref /** * Whether the input has a placeholder */ hasPlaceholder?: Ref /** * Input size for positioning */ size?: Ref<'sm' | 'md' | 'lg'> /** * Whether there's an error state */ hasError?: Ref } /** * Composable for managing floating label behavior * Provides consistent floating label logic that can be reused across input components */ export function useFloatingLabel(options: FloatingLabelOptions) { const { hasValue, isFocused, hasPlaceholder = ref(false), size = ref('md'), hasError = ref(false) } = options /** * Determines if the label should be in floating state */ const shouldFloat = computed(() => { return isFocused.value || hasValue.value || hasPlaceholder.value }) /** * Returns the appropriate classes for the floating label */ const labelClasses = computed(() => { const classes = [ 'absolute', 'left-0', 'transition-all', 'duration-200', 'ease-in-out', 'origin-left', 'pointer-events-none', 'select-none', ] if (shouldFloat.value) { // Floating state classes.push( 'text-xs', 'font-medium', 'top-0', 'scale-90' ) // Color when floating if (hasError.value) { classes.push('text-red-600') } else if (isFocused.value) { classes.push('text-titan-green-800') } else { classes.push('text-gray-600') } } else { // Default state classes.push( 'text-base', 'font-normal', 'scale-100' ) // Position based on input size switch (size.value) { case 'sm': classes.push('top-2') break case 'lg': classes.push('top-4') break default: // md classes.push('top-3') } // Color when not floating if (hasError.value) { classes.push('text-red-500') } else { classes.push('text-gray-500') } } return classes.join(' ') }) /** * Returns the appropriate padding classes for the input to accommodate the floating label */ const inputPaddingClasses = computed(() => { const classes: string[] = [] // Add top padding to accommodate floating label classes.push('pt-6') // Add bottom padding based on size switch (size.value) { case 'sm': classes.push('pb-2') break case 'lg': classes.push('pb-4') break default: // md classes.push('pb-3') } return classes }) /** * Base styles for inputs that work with floating labels */ const baseInputClasses = computed(() => [ 'w-full', 'border-b-2', 'bg-transparent', 'text-base', 'leading-6', 'transition-all', 'duration-200', 'ease-in-out', 'outline-none', 'appearance-none', 'text-gray-900', 'font-medium', 'focus:outline-none', ]) /** * Border classes based on state */ const borderClasses = computed(() => { if (hasError.value) { return ['border-red-500', 'focus:border-red-600'] } else { return [ 'border-gray-300', 'focus:border-titan-green-800', 'hover:border-gray-400' ] } }) return { shouldFloat, labelClasses, inputPaddingClasses, baseInputClasses, borderClasses } }