feat: componente SearchBar con debounce y clear

Input de busqueda con icono, debounce configurable y boton de limpiar.
Exportado desde index.ts del barrel de UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 15:02:29 +02:00
parent 145a6fce8f
commit 74b4c40f18
3 changed files with 133 additions and 0 deletions
+4
View File
@@ -114,6 +114,10 @@ export type { TextareaProps } from './textarea'
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } from './toast'
export type { ToastEntry, ToastProps, ToastViewportProps } from './toast'
// Search
export { SearchBar } from './search_bar'
export type { SearchBarProps } from './search_bar'
// Hooks — Canvas
export { useAnimatedCanvas } from './use_animated_canvas'
+64
View File
@@ -0,0 +1,64 @@
---
name: search_bar
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "SearchBar(props: SearchBarProps): JSX.Element"
description: "Search input with debounce, search icon, and clear button"
tags: [component, ui, search, input, debounce]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["lucide-react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/search_bar.tsx"
props:
- name: onSearch
type: "(query: string) => void"
required: true
description: "Called with the debounced search query"
- name: placeholder
type: "string"
required: false
description: "Placeholder text (default: Search...)"
- name: debounceMs
type: "number"
required: false
description: "Debounce delay in ms (default: 300)"
- name: className
type: "string"
required: false
description: "Additional CSS classes"
emits: []
has_state: true
framework: react
variant: []
---
## Ejemplo
```tsx
import { SearchBar } from '@fn_library'
function MyPage() {
return (
<SearchBar
onSearch={(query) => console.log('search:', query)}
placeholder="Search entities..."
debounceMs={300}
/>
)
}
```
## Notas
- Debounce usa ref para evitar re-renders innecesarios del callback
- El icono de clear solo aparece cuando hay texto
- Usa CSS variables del tema para colores (border, input, foreground, muted-foreground)
+65
View File
@@ -0,0 +1,65 @@
import * as React from "react"
import { cn } from "../core/cn"
import { Search, X } from "lucide-react"
interface SearchBarProps {
/** Called with the debounced search query */
onSearch: (query: string) => void
/** Placeholder text */
placeholder?: string
/** Debounce delay in ms (default 300) */
debounceMs?: number
/** Additional CSS classes for the outer wrapper */
className?: string
}
function SearchBar({
onSearch,
placeholder = "Search...",
debounceMs = 300,
className,
}: SearchBarProps) {
const [query, setQuery] = React.useState("")
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
const onSearchRef = React.useRef(onSearch)
onSearchRef.current = onSearch
React.useEffect(() => {
if (timerRef.current) clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
onSearchRef.current(query)
}, debounceMs)
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [query, debounceMs])
return (
<div
className={cn(
"flex flex-1 items-center gap-2 rounded border border-border bg-input px-2 py-1",
className,
)}
>
<Search size={14} className="text-muted-foreground shrink-0" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
className="flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
/>
{query && (
<button
onClick={() => setQuery("")}
className="p-0.5 text-muted-foreground hover:text-foreground"
aria-label="Clear search"
>
<X size={12} />
</button>
)}
</div>
)
}
export { SearchBar }
export type { SearchBarProps }