Sidebar 替换为 shadcn + 系统管理嵌套路由 #36

Closed
opened 2026-08-25 19:16:50 +08:00 by tao.chen · 9 comments
Owner

components/common/Sidebar.tsx(117 行)替换为 components/ui/sidebar.tsx(shadcn 727 行),并将 /system 单路由拆为嵌套路由(/system/users/system/projects),让"系统管理"在 sidebar 内变为带 SidebarMenuSub 的父节点。

Plan: /Users/taochen/.claude/plans/crispy-nibbling-perlis.md

改动文件:

  • frontend/app/routes.ts(system 改 nested)
  • frontend/app/features/admin/SystemAdminRoute.tsx(缩成 <Outlet/> 壳)
  • frontend/app/features/admin/SystemAdminPage.tsx(删除)
  • frontend/app/features/admin/SystemAdminIndexRoute.tsx(新增,<Navigate>)
  • frontend/app/features/admin/UserManagementRoute.tsx(新增 + key-remount)
  • frontend/app/features/admin/ProjectManagementRoute.tsx(新增 + key-remount)
  • frontend/app/components/common/Sidebar.tsx(删除)
  • frontend/app/routes/platform.tsx(shadcn Sidebar + SidebarProvider + guardedNavigate 闭包 + 修 pageFromPath 隐藏 bug)
  • frontend/app/styles/*.css(新增 --sidebar-* CSS 变量,沿用现蓝色板)

关键约束:

  • 编辑会话守卫从 Sidebar 内部上移到 platform.tsxguardedNavigate 闭包
  • shadcn 无禁用 mobile-sheet 的 API,<aside> 外层 md:block hidden 兜底
  • 移动端 sheet 本次不启用,保持桌面端单 rail
  • 图标改 lucide-react(Home/Code/Calendar/Settings/Users/Folder/PanelLeft),brand 仍走 <Icon name="brand" />
  • 主题色用 CSS 变量,保持现有视觉(避免 shadcn 默认黑色版)
  • Topbar / UserManagementPage / ProjectManagementPage / 后端 API / useEditSessionLifecycle 全部零改动

验收:

  • pnpm typecheck
  • pnpm build
  • 手动烟测 11 项见 plan 文件末尾

Task: n99qu3rpnd5aszvcifewefpv

把 `components/common/Sidebar.tsx`(117 行)替换为 `components/ui/sidebar.tsx`(shadcn 727 行),并将 `/system` 单路由拆为嵌套路由(`/system/users`、`/system/projects`),让"系统管理"在 sidebar 内变为带 `SidebarMenuSub` 的父节点。 **Plan**: /Users/taochen/.claude/plans/crispy-nibbling-perlis.md **改动文件**: - `frontend/app/routes.ts`(system 改 nested) - `frontend/app/features/admin/SystemAdminRoute.tsx`(缩成 `<Outlet/>` 壳) - `frontend/app/features/admin/SystemAdminPage.tsx`(删除) - `frontend/app/features/admin/SystemAdminIndexRoute.tsx`(新增,`<Navigate>`) - `frontend/app/features/admin/UserManagementRoute.tsx`(新增 + key-remount) - `frontend/app/features/admin/ProjectManagementRoute.tsx`(新增 + key-remount) - `frontend/app/components/common/Sidebar.tsx`(删除) - `frontend/app/routes/platform.tsx`(shadcn Sidebar + SidebarProvider + guardedNavigate 闭包 + 修 `pageFromPath` 隐藏 bug) - `frontend/app/styles/*.css`(新增 `--sidebar-*` CSS 变量,沿用现蓝色板) **关键约束**: - 编辑会话守卫从 Sidebar 内部上移到 `platform.tsx` 的 `guardedNavigate` 闭包 - shadcn 无禁用 mobile-sheet 的 API,`<aside>` 外层 `md:block hidden` 兜底 - 移动端 sheet 本次不启用,保持桌面端单 rail - 图标改 `lucide-react`(Home/Code/Calendar/Settings/Users/Folder/PanelLeft),brand 仍走 `<Icon name="brand" />` - 主题色用 CSS 变量,保持现有视觉(避免 shadcn 默认黑色版) - Topbar / UserManagementPage / ProjectManagementPage / 后端 API / useEditSessionLifecycle 全部零改动 **验收**: - `pnpm typecheck` 过 - `pnpm build` 过 - 手动烟测 11 项见 plan 文件末尾 --- <sub>Task: n99qu3rpnd5aszvcifewefpv</sub>
Author
Owner
[MODEL-DEVELOP-PLATFORM-62](https://kaneo.maimaicuizhiji.top/dashboard/workspace/UVjqrB68D9jAIx4sh6ucRfsPbbUCj97c/project/hna942mjy8cwyv1o77mw0cnv/task/n0uki0yzvw9l6xusuchuth0r)
tao.chen added the priority:mediumstatus:in-progress labels 2026-08-25 19:16:56 +08:00
Author
Owner

tao.chen commented:

Dispatched to Codex (background task k8crbbc32).

Plan: /Users/taochen/.claude/plans/crispy-nibbling-perlis.md

Scope (modify only these):

  • frontend/app/routes.ts — system nested
  • frontend/app/features/admin/SystemAdminRoute.tsx — Outlet shell
  • frontend/app/features/admin/SystemAdminPage.tsx — DELETE
  • frontend/app/features/admin/SystemAdminIndexRoute.tsx — NEW
  • frontend/app/features/admin/UserManagementRoute.tsx — NEW (key-remount)
  • frontend/app/features/admin/ProjectManagementRoute.tsx — NEW (key-remount)
  • frontend/app/components/common/Sidebar.tsx — DELETE
  • frontend/app/routes/platform.tsx — shadcn + guardedNavigate + pageFromPath fix
  • CSS variables (append to existing global stylesheet, no new file)

Awaiting Codex completion.

**tao.chen** commented: > Dispatched to Codex (background task k8crbbc32). Plan: `/Users/taochen/.claude/plans/crispy-nibbling-perlis.md` Scope (modify only these): - `frontend/app/routes.ts` — system nested - `frontend/app/features/admin/SystemAdminRoute.tsx` — Outlet shell - `frontend/app/features/admin/SystemAdminPage.tsx` — DELETE - `frontend/app/features/admin/SystemAdminIndexRoute.tsx` — NEW - `frontend/app/features/admin/UserManagementRoute.tsx` — NEW (key-remount) - `frontend/app/features/admin/ProjectManagementRoute.tsx` — NEW (key-remount) - `frontend/app/components/common/Sidebar.tsx` — DELETE - `frontend/app/routes/platform.tsx` — shadcn + guardedNavigate + pageFromPath fix - CSS variables (append to existing global stylesheet, no new file) Awaiting Codex completion.
Author
Owner

tao.chen commented:

Implementation complete + validated.

Validation:

  • pnpm typecheck — passed (EXIT=0)
  • pnpm build — passed (EXIT=0; client + ssr bundles built; SystemAdminIndexRoute/UserManagementRoute/ProjectManagementRoute chunks emitted)
  • Git diff scope: 9 files exactly, no scope leak (vite.config.ts M is pre-existing dev proxy target, unrelated)

Build outputs of interest:

  • platform-*.js 128.69 kB (main layout with shadcn Sidebar)
  • SystemAdminIndexRoute-*.js 0.15 kB (Navigate redirect)
  • UserManagementRoute-*.js 9.12 kB / ProjectManagementRoute-*.js 17.68 kB
  • Icon-*.js retained (brand mark)

Codex-resolved ambiguities (all good calls):

  • --sidebar-background: #09233f (dark navy) instead of plan's #ffffff — preserves existing visual; white would have unreadable light-on-light text
  • SidebarProvider open={!sidebarCollapsed} with inverted onOpenChange — fixes the literal bug in my prompt (was open={sidebarCollapsed} which would render collapsed on first mount)
  • data-active="true" : undefined (Tailwind attribute presence, not boolean) — matches repo convention
  • Added @theme inline { --color-sidebar-* } mapping in app.css — required for shadcn's bg-sidebar/etc. utilities to resolve CSS vars in Tailwind v4

Compatibility note:

  • <SidebarTrigger> overrides its children with shadcn's built-in PanelLeftIcon + sr-only label; the <span>收起菜单</span> text I included won't show. Trigger still works for collapse/expand. Acceptable shadcn standard behavior; original common/Sidebar.tsx showed the text when expanded — minor visible regression, not a functional one.

Manual smoke test still required in browser (per plan §Verification):
1-11 covering workbench/scripts/system navigation, edit-session guard, workspace switch key-remount, isSystemAdmin gating, mobile hide, etc.

**tao.chen** commented: > Implementation complete + validated. **Validation:** - `pnpm typecheck` — passed (EXIT=0) - `pnpm build` — passed (EXIT=0; client + ssr bundles built; SystemAdminIndexRoute/UserManagementRoute/ProjectManagementRoute chunks emitted) - Git diff scope: 9 files exactly, no scope leak (vite.config.ts M is pre-existing dev proxy target, unrelated) **Build outputs of interest:** - `platform-*.js` 128.69 kB (main layout with shadcn Sidebar) - `SystemAdminIndexRoute-*.js` 0.15 kB (Navigate redirect) - `UserManagementRoute-*.js` 9.12 kB / `ProjectManagementRoute-*.js` 17.68 kB - `Icon-*.js` retained (brand mark) **Codex-resolved ambiguities (all good calls):** - `--sidebar-background: #09233f` (dark navy) instead of plan's `#ffffff` — preserves existing visual; white would have unreadable light-on-light text - `SidebarProvider open={!sidebarCollapsed}` with inverted `onOpenChange` — fixes the literal bug in my prompt (was `open={sidebarCollapsed}` which would render collapsed on first mount) - `data-active="true" : undefined` (Tailwind attribute presence, not boolean) — matches repo convention - Added `@theme inline { --color-sidebar-* }` mapping in app.css — required for shadcn's `bg-sidebar`/etc. utilities to resolve CSS vars in Tailwind v4 **Compatibility note:** - `<SidebarTrigger>` overrides its children with shadcn's built-in `PanelLeftIcon` + sr-only label; the `<span>收起菜单</span>` text I included won't show. Trigger still works for collapse/expand. Acceptable shadcn standard behavior; original `common/Sidebar.tsx` showed the text when expanded — minor visible regression, not a functional one. **Manual smoke test** still required in browser (per plan §Verification): 1-11 covering workbench/scripts/system navigation, edit-session guard, workspace switch key-remount, isSystemAdmin gating, mobile hide, etc.
tao.chen added status:done and removed status:in-progress labels 2026-08-25 19:31:55 +08:00
Author
Owner

tao.chen commented:

Follow-up fix: Main area 不占满空间。

Root cause: shadcn Sidebar 桌面分支的内部 sidebar-container 用 position: fixed; inset-y-0(绝对定位,出流),真正占布局空间的是同级的 data-slot="sidebar-gap" div(w-(--sidebar-width))。原本的 <div className="flex min-h-screen bg-[#eef2f6]"> + <aside className="hidden md:block"> 双层包装与 SidebarProvider 内置的 flex min-h-svh w-full 叠加,把 gap div 隔离在 aside 内,SidebarInset 拿不到 gap 推动,布局错位。

Fix: 删掉外层 flex div 和 aside wrapper,<Sidebar><SidebarInset> 直接做 SidebarProvider 子节点;bg-[#eef2f6] 移到 SidebarProvider 的 className 上。<Toast> 作为 SidebarProvider 末位 sibling 保留。

验证:

  • pnpm typecheck — EXIT=0
  • pnpm build — EXIT=0
  • 改动 1 文件,纯结构简化,无业务逻辑变更。
**tao.chen** commented: > Follow-up fix: Main area 不占满空间。 **Root cause**: shadcn Sidebar 桌面分支的内部 sidebar-container 用 `position: fixed; inset-y-0`(绝对定位,出流),真正占布局空间的是同级的 `data-slot="sidebar-gap"` div(`w-(--sidebar-width)`)。原本的 `<div className="flex min-h-screen bg-[#eef2f6]">` + `<aside className="hidden md:block">` 双层包装与 SidebarProvider 内置的 `flex min-h-svh w-full` 叠加,把 gap div 隔离在 aside 内,SidebarInset 拿不到 gap 推动,布局错位。 **Fix**: 删掉外层 flex div 和 aside wrapper,`<Sidebar>` 和 `<SidebarInset>` 直接做 `SidebarProvider` 子节点;`bg-[#eef2f6]` 移到 SidebarProvider 的 className 上。`<Toast>` 作为 SidebarProvider 末位 sibling 保留。 **验证**: - `pnpm typecheck` — EXIT=0 - `pnpm build` — EXIT=0 - 改动 1 文件,纯结构简化,无业务逻辑变更。
Author
Owner

tao.chen commented:

Follow-up: Sidebar 系统管理加可折叠。

方案: 用 base-ui 的 Collapsible(从 @base-ui/react/collapsible,base-ui 1.7.0 已装)包住父 SidebarMenuItem

  • Collapsible.Root defaultOpen={activePage === "system"} — 首屏按 URL 决定展开/折叠
  • Collapsible.Trigger render={<SidebarMenuButton ... />} — base-ui 的 render prop 等价 radix asChild,把触发器行为合并到菜单按钮
  • Collapsible.Panel 包住 SidebarMenuSub
  • ChevronRight 用 group-data-[open]/collapsible:rotate-90(Collapsible.Rootgroup/collapsible,展开时 data-open 自动 rotate)

UX:

  • /system/users/system/projects → 子菜单默认展开
  • 离开 /system/* → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改
  • 点父按钮 → 仅切换折叠,不导航
  • 点子项 → 导航到对应 URL
  • 折叠时父按钮的 active 高亮保留(基于 activePage === "system")

依赖: @base-ui/react@^1.7.0 已存在,无需新增包。

验证:

  • pnpm typecheck — EXIT=0
  • pnpm build — EXIT=0
**tao.chen** commented: > Follow-up: Sidebar 系统管理加可折叠。 **方案**: 用 base-ui 的 `Collapsible`(从 `@base-ui/react/collapsible`,base-ui 1.7.0 已装)包住父 `SidebarMenuItem`。 - `Collapsible.Root defaultOpen={activePage === "system"}` — 首屏按 URL 决定展开/折叠 - `Collapsible.Trigger render={<SidebarMenuButton ... />}` — base-ui 的 `render` prop 等价 radix `asChild`,把触发器行为合并到菜单按钮 - `Collapsible.Panel` 包住 `SidebarMenuSub` - ChevronRight 用 `group-data-[open]/collapsible:rotate-90`(`Collapsible.Root` 设 `group/collapsible`,展开时 `data-open` 自动 rotate) **UX**: - 在 `/system/users` 或 `/system/projects` → 子菜单默认展开 - 离开 `/system/*` → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改 - 点父按钮 → 仅切换折叠,不导航 - 点子项 → 导航到对应 URL - 折叠时父按钮的 active 高亮保留(基于 `activePage === "system"`) **依赖**: `@base-ui/react@^1.7.0` 已存在,无需新增包。 **验证**: - `pnpm typecheck` — EXIT=0 - `pnpm build` — EXIT=0
Author
Owner

tao.chen commented:

Follow-up: Sidebar 系统管理加可折叠。

方案: 用 base-ui 的 Collapsible(从 @base-ui/react/collapsible,base-ui 1.7.0 已装)包住父 SidebarMenuItem

  • Collapsible.Root defaultOpen={activePage === "system"} — 首屏按 URL 决定展开/折叠
  • Collapsible.Trigger render={<SidebarMenuButton ... />} — base-ui 的 render prop 等价 radix asChild,把触发器行为合并到菜单按钮
  • Collapsible.Panel 包住 SidebarMenuSub
  • ChevronRight 用 group-data-[open]/collapsible:rotate-90(Collapsible.Rootgroup/collapsible,展开时 data-open 自动 rotate)

UX:

  • /system/users/system/projects → 子菜单默认展开
  • 离开 /system/* → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改
  • 点父按钮 → 仅切换折叠,不导航
  • 点子项 → 导航到对应 URL
  • 折叠时父按钮的 active 高亮保留(基于 activePage === "system")

依赖: @base-ui/react@^1.7.0 已存在,无需新增包。

验证:

  • pnpm typecheck — EXIT=0
  • pnpm build — EXIT=0
**tao.chen** commented: > Follow-up: Sidebar 系统管理加可折叠。 **方案**: 用 base-ui 的 `Collapsible`(从 `@base-ui/react/collapsible`,base-ui 1.7.0 已装)包住父 `SidebarMenuItem`。 - `Collapsible.Root defaultOpen={activePage === "system"}` — 首屏按 URL 决定展开/折叠 - `Collapsible.Trigger render={<SidebarMenuButton ... />}` — base-ui 的 `render` prop 等价 radix `asChild`,把触发器行为合并到菜单按钮 - `Collapsible.Panel` 包住 `SidebarMenuSub` - ChevronRight 用 `group-data-[open]/collapsible:rotate-90`(`Collapsible.Root` 设 `group/collapsible`,展开时 `data-open` 自动 rotate) **UX**: - 在 `/system/users` 或 `/system/projects` → 子菜单默认展开 - 离开 `/system/*` → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改 - 点父按钮 → 仅切换折叠,不导航 - 点子项 → 导航到对应 URL - 折叠时父按钮的 active 高亮保留(基于 `activePage === "system"`) **依赖**: `@base-ui/react@^1.7.0` 已存在,无需新增包。 **验证**: - `pnpm typecheck` — EXIT=0 - `pnpm build` — EXIT=0
Author
Owner

tao.chen commented:

Follow-up: Sidebar 系统管理加可折叠。

方案: 用 base-ui 的 Collapsible(从 @base-ui/react/collapsible,base-ui 1.7.0 已装)包住父 SidebarMenuItem

  • Collapsible.Root defaultOpen={activePage === "system"} — 首屏按 URL 决定展开/折叠
  • Collapsible.Trigger render={<SidebarMenuButton ... />} — base-ui 的 render prop 等价 radix asChild,把触发器行为合并到菜单按钮
  • Collapsible.Panel 包住 SidebarMenuSub
  • ChevronRight 用 group-data-[open]/collapsible:rotate-90(Collapsible.Rootgroup/collapsible,展开时 data-open 自动 rotate)

UX:

  • /system/users/system/projects → 子菜单默认展开
  • 离开 /system/* → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改
  • 点父按钮 → 仅切换折叠,不导航
  • 点子项 → 导航到对应 URL
  • 折叠时父按钮的 active 高亮保留(基于 activePage === "system")

依赖: @base-ui/react@^1.7.0 已存在,无需新增包。

验证:

  • pnpm typecheck — EXIT=0
  • pnpm build — EXIT=0
**tao.chen** commented: > Follow-up: Sidebar 系统管理加可折叠。 **方案**: 用 base-ui 的 `Collapsible`(从 `@base-ui/react/collapsible`,base-ui 1.7.0 已装)包住父 `SidebarMenuItem`。 - `Collapsible.Root defaultOpen={activePage === "system"}` — 首屏按 URL 决定展开/折叠 - `Collapsible.Trigger render={<SidebarMenuButton ... />}` — base-ui 的 `render` prop 等价 radix `asChild`,把触发器行为合并到菜单按钮 - `Collapsible.Panel` 包住 `SidebarMenuSub` - ChevronRight 用 `group-data-[open]/collapsible:rotate-90`(`Collapsible.Root` 设 `group/collapsible`,展开时 `data-open` 自动 rotate) **UX**: - 在 `/system/users` 或 `/system/projects` → 子菜单默认展开 - 离开 `/system/*` → 子菜单保持上次状态(uncontrolled),不在 URL 变化时强改 - 点父按钮 → 仅切换折叠,不导航 - 点子项 → 导航到对应 URL - 折叠时父按钮的 active 高亮保留(基于 `activePage === "system"`) **依赖**: `@base-ui/react@^1.7.0` 已存在,无需新增包。 **验证**: - `pnpm typecheck` — EXIT=0 - `pnpm build` — EXIT=0
Author
Owner

tao.chen commented:

Follow-up: 调整 sidebar 间距。

改动:frontend/app/components/ui/sidebar.tsx shadcn 默认值

  • SidebarMenu: gap-0.5gap-1.5(顶层项之间从 2px 拉到 6px)
  • sidebarMenuButtonVariants base: py-2py-3(按钮垂直内边距从 8px 增到 12px,实际高度从 ~40px 升到 ~48px,贴近原 h-12)

验证:

  • pnpm typecheck — EXIT=0
  • pnpm build — EXIT=0

子按钮高度未调整(用户未要求)。如果也要跟着调,告诉我。

**tao.chen** commented: > Follow-up: 调整 sidebar 间距。 **改动**:`frontend/app/components/ui/sidebar.tsx` shadcn 默认值 - `SidebarMenu`: `gap-0.5` → `gap-1.5`(顶层项之间从 2px 拉到 6px) - `sidebarMenuButtonVariants` base: `py-2` → `py-3`(按钮垂直内边距从 8px 增到 12px,实际高度从 ~40px 升到 ~48px,贴近原 `h-12`) **验证**: - `pnpm typecheck` — EXIT=0 - `pnpm build` — EXIT=0 子按钮高度未调整(用户未要求)。如果也要跟着调,告诉我。
Author
Owner

tao.chen commented:

2026-08-25 收尾:回滚 ui/* 组件样式改动

迁移交付后,在 sidebar 间距调优阶段(顶层 SidebarMenu gap 0.5 → 1.5, sidebarMenuButtonVariants base py-2py-3)触及了 frontend/app/components/ui/sidebar.tsx

用户指示回滚这两处改动 + 立规:今后禁止修改 components/ui/* 下任何组件样式

已回滚

  • components/ui/sidebar.tsx SidebarMenu 类名:gap-1.5gap-0.5
  • components/ui/sidebar.tsx sidebarMenuButtonVariants base:py-3py-2

验证

  • pnpm typecheck 干净通过
  • pnpm build 干净通过(client + ssr bundle 全部生成)

新约束(已存 memory: never-modify-ui-components.md)

components/ui/* 是 shadcn base-ui 风格的原子原语,被多处复用。改一处污染所有消费方,且会让后续 shadcn 升级/diff-copy 产生合并冲突。

未来任何视觉调整(间距、字号、颜色、动效)必须通过 className 在调用方覆盖,例如:

<SidebarMenu className="gap-1.5">
  <SidebarMenuButton className="py-3">...</SidebarMenuButton>
</SidebarMenu>

或在调用方引入本地 css(如 routes/platform.css)集中 @apply。调整前先问用户"是单点覆盖还是全局默认?"。

**tao.chen** commented: > ## 2026-08-25 收尾:回滚 ui/* 组件样式改动 迁移交付后,在 sidebar 间距调优阶段(顶层 `SidebarMenu` gap 0.5 → 1.5, `sidebarMenuButtonVariants` base `py-2` → `py-3`)触及了 `frontend/app/components/ui/sidebar.tsx`。 用户指示**回滚**这两处改动 + 立规:**今后禁止修改 `components/ui/*` 下任何组件样式**。 ### 已回滚 - `components/ui/sidebar.tsx` `SidebarMenu` 类名:`gap-1.5` → `gap-0.5` - `components/ui/sidebar.tsx` `sidebarMenuButtonVariants` base:`py-3` → `py-2` ### 验证 - `pnpm typecheck` 干净通过 - `pnpm build` 干净通过(client + ssr bundle 全部生成) ### 新约束(已存 memory: `never-modify-ui-components.md`) `components/ui/*` 是 shadcn base-ui 风格的原子原语,被多处复用。改一处污染所有消费方,且会让后续 shadcn 升级/diff-copy 产生合并冲突。 **未来任何视觉调整**(间距、字号、颜色、动效)必须通过 `className` 在调用方覆盖,例如: ```tsx <SidebarMenu className="gap-1.5"> <SidebarMenuButton className="py-3">...</SidebarMenuButton> </SidebarMenu> ``` 或在调用方引入本地 css(如 `routes/platform.css`)集中 `@apply`。调整前先问用户"是单点覆盖还是全局默认?"。
Sign in to join this conversation.