Skip to content

Commit a0f7c97

Browse files
committed
feat(sidebar): enhance sidebar functionality for collapsing
1 parent 8f98317 commit a0f7c97

File tree

3 files changed

+140
-24
lines changed

3 files changed

+140
-24
lines changed

dashboard/src/components/layout/sidebar.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { NavUser } from '@/components/layout/nav-user'
99
import { useTheme } from '@/components/common/theme-provider'
1010
import { ThemeToggle } from '@/components/common/theme-toggle'
1111
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, useSidebar } from '@/components/ui/sidebar'
12-
import { TooltipProvider } from '@/components/ui/tooltip'
12+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
13+
import { Button } from '@/components/ui/button'
1314
import { Link } from 'react-router'
1415
import { DISCUSSION_GROUP, DOCUMENTATION, DONATION_URL, REPO_URL } from '@/constants/Project'
1516
import { useAdmin } from '@/hooks/use-admin'
1617
import useDirDetection from '@/hooks/use-dir-detection'
18+
import { useVersionCheck } from '@/hooks/use-version-check'
1719
import { cn } from '@/lib/utils'
1820
import { getSystemStats } from '@/service/api'
1921
import {
@@ -33,6 +35,8 @@ import {
3335
Lock,
3436
MessageCircle,
3537
Palette,
38+
PanelLeftClose,
39+
PanelLeftOpen,
3640
PieChart,
3741
RssIcon,
3842
Send,
@@ -54,8 +58,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
5458
const { t } = useTranslation()
5559
const [version, setVersion] = useState<string>('')
5660
const { admin } = useAdmin()
57-
const { setOpenMobile, openMobile, state, isMobile } = useSidebar()
61+
const { setOpenMobile, openMobile, state, isMobile, toggleSidebar } = useSidebar()
5862
const { resolvedTheme } = useTheme()
63+
const [showCollapseButton, setShowCollapseButton] = useState(false)
64+
const currentVersion = version.replace(/[^0-9.]/g, '') || null
65+
const { hasUpdate } = useVersionCheck(currentVersion)
5966
const touchStartX = useRef<number | null>(null)
6067
const touchEndX = useRef<number | null>(null)
6168
const minSwipeDistance = 50
@@ -331,21 +338,108 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
331338
<SidebarMenu>
332339
<SidebarMenuItem>
333340
{state === 'collapsed' && !isMobile ? (
334-
<SidebarMenuButton size="lg" asChild className="relative">
335-
<a href={REPO_URL} target="_blank" className="justify-center !gap-0">
341+
<div
342+
className="relative group"
343+
onMouseEnter={() => setShowCollapseButton(true)}
344+
onMouseLeave={() => setShowCollapseButton(false)}
345+
>
346+
{/* Badge - always visible, positioned on top layer */}
347+
<div className="absolute inset-0 pointer-events-none z-30">
348+
<div className="relative w-full h-full">
349+
<VersionBadge currentVersion={currentVersion} />
350+
</div>
351+
</div>
352+
{/* Logo - fades out on hover */}
353+
<SidebarMenuButton
354+
size="lg"
355+
asChild
356+
className={cn(
357+
"relative justify-center !gap-0 w-full transition-opacity duration-200 ease-in-out",
358+
showCollapseButton ? "opacity-0 pointer-events-none" : "opacity-100"
359+
)}
360+
>
361+
<a href={REPO_URL} target="_blank">
362+
<img
363+
src={resolvedTheme === 'dark' ? window.location.pathname + 'statics/favicon/logo.png' : window.location.pathname + 'statics/favicon/logo-dark.png'}
364+
alt="PasarGuard Logo"
365+
className="h-6 w-6 flex-shrink-0 object-contain"
366+
/>
367+
{hasUpdate && (
368+
<TooltipProvider>
369+
<VersionBadge currentVersion={currentVersion} />
370+
</TooltipProvider>
371+
)}
372+
</a>
373+
</SidebarMenuButton>
374+
{/* Expand button - fades in on hover */}
375+
<TooltipProvider>
376+
<Tooltip>
377+
<TooltipTrigger asChild>
378+
<SidebarMenuButton
379+
size="lg"
380+
className={cn(
381+
"absolute inset-0 justify-center !gap-0 w-full transition-opacity duration-200 ease-in-out",
382+
showCollapseButton ? "opacity-100" : "opacity-0 pointer-events-none"
383+
)}
384+
onClick={toggleSidebar}
385+
>
386+
<PanelLeftOpen className={cn("h-6 w-6 flex-shrink-0", isRTL && "scale-x-[-1]")} />
387+
<span className="sr-only">Expand Sidebar</span>
388+
{hasUpdate && <VersionBadge currentVersion={currentVersion} />}
389+
</SidebarMenuButton>
390+
</TooltipTrigger>
391+
<TooltipContent side={isRTL ? 'left' : 'right'}>
392+
<p>{t('sidebar.expand')}</p>
393+
</TooltipContent>
394+
</Tooltip>
395+
</TooltipProvider>
396+
</div>
397+
) : state !== 'collapsed' && !isMobile ? (
398+
<SidebarMenuButton size="lg" className={cn('relative !gap-2', isRTL ? 'pl-10' : 'pr-10')}>
399+
<a href={REPO_URL} target="_blank" className="flex items-center gap-2 flex-1 min-w-0">
336400
<img
337401
src={resolvedTheme === 'dark' ? window.location.pathname + 'statics/favicon/logo.png' : window.location.pathname + 'statics/favicon/logo-dark.png'}
338402
alt="PasarGuard Logo"
339-
className="h-6 w-6 flex-shrink-0 object-contain"
403+
className="h-8 w-8 flex-shrink-0 object-contain"
340404
/>
341-
<TooltipProvider>
342-
<VersionBadge currentVersion={version.replace(/[^0-9.]/g, '')} />
343-
</TooltipProvider>
405+
<div className="flex flex-col overflow-hidden min-w-0 flex-1">
406+
<span className={cn(isRTL ? 'text-right' : 'text-left', 'truncate text-sm font-semibold leading-tight')}>{t('pasarguard')}</span>
407+
<div className="flex items-center gap-1.5 min-w-0">
408+
<span className="text-xs opacity-45 shrink-0">{version}</span>
409+
<div className="min-w-0 flex-1">
410+
<TooltipProvider>
411+
<VersionBadge currentVersion={currentVersion} />
412+
</TooltipProvider>
413+
</div>
414+
</div>
415+
</div>
344416
</a>
417+
<TooltipProvider>
418+
<Tooltip>
419+
<TooltipTrigger asChild>
420+
<Button
421+
variant="ghost"
422+
size="icon"
423+
className={cn('absolute h-8 w-8 shrink-0 z-10', isRTL ? 'left-2' : 'right-2')}
424+
onClick={(e) => {
425+
e.preventDefault()
426+
e.stopPropagation()
427+
toggleSidebar()
428+
}}
429+
>
430+
<PanelLeftClose className={cn("h-4 w-4", isRTL && "scale-x-[-1]")} />
431+
<span className="sr-only">Collapse Sidebar</span>
432+
</Button>
433+
</TooltipTrigger>
434+
<TooltipContent side={isRTL ? 'left' : 'right'}>
435+
<p>{t('sidebar.collapse')}</p>
436+
</TooltipContent>
437+
</Tooltip>
438+
</TooltipProvider>
345439
</SidebarMenuButton>
346440
) : (
347-
<SidebarMenuButton size="lg" asChild>
348-
<a href={REPO_URL} target="_blank" className="!gap-2">
441+
<SidebarMenuButton size="lg" asChild className="!gap-2">
442+
<a href={REPO_URL} target="_blank">
349443
<img
350444
src={resolvedTheme === 'dark' ? window.location.pathname + 'statics/favicon/logo.png' : window.location.pathname + 'statics/favicon/logo-dark.png'}
351445
alt="PasarGuard Logo"

dashboard/src/components/layout/version-badge.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ export function VersionBadge({ currentVersion, className }: VersionBadgeProps) {
2929
className={cn(
3030
'absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full',
3131
'bg-amber-500 dark:bg-amber-400',
32-
'border border-background',
32+
'border-2 border-background',
3333
'translate-x-1/2 translate-y-1/2',
34-
'z-10'
34+
'z-20',
35+
'shadow-sm'
3536
)}
3637
aria-label="Update available"
3738
/>
@@ -47,11 +48,11 @@ export function VersionBadge({ currentVersion, className }: VersionBadgeProps) {
4748
href={releaseLink}
4849
target="_blank"
4950
rel="noopener noreferrer"
50-
className={cn('inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 hover:underline truncate max-w-full', className)}
51+
className={cn('inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 hover:underline min-w-0 max-w-full', className)}
5152
onClick={(e) => e.stopPropagation()}
5253
>
5354
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400 shrink-0" />
54-
<span className="truncate">{t('version.needsUpdate')}</span>
55+
<span className="truncate min-w-0">{t('version.needsUpdate')}</span>
5556
</a>
5657
</TooltipTrigger>
5758
<TooltipContent side="bottom" className="p-1.5">

dashboard/src/components/ui/sidebar.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ SidebarTrigger.displayName = 'SidebarTrigger'
280280
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>(({ className, ...props }, ref) => {
281281
const { toggleSidebar, state } = useSidebar()
282282
const { t } = useTranslation()
283+
const dir = useDirDetection()
284+
const isRTL = dir === 'rtl'
285+
283286
return (
284287
<Tooltip>
285288
<TooltipTrigger asChild>
@@ -289,15 +292,26 @@ const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'bu
289292
tabIndex={0}
290293
onClick={toggleSidebar}
291294
className={cn(
292-
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
293-
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
294-
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
295-
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
296-
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
297-
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
298-
'group-data-[collapsible=icon]:translate-x-0 group-data-[collapsible=icon]:after:left-full group-data-[collapsible=icon]:after:w-[1px]',
299-
'[[data-side=left][data-collapsible=icon]_&]:right-0 [[data-side=left][data-collapsible=icon]_&]:translate-x-0',
300-
'[[data-side=right][data-collapsible=icon]_&]:left-0 [[data-side=right][data-collapsible=icon]_&]:translate-x-0',
295+
'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:w-[2px] hover:after:bg-sidebar-border sm:flex',
296+
// Positioning for left side (LTR) or right side (RTL)
297+
isRTL
298+
? 'group-data-[side=right]:-left-4 group-data-[side=left]:right-0 translate-x-1/2 after:right-1/2'
299+
: 'group-data-[side=left]:-right-4 group-data-[side=right]:left-0 -translate-x-1/2 after:left-1/2',
300+
// Cursor styles - RTL-aware
301+
isRTL
302+
? '[[data-side=right]_&]:cursor-w-resize [[data-side=left]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-e-resize [[data-side=left][data-state=collapsed]_&]:cursor-w-resize'
303+
: '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize [[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
304+
// Offcanvas mode
305+
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar',
306+
isRTL
307+
? 'group-data-[collapsible=offcanvas]:after:right-full [[data-side=right][data-collapsible=offcanvas]_&]:-left-2 [[data-side=left][data-collapsible=offcanvas]_&]:-right-2'
308+
: 'group-data-[collapsible=offcanvas]:after:left-full [[data-side=left][data-collapsible=offcanvas]_&]:-right-2 [[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
309+
// Icon mode
310+
'group-data-[collapsible=icon]:translate-x-0 group-data-[collapsible=icon]:after:w-[1px]',
311+
isRTL
312+
? 'group-data-[collapsible=icon]:after:right-full [[data-side=right][data-collapsible=icon]_&]:left-0 [[data-side=left][data-collapsible=icon]_&]:right-0'
313+
: 'group-data-[collapsible=icon]:after:left-full [[data-side=left][data-collapsible=icon]_&]:right-0 [[data-side=right][data-collapsible=icon]_&]:left-0',
314+
// Hover effects
301315
'hover:after:w-[4px] hover:after:bg-sidebar-accent',
302316
'group-data-[collapsible=icon]:hover:after:w-[2px]',
303317
'after:transition-all after:duration-200',
@@ -309,7 +323,14 @@ const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'bu
309323
{...props}
310324
/>
311325
</TooltipTrigger>
312-
<TooltipContent className="font-semibold" side={state === 'collapsed' ? 'right' : 'left'} align="center">
326+
<TooltipContent
327+
className="font-semibold"
328+
side={isRTL
329+
? (state === 'collapsed' ? 'left' : 'right')
330+
: (state === 'collapsed' ? 'right' : 'left')
331+
}
332+
align="center"
333+
>
313334
{state === 'collapsed' ? t('sidebar.expand') : t('sidebar.collapse')}
314335
</TooltipContent>
315336
</Tooltip>

0 commit comments

Comments
 (0)