| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- /**
- * Sidebar Component
- *
- * Navigation sidebar for the annotation platform.
- * Displays menu items and handles navigation.
- *
- * Requirements: 7.2, 7.5
- */
- import React, { useState } from 'react';
- import { useLocation, Link } from 'react-router-dom';
- import { IconFolder, IconClipboardCheck, IconAnnotation, IconMenu, IconX } from '@humansignal/ui';
- import './sidebar.module.scss';
- /**
- * Menu item interface
- */
- interface MenuItem {
- id: string;
- label: string;
- path: string;
- icon: React.ReactNode;
- }
- /**
- * Menu items configuration
- */
- const menuItems: MenuItem[] = [
- {
- id: 'projects',
- label: '项目管理',
- path: '/projects',
- icon: <IconFolder className="size-5" />,
- },
- {
- id: 'tasks',
- label: '任务管理',
- path: '/tasks',
- icon: <IconClipboardCheck className="size-5" />,
- },
- {
- id: 'annotations',
- label: '我的标注',
- path: '/annotations',
- icon: <IconAnnotation className="size-5" />,
- },
- ];
- export const Sidebar: React.FC = () => {
- const location = useLocation();
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
- /**
- * Check if a menu item is active based on current route
- */
- const isActive = (path: string): boolean => {
- return location.pathname === path || location.pathname.startsWith(path + '/');
- };
- const toggleMobileMenu = () => {
- setIsMobileMenuOpen(!isMobileMenuOpen);
- };
- const closeMobileMenu = () => {
- setIsMobileMenuOpen(false);
- };
- return (
- <>
- {/* Mobile Menu Button */}
- <button
- onClick={toggleMobileMenu}
- className="lg:hidden fixed top-4 left-4 z-50 p-tight bg-primary-background border border-neutral-border rounded-md shadow-md"
- aria-label="Toggle menu"
- >
- {isMobileMenuOpen ? (
- <IconX className="size-6 text-primary-foreground" />
- ) : (
- <IconMenu className="size-6 text-primary-foreground" />
- )}
- </button>
- {/* Mobile Overlay */}
- {isMobileMenuOpen && (
- <div
- className="lg:hidden fixed inset-0 bg-black bg-opacity-50 z-40"
- onClick={closeMobileMenu}
- />
- )}
- {/* Sidebar */}
- <aside
- className={`
- fixed lg:static inset-y-0 left-0 z-40
- w-64 bg-secondary-background border-r border-neutral-border flex flex-col
- transform transition-transform duration-300 ease-in-out
- ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
- `}
- >
- {/* Logo/Title */}
- <Link
- to="/"
- className="p-comfortable border-b border-neutral-border hover:bg-hover transition-colors"
- onClick={closeMobileMenu}
- >
- <h1 className="text-heading-large font-bold text-primary-foreground">
- 标注平台
- </h1>
- <p className="text-body-small text-secondary-foreground mt-tighter">
- Annotation Platform
- </p>
- </Link>
- {/* Navigation Menu */}
- <nav className="flex-1 p-tight overflow-y-auto">
- <ul className="space-y-tight">
- {menuItems.map((item) => {
- const active = isActive(item.path);
-
- return (
- <li key={item.id}>
- <Link
- to={item.path}
- onClick={closeMobileMenu}
- className={`
- block px-comfortable py-cozy rounded-md
- text-body-medium font-medium
- transition-all duration-200
- ${
- active
- ? 'bg-primary text-primary-foreground shadow-sm'
- : 'text-secondary-foreground hover:bg-hover hover:text-primary-foreground'
- }
- `}
- >
- <div className="flex items-center gap-cozy">
- <span className="flex-shrink-0">{item.icon}</span>
- <span>{item.label}</span>
- </div>
- </Link>
- </li>
- );
- })}
- </ul>
- </nav>
- {/* Footer */}
- <div className="p-comfortable border-t border-neutral-border">
- <p className="text-body-small text-secondary-foreground">
- 版本 1.0.0
- </p>
- <p className="text-body-small text-muted-foreground mt-tighter">
- © 2024 标注平台
- </p>
- </div>
- </aside>
- </>
- );
- };
|