mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-24 02:24:27 +00:00
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:
+1
-1
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user