* chore: run Tailwind v4 upgrade tool (Phase 1) - Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss - Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css - Replaced @tailwind directives with @import 'tailwindcss' - Added @custom-variant dark, @utility blocks for custom utilities - Updated class names across 128 files (shadow-sm → shadow-xs, etc.) - Removed autoprefixer (built into v4) - Added migration plan doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: switch from @tailwindcss/postcss to @tailwindcss/vite (Phase 2) - Replaced @tailwindcss/postcss with @tailwindcss/vite plugin - Deleted postcss.config.js (no longer needed) - Tailwind now runs as a native Vite plugin for faster HMR Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: convert to OKLCH colors, move keyframes into @theme (Phase 3-4) - Replaced all HSL color indirection with direct OKLCH values in @theme - Moved all keyframes inside @theme block (v4 pattern) - Eliminated hsl(var(--x)) double-indirection across 17 component files - Replaced hsl() inline styles with var(--color-*) theme references - Cleaned up redundant rdp-* utility blocks - Fixed @custom-variant dark syntax to use :where() - Added sidebar/glass/shadow vars as OKLCH in :root Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
import { Search, X } from 'lucide-react'
|
|
import { debounce } from '@/lib/debounce'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface SearchInputProps {
|
|
value?: string
|
|
onSearch: (value: string) => void
|
|
placeholder?: string
|
|
className?: string
|
|
}
|
|
|
|
export function SearchInput({ value = '', onSearch, placeholder = 'Search...', className }: SearchInputProps) {
|
|
const [localValue, setLocalValue] = useState(value)
|
|
const debouncedRef = useRef<ReturnType<typeof debounce> | null>(null)
|
|
|
|
useEffect(() => {
|
|
setLocalValue(value)
|
|
}, [value])
|
|
|
|
useEffect(() => {
|
|
debouncedRef.current = debounce((v: string) => {
|
|
onSearch(v)
|
|
}, 300)
|
|
return () => {
|
|
debouncedRef.current?.cancel()
|
|
}
|
|
}, [onSearch])
|
|
|
|
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const v = e.target.value
|
|
setLocalValue(v)
|
|
debouncedRef.current?.(v)
|
|
}, [])
|
|
|
|
const handleClear = () => {
|
|
setLocalValue('')
|
|
onSearch('')
|
|
}
|
|
|
|
return (
|
|
<div className={cn('relative', className)}>
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
value={localValue}
|
|
onChange={handleChange}
|
|
placeholder={placeholder}
|
|
className={cn(
|
|
'h-9 w-full rounded-md border border-border bg-card pl-9 pr-8 text-sm text-foreground',
|
|
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
|
|
)}
|
|
/>
|
|
{localValue && (
|
|
<button
|
|
onClick={handleClear}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default SearchInput
|