sidebar.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /**
  2. * Sidebar Component
  3. *
  4. * Navigation sidebar for the annotation platform.
  5. * Displays menu items and handles navigation.
  6. *
  7. * Requirements: 7.2, 7.5
  8. */
  9. import React, { useState } from 'react';
  10. import { useLocation, Link } from 'react-router-dom';
  11. import { IconFolder, IconClipboardCheck, IconAnnotation, IconMenu, IconX } from '@humansignal/ui';
  12. import './sidebar.module.scss';
  13. /**
  14. * Menu item interface
  15. */
  16. interface MenuItem {
  17. id: string;
  18. label: string;
  19. path: string;
  20. icon: React.ReactNode;
  21. }
  22. /**
  23. * Menu items configuration
  24. */
  25. const menuItems: MenuItem[] = [
  26. {
  27. id: 'projects',
  28. label: '项目管理',
  29. path: '/projects',
  30. icon: <IconFolder className="size-5" />,
  31. },
  32. {
  33. id: 'tasks',
  34. label: '任务管理',
  35. path: '/tasks',
  36. icon: <IconClipboardCheck className="size-5" />,
  37. },
  38. {
  39. id: 'annotations',
  40. label: '我的标注',
  41. path: '/annotations',
  42. icon: <IconAnnotation className="size-5" />,
  43. },
  44. ];
  45. export const Sidebar: React.FC = () => {
  46. const location = useLocation();
  47. const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
  48. /**
  49. * Check if a menu item is active based on current route
  50. */
  51. const isActive = (path: string): boolean => {
  52. return location.pathname === path || location.pathname.startsWith(path + '/');
  53. };
  54. const toggleMobileMenu = () => {
  55. setIsMobileMenuOpen(!isMobileMenuOpen);
  56. };
  57. const closeMobileMenu = () => {
  58. setIsMobileMenuOpen(false);
  59. };
  60. return (
  61. <>
  62. {/* Mobile Menu Button */}
  63. <button
  64. onClick={toggleMobileMenu}
  65. className="lg:hidden fixed top-4 left-4 z-50 p-tight bg-primary-background border border-neutral-border rounded-md shadow-md"
  66. aria-label="Toggle menu"
  67. >
  68. {isMobileMenuOpen ? (
  69. <IconX className="size-6 text-primary-foreground" />
  70. ) : (
  71. <IconMenu className="size-6 text-primary-foreground" />
  72. )}
  73. </button>
  74. {/* Mobile Overlay */}
  75. {isMobileMenuOpen && (
  76. <div
  77. className="lg:hidden fixed inset-0 bg-black bg-opacity-50 z-40"
  78. onClick={closeMobileMenu}
  79. />
  80. )}
  81. {/* Sidebar */}
  82. <aside
  83. className={`
  84. fixed lg:static inset-y-0 left-0 z-40
  85. w-64 bg-secondary-background border-r border-neutral-border flex flex-col
  86. transform transition-transform duration-300 ease-in-out
  87. ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
  88. `}
  89. >
  90. {/* Logo/Title */}
  91. <Link
  92. to="/"
  93. className="p-comfortable border-b border-neutral-border hover:bg-hover transition-colors"
  94. onClick={closeMobileMenu}
  95. >
  96. <h1 className="text-heading-large font-bold text-primary-foreground">
  97. 标注平台
  98. </h1>
  99. <p className="text-body-small text-secondary-foreground mt-tighter">
  100. Annotation Platform
  101. </p>
  102. </Link>
  103. {/* Navigation Menu */}
  104. <nav className="flex-1 p-tight overflow-y-auto">
  105. <ul className="space-y-tight">
  106. {menuItems.map((item) => {
  107. const active = isActive(item.path);
  108. return (
  109. <li key={item.id}>
  110. <Link
  111. to={item.path}
  112. onClick={closeMobileMenu}
  113. className={`
  114. block px-comfortable py-cozy rounded-md
  115. text-body-medium font-medium
  116. transition-all duration-200
  117. ${
  118. active
  119. ? 'bg-primary text-primary-foreground shadow-sm'
  120. : 'text-secondary-foreground hover:bg-hover hover:text-primary-foreground'
  121. }
  122. `}
  123. >
  124. <div className="flex items-center gap-cozy">
  125. <span className="flex-shrink-0">{item.icon}</span>
  126. <span>{item.label}</span>
  127. </div>
  128. </Link>
  129. </li>
  130. );
  131. })}
  132. </ul>
  133. </nav>
  134. {/* Footer */}
  135. <div className="p-comfortable border-t border-neutral-border">
  136. <p className="text-body-small text-secondary-foreground">
  137. 版本 1.0.0
  138. </p>
  139. <p className="text-body-small text-muted-foreground mt-tighter">
  140. © 2024 标注平台
  141. </p>
  142. </div>
  143. </aside>
  144. </>
  145. );
  146. };