feat(ui): redesign sidebar and fix disclaimer

- Redesign AppSidebar with collapsible groups ("Identity & Access", "System")
- Nest "Shared Data" under "Accounts" in sidebar
- Add collapsible UI component for sidebar nesting
- Fix LocalhostDisclaimer by moving it inside SidebarProvider
- Add roadmap entry for sidebar redesign
This commit is contained in:
kaitranntt
2025-12-07 17:21:24 -05:00
parent cf072c03b2
commit c8890f33c2
7 changed files with 157 additions and 29 deletions
+10
View File
@@ -437,6 +437,16 @@ src/types/
- Deployment pipelines with type checks
- Infrastructure as Code with TypeScript
#### Phase 4: UI Enhancements - Sidebar Redesign (v4.7.0)
**Timeline**: 1-2 months (TBD)
**Priorities**:
1. **Sidebar Redesign Implementation**
- Implement new sidebar component with modern UI/UX principles.
- Ensure responsiveness across different screen sizes.
- Integrate with existing routing and navigation logic.
- Improve accessibility features for the sidebar.
### Technical Debt Resolution
#### Immediate Priorities (v4.4.1)
+3
View File
@@ -6,6 +6,7 @@
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
@@ -189,6 +190,8 @@
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+1
View File
@@ -17,6 +17,7 @@
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
+1 -1
View File
@@ -29,6 +29,7 @@ function Layout() {
</div>
</header>
<Outlet />
<LocalhostDisclaimer />
</main>
</SidebarProvider>
);
@@ -50,7 +51,6 @@ export default function App() {
</Route>
</Routes>
<Toaster position="top-right" />
<LocalhostDisclaimer />
</BrowserRouter>
</QueryClientProvider>
);
+106 -21
View File
@@ -1,51 +1,136 @@
import { Link, useLocation } from 'react-router-dom';
import { Home, Key, Zap, Users, Settings, Activity, FolderOpen } from 'lucide-react';
import { Home, Key, Zap, Users, Settings, Activity, FolderOpen, ChevronRight } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarMenuSubButton,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarFooter,
SidebarTrigger,
SidebarGroupContent,
} from '@/components/ui/sidebar';
import { CcsLogo } from '@/components/ccs-logo';
import { useSidebar } from '@/hooks/use-sidebar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
const navItems = [
{ path: '/', icon: Home, label: 'Home' },
{ path: '/api', icon: Key, label: 'API Profiles' },
{ path: '/cliproxy', icon: Zap, label: 'CLIProxy' },
{ path: '/accounts', icon: Users, label: 'Accounts' },
{ path: '/settings', icon: Settings, label: 'Settings' },
{ path: '/health', icon: Activity, label: 'Health' },
{ path: '/shared', icon: FolderOpen, label: 'Shared Data' },
// Define navigation groups
const navGroups = [
{
title: 'General',
items: [{ path: '/', icon: Home, label: 'Home' }],
},
{
title: 'Identity & Access',
items: [
{ path: '/api', icon: Key, label: 'API Profiles' },
{ path: '/cliproxy', icon: Zap, label: 'CLIProxy' },
{
path: '/accounts',
icon: Users,
label: 'Accounts',
isCollapsible: true,
children: [
{ path: '/accounts', label: 'All Accounts' },
{ path: '/shared', icon: FolderOpen, label: 'Shared Data' },
],
},
],
},
{
title: 'System',
items: [
{ path: '/health', icon: Activity, label: 'Health' },
{ path: '/settings', icon: Settings, label: 'Settings' },
],
},
];
export function AppSidebar() {
const location = useLocation();
const { state } = useSidebar();
// Helper to check if a route is active (including sub-routes if needed)
const isRouteActive = (path: string) => location.pathname === path;
// Helper to check if a group/parent should be open based on active child
const isParentActive = (children: { path: string }[]) => {
return children.some((child) => isRouteActive(child.path));
};
return (
<Sidebar collapsible="icon">
<SidebarHeader className="h-12 flex items-center justify-center">
<CcsLogo size="sm" showText={state === 'expanded'} />
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{navItems.map((item) => (
<SidebarMenuItem key={item.path}>
<SidebarMenuButton asChild isActive={location.pathname === item.path}>
<Link to={item.path}>
<item.icon className="w-4 h-4" />
<span className="group-data-[collapsible=icon]:hidden">{item.label}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
{navGroups.map((group, index) => (
<SidebarGroup key={group.title || index}>
{group.title && <SidebarGroupLabel>{group.title}</SidebarGroupLabel>}
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => (
<SidebarMenuItem key={item.path}>
{item.isCollapsible && item.children ? (
<Collapsible
defaultOpen={isParentActive(item.children) || isRouteActive(item.path)}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.label}>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="group-data-[collapsible=icon]:hidden">
{item.label}
</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children.map((child) => (
<SidebarMenuSubItem key={child.path}>
<SidebarMenuSubButton
asChild
isActive={isRouteActive(child.path)}
>
<Link to={child.path}>
<span>{child.label}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
) : (
<SidebarMenuButton
asChild
isActive={isRouteActive(item.path)}
tooltip={item.label}
>
<Link to={item.path}>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="group-data-[collapsible=icon]:hidden">{item.label}</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<SidebarFooter className="p-4 border-t flex items-center justify-center">
<SidebarTrigger />
</SidebarFooter>
+25 -7
View File
@@ -1,24 +1,42 @@
import { Shield, X } from 'lucide-react';
import { useState } from 'react';
import { useSidebar } from '@/hooks/use-sidebar';
export function LocalhostDisclaimer() {
const [dismissed, setDismissed] = useState(false);
const { state, isMobile } = useSidebar();
if (dismissed) return null;
// Calculate the left margin based on sidebar state
// When expanded: sidebar width is 16rem
// When collapsed: sidebar width is 3rem
// On mobile: sidebar is overlay, no margin needed
const getLeftMargin = () => {
if (isMobile) return '0';
return state === 'expanded' ? '16rem' : '3rem';
};
return (
<div className="fixed bottom-0 left-0 right-0 bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2">
<div className="flex items-center justify-between max-w-7xl mx-auto">
<div
className="fixed bottom-0 bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 transition-all duration-200 ease-linear z-50"
style={{
left: getLeftMargin(),
right: '0',
}}
>
<div className="flex items-center justify-between max-w-7xl">
<div className="flex items-center gap-2 text-sm text-yellow-800 dark:text-yellow-200">
<Shield className="w-4 h-4" />
<span>
This dashboard runs locally. All data stays on your machine. Never expose this server to
the internet.
<Shield className="w-4 h-4 flex-shrink-0" />
<span className="hidden sm:inline">
This dashboard runs locally. All data stays on your machine.
</span>
<span className="sm:hidden">Local dashboard - data stays on your device.</span>
</div>
<button
onClick={() => setDismissed(true)}
className="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400"
className="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 flex-shrink-0 p-1 rounded hover:bg-yellow-100 dark:hover:bg-yellow-800/30 transition-colors"
aria-label="Dismiss disclaimer"
>
<X className="w-4 h-4" />
</button>
+11
View File
@@ -0,0 +1,11 @@
'use client';
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };