11'use client'
22
3- import { useState , useMemo , useCallback , memo , useEffect } from 'react'
3+ import { useState , useMemo , useCallback , memo , useEffect , useRef } from 'react'
44import { useQuery } from '@tanstack/react-query'
55import { useSession } from 'next-auth/react'
66import { motion } from 'framer-motion'
@@ -183,18 +183,76 @@ const AgentStorePage = () => {
183183
184184 // Only create virtualizer when we have data and the component is mounted
185185 const [ isMounted , setIsMounted ] = useState ( false )
186+ const measurementCache = useRef < Map < number , number > > ( new Map ( ) )
186187
187188 useEffect ( ( ) => {
188189 setIsMounted ( true )
189190 } , [ ] )
190191
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+
191208 // Virtualizer for All Agents section only
192209 const allAgentsVirtualizer = useWindowVirtualizer ( {
193210 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+ } ,
196219 } )
197220
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+
198256 // Determine if we should use virtualization for All Agents section
199257 const shouldVirtualizeAllAgents = isMounted && totalRows > 6
200258
@@ -257,7 +315,9 @@ const AgentStorePage = () => {
257315 className = { cn (
258316 'relative h-full transition-all duration-200 cursor-pointer border bg-card/50' ,
259317 '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]'
261321 ) }
262322 style = { {
263323 // Use CSS transforms for hover effects instead of Framer Motion
@@ -286,12 +346,6 @@ const AgentStorePage = () => {
286346 < h3 className = "text-xl font-bold font-mono text-foreground truncate group-hover:text-primary transition-colors" >
287347 { agent . id }
288348 </ 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 >
295349 </ div >
296350 < div className = "flex items-center gap-1" >
297351 < div onClick = { ( e ) => e . preventDefault ( ) } >
@@ -304,7 +358,7 @@ const AgentStorePage = () => {
304358 description : `Agent run command copied to clipboard!` ,
305359 } )
306360 } }
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"
308362 title = { `Copy: codebuff --agent ${ agent . publisher . id } /${ agent . id } @${ agent . version } ` }
309363 >
310364 < Copy className = "h-4 w-4 text-muted-foreground hover:text-foreground" />
@@ -398,31 +452,6 @@ const AgentStorePage = () => {
398452 < p className = "text-xs text-muted-foreground" > Users</ p >
399453 </ div >
400454 </ 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- ) }
426455 </ CardContent >
427456 </ Card >
428457 </ Link >
@@ -556,16 +585,21 @@ const AgentStorePage = () => {
556585 return (
557586 < div
558587 key = { virtualItem . key }
588+ ref = { ( node ) =>
589+ allAgentsVirtualizer . measureElement ( node )
590+ }
591+ data-index = { virtualItem . index }
559592 style = { {
560593 position : 'absolute' ,
561594 top : 0 ,
562595 left : 0 ,
563596 width : '100%' ,
564- height : `${ virtualItem . size } px` ,
565597 transform : `translateY(${ virtualItem . start } px)` ,
598+ // Include padding/margin in measured element
599+ paddingBottom : '24px' ,
566600 } }
567601 >
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" >
569603 { agents ?. map ( ( agent ) => (
570604 // No motion for virtualized items - use CSS transitions instead
571605 < div
0 commit comments