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,91 @@ 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+ lastReportedRef . current = next
145+ search . onChange ( next )
146+ } ,
147+ [ search . onChange ]
148+ )
149+
150+ const handleClearAll = useCallback ( ( ) => {
151+ setLocalValue ( '' )
152+ lastReportedRef . current = ''
153+ search . onClearAll ?.( )
154+ } , [ search . onClearAll ] )
155+
156+ return (
157+ < div className = 'relative flex flex-1 items-center' >
158+ { SEARCH_ICON }
159+ < div className = 'flex flex-1 items-center gap-1.5 overflow-x-auto pl-2.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' >
160+ { search . tags ?. map ( ( tag , i ) => (
161+ < Button
162+ key = { `${ tag . label } -${ tag . value } -${ i } ` }
163+ variant = 'subtle'
164+ className = { cn (
165+ 'shrink-0 px-2 py-1 text-caption' ,
166+ search . highlightedTagIndex === i && 'ring-1 ring-[var(--border-focus)] ring-offset-1'
167+ ) }
168+ onClick = { tag . onRemove }
169+ >
170+ { tag . label } : { tag . value }
171+ < span className = 'ml-1 text-[var(--text-icon)] text-micro' > ✕</ span >
172+ </ Button >
173+ ) ) }
174+ < input
175+ ref = { search . inputRef }
176+ type = 'text'
177+ value = { localValue }
178+ onChange = { handleInputChange }
179+ onKeyDown = { search . onKeyDown }
180+ onFocus = { search . onFocus }
181+ onBlur = { search . onBlur }
182+ placeholder = { search . tags ?. length ? '' : ( search . placeholder ?? 'Search...' ) }
183+ className = 'min-w-[80px] flex-1 bg-transparent py-1 text-[var(--text-secondary)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
184+ />
185+ </ div >
186+ { search . tags ?. length || localValue ? (
187+ < button
188+ type = 'button'
189+ 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)]'
190+ onClick = { handleClearAll }
191+ >
192+ < span className = 'text-caption' > ✕</ span >
193+ </ button >
194+ ) : null }
195+ { search . dropdown && (
196+ < div
197+ ref = { search . dropdownRef }
198+ className = 'absolute top-full left-0 z-50 mt-1.5 w-full rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
199+ >
200+ { search . dropdown }
201+ </ div >
202+ ) }
203+ </ div >
204+ )
205+ } )
206+
207+ const SortDropdown = memo ( function SortDropdown ( { config } : { config : SortConfig } ) {
174208 const { options, active, onSort, onClear } = config
175209
176210 return (
177211 < DropdownMenu >
178212 < DropdownMenuTrigger asChild >
179213 < Button variant = 'subtle' className = 'px-2 py-1 text-caption' >
180- < ArrowUpDown className = 'mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
214+ { SORT_ICON }
181215 Sort
182216 </ Button >
183217 </ DropdownMenuTrigger >
@@ -218,4 +252,4 @@ function SortDropdown({ config }: { config: SortConfig }) {
218252 </ DropdownMenuContent >
219253 </ DropdownMenu >
220254 )
221- }
255+ } )
0 commit comments