Skip to content

Commit 4370ae9

Browse files
committed
refactor: consolidate Radix UI components under a unified radix-ui package, update z-index management with CSS variables, and remove deprecated imports
1 parent 4cd19c1 commit 4370ae9

37 files changed

Lines changed: 930 additions & 138 deletions

File tree

marchen/.search/index.sqlite

100 KB
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: refactor-ui-foundations
2+
schema: full
3+
createdAt: '2026-05-08T12:19:59.662Z'
4+
status: archived
5+
archivedAt: '2026-05-08T13:47:03.572Z'
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
## 背景
2+
3+
项目使用 shadcn/ui 的手动复制模式(无 `components.json`),依赖 17 个独立的 `@radix-ui/react-*` 包。同时项目有一个自定义的 ModalStack 系统(z-index 从 100 开始动态递增),和 shadcn 默认的 `z-50` 层级冲突,导致最近实现 AI Settings Tab 时出现 Provider Dialog 被 ModalStack 遮挡的问题。目前的"修复"是在 ProviderDialog 里硬编码 `z-[200]`,是临时方案。
4+
5+
当前 Radix import 模式:
6+
7+
```typescript
8+
import * as DialogPrimitive from '@radix-ui/react-dialog'
9+
const Dialog = DialogPrimitive.Root // 本地重命名
10+
```
11+
12+
当前 z-index 分布:
13+
- ModalStack: 100 + index
14+
- Dialog overlay/content: z-50(默认)
15+
- Popover: z-50 → 临时改到 z-[250]
16+
- Select: z-[150] → 临时改到 z-[250]
17+
- Tooltip: z-50
18+
19+
## 目标与非目标
20+
21+
**目标:**
22+
- 将 17 个 `@radix-ui/react-*` 包合并为 1 个 `radix-ui` 统一包
23+
- 建立集中管理的 z-index 层级体系
24+
- 所有浮层组件迁移到新 z-index 体系
25+
- 保持组件 API、行为、视觉完全不变
26+
27+
**非目标:**
28+
- 不升级组件功能或改 API(纯基础设施重构)
29+
- 不改目录结构(保持 `ui/dialog/Dialog.tsx` 的目录组织方式)
30+
- 不替换 toast 组件(sonner 迁移另做)
31+
- 不改 shadcn 组件的样式和交互逻辑
32+
33+
## 决策
34+
35+
### 1. Radix 统一包的 import 策略
36+
37+
采用 **as 别名** 方式,最小改动:
38+
39+
```typescript
40+
// 改前
41+
import * as DialogPrimitive from '@radix-ui/react-dialog'
42+
43+
// 改后
44+
import { Dialog as DialogPrimitive } from 'radix-ui'
45+
```
46+
47+
**理由:**
48+
- 不动组件内部代码(`DialogPrimitive.Root` 等引用全部保留)
49+
- 唯一改动是 import 行,diff 最小
50+
- 避免命名冲突(组件已有 `const Dialog = DialogPrimitive.Root`
51+
52+
**替代方案(不选):** 直接用 `import { Dialog } from 'radix-ui'`。这样会和组件内 `const Dialog = ...` 重命名冲突,需要大幅改动组件内部。
53+
54+
### 2. z-index 双份定义(TS 常量 + CSS 变量)
55+
56+
```typescript
57+
// src/renderer/src/lib/constants/z-index.ts
58+
export const Z_INDEX = {
59+
modalStack: 100,
60+
dialog: 200,
61+
popover: 250,
62+
tooltip: 280,
63+
toast: 300,
64+
} as const
65+
```
66+
67+
```css
68+
/* src/renderer/src/styles/shadcn.css */
69+
:root {
70+
--z-modal-stack: 100;
71+
--z-dialog: 200;
72+
--z-popover: 250;
73+
--z-tooltip: 280;
74+
--z-toast: 300;
75+
}
76+
```
77+
78+
**理由:**
79+
- 纯 CSS 组件(Dialog、Popover)用 `z-[var(--z-xxx)]` 语法引用 CSS 变量
80+
- 需要动态计算的地方(ModalStack 的 `100 + index`)用 TS 常量
81+
- 两份定义通过代码注释互相标注,虽有重复但实际场景最灵活
82+
83+
**替代方案(不选):**
84+
- **只用 TS 常量**:inline style 会让 Tailwind 体系不统一,debug 视觉层级不方便
85+
- **只用 CSS 变量**:ModalStack 的动态计算没法纯 CSS 实现
86+
87+
### 3. 层级规划
88+
89+
```
90+
层级 值 用途
91+
─────────────────────────
92+
base 0 基础内容
93+
sticky 20 粘性元素
94+
dropdown 30 普通下拉
95+
header 40 固定顶栏
96+
overlay 50 一般遮罩
97+
modalStack 100+ ModalStack 层(实际值 = 100 + stackIndex)
98+
dialog 200 shadcn Dialog(覆盖 ModalStack)
99+
popover 250 Popover / Select(在 Dialog 内部)
100+
tooltip 280 Tooltip(在 Popover 之上)
101+
toast 300 Toast(永远最上层)
102+
```
103+
104+
**理由:** 每个层级之间留出 50 以上空间,应对未来扩展。ModalStack 的 100-199 区间足够支持近 100 层堆叠。
105+
106+
### 4. 分阶段实施(单 PR,多 commit)
107+
108+
在一个 change / 一个 PR 内分两个 commit:
109+
1. `refactor: migrate to unified radix-ui package` — 只改 import
110+
2. `refactor: unify z-index system with CSS variables` — 建立 z-index 体系并清理硬编码
111+
112+
**理由:** 两个改动虽然合并到一个 PR,但职责分离。出问题时可以 git bisect 到具体 commit。
113+
114+
### 5. 验证策略
115+
116+
- TypeScript typecheck 全程必须通过
117+
- 启动 dev server(Web + Electron)手动验证关键场景:
118+
- 设置弹窗(ModalStack)打开
119+
- AI Settings 里添加 Provider(Dialog in ModalStack)
120+
- 类型选择下拉(Select in Dialog)
121+
- 模型搜索 Popover(Popover in Dialog)
122+
- 各 Toast 提示
123+
124+
## 风险与权衡
125+
126+
| 风险 | 影响 | 缓解 |
127+
|------|------|------|
128+
| Radix 统一包某些子组件导出名与预期不符 | 编译错误 | 先改 1-2 个组件做 spike,确认 API 后批量改 |
129+
| 业务代码 (`modules/`) 直接引用 Radix,容易漏改 | 运行时错误 | grep 全局搜索 `@radix-ui/react-` 确保全部迁移 |
130+
| Tailwind `z-[var(--z-dialog)]` 在 Tailwind v4 下失效 | z-index 错误 | 先 spike 验证语法支持,必要时降级为 inline style |
131+
| TypeScript 类型签名微妙变化 | 类型错误 | typecheck 全程通过;出错时对应调整类型声明 |
132+
| ModalStack 的 `100 + index` 和 Dialog 的 200 冲突 | 第 100 层后的 ModalStack 会超过 Dialog 层级 | 实际不会有 100 层嵌套;文档明确 ModalStack 限制 |
133+
| 迁移过程中 PR 巨大,review 困难 | review 质量下降 | 分两个清晰的 commit;改动 99% 是机械替换 |
134+
| 已有临时硬编码(如 ProviderDialog 的 z-[200])清理不干净 | 特殊场景失效 | 迁移最后一步全局搜索 `z-\[[0-9]` 确认无残留 |
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## 动机
2+
3+
项目的 shadcn UI 组件随着时间推移积累了一些基础性问题:
4+
5+
1. **依赖臃肿**:17 个独立的 `@radix-ui/react-*` 包,package.json 冗长,升级维护不便
6+
2. **z-index 体系混乱**:组件的 z-index 值散落各处(z-50、z-150、z-200、z-250),没有统一规则。最近为了解决 Provider Dialog 被 ModalStack 遮挡问题,临时在组件层硬编码提高 z-index,缺乏全局规划
7+
3. **ModalStack 和 shadcn 层级冲突**:自定义 ModalStack(z-index 100+)和 shadcn 原生组件(z-50)层级不匹配,导致 Dialog 嵌套场景下频繁出现遮挡问题
8+
9+
现在做这个重构的时机合适:AI Settings Tab 刚上线暴露了层级问题,趁记忆还新,建立统一规范。
10+
11+
## 变更内容
12+
13+
- 迁移所有 `@radix-ui/react-*` 到统一的 `radix-ui`
14+
- 建立全局 z-index 体系(CSS 变量 + TS 常量双份同步)
15+
- 更新所有浮层组件(Dialog、Popover、Select、Tooltip、ModalStack)使用统一层级
16+
- 清理之前为应急加的硬编码 z-index(如 ProviderDialog 里的 `z-[200]`
17+
18+
不涉及:
19+
- 不升级组件功能(API 保持不变)
20+
- 不改目录结构(保持 `ui/dialog/Dialog.tsx` 目录式组织)
21+
- 不替换 toast 组件(后续独立变更)
22+
23+
## 能力
24+
25+
### 新增能力
26+
27+
- `radix-ui-unified`:使用统一的 `radix-ui` 包替代分散的独立包,保持组件行为不变
28+
- `z-index-system`:建立全局 z-index 层级规范,所有浮层组件遵循统一规则
29+
30+
### 修改能力
31+
32+
- 无(只是基础设施重构,不改变用户可见行为)
33+
34+
## 影响范围
35+
36+
**依赖变化:**
37+
- 移除 17 个 `@radix-ui/react-*`
38+
- 新增 `radix-ui` 统一包
39+
40+
**代码改动:**
41+
- `src/renderer/src/components/ui/` 下所有使用 Radix 的组件(17 个文件)import 语句重写
42+
- `src/renderer/src/components/modules/` 下少量直接引用 Radix 的业务文件(如 `DanmakuSource.tsx``SettingSlider.tsx`
43+
- 新建 `src/renderer/src/lib/constants/z-index.ts`
44+
- 修改 `src/renderer/src/styles/shadcn.css` 添加 z-index CSS 变量
45+
- 更新 Dialog、Popover、Select、Tooltip、ModalStack 组件使用新 z-index 体系
46+
- 清理 `components/modules/settings/views/ai/ProviderDialog.tsx` 的临时 z-index 硬编码
47+
48+
**不影响:**
49+
- 组件 API 和用法
50+
- 已有的视觉和交互行为
51+
- 非浮层组件(Input、Button、Switch 等)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## 目的
2+
3+
使用统一的 `radix-ui` npm 包替代多个独立的 `@radix-ui/react-*` 包,简化依赖管理并保持所有组件行为不变。
4+
5+
### 需求: 单一依赖源
6+
7+
系统 SHALL 仅依赖统一的 `radix-ui` 包提供 Radix primitive,不再使用独立的 `@radix-ui/react-*` 包。
8+
9+
#### 场景: 依赖清单只保留统一包
10+
11+
- **GIVEN** 项目的 package.json
12+
- **WHEN** 检查 Radix 相关依赖
13+
- **THEN** 应只包含 `radix-ui` 一个包
14+
- **AND** 不包含任何 `@radix-ui/react-*` 形式的独立包
15+
16+
### 需求: 保持组件行为不变
17+
18+
系统 SHALL 在迁移后保持所有使用 Radix primitive 的组件的行为、API 和视觉表现完全一致。
19+
20+
#### 场景: Dialog 组件行为保持
21+
22+
- **GIVEN** 迁移前 Dialog 组件可以打开、关闭、展示内容
23+
- **WHEN** 迁移到统一 `radix-ui`
24+
- **THEN** Dialog 仍然可以正常打开、关闭、展示内容
25+
- **AND** 所有 props(open、onOpenChange、onInteractOutside 等)行为一致
26+
27+
#### 场景: 所有浮层组件行为保持
28+
29+
- **GIVEN** 迁移前 Popover、Select、Tooltip、Dropdown 等组件工作正常
30+
- **WHEN** 迁移到统一 `radix-ui`
31+
- **THEN** 所有这些组件继续正常工作
32+
- **AND** 不引入新的运行时错误
33+
34+
### 需求: 业务代码兼容
35+
36+
系统 SHALL 确保 `components/modules/` 下直接引用 Radix 的业务代码在迁移后仍能正常工作。
37+
38+
#### 场景: 业务组件的类型导入
39+
40+
- **GIVEN** 业务文件(如 SettingSlider、DanmakuSource)从 `@radix-ui/react-*` 导入类型
41+
- **WHEN** 迁移到统一包
42+
- **THEN** 这些文件改为从 `radix-ui` 导入对应类型
43+
- **AND** TypeScript 类型检查通过
44+
45+
### 需求: TypeScript 类型检查通过
46+
47+
系统 SHALL 在迁移完成后通过所有 TypeScript 类型检查。
48+
49+
#### 场景: 执行 typecheck
50+
51+
- **GIVEN** 迁移完成后的代码库
52+
- **WHEN** 运行 `pnpm typecheck`
53+
- **THEN** 无新增类型错误(已有历史错误除外)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## 目的
2+
3+
建立全局 z-index 层级体系,让所有浮层组件(Dialog、Popover、Select、Tooltip、ModalStack、Toast)遵循统一规则,避免层级冲突。
4+
5+
### 需求: 定义明确的层级规范
6+
7+
系统 SHALL 定义清晰的 z-index 层级,不同类型的浮层有固定的层级区间。
8+
9+
#### 场景: 层级从低到高排列
10+
11+
- **GIVEN** 系统中有多种浮层类型
12+
- **WHEN** 查看层级定义
13+
- **THEN** 层级按语义从低到高排列:ModalStack (100) < Dialog (200) < Popover/Select (250) < Tooltip (280) < Toast (300)
14+
- **AND** 每种浮层都有明确的所属层级
15+
16+
### 需求: 层级定义集中管理
17+
18+
系统 SHALL 将 z-index 值集中定义在两个对应的位置(CSS 变量和 TS 常量),保持值同步。
19+
20+
#### 场景: CSS 变量可用于 Tailwind arbitrary value
21+
22+
- **GIVEN** 一个组件需要设置 z-index
23+
- **WHEN** 使用 Tailwind 的 `z-[var(--z-xxx)]` 语法
24+
- **THEN** 能够正确引用全局定义的 CSS 变量
25+
26+
#### 场景: TS 常量可用于 inline style
27+
28+
- **GIVEN** 组件需要动态计算 z-index(如 ModalStack 的 100 + stackIndex)
29+
- **WHEN** 通过 inline style 设置 zIndex
30+
- **THEN** 能引用 TS 常量进行计算
31+
32+
### 需求: 浮层组件正确分层
33+
34+
系统 SHALL 确保浮层组件在嵌套场景下的可见性符合层级规则。
35+
36+
#### 场景: Dialog 嵌套在 ModalStack 中能正常显示
37+
38+
- **GIVEN** 用户打开了 ModalStack 级别的设置弹窗
39+
- **WHEN** 在设置内触发一个 Dialog(如 AI Provider 编辑)
40+
- **THEN** Dialog 及其 overlay 完全覆盖在 ModalStack 之上
41+
42+
#### 场景: Popover/Select 在 Dialog 内正常显示
43+
44+
- **GIVEN** Dialog 已打开
45+
- **WHEN** 在 Dialog 内触发 Popover 或 Select 下拉
46+
- **THEN** 下拉内容显示在 Dialog 内容之上
47+
- **AND** 不被 Dialog 的 overlay 遮挡
48+
49+
#### 场景: Toast 永远显示在最上层
50+
51+
- **GIVEN** 任意数量的浮层已打开(ModalStack、Dialog、Popover)
52+
- **WHEN** 触发一个 Toast 提示
53+
- **THEN** Toast 显示在所有其他浮层之上
54+
55+
### 需求: 清理临时硬编码
56+
57+
系统 SHALL 移除之前为应急加的 z-index 硬编码,改为引用统一定义。
58+
59+
#### 场景: ProviderDialog 不再硬编码 z-index
60+
61+
- **GIVEN** 之前 ProviderDialog 手动传入 `className="z-[200]"``overlayClassName="z-[200]"`
62+
- **WHEN** 新体系建立后
63+
- **THEN** ProviderDialog 不再需要手动传入 z-index 参数
64+
- **AND** 通过 Dialog 组件默认的层级生效
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## 1. Radix UI 统一包迁移
2+
3+
- [x] 1.1 安装 `radix-ui` 统一包,spike 验证一个简单组件(Label)迁移可行性
4+
- [x] 1.2 迁移 ui/ 下所有组件的 Radix import(accordion、checkbox、dialog、dropdownMenu、label、menu、popover、progress、scrollArea、select、slider、switch、tabs、toast、toggle、Tooltip、sheet、command、modal/stacked)
5+
- [x] 1.3 迁移 modules/ 下直接使用 Radix 的业务文件(SettingSlider、DanmakuSource)
6+
- [x] 1.4 全局搜索 `@radix-ui/react-` 确保无残留
7+
- [x] 1.5 卸载所有独立 `@radix-ui/react-*`
8+
- [x] 1.6 运行 typecheck,修复可能的类型问题
9+
- [x] 1.7 手动验证关键浮层场景(Dialog、Popover、Select、Tooltip、Modal)
10+
11+
## 2. Z-index 体系建立
12+
13+
- [x] 2.1 新建 `src/renderer/src/lib/constants/z-index.ts` 定义 TS 常量
14+
- [x] 2.2 在 `src/renderer/src/styles/shadcn.css` 添加 `--z-*` CSS 变量
15+
- [x] 2.3 更新 Dialog 组件使用 `z-[var(--z-dialog)]`(content 和 overlay)
16+
- [x] 2.4 更新 Popover 组件使用 `z-[var(--z-popover)]`
17+
- [x] 2.5 更新 Select 组件使用 `z-[var(--z-popover)]`
18+
- [x] 2.6 更新 Tooltip 组件使用 `z-[var(--z-tooltip)]`
19+
- [x] 2.7 更新 ModalStack 的 `MODAL_STACK_Z_INDEX` 从 TS 常量导入
20+
- [x] 2.8 更新 Toast 组件使用 `z-[var(--z-toast)]`
21+
- [x] 2.9 清理 ProviderDialog 里的 `z-[200]` / `overlayClassName="z-[200]"` 硬编码
22+
- [x] 2.10 移除 Dialog 组件的 `overlayClassName` prop(如不再需要)
23+
- [x] 2.11 全局搜索 `z-\[[0-9]` 确保无硬编码残留(排除新体系下的 var 引用)
24+
25+
## 3. 验证
26+
27+
- [x] 3.1 TypeScript typecheck 通过(排除历史已有错误)
28+
- [x] 3.2 Web dev server 启动验证:打开设置 → AI Tab → 添加服务商 → 类型下拉 + 模型搜索 → 测试连接 → Toast 提示
29+
- [~] 3.3 Electron dev 启动验证同样场景
30+
- [x] 3.4 确认所有浮层层级正确:ModalStack → Dialog → Popover/Select → Tooltip → Toast

marchen/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- 2026-05-07: [fix-danmaku-font-reset](./archive/2026-05-07-fix-danmaku-font-reset/) — 修复热更新弹幕后字体大小重置为默认值的问题
1010
- 2026-05-07: [loading-step-description](./archive/2026-05-07-loading-step-description/) — 加载Timeline stepper下方新增步骤描述副标题,显示文件名、匹配结果和弹幕数量
1111
- 2026-05-07: [add-ai-settings-tab](./archive/2026-05-07-add-ai-settings-tab/) — 新增AI设置Tab,支持多Provider管理(OpenAI/Anthropic)、模型列表获取、连接测试和AI SDK客户端工厂
12+
- 2026-05-08: [refactor-ui-foundations](./archive/2026-05-08-refactor-ui-foundations/) — shadcn UI 基础设施重构:17 个独立 Radix 包合并为统一 radix-ui,建立全局 z-index 层级体系(CSS 变量 + TS 常量),清理硬编码层级

package.json

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"@ffmpeg-installer/ffmpeg": "^1.1.0",
4040
"@ffprobe-installer/ffprobe": "^2.1.2",
4141
"ai": "^6.0.175",
42-
"nanoid": "^5.1.11"
42+
"nanoid": "^5.1.11",
43+
"radix-ui": "^1.4.3"
4344
},
4445
"devDependencies": {
4546
"@antfu/eslint-config": "^8.2.0",
@@ -54,23 +55,6 @@
5455
"@jellyfin/libass-wasm": "^4.2.4",
5556
"@marchen/electron-ipc": "workspace:*",
5657
"@marchen/shared": "workspace:*",
57-
"@radix-ui/react-accordion": "^1.2.12",
58-
"@radix-ui/react-checkbox": "^1.3.3",
59-
"@radix-ui/react-context-menu": "^2.2.16",
60-
"@radix-ui/react-dialog": "^1.1.15",
61-
"@radix-ui/react-dropdown-menu": "^2.1.16",
62-
"@radix-ui/react-label": "^2.1.8",
63-
"@radix-ui/react-popover": "^1.1.15",
64-
"@radix-ui/react-progress": "^1.1.8",
65-
"@radix-ui/react-scroll-area": "^1.2.10",
66-
"@radix-ui/react-select": "^2.2.6",
67-
"@radix-ui/react-slider": "^1.3.6",
68-
"@radix-ui/react-slot": "^1.2.4",
69-
"@radix-ui/react-switch": "^1.2.6",
70-
"@radix-ui/react-tabs": "^1.1.13",
71-
"@radix-ui/react-toast": "^1.2.15",
72-
"@radix-ui/react-toggle": "^1.1.10",
73-
"@radix-ui/react-tooltip": "^1.2.8",
7458
"@sentry/react": "^10.49.0",
7559
"@suemor/xgplayer": "^3.0.21",
7660
"@tailwindcss/vite": "^4.2.2",

0 commit comments

Comments
 (0)