11import { useState , useRef , useCallback } from 'react'
2- import { Search , Plus , FolderPlus , ChevronDown , ChevronRight , Server , Router , Monitor , Key , Usb , Pencil , Trash2 , Download , Upload , MoreHorizontal } from 'lucide-react'
2+ import { Search , Plus , FolderPlus , ChevronDown , ChevronRight , Server , Router , Monitor , Key , Usb , Pencil , Trash2 , Download , Upload , MoreHorizontal , Clock , Zap } from 'lucide-react'
33import { useAppStore } from '../../store'
44import { Connection , ConnectionGroup } from '../../types'
55import { ConnectionContextMenu } from './ConnectionContextMenu'
66import { GroupDialog } from './GroupDialog'
77import { SSHKeyDialog } from '../dialogs/SSHKeyDialog'
88import { ExportImportDialog } from '../dialogs/ExportImportDialog'
9- import { cn } from '../../lib/utils'
9+ import { cn , timeAgo } from '../../lib/utils'
1010
1111const GROUP_COLORS = [
1212 '#8b5cf6' , '#3b82f6' , '#06b6d4' , '#10b981' ,
@@ -31,7 +31,7 @@ export function Sidebar(): JSX.Element {
3131 sidebarWidth, setSidebarWidth,
3232 setConnectionDialogOpen, setQuickConnectOpen,
3333 openSession, openSftpSession, exportConnections, importConnections,
34- saveConnection,
34+ saveConnection, connectionsLoaded ,
3535 } = useAppStore ( )
3636
3737 const [ importMsg , setImportMsg ] = useState < string | null > ( null )
@@ -147,16 +147,52 @@ export function Sidebar(): JSX.Element {
147147
148148 { /* Connection list */ }
149149 < div className = "flex-1 overflow-y-auto py-1" >
150- { connections . length === 0 && (
151- < div className = "flex flex-col items-center gap-2 p-6 text-center" >
152- < Server className = "w-8 h-8 text-sidebar-foreground/20" />
153- < p className = "text-xs text-sidebar-foreground/40" > No connections yet</ p >
154- < button
155- onClick = { ( ) => setQuickConnectOpen ( true ) }
156- className = "text-xs text-primary hover:underline"
157- >
158- Quick connect
159- </ button >
150+
151+ { /* Skeleton loading */ }
152+ { ! connectionsLoaded && (
153+ < div className = "px-2 py-1 space-y-1" >
154+ { [ ...Array ( 5 ) ] . map ( ( _ , i ) => (
155+ < div key = { i } className = "flex items-center gap-2.5 px-2.5 py-2 rounded-lg mx-1 animate-pulse" >
156+ < div className = "w-8 h-8 rounded-lg bg-sidebar-accent/60 shrink-0" />
157+ < div className = "flex-1 space-y-1.5" >
158+ < div className = "h-3 rounded bg-sidebar-accent/60" style = { { width : `${ 55 + ( i * 13 ) % 35 } %` } } />
159+ < div className = "h-2.5 rounded bg-sidebar-accent/40" style = { { width : `${ 35 + ( i * 17 ) % 30 } %` } } />
160+ </ div >
161+ </ div >
162+ ) ) }
163+ </ div >
164+ ) }
165+
166+ { /* Better empty state */ }
167+ { connectionsLoaded && connections . length === 0 && (
168+ < div className = "flex flex-col items-center gap-3 px-4 py-10 text-center" >
169+ < div className = "w-12 h-12 rounded-2xl bg-sidebar-accent/50 flex items-center justify-center" >
170+ < Server className = "w-6 h-6 text-sidebar-foreground/20" />
171+ </ div >
172+ < div >
173+ < p className = "text-[13px] font-medium text-sidebar-foreground/50" > No connections yet</ p >
174+ < p className = "text-[11px] text-sidebar-foreground/30 mt-1" > Add your first host to get started</ p >
175+ </ div >
176+ < div className = "flex flex-col gap-1.5 w-full" >
177+ < button
178+ onClick = { ( ) => setConnectionDialogOpen ( true ) }
179+ className = "w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-primary/10 text-primary text-[12px] font-medium hover:bg-primary/20 transition-colors cursor-pointer"
180+ >
181+ < Plus className = "w-3.5 h-3.5" /> New Connection
182+ </ button >
183+ < button
184+ onClick = { ( ) => setQuickConnectOpen ( true ) }
185+ className = "w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-sidebar-accent/50 text-sidebar-foreground/60 text-[12px] hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors cursor-pointer"
186+ >
187+ < Zap className = "w-3.5 h-3.5" /> Quick Connect
188+ </ button >
189+ < button
190+ onClick = { ( ) => setImportDialogOpen ( true ) }
191+ className = "w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-sidebar-accent/50 text-sidebar-foreground/60 text-[12px] hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors cursor-pointer"
192+ >
193+ < Upload className = "w-3.5 h-3.5" /> Import
194+ </ button >
195+ </ div >
160196 </ div >
161197 ) }
162198
@@ -370,19 +406,44 @@ function ConnectionItem({
370406
371407 return (
372408 < >
409+ { /* Tooltip wrapper */ }
410+ < div className = "relative group/tip mx-1" style = { { width : 'calc(100% - 8px)' } } >
411+ { /* Hover tooltip */ }
412+ < div className = { cn (
413+ 'pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 z-50' ,
414+ 'w-52 bg-popover border border-border rounded-xl shadow-xl p-3 text-left' ,
415+ 'opacity-0 group-hover/tip:opacity-100 transition-opacity duration-150' ,
416+ 'hidden group-hover/tip:block'
417+ ) } >
418+ < p className = "text-[12px] font-semibold text-foreground truncate" > { connection . name } </ p >
419+ < p className = "text-[11px] text-muted-foreground font-mono mt-0.5" >
420+ { connection . protocol . toUpperCase ( ) } · { connection . host } { connection . port ? `:${ connection . port } ` : '' }
421+ </ p >
422+ { connection . lastConnectedAt && (
423+ < div className = "flex items-center gap-1 mt-1.5 text-[10px] text-muted-foreground/60" >
424+ < Clock className = "w-2.5 h-2.5 shrink-0" />
425+ Last connected { timeAgo ( connection . lastConnectedAt ) }
426+ </ div >
427+ ) }
428+ { connection . notes && (
429+ < p className = "text-[11px] text-muted-foreground/70 mt-1.5 border-t border-border pt-1.5 line-clamp-3" >
430+ { connection . notes }
431+ </ p >
432+ ) }
433+ </ div >
434+
373435 < button
374436 draggable
375437 onDragStart = { ( e ) => { e . dataTransfer . effectAllowed = 'move' ; onDragStart ?.( ) } }
376438 onDoubleClick = { onConnect }
377439 onContextMenu = { handleContextMenu }
378440 className = { cn (
379- 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left group transition-all rounded-lg mx-1 cursor-pointer' ,
441+ 'w-full flex items-center gap-2.5 px-2.5 py-2 text-left group transition-all rounded-lg cursor-pointer' ,
380442 indent && 'pl-6' ,
381443 isActive
382444 ? 'bg-sidebar-accent text-sidebar-foreground'
383445 : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/70 hover:text-sidebar-foreground'
384446 ) }
385- style = { { width : 'calc(100% - 8px)' } }
386447 >
387448 < div
388449 className = "relative w-8 h-8 rounded-lg flex items-center justify-center shrink-0"
@@ -397,9 +458,12 @@ function ConnectionItem({
397458 < div className = "flex-1 min-w-0" >
398459 < p className = "text-[13px] font-medium truncate leading-tight" > { connection . name } </ p >
399460 < p className = "text-[11px] text-sidebar-foreground/40 truncate mt-0.5" >
400- { connection . protocol === 'serial'
401- ? ( connection . serialConfig ?. path ?? connection . host )
402- : connection . host }
461+ { connection . lastConnectedAt
462+ ? < span className = "flex items-center gap-1" > < Clock className = "w-2.5 h-2.5 shrink-0" /> { timeAgo ( connection . lastConnectedAt ) } </ span >
463+ : ( connection . protocol === 'serial'
464+ ? ( connection . serialConfig ?. path ?? connection . host )
465+ : connection . host )
466+ }
403467 </ p >
404468 </ div >
405469
@@ -426,6 +490,7 @@ function ConnectionItem({
426490 </ button >
427491 </ div >
428492 </ button >
493+ </ div >
429494
430495 { menuPos && (
431496 < ConnectionContextMenu
0 commit comments