Files
ftb-project-management/apps/web/components/layout/Sidebar.tsx
Script Generator 0099b23a86 style: 重做整体视觉系统(编辑设计 × 建筑精度)
- Fraunces 衬线 display + DM Sans body + JetBrains Mono 数字标签
- 暖米白主题(#f7f4ee)+ 蓝色强调色(#2563eb)
- 引入 lucide-react 图标库替代 emoji
- Sidebar 加入编号索引、Logo、用户区域、杂志风分组标题
- 产品列表页加入大号 display 标题、搜索、卡片悬浮动画
- 产品详情页面包屑、tab 编号、需求表格重做
- 状态徽章改为色点 + 文字,"已拒绝"用红色避免与"评审中"冲突
- 全局加入 grain 噪点纹理、入场交错动画

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 12:37:17 +08:00

116 lines
4.4 KiB
TypeScript

'use client';
import { usePathname, useRouter } from 'next/navigation';
import { Inbox, Package, FolderKanban, Tag, Users, Sparkles } from 'lucide-react';
const NAV_ITEMS = [
{ label: '与我相关', path: '/workspace', icon: Inbox, index: '01' },
{ label: '产品', path: '/products', icon: Package, index: '02' },
{ label: '项目', path: '/projects', icon: FolderKanban, index: '03' },
{ label: '版本', path: '/versions', icon: Tag, index: '04' },
];
const ADMIN_ITEMS = [
{ label: '成员管理', path: '/admin/members', icon: Users },
];
export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const isActive = (path: string) =>
pathname === path || (path !== '/' && pathname.startsWith(path));
return (
<aside
className="relative flex h-screen w-64 flex-col border-r border-[var(--line)] bg-[var(--bg-elevated)]"
style={{ background: 'var(--bg-elevated)' }}
>
<div className="flex h-20 items-end px-6 pb-4">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-[var(--ink)]">
<Sparkles className="h-4 w-4 text-[var(--bg)]" strokeWidth={2.5} />
</div>
<div>
<p className="font-display text-base font-semibold leading-none tracking-tight text-[var(--ink)]">
FTB
</p>
<p className="mt-0.5 text-[10px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
Project Studio
</p>
</div>
</div>
</div>
<div className="px-6 pb-3">
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-[var(--ink-muted)]">
Workspace
</p>
</div>
<nav className="flex-1 px-3">
{NAV_ITEMS.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => router.push(item.path)}
className={`group relative flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-all ${
active
? 'bg-[var(--accent-soft)] text-[var(--accent)]'
: 'text-[var(--ink-soft)] hover:bg-[var(--line-soft)] hover:text-[var(--ink)]'
}`}
>
<span className={`font-mono text-[10px] tabular-nums ${active ? 'text-[var(--accent)]' : 'text-[var(--ink-muted)]'}`}>
{item.index}
</span>
<Icon className="h-[18px] w-[18px]" strokeWidth={1.75} />
<span className="text-[14px] font-medium tracking-tight">{item.label}</span>
{active && (
<span className="ml-auto h-1 w-1 rounded-full bg-[var(--accent)]" />
)}
</button>
);
})}
</nav>
<div className="border-t border-[var(--line)] px-3 py-4">
<p className="mb-2 px-3 font-mono text-[10px] uppercase tracking-[0.2em] text-[var(--ink-muted)]">
Admin
</p>
{ADMIN_ITEMS.map((item) => {
const Icon = item.icon;
const active = isActive(item.path);
return (
<button
key={item.path}
onClick={() => router.push(item.path)}
className={`flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition-all ${
active
? 'bg-[var(--accent-soft)] text-[var(--accent)]'
: 'text-[var(--ink-soft)] hover:bg-[var(--line-soft)] hover:text-[var(--ink)]'
}`}
>
<Icon className="h-[18px] w-[18px]" strokeWidth={1.75} />
<span className="text-[14px] font-medium tracking-tight">{item.label}</span>
</button>
);
})}
</div>
<div className="border-t border-[var(--line)] px-6 py-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--ink)] font-display text-xs font-semibold text-[var(--bg)]">
A
</div>
<div className="min-w-0">
<p className="truncate text-[13px] font-medium text-[var(--ink)]">Admin</p>
<p className="truncate text-[11px] text-[var(--ink-muted)]">administrator</p>
</div>
</div>
</div>
</aside>
);
}