- Bulk replace text-white → text-foreground across 116 component files (preserving text-white/ opacity variants) - Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow classes from all components - Replace --duration-normal/--duration-immersive/--duration-slow with --sumi-duration-normal/--sumi-duration-slow across 130+ files - Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out - Replace focus:ring-blue-500 → focus:ring-primary (4 files) - Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms (SUMI anti-pattern: no scale on hover) - Clean up stale kodo- references in comments Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
4.4 KiB
TypeScript
94 lines
4.4 KiB
TypeScript
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useId, useState } from 'react';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
|
|
export interface FloatingInputProps
|
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
label: string;
|
|
error?: string;
|
|
icon?: React.ReactNode;
|
|
showPasswordToggle?: boolean;
|
|
}
|
|
|
|
const FloatingInput = React.forwardRef<HTMLInputElement, FloatingInputProps>(
|
|
({ className, label, error, icon, type, id, showPasswordToggle, ...props }, ref) => {
|
|
const generatedId = useId();
|
|
const inputId = id || generatedId;
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const inputType = type === 'password' && showPassword ? 'text' : type;
|
|
const hasToggle = type === 'password' && showPasswordToggle;
|
|
|
|
return (
|
|
<div className="relative group w-full mb-5">
|
|
<div className="relative">
|
|
<input
|
|
type={inputType}
|
|
id={inputId}
|
|
className={cn(
|
|
"block px-4 pb-2.5 pt-5 w-full text-sm text-foreground bg-muted/40 rounded-xl border appearance-none focus:outline-none focus:ring-0 peer transition-all duration-[var(--duration-fast)]",
|
|
// Focus glow
|
|
"focus:shadow-[0_0_0_3px_oklch(var(--primary)/0.15),0_0_12px_oklch(var(--primary)/0.1)]",
|
|
// Borders & Colors
|
|
error
|
|
? "border-destructive focus:border-destructive"
|
|
: "border-white/10 hover:border-white/20 focus:border-primary",
|
|
// Glassmorphism
|
|
"backdrop-blur-sm",
|
|
icon ? "pl-11" : "",
|
|
hasToggle ? "pr-10" : "",
|
|
className
|
|
)}
|
|
placeholder=" "
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
|
|
{/* Icon */}
|
|
{icon && (
|
|
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground peer-focus:text-primary transition-colors duration-[var(--duration-fast)] pointer-events-none">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
|
|
{/* Password visibility toggle */}
|
|
{hasToggle && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-opacity transition-colors duration-[var(--duration-fast)]"
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
)}
|
|
|
|
{/* Floating Label */}
|
|
<label
|
|
htmlFor={inputId}
|
|
className={cn(
|
|
"absolute text-sm duration-[var(--duration-fast)] transform -translate-y-3 scale-75 top-4 z-10 origin-[0] peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-3 pointer-events-none transition-transform",
|
|
icon ? "left-11 peer-focus:left-11 peer-placeholder-shown:left-11" : "left-4 peer-focus:left-4 peer-placeholder-shown:left-4",
|
|
error
|
|
? "text-destructive"
|
|
: "text-muted-foreground peer-focus:text-primary group-hover:text-white/70"
|
|
)}
|
|
>
|
|
{label}
|
|
</label>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<p className="mt-1 text-xs text-destructive animate-shake">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
FloatingInput.displayName = 'FloatingInput';
|
|
|
|
export { FloatingInput };
|