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:
@@ -114,6 +114,10 @@ export type { TextareaProps } from './textarea'
|
|||||||
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } from './toast'
|
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } from './toast'
|
||||||
export type { ToastEntry, ToastProps, ToastViewportProps } from './toast'
|
export type { ToastEntry, ToastProps, ToastViewportProps } from './toast'
|
||||||
|
|
||||||
|
// Search
|
||||||
|
export { SearchBar } from './search_bar'
|
||||||
|
export type { SearchBarProps } from './search_bar'
|
||||||
|
|
||||||
// Hooks — Canvas
|
// Hooks — Canvas
|
||||||
export { useAnimatedCanvas } from './use_animated_canvas'
|
export { useAnimatedCanvas } from './use_animated_canvas'
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 }
|
||||||
Reference in New Issue
Block a user