From f4c8096f7d587bea122015c31a4812351176e960 Mon Sep 17 00:00:00 2001 From: CoreyHayward Date: Fri, 22 May 2026 07:15:46 +0100 Subject: [PATCH] feat(web): make dashboard sidebar collapsible --- web/src/App.tsx | 80 +++++++++++++++++++---- web/src/components/SidebarFooter.tsx | 3 +- web/src/components/SidebarStatusStrip.tsx | 3 +- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 987252ce0bb6b..d4c9243f92739 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -31,6 +31,8 @@ import { Menu, MessageSquare, Package, + PanelLeftClose, + PanelLeftOpen, Puzzle, RotateCw, Settings, @@ -311,7 +313,12 @@ export default function App() { const { manifests, loading: pluginsLoading } = usePlugins(); const { theme } = useTheme(); const [mobileOpen, setMobileOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const closeMobile = useCallback(() => setMobileOpen(false), []); + const toggleSidebarCollapsed = useCallback( + () => setSidebarCollapsed((value) => !value), + [], + ); const isDocsRoute = pathname === "/docs" || pathname === "/docs/"; const normalizedPath = pathname.replace(/\/$/, "") || "/"; const isChatRoute = normalizedPath === "/chat"; @@ -475,12 +482,13 @@ export default function App() { id="app-sidebar" aria-label={t.app.navigation} className={cn( - "fixed top-0 left-0 z-50 flex h-dvh max-h-dvh w-64 min-h-0 flex-col", + "fixed top-0 left-0 z-50 flex h-dvh max-h-dvh w-64 min-h-0 flex-col overflow-hidden", "border-r border-current/20", "bg-background-base/95 backdrop-blur-sm", - "transition-transform duration-200 ease-out", + "transition-[width,transform] duration-200 ease-out", mobileOpen ? "translate-x-0" : "-translate-x-full", "lg:sticky lg:top-0 lg:translate-x-0 lg:shrink-0", + sidebarCollapsed ? "lg:w-16" : "lg:w-64", )} style={{ background: "var(--component-sidebar-background)", @@ -494,11 +502,14 @@ export default function App() { "border-b border-current/20", )} > -
+
Hermes @@ -507,12 +518,26 @@ export default function App() {
+ + @@ -526,6 +551,7 @@ export default function App() { {sidebarNav.coreItems.map((item) => ( 0 && (
( - +
@@ -579,7 +610,7 @@ export default function App() {
- + @@ -654,7 +685,12 @@ export default function App() { ); } -function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) { +function SidebarNavLink({ + closeMobile, + collapsed, + item, + t, +}: SidebarNavLinkProps) { const { path, label, labelKey, icon: Icon } = item; const navLabel = labelKey @@ -667,6 +703,8 @@ function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) { to={path} end={path === "/sessions"} onClick={closeMobile} + aria-label={navLabel} + title={navLabel} className={({ isActive }) => cn( "group relative flex items-center gap-3", @@ -675,6 +713,7 @@ function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) { "whitespace-nowrap transition-colors cursor-pointer", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground", isActive ? "text-midground" : "opacity-60 hover:opacity-100", + collapsed && "lg:justify-center lg:px-0", ) } style={{ @@ -684,7 +723,9 @@ function SidebarNavLink({ closeMobile, item, t }: SidebarNavLinkProps) { {({ isActive }) => ( <> - {navLabel} + + {navLabel} + void }) { +function SidebarSystemActions({ + onNavigate, + collapsed, +}: { + onNavigate: () => void; + collapsed: boolean; +}) { const { t } = useI18n(); const navigate = useNavigate(); const { activeAction, isBusy, isRunning, pendingAction, runAction } = @@ -747,12 +794,13 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) { className={cn( "px-5 pt-0.5 pb-0.5", "font-mondwest text-[0.6rem] tracking-[0.15em] uppercase opacity-30", + collapsed && "lg:hidden", )} > {t.app.system} - +
    {items.map(({ action, icon: Icon, label, runningLabel, spin }) => { @@ -770,6 +818,8 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) { disabled={disabled} aria-busy={busy} active={busy} + title={displayLabel} + aria-label={displayLabel} className={cn( "gap-3 px-5 py-1.5 whitespace-nowrap", "font-mondwest text-[0.75rem] tracking-[0.1em]", @@ -778,6 +828,7 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) { ? "text-midground opacity-100" : "opacity-60 hover:opacity-100", "disabled:opacity-30", + collapsed && "lg:justify-center lg:px-0 lg:gap-0", )} > {isPending ? ( @@ -793,7 +844,9 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) { /> )} - {displayLabel} + + {displayLabel} + void; + collapsed: boolean; item: NavItem; t: Translations; } diff --git a/web/src/components/SidebarFooter.tsx b/web/src/components/SidebarFooter.tsx index c1810f10e0ed0..2845c89bae2f7 100644 --- a/web/src/components/SidebarFooter.tsx +++ b/web/src/components/SidebarFooter.tsx @@ -3,7 +3,7 @@ import { useSidebarStatus } from "@/hooks/useSidebarStatus"; import { cn } from "@/lib/utils"; import { useI18n } from "@/i18n"; -export function SidebarFooter() { +export function SidebarFooter({ collapsed = false }: { collapsed?: boolean }) { const status = useSidebarStatus(); const { t } = useI18n(); @@ -13,6 +13,7 @@ export function SidebarFooter() { "flex shrink-0 items-center justify-between gap-2", "px-5 py-2.5", "border-t border-current/10", + collapsed && "lg:hidden", )} >