1- import { memo , type ReactNode } from 'react'
1+ import { memo , type ReactNode , useCallback , useRef , useState } from 'react'
22import * as PopoverPrimitive from '@radix-ui/react-popover'
33import {
44 ArrowDown ,
@@ -16,6 +16,12 @@ import {
1616} from '@/components/emcn'
1717import { cn } from '@/lib/core/utils/cn'
1818
19+ const SEARCH_ICON = (
20+ < Search className = 'pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
21+ )
22+ const FILTER_ICON = < ListFilter className = 'mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
23+ const SORT_ICON = < ArrowUpDown className = 'mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
24+
1925type SortDirection = 'asc' | 'desc'
2026
2127export interface ColumnOption {
@@ -79,56 +85,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
7985 return (
8086 < div className = { cn ( 'border-[var(--border)] border-b py-2.5' , search ? 'px-6' : 'px-4' ) } >
8187 < div className = 'flex items-center justify-between' >
82- { search && (
83- < div className = 'relative flex flex-1 items-center' >
84- < Search className = 'pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
85- < div className = 'flex flex-1 items-center gap-1.5 overflow-x-auto pl-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' >
86- { search . tags ?. map ( ( tag , i ) => (
87- < Button
88- key = { `${ tag . label } -${ tag . value } -${ i } ` }
89- variant = 'subtle'
90- className = { cn (
91- 'shrink-0 px-2 py-1 text-caption' ,
92- search . highlightedTagIndex === i &&
93- 'ring-1 ring-[var(--border-focus)] ring-offset-1'
94- ) }
95- onClick = { tag . onRemove }
96- >
97- { tag . label } : { tag . value }
98- < span className = 'ml-1 text-[var(--text-icon)] text-micro' > ✕</ span >
99- </ Button >
100- ) ) }
101- < input
102- ref = { search . inputRef }
103- type = 'text'
104- value = { search . value }
105- onChange = { ( e ) => search . onChange ( e . target . value ) }
106- onKeyDown = { search . onKeyDown }
107- onFocus = { search . onFocus }
108- onBlur = { search . onBlur }
109- placeholder = { search . tags ?. length ? '' : ( search . placeholder ?? 'Search...' ) }
110- className = 'min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
111- />
112- </ div >
113- { search . tags ?. length || search . value ? (
114- < button
115- type = 'button'
116- className = 'mr-0.5 flex h-[14px] w-[14px] shrink-0 items-center justify-center text-[var(--text-subtle)] transition-colors hover-hover:text-[var(--text-secondary)]'
117- onClick = { search . onClearAll }
118- >
119- < span className = 'text-caption' > ✕</ span >
120- </ button >
121- ) : null }
122- { search . dropdown && (
123- < div
124- ref = { search . dropdownRef }
125- className = 'absolute top-full left-0 z-50 mt-1.5 w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
126- >
127- { search . dropdown }
128- </ div >
129- ) }
130- </ div >
131- ) }
88+ { search && < SearchSection search = { search } /> }
13289 < div className = 'flex items-center gap-1.5' >
13390 { extras }
13491 { filterTags ?. map ( ( tag ) => (
@@ -146,7 +103,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
146103 < PopoverPrimitive . Root >
147104 < PopoverPrimitive . Trigger asChild >
148105 < Button variant = 'subtle' className = 'px-2 py-1 text-caption' >
149- < ListFilter className = 'mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
106+ { FILTER_ICON }
150107 Filter
151108 </ Button >
152109 </ PopoverPrimitive . Trigger >
@@ -170,14 +127,94 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
170127 )
171128} )
172129
173- function SortDropdown ( { config } : { config : SortConfig } ) {
130+ const SearchSection = memo ( function SearchSection ( { search } : { search : SearchConfig } ) {
131+ const [ localValue , setLocalValue ] = useState ( search . value )
132+
133+ const lastReportedRef = useRef ( search . value )
134+
135+ if ( search . value !== lastReportedRef . current ) {
136+ setLocalValue ( search . value )
137+ lastReportedRef . current = search . value
138+ }
139+
140+ const handleInputChange = useCallback (
141+ ( e : React . ChangeEvent < HTMLInputElement > ) => {
142+ const next = e . target . value
143+ setLocalValue ( next )
144+ search . onChange ( next )
145+ } ,
146+ [ search . onChange ]
147+ )
148+
149+ const handleClearAll = useCallback ( ( ) => {
150+ setLocalValue ( '' )
151+ lastReportedRef . current = ''
152+ if ( search . onClearAll ) {
153+ search . onClearAll ( )
154+ } else {
155+ search . onChange ( '' )
156+ }
157+ } , [ search . onClearAll , search . onChange ] )
158+
159+ return (
160+ < div className = 'relative flex flex-1 items-center' >
161+ { SEARCH_ICON }
162+ < div className = 'flex flex-1 items-center gap-1.5 overflow-x-auto pl-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' >
163+ { search . tags ?. map ( ( tag , i ) => (
164+ < Button
165+ key = { `${ tag . label } -${ tag . value } -${ i } ` }
166+ variant = 'subtle'
167+ className = { cn (
168+ 'shrink-0 px-2 py-1 text-caption' ,
169+ search . highlightedTagIndex === i && 'ring-1 ring-[var(--border-focus)] ring-offset-1'
170+ ) }
171+ onClick = { tag . onRemove }
172+ >
173+ { tag . label } : { tag . value }
174+ < span className = 'ml-1 text-[var(--text-icon)] text-micro' > ✕</ span >
175+ </ Button >
176+ ) ) }
177+ < input
178+ ref = { search . inputRef }
179+ type = 'text'
180+ value = { localValue }
181+ onChange = { handleInputChange }
182+ onKeyDown = { search . onKeyDown }
183+ onFocus = { search . onFocus }
184+ onBlur = { search . onBlur }
185+ placeholder = { search . tags ?. length ? '' : ( search . placeholder ?? 'Search...' ) }
186+ className = 'min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
187+ />
188+ </ div >
189+ { search . tags ?. length || localValue ? (
190+ < button
191+ type = 'button'
192+ className = 'mr-0.5 flex h-[14px] w-[14px] shrink-0 items-center justify-center text-[var(--text-subtle)] transition-colors hover-hover:text-[var(--text-secondary)]'
193+ onClick = { handleClearAll }
194+ >
195+ < span className = 'text-caption' > ✕</ span >
196+ </ button >
197+ ) : null }
198+ { search . dropdown && (
199+ < div
200+ ref = { search . dropdownRef }
201+ className = 'absolute top-full left-0 z-50 mt-1.5 w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
202+ >
203+ { search . dropdown }
204+ </ div >
205+ ) }
206+ </ div >
207+ )
208+ } )
209+
210+ const SortDropdown = memo ( function SortDropdown ( { config } : { config : SortConfig } ) {
174211 const { options, active, onSort, onClear } = config
175212
176213 return (
177214 < DropdownMenu >
178215 < DropdownMenuTrigger asChild >
179216 < Button variant = 'subtle' className = 'px-2 py-1 text-caption' >
180- < ArrowUpDown className = 'mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
217+ { SORT_ICON }
181218 Sort
182219 </ Button >
183220 </ DropdownMenuTrigger >
@@ -218,4 +255,4 @@ function SortDropdown({ config }: { config: SortConfig }) {
218255 </ DropdownMenuContent >
219256 </ DropdownMenu >
220257 )
221- }
258+ } )
0 commit comments