1
1
'use client'
2
2
3
- import { useState , useMemo , useCallback , memo , useEffect } from 'react'
3
+ import { useState , useMemo , useCallback , memo , useEffect , useRef } from 'react'
4
4
import { useQuery } from '@tanstack/react-query'
5
5
import { useSession } from 'next-auth/react'
6
6
import { motion } from 'framer-motion'
@@ -183,18 +183,76 @@ const AgentStorePage = () => {
183
183
184
184
// Only create virtualizer when we have data and the component is mounted
185
185
const [ isMounted , setIsMounted ] = useState ( false )
186
+ const measurementCache = useRef < Map < number , number > > ( new Map ( ) )
186
187
187
188
useEffect ( ( ) => {
188
189
setIsMounted ( true )
189
190
} , [ ] )
190
191
192
+ // Dynamic overscan based on device/viewport
193
+ const getOverscan = ( ) => {
194
+ if ( typeof window === 'undefined' ) return 6
195
+ const isMobile = window . innerWidth < 768
196
+ const isTouchDevice = 'ontouchstart' in window
197
+ return isMobile || isTouchDevice ? 15 : 8
198
+ }
199
+
200
+ // Dynamic height estimation based on columns
201
+ const getEstimatedSize = ( ) => {
202
+ // Base card height + gap between rows
203
+ const baseCardHeight = 240 // More conservative estimate
204
+ const rowGap = 24 // gap-6 = 24px
205
+ return baseCardHeight + rowGap
206
+ }
207
+
191
208
// Virtualizer for All Agents section only
192
209
const allAgentsVirtualizer = useWindowVirtualizer ( {
193
210
count : isMounted ? totalRows : 0 ,
194
- estimateSize : ( ) => 270 , // Height for agent rows (card + gap)
195
- overscan : 6 ,
211
+ estimateSize : getEstimatedSize ,
212
+ overscan : getOverscan ( ) ,
213
+ measureElement : ( element ) => {
214
+ // Cache measurements for better performance
215
+ const height =
216
+ element ?. getBoundingClientRect ( ) . height ?? getEstimatedSize ( )
217
+ return height
218
+ } ,
196
219
} )
197
220
221
+ // Remeasure when columns change or data changes significantly
222
+ useEffect ( ( ) => {
223
+ if ( allAgentsVirtualizer && isMounted ) {
224
+ // Clear cache and remeasure when layout changes
225
+ measurementCache . current . clear ( )
226
+ allAgentsVirtualizer . measure ( )
227
+ }
228
+ } , [ columns , filteredAndSortedAgents . length , allAgentsVirtualizer , isMounted ] )
229
+
230
+ // Handle viewport/orientation changes
231
+ useEffect ( ( ) => {
232
+ if ( ! isMounted || ! allAgentsVirtualizer ) return
233
+
234
+ const handleResize = ( ) => {
235
+ // Debounce resize events
236
+ measurementCache . current . clear ( )
237
+ allAgentsVirtualizer . measure ( )
238
+ }
239
+
240
+ let resizeTimeout : NodeJS . Timeout
241
+ const debouncedResize = ( ) => {
242
+ clearTimeout ( resizeTimeout )
243
+ resizeTimeout = setTimeout ( handleResize , 150 )
244
+ }
245
+
246
+ window . addEventListener ( 'resize' , debouncedResize )
247
+ window . addEventListener ( 'orientationchange' , debouncedResize )
248
+
249
+ return ( ) => {
250
+ window . removeEventListener ( 'resize' , debouncedResize )
251
+ window . removeEventListener ( 'orientationchange' , debouncedResize )
252
+ clearTimeout ( resizeTimeout )
253
+ }
254
+ } , [ allAgentsVirtualizer , isMounted ] )
255
+
198
256
// Determine if we should use virtualization for All Agents section
199
257
const shouldVirtualizeAllAgents = isMounted && totalRows > 6
200
258
@@ -257,7 +315,9 @@ const AgentStorePage = () => {
257
315
className = { cn (
258
316
'relative h-full transition-all duration-200 cursor-pointer border bg-card/50' ,
259
317
'hover:border-accent/50 hover:bg-card/80' ,
260
- isEditorsChoice && 'ring-2 ring-amber-400/50 border-amber-400/30'
318
+ isEditorsChoice && 'ring-2 ring-amber-400/50 border-amber-400/30' ,
319
+ // Ensure consistent minimum height to reduce layout shifts
320
+ 'min-h-[220px]'
261
321
) }
262
322
style = { {
263
323
// Use CSS transforms for hover effects instead of Framer Motion
@@ -286,12 +346,6 @@ const AgentStorePage = () => {
286
346
< h3 className = "text-xl font-bold font-mono text-foreground truncate group-hover:text-primary transition-colors" >
287
347
{ agent . id }
288
348
</ h3 >
289
- < Badge
290
- variant = "outline"
291
- className = "text-xs font-mono px-2 py-1 border-border/50 bg-muted/30 shrink-0"
292
- >
293
- v{ agent . version }
294
- </ Badge >
295
349
</ div >
296
350
< div className = "flex items-center gap-1" >
297
351
< div onClick = { ( e ) => e . preventDefault ( ) } >
@@ -304,7 +358,7 @@ const AgentStorePage = () => {
304
358
description : `Agent run command copied to clipboard!` ,
305
359
} )
306
360
} }
307
- className = "p-2 hover:bg-muted/50 rounded-lg transition-all duration-200 opacity-60 group-hover:opacity-100 hover:scale-110 active:scale-95"
361
+ className = "hidden md:flex p-2 hover:bg-muted/50 rounded-lg transition-all duration-200 opacity-60 group-hover:opacity-100 hover:scale-110 active:scale-95"
308
362
title = { `Copy: codebuff --agent ${ agent . publisher . id } /${ agent . id } @${ agent . version } ` }
309
363
>
310
364
< Copy className = "h-4 w-4 text-muted-foreground hover:text-foreground" />
@@ -398,31 +452,6 @@ const AgentStorePage = () => {
398
452
< p className = "text-xs text-muted-foreground" > Users</ p >
399
453
</ div >
400
454
</ div >
401
-
402
- { /* Tags - Improved design and spacing */ }
403
- { agent . tags && agent . tags . length > 0 && (
404
- < div className = "pt-2" >
405
- < div className = "flex flex-wrap gap-1.5" >
406
- { agent . tags . slice ( 0 , 4 ) . map ( ( tag ) => (
407
- < Badge
408
- key = { tag }
409
- variant = "secondary"
410
- className = "text-xs px-2.5 py-1 bg-muted/40 hover:bg-muted/60 transition-colors border-0 rounded-full"
411
- >
412
- { tag }
413
- </ Badge >
414
- ) ) }
415
- { agent . tags . length > 4 && (
416
- < Badge
417
- variant = "secondary"
418
- className = "text-xs px-2.5 py-1 bg-muted/40 border-0 rounded-full opacity-60"
419
- >
420
- +{ agent . tags . length - 4 }
421
- </ Badge >
422
- ) }
423
- </ div >
424
- </ div >
425
- ) }
426
455
</ CardContent >
427
456
</ Card >
428
457
</ Link >
@@ -556,16 +585,21 @@ const AgentStorePage = () => {
556
585
return (
557
586
< div
558
587
key = { virtualItem . key }
588
+ ref = { ( node ) =>
589
+ allAgentsVirtualizer . measureElement ( node )
590
+ }
591
+ data-index = { virtualItem . index }
559
592
style = { {
560
593
position : 'absolute' ,
561
594
top : 0 ,
562
595
left : 0 ,
563
596
width : '100%' ,
564
- height : `${ virtualItem . size } px` ,
565
597
transform : `translateY(${ virtualItem . start } px)` ,
598
+ // Include padding/margin in measured element
599
+ paddingBottom : '24px' ,
566
600
} }
567
601
>
568
- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6 " >
602
+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
569
603
{ agents ?. map ( ( agent ) => (
570
604
// No motion for virtualized items - use CSS transitions instead
571
605
< div
0 commit comments