- Create Tailwind CSS configuration with design tokens from UIDesignSpec - Create globals.css with CSS variables and component styles - Add React component library: - UI components: Button, Card, Tag, Input, Select, ProgressBar, Modal - Navigation: BottomNav, Sidebar, StatusBar - Layout: MobileLayout, DesktopLayout - Add constants for colors, icons, and layout - Update tasks.md with 31 UI development tasks linked to design node IDs - Configure package.json, tsconfig.json, and postcss.config.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
137 lines
3.1 KiB
TypeScript
137 lines
3.1 KiB
TypeScript
/**
|
|
* Sidebar 侧边栏导航组件 (桌面端)
|
|
* 设计稿参考: UIDesignSpec.md 3.7
|
|
*/
|
|
import React from 'react';
|
|
import { LucideIcon } from 'lucide-react';
|
|
|
|
export interface SidebarItem {
|
|
id: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
href?: string;
|
|
badge?: number;
|
|
children?: SidebarItem[];
|
|
}
|
|
|
|
export interface SidebarSection {
|
|
title?: string;
|
|
items: SidebarItem[];
|
|
}
|
|
|
|
export interface SidebarProps {
|
|
logo?: React.ReactNode;
|
|
sections: SidebarSection[];
|
|
activeId: string;
|
|
onItemClick?: (id: string) => void;
|
|
footer?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({
|
|
logo,
|
|
sections,
|
|
activeId,
|
|
onItemClick,
|
|
footer,
|
|
className = '',
|
|
}) => {
|
|
return (
|
|
<aside
|
|
className={`
|
|
fixed left-0 top-0 bottom-0 z-sidebar
|
|
w-sidebar bg-bg-card
|
|
flex flex-col
|
|
border-r border-border-subtle
|
|
${className}
|
|
`}
|
|
>
|
|
{/* Logo */}
|
|
{logo && (
|
|
<div className="px-4 py-5 border-b border-border-subtle">
|
|
{logo}
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 overflow-y-auto py-4 px-3">
|
|
{sections.map((section, sectionIndex) => (
|
|
<div key={sectionIndex} className="mb-6">
|
|
{section.title && (
|
|
<h4 className="px-3 mb-2 text-small text-text-tertiary uppercase tracking-wider">
|
|
{section.title}
|
|
</h4>
|
|
)}
|
|
<ul className="space-y-1">
|
|
{section.items.map((item) => (
|
|
<SidebarNavItem
|
|
key={item.id}
|
|
item={item}
|
|
isActive={item.id === activeId}
|
|
onClick={() => onItemClick?.(item.id)}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
{footer && (
|
|
<div className="px-4 py-4 border-t border-border-subtle">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
interface SidebarNavItemProps {
|
|
item: SidebarItem;
|
|
isActive: boolean;
|
|
onClick: () => void;
|
|
}
|
|
|
|
const SidebarNavItem: React.FC<SidebarNavItemProps> = ({
|
|
item,
|
|
isActive,
|
|
onClick,
|
|
}) => {
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<li>
|
|
<button
|
|
onClick={onClick}
|
|
className={`
|
|
w-full flex items-center gap-2.5
|
|
px-3 py-2.5 rounded-btn
|
|
transition-colors duration-200
|
|
${isActive
|
|
? 'bg-bg-elevated text-accent-indigo font-semibold'
|
|
: 'text-text-secondary hover:bg-bg-elevated'
|
|
}
|
|
`}
|
|
>
|
|
<Icon size={20} className="flex-shrink-0" />
|
|
<span className="flex-1 text-left text-body">{item.label}</span>
|
|
{item.badge !== undefined && item.badge > 0 && (
|
|
<span
|
|
className="
|
|
min-w-[20px] h-5 px-1.5
|
|
flex items-center justify-center
|
|
bg-accent-coral text-white
|
|
text-[11px] font-semibold
|
|
rounded-full
|
|
"
|
|
>
|
|
{item.badge > 99 ? '99+' : item.badge}
|
|
</span>
|
|
)}
|
|
</button>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
export default Sidebar;
|