@@ -13,6 +13,11 @@ import type { ComponentMetadata } from '@/schemas/component'
1313import { cn } from '@/lib/utils'
1414import { env } from '@/config/env'
1515import { Skeleton } from '@/components/ui/skeleton'
16+ import {
17+ type ComponentCategory ,
18+ getCategorySeparatorColor
19+ } from '@/utils/categoryColors'
20+ import { useThemeStore } from '@/store/themeStore'
1621
1722// Use backend-provided category configuration
1823// The frontend will no longer categorize components - it will use backend data
@@ -166,10 +171,17 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
166171 const { getAllComponents, fetchComponents, loading, error } = useComponentStore ( )
167172 const [ searchQuery , setSearchQuery ] = useState ( '' )
168173 const [ viewMode , setViewMode ] = useState < ViewMode > ( 'list' )
174+ const theme = useThemeStore ( ( state ) => state . theme )
175+ const isDarkMode = theme === 'dark'
169176 const frontendBranch = env . VITE_FRONTEND_BRANCH . trim ( )
170177 const backendBranch = env . VITE_BACKEND_BRANCH . trim ( )
171178 const hasBranchInfo = Boolean ( frontendBranch || backendBranch )
172179
180+ // Get category accent color (for left border) - uses separator colors for brightness
181+ const getCategoryAccentColor = ( category : string ) : string | undefined => {
182+ return getCategorySeparatorColor ( category as ComponentCategory , isDarkMode )
183+ }
184+
173185 // Custom scrollbar state
174186 const scrollContainerRef = useRef < HTMLDivElement > ( null )
175187 const [ scrollbarVisible , setScrollbarVisible ] = useState ( false )
@@ -289,7 +301,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
289301 // eslint-disable-next-line react-hooks/exhaustive-deps
290302 } , [ searchQuery ] )
291303
292- // Custom scrollbar logic
304+ // Custom scrollbar logic - setup event listeners (only once)
293305 useEffect ( ( ) => {
294306 const container = scrollContainerRef . current
295307 if ( ! container ) return
@@ -349,7 +361,43 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
349361 clearTimeout ( scrollTimeoutRef . current )
350362 }
351363 }
352- } , [ filteredComponentsByCategory , loading ] )
364+ // Only setup event listeners once - don't depend on content changes
365+ } , [ ] )
366+
367+ // Update scrollbar when content changes (without re-setting up listeners)
368+ // Calculate a stable value for component count to use as dependency
369+ const componentCount = useMemo ( ( ) => {
370+ return Object . values ( filteredComponentsByCategory ) . reduce (
371+ ( total , components ) => total + components . length ,
372+ 0
373+ )
374+ } , [ filteredComponentsByCategory ] )
375+
376+ useEffect ( ( ) => {
377+ const container = scrollContainerRef . current
378+ if ( ! container ) return
379+
380+ // Use requestAnimationFrame to ensure DOM has updated after content changes
381+ requestAnimationFrame ( ( ) => {
382+ const { scrollTop, scrollHeight, clientHeight } = container
383+ const maxScroll = scrollHeight - clientHeight
384+
385+ if ( maxScroll <= 0 ) {
386+ setScrollbarVisible ( false )
387+ setScrollbarHeight ( 0 )
388+ setScrollbarPosition ( 0 )
389+ return
390+ }
391+
392+ // Calculate scrollbar thumb position and height
393+ const thumbHeight = Math . max ( ( clientHeight / scrollHeight ) * clientHeight , 30 )
394+ const thumbPosition = ( scrollTop / maxScroll ) * ( clientHeight - thumbHeight )
395+
396+ setScrollbarHeight ( thumbHeight )
397+ setScrollbarPosition ( thumbPosition )
398+ // Don't show scrollbar automatically when content changes - only on scroll
399+ } )
400+ } , [ componentCount , loading ] ) // Use stable componentCount instead of filteredComponentsByCategory object
353401
354402 return (
355403 < div className = "h-full w-full max-w-[320px] border-r bg-background flex flex-col" >
@@ -480,16 +528,24 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
480528
481529 const categoryConfig = components [ 0 ] ?. categoryConfig
482530
531+ const categoryAccentColor = getCategoryAccentColor ( category )
532+
483533 return (
484534 < AccordionItem
485535 key = { category }
486536 value = { category }
487537 className = "border border-border/50 rounded-sm px-3 py-1 transition-colors"
488538 >
489- < AccordionTrigger className = { cn (
490- 'py-3 px-0 hover:no-underline hover:bg-muted/50 rounded-sm -mx-3 -my-1 px-3 [&[data-state=open]]:text-foreground' ,
491- 'group transition-colors'
492- ) } >
539+ < AccordionTrigger
540+ className = { cn (
541+ 'py-3 hover:no-underline hover:bg-muted/50 rounded-sm -mx-3 -my-1 px-3 [&[data-state=open]]:text-foreground' ,
542+ 'group transition-colors relative'
543+ ) }
544+ style = { {
545+ borderLeftWidth : categoryAccentColor ? '3px' : undefined ,
546+ borderLeftColor : categoryAccentColor || undefined ,
547+ } }
548+ >
493549 < div className = "flex flex-col items-start gap-0.5 w-full" >
494550 < div className = "flex items-center justify-between w-full" >
495551 < div className = "flex items-center gap-2" >
0 commit comments