style: 切换为现代 SaaS Dashboard 风格(Linear/Vercel/Notion 风)
- 调色板:zinc 中性色 + 冷静蓝色(#3b82f6)+ 卡片阴影 token - Sidebar:紧凑 60 宽度,加入顶部搜索框,分组导航,圆角 8px - 卡片:rounded-2xl + shadow-sm/md,留白节奏统一 - 表格:圆角卡片包裹,灰色表头小写大写字母 tracking - 表单:h-9 输入框,圆角 8px,focus ring 用半透明蓝 - 按钮分级:实心蓝(主操作)/ 边框白底(次操作)/ 暗黑实心(对比强调) - 状态徽章:ring-1 inset 内边框,色板更柔和 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Inbox, Package, FolderKanban, Tag, Users, Sparkles } from 'lucide-react';
|
||||
import { Inbox, Package, FolderKanban, Tag, Users, LayoutGrid, Search } 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 },
|
||||
const NAV_GROUPS = [
|
||||
{
|
||||
label: '工作区',
|
||||
items: [
|
||||
{ label: '与我相关', path: '/workspace', icon: Inbox },
|
||||
{ label: '产品', path: '/products', icon: Package },
|
||||
{ label: '项目', path: '/projects', icon: FolderKanban },
|
||||
{ label: '版本', path: '/versions', icon: Tag },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '管理',
|
||||
items: [
|
||||
{ label: '成员', path: '/admin/members', icon: Users },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
@@ -22,93 +29,62 @@ export function Sidebar() {
|
||||
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>
|
||||
<aside className="flex h-screen w-60 flex-col border-r border-[var(--line)] bg-[var(--bg-card)]">
|
||||
<div className="flex h-14 items-center gap-2 px-4">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-[var(--accent)]">
|
||||
<LayoutGrid className="h-4 w-4 text-white" strokeWidth={2.25} />
|
||||
</div>
|
||||
<span className="text-[14px] font-semibold tracking-tight text-[var(--ink)]">FTB</span>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pb-2">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--ink-muted)]" strokeWidth={2} />
|
||||
<input
|
||||
placeholder="搜索"
|
||||
className="h-8 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-subtle)] pl-8 pr-2 text-[12.5px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:bg-[var(--bg-card)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]"
|
||||
/>
|
||||
</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 className="flex-1 px-2 pt-2">
|
||||
{NAV_GROUPS.map((group) => (
|
||||
<div key={group.label} className="mb-4">
|
||||
<p className="mb-1 px-2 text-[11px] font-medium text-[var(--ink-muted)]">
|
||||
{group.label}
|
||||
</p>
|
||||
{group.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-2.5 rounded-lg px-2 py-1.5 text-left text-[13px] transition-colors ${
|
||||
active
|
||||
? 'bg-[var(--accent-soft)] font-medium text-[var(--accent-hover)]'
|
||||
: 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" strokeWidth={1.75} />
|
||||
<span className="truncate">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</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)]">
|
||||
<div className="border-t border-[var(--line)] p-2">
|
||||
<button className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-[var(--bg-subtle)]">
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--accent)] text-[11px] font-semibold text-white">
|
||||
A
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[13px] font-medium text-[var(--ink)]">Admin</p>
|
||||
<p className="truncate text-[11px] text-[var(--ink-muted)]">administrator</p>
|
||||
<p className="truncate text-[11px] text-[var(--ink-muted)]">管理员</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user