@@ -24,8 +24,17 @@ import { Search, X, SlidersHorizontal } from "lucide-react";
2424import { useSyncStatus } from "@/hooks/useSyncStatus" ;
2525import { SyncStatusIndicator } from "@/components/SyncStatusIndicator" ;
2626import { RelativeTimestamp } from "@/components/RelativeTimestamp" ;
27- import { DataPanelError } from "@/components/DataPanelError" ;
28- import { classifyError } from "@/hooks/usePanelError" ;
27+ import { NetworkErrorState } from "@/components/NetworkErrorState" ;
28+ import { useI18n } from "@/hooks/useI18n" ;
29+ import { fetchSignals } from "@/lib/api" ;
30+ import { queryOptions } from "@/lib/queryOptions" ;
31+ import { usePullToRefresh } from "@/hooks/usePullToRefresh" ;
32+ import {
33+ readPersistedSplitRatio ,
34+ persistSplitRatio ,
35+ clampSplitRatio ,
36+ computeSplitRatioFromClientX ,
37+ } from "@/lib/splitView" ;
2938
3039interface SignalResponse {
3140 items : Signal [ ] ;
@@ -60,6 +69,10 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
6069 const snoozedMap = useSnoozeStore ( ( state ) => state . snoozed ) ;
6170 const pruneExpiredSnoozes = useSnoozeStore ( ( state ) => state . pruneExpired ) ;
6271 const [ providerSearch , setProviderSearch ] = useState ( provider ) ;
72+ // #685: debounce the free-text search term used for filtering so a fast
73+ // typist doesn't trigger a full re-filter of the signal list on every
74+ // keystroke. The input itself still updates instantly via providerSearch.
75+ const [ debouncedSearch , setDebouncedSearch ] = useState ( provider ) ;
6376 const [ filterSheetOpen , setFilterSheetOpen ] = useState ( false ) ;
6477 const { addView } = useRecentlyViewedStore ( ) ;
6578 // Bumped on a timer so expired snoozes are re-evaluated and signals return.
@@ -100,7 +113,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
100113 } ,
101114 getNextPageParam : ( lastPage : SignalResponse ) => lastPage . nextPage ,
102115 initialPageParam : 1 ,
103- staleTime : queryOpts . signal . staleTime ,
116+ staleTime : queryOptions . signal . staleTime ,
104117 placeholderData : ( prev ) => prev ,
105118 // Seed the cache with the server-fetched first page so no client waterfall occurs
106119 ...( initialData && {
@@ -116,6 +129,11 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
116129 [ data ]
117130 ) ;
118131
132+ useEffect ( ( ) => {
133+ const handle = setTimeout ( ( ) => setDebouncedSearch ( providerSearch ) , 150 ) ;
134+ return ( ) => clearTimeout ( handle ) ;
135+ } , [ providerSearch ] ) ;
136+
119137 const availableProviders = useMemo (
120138 ( ) => [ ...new Set ( allSignals . map ( ( s ) => s . ticker ) ) ] . sort ( ) ,
121139 [ allSignals ]
@@ -128,7 +146,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
128146
129147 const filteredSignals = useMemo < Signal [ ] > ( ( ) => {
130148 let filtered = [ ...allSignals ] ;
131- const searchTerm = providerSearch . trim ( ) . toLowerCase ( ) ;
149+ const searchTerm = debouncedSearch . trim ( ) . toLowerCase ( ) ;
132150
133151 if ( direction !== "ALL" ) {
134152 filtered = filtered . filter ( ( s ) => s . action === direction ) ;
@@ -168,7 +186,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
168186 direction ,
169187 asset ,
170188 provider ,
171- providerSearch ,
189+ debouncedSearch ,
172190 bookmarkedOnly ,
173191 bookmarkedIds ,
174192 snoozedMap ,
@@ -421,8 +439,12 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
421439 < div className = "flex flex-col items-end gap-2" >
422440 { /* Sort controls — persistent across browsing */ }
423441 < SignalSortControls />
424- { /* Price precision toggle */ }
425- < PricePrecisionToggle />
442+ < div className = "flex items-center gap-2" >
443+ { /* Price precision toggle */ }
444+ < PricePrecisionToggle />
445+ { /* Density toggle — persisted across sessions */ }
446+ < FeedDensityToggle />
447+ </ div >
426448 { /* #574: last-updated / stale status, with a manual refresh action */ }
427449 < div className = "flex items-center gap-2" >
428450 < SyncStatusIndicator status = { syncStatus } />
@@ -435,11 +457,6 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
435457 >
436458 Refresh
437459 </ button >
438- < div className = "flex items-center gap-2" >
439- { /* Price precision toggle */ }
440- < PricePrecisionToggle />
441- { /* Density toggle — persisted across sessions */ }
442- < FeedDensityToggle />
443460 </ div >
444461 { /* #98: show consistent loading state */ }
445462 < div
@@ -545,41 +562,6 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
545562 availableMarkets = { availableAssets }
546563 />
547564
548- < div
549- ref = { parentRef }
550- className = "max-h-[70vh] overflow-auto"
551- role = "feed"
552- aria-busy = { isLoading }
553- aria-label = "Signal list"
554- onKeyDown = { ( e ) => {
555- if ( e . key !== "ArrowDown" && e . key !== "ArrowUp" ) return ;
556- const articles = Array . from (
557- ( e . currentTarget as HTMLElement ) . querySelectorAll < HTMLElement > ( "article[tabindex]" )
558- ) ;
559- const idx = articles . indexOf ( document . activeElement as HTMLElement ) ;
560- if ( idx === - 1 ) return ;
561- e . preventDefault ( ) ;
562- const next = e . key === "ArrowDown" ? articles [ idx + 1 ] : articles [ idx - 1 ] ;
563- next ?. focus ( ) ;
564- } }
565- >
566- { isError && (
567- < DataPanelError
568- errorInfo = { classifyError ( error ) }
569- onRetry = { ( ) => refetch ( ) }
570- />
571- ) }
572-
573- { isError && signals . length > 0 && (
574- < div className = "mb-3" >
575- < NetworkErrorState
576- context = "the latest signals"
577- onRetry = { ( ) => refetch ( ) }
578- variant = "banner"
579- />
580- </ div >
581- ) }
582-
583565 < div
584566 ref = { splitContainerRef }
585567 className = "lg:grid lg:items-start lg:gap-0"
@@ -595,7 +577,10 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
595577 >
596578 < div className = "min-w-0" >
597579 < div
598- ref = { setScrollEl }
580+ ref = { ( el ) => {
581+ setScrollEl ( el ) ;
582+ parentRef . current = el ;
583+ } }
599584 className = "max-h-[70vh] overflow-auto"
600585 role = "feed"
601586 aria-busy = { isLoading }
0 commit comments