Skip to content

Commit cf83316

Browse files
committed
feat: improve DetailOverlay scrolling performance and consistency, enhancing user navigation between content sections
1 parent 84a73ec commit cf83316

7 files changed

Lines changed: 384 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name: tailwind-unify-library-sidebar
2+
schema: full
3+
createdAt: '2026-05-24T12:48:47.267Z'
4+
status: open
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
## 背景
2+
3+
项目主体样式表达走 Tailwind 4(`@theme` + utility class + shadcn),但 library / sidebar / app-header 是 bespoke `.css` 写成的(合计约 1330 行)。两套范式割裂带来认知成本和 token 系统不连贯。
4+
5+
Tailwind 4 的 `@theme` 块支持把 CSS 变量直接登记为 utility 源——这给「桥接现有 var → Tailwind 词典」提供了天然机制,不需要重写真值层。
6+
7+
## 目标与非目标
8+
9+
**目标:**
10+
11+
-`bg-library-bg-2 / text-library-fg-3 / shadow-library-card` 等 Tailwind utility 可直接使用
12+
-`library.css` 1154 行压缩到约 300 行以内
13+
- 把保留的 bespoke CSS 拆分到组件同级目录,避免单文件膨胀
14+
- 视觉与行为零回归(亮 / 暗双模式均逐组件验证)
15+
- 在 CLAUDE.md 明文写下「Tailwind 优先 / CSS 兜底」规则,让后续不需要再决策
16+
17+
**非目标:**
18+
19+
- 不重写 token 真值(`:root / .dark` 块不动)
20+
- 不改主题切换机制
21+
- 不迁 shadcn / player / settings / 其它已经是 Tailwind 的组件
22+
- 不重新设计视觉
23+
- 不引入 `cva` 等额外抽象(除非个别组件 className 真的复杂到必须)
24+
25+
## 决策
26+
27+
### 1. Token 桥接走 `@theme` 内的 `var()` 引用,而非复制真值
28+
29+
```
30+
@theme {
31+
--color-library-bg-2: var(--library-bg-2);
32+
}
33+
34+
:root { --library-bg-2: #ffffff; }
35+
.dark { --library-bg-2: #0c0e12; }
36+
```
37+
38+
不选择「把真值直接写进 @theme」,因为:
39+
40+
- 真值放 `:root / .dark` 才能让 dark mode 切换通过 cascade 触发
41+
- `@theme` 在 Tailwind 4 中是「注册词典」的语义层;混入真值会让职责不清
42+
- 这种双层指向(`@theme` 引用 var,var 在 :root/dark 真值切换)已被 shadcn 默认模式验证可用
43+
44+
### 2. 不全部桥接,渐变 / color-mix / 复杂滤镜保留 CSS var
45+
46+
判定准则:**Tailwind utility 是否表达得比 CSS 短/清晰**
47+
48+
| token 类型 | 处理 | 例子 |
49+
|---|---|---|
50+
| 单色 | `@theme --color-library-*` | `--library-bg-2` |
51+
| 阴影 | `@theme --shadow-library-*` | `--library-shadow-card` |
52+
| 半径 | `@theme --radius-library-*` | `--library-radius-card` |
53+
| 尺寸 | `@theme --spacing-*` | `--library-rail-card-w` |
54+
| 渐变 | **保留 CSS var**,需要时用 arbitrary value | `--library-poster-shade` |
55+
| color-mix | **保留 CSS var** | `oklch(0.66 0.22 22 / 0.5)` 可登记,但 `color-mix(in oklch, ...)` 保留 |
56+
| 多层背景 | **保留 CSS var** | Hero `::after` 的多层渐变 |
57+
58+
### 3. 状态切换从 className 改为 data-* 属性
59+
60+
`.library-ep-tile.watched` 改写为 `data-watched` 后用 `data-[watched]:` variant 表达。理由:
61+
62+
- 避免 JSX 内堆条件 className 字符串(`cn('base', watched && 'watched')``data-watched={watched ? '' : undefined}`
63+
- Tailwind 4 对 `data-[]` variant 支持完整
64+
- 语义上 data 属性表达状态比 class 拼接更清晰
65+
- 与 shadcn 已有模式(`data-[state=open]:`)一致
66+
67+
### 4. 分级迁移,DetailOverlay 仅做轻迁移
68+
69+
- Tier 1(7 个组件,全部 utility):直接迁
70+
- Tier 2(3 个组件,hybrid):简单部分 utility,伪元素/`:has()`/keyframes 保留为 `Hero.css` 等同级文件
71+
- Tier 3(DetailOverlay):只迁文本/间距,banner/poster/scroll 主结构与刚修好的滚动行为完全不动
72+
73+
DetailOverlay 上一个变更刚改过结构(unified scroll),动它有回归风险且收益有限——结构层 CSS 本来就是少量、稳定的。
74+
75+
### 5. 保留 CSS 按组件 co-located
76+
77+
```
78+
src/renderer/src/page/library/
79+
├── Hero.tsx
80+
├── Hero.css ← 仅 Hero 用的 ::after / keyframes
81+
├── DetailOverlay.tsx
82+
├── DetailOverlay.css ← 仅 DT 用的 scroll / banner / poster / keyframes
83+
├── EpisodeGrid.tsx
84+
├── EpisodeGrid.css ← 复合状态需要的 :has / 状态嵌套
85+
└── ...
86+
87+
src/renderer/src/styles/
88+
├── library.css ← 仅剩 :root/.dark token 真值、library-shell 根容器、跨组件 scrim
89+
├── sidebar.css ← 大部分迁完,剩个别(如 active 指示器)
90+
└── app-header.css ← 大部分迁完
91+
```
92+
93+
理由:找 Hero 的样式直接到 Hero.css,不用翻 1154 行大文件;CSS 文件大小自然界定为「这个组件的复杂度」。
94+
95+
### 6. 增量提交策略
96+
97+
```
98+
PR 1 = B(token 桥接) + A1(Tier 1: 卡片类 7 个)
99+
PR 2 = A2(Tier 2: Hero / EpisodeGrid / LibraryShell)
100+
PR 3 = A3(DetailOverlay 轻迁移) + A4(清理、文档、CLAUDE.md)
101+
```
102+
103+
PR 1 是最大块,原子提交利于回滚;后续 PR 在 PR 1 基础上增量。
104+
105+
## 风险与权衡
106+
107+
| 风险 | 影响 | 缓解 |
108+
|---|---|---|
109+
| 视觉回归(尤其 dark mode) | 中-高 | 每组件迁完手动切深色对比;保留 git diff 便于回退 |
110+
| className 字符串过长 ||`cn()` 分行;超 ~80 字符必须拆 |
111+
| Tailwind 桥接后真值未在 `:root` 同步 || 桥接前先全表扫描 token;编写检查脚本(grep 出所有 `--library-*` 声明) |
112+
| DetailOverlay 动 keyframes 引发滚动 / 动画破坏 || Tier 3 明确规则只迁文本/间距,主结构不动 |
113+
| 短期内 hybrid 阶段两套语法并存 || 中间状态会有 ~1 周;CLAUDE.md 写清晰;分级迁移可加速度过 |
114+
| Tailwind 词典爆炸 || 只桥接实际使用的 token;保留 grep + 验证脚本 |
115+
116+
**权衡:**
117+
118+
- 选择 `@theme` 引用 var 而不是直接放真值 → 增加一层指向,但换得 dark mode 行为不变 + 职责清晰
119+
- 选择「按 Tier 分级」而非「一次全迁」 → 多了 3 个 PR 的协调成本,换得回归风险可控
120+
- 选择 `data-*` 而非 className 状态拼接 → 写法稍冗,但与 shadcn 风格一致
121+
- 选择保留 ~300 行 bespoke CSS 而非「100% Tailwind」 → 接受混合范式,因为强行 utility 化复杂选择器会比 CSS 更难读
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## 动机
2+
3+
项目 UI 当前存在两套样式范式并存:
4+
5+
- **shadcn / 主要业务页** 走 Tailwind 4 utility-first
6+
- **library / sidebar / app-header** 走 bespoke `.css`~1330 行)
7+
8+
这是历史遗留——library 是按 Infuse 设计稿一气呵成写的,sidebar 跟着同样模式。两套范式的代价已经显现:
9+
10+
- 新人 / 协作者要在两种心智模型之间来回切(看 shadcn 还得切 `library.css` 翻 1100 行)
11+
- token 系统割裂:shadcn 的 `--color-primary``--library-bg-2` 没有桥
12+
- 改一处样式要决定「这里能直接 className 吗」,每次都需上下文判断
13+
- `library.css` 单文件膨胀到 1154 行,搜索/审阅都笨重
14+
15+
目标:**把样式表达统一到 Tailwind 优先 + CSS 兜底**的混合范式,建立清晰规则,让后续开发不再面对范式选择。
16+
17+
## 变更内容
18+
19+
1. **Token 统一(Tier B)**:把 `--library-*` / `--sidebar-*` / `--app-header-*` 经由 `@theme` 注册成 Tailwind 词典里的命名空间,可直接 `bg-library-bg-2 / text-library-fg-3 / shadow-library-card` 等使用。真值仍留在 `:root / .dark`,只是多了一层映射。
20+
2. **组件迁移(Tier A)**:按组件复杂度分三档逐步迁 className:
21+
- Tier 1(卡片类,简单原子):PosterCard / LandscapeCard / Rail / PosterGrid / EmptyState / Sidebar / AppHeader — 完全 Tailwind
22+
- Tier 2(hybrid):Hero / EpisodeGrid / LibraryShell — Tailwind 优先,复合选择器/伪元素/keyframes 保留小段 .css(拆到组件目录同级)
23+
- Tier 3(重,结构留 bespoke):DetailOverlay — 仅迁文本/间距,banner/poster/scroll 主结构与 keyframes 保留
24+
3. **规则与文档**:在 CLAUDE.md 增补「样式规范」一节,明确「什么时候用 Tailwind / 什么时候保留 .css」的判定准则;后续 AI / 协作者按规则写不再纠结。
25+
4. **CSS 文件治理**:迁完后 `library.css` 1154 行 → 预期 ~300 行(只剩动画 / 复合选择器 / 滚动条 / 渐变)。剩余规则按组件拆到组件目录(Hero.css / DetailOverlay.css / EpisodeGrid.css),跟 .tsx 同级,找样式不用翻大文件。
26+
27+
## 能力
28+
29+
### 新增能力
30+
31+
- `token-system`:定义 `@theme` 命名空间约定、token 注册规范(颜色 / 阴影 / 半径 / 尺寸 / 动画 keyframes 的桥接方式)、保留 CSS var 而非登记 Tailwind 的判断(渐变 / color-mix / 复杂滤镜)
32+
- `component-migration`:分级迁移规则(Tier 1/2/3 划分依据、每档允许的语法、复合状态如 `.watched` 如何迁成 `data-watched`
33+
- `css-fallback`:哪些场景必须保留 bespoke CSS(keyframes / `:has()` / `::-webkit-scrollbar-*` / 复合状态选择器 / 渐变 token),以及 fallback 的代码组织方式(co-located 在组件目录)
34+
35+
### 修改能力
36+
37+
不修改现有功能能力。本变更仅触及样式表达层,行为完全不变。
38+
39+
## 影响范围
40+
41+
**代码层**
42+
43+
- `src/renderer/src/styles/tailwind.css`:新增 `@theme` 内 library/sidebar/app-header token 映射
44+
- `src/renderer/src/styles/library.css` / `sidebar.css` / `app-header.css`:删除大部分规则,剩余按组件拆分
45+
- `src/renderer/src/page/library/*.tsx`(约 10 个组件):className 重写
46+
- `src/renderer/src/components/layout/sidebar/index.tsx` / `app-header/AppHeader.tsx`:className 重写
47+
48+
**不动**
49+
50+
- shadcn 组件(本就 Tailwind)
51+
- player / settings / shared 业务组件
52+
- main / preload / IPC / 数据库 / player-core 等非 UI 层
53+
- 主题切换机制(dark mode 依赖 `.dark` 覆盖 var 真值,不动 `:root / .dark` 块)
54+
55+
**风险点**
56+
57+
- 视觉回归——尤其是 dark mode,需逐组件比对
58+
- 长 className 字符串可读性——通过 `cn()` 或必要时 `cva` 缓解
59+
- DetailOverlay 刚修好的滚动 / 动画——只做轻迁移,结构与 keyframes 不动
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
## 目的
2+
3+
按组件复杂度分级,将 library / sidebar / app-header 模块的样式表达从 bespoke `.css` 类名迁移到 Tailwind utility class,保持视觉与行为完全一致。
4+
5+
### 需求: 组件按 Tier 1 / Tier 2 / Tier 3 分级处理
6+
7+
每个目标组件 MUST 被归入 Tier 1(纯 Tailwind)/ Tier 2(hybrid)/ Tier 3(结构保留 bespoke)之一。分级依据是结构复杂度与是否含有伪元素 / keyframes / 复合状态选择器。
8+
9+
#### 场景: Tier 1 组件全部 className 由 Tailwind utility 表达
10+
11+
- **GIVEN** 组件 PosterCard / LandscapeCard / Rail / PosterGrid / EmptyState / Sidebar / AppHeader 被归类为 Tier 1
12+
- **WHEN** 完成迁移
13+
- **THEN** 这些组件的 JSX SHALL NOT 引用任何 `library-* / sidebar-* / app-header-*` 前缀的非 utility class
14+
- **AND** 对应的 `.css` 文件中相关规则 SHALL 被删除
15+
16+
#### 场景: Tier 2 组件保留必要 bespoke CSS
17+
18+
- **GIVEN** 组件 Hero / EpisodeGrid / LibraryShell 含有 `::after` 渐变层、`:has()` 选择器或 keyframes 引用
19+
- **WHEN** 完成迁移
20+
- **THEN** 这些组件的简单属性(间距 / 字号 / 颜色 / flex)SHALL 改写为 Tailwind utility
21+
- **AND** 不可表达的复合规则(伪元素、`:has`、scrollbar 样式)MAY 保留为 bespoke CSS
22+
- **AND** 保留的 CSS SHALL 拆分到组件同级目录(如 `Hero.css``Hero.tsx` 同级)
23+
24+
#### 场景: Tier 3 仅做轻迁移
25+
26+
- **GIVEN** DetailOverlay 含有 keyframes、scrollbar、复杂 z-index 叠层与近期修复的滚动结构
27+
- **WHEN** 完成迁移
28+
- **THEN** 仅文本 / 间距 / 颜色相关 className SHALL 改写为 Tailwind utility
29+
- **AND** banner / scroll / poster 主结构与 keyframes SHALL 保留 bespoke CSS 不动
30+
31+
### 需求: 状态选择器迁移用 data-* 属性表达
32+
33+
组件中的状态复合选择器(如 `.library-ep-tile.watched`)迁移后 MUST 改用 `data-*` 属性 + Tailwind `data-[xxx]:` variant 表达,避免 JSX 内堆叠条件 className 字符串。
34+
35+
#### 场景: 已观看状态用 data-watched
36+
37+
- **GIVEN** EpisodeGrid 的 ep-tile 当前根据 `watched` 布尔切换 `.watched` class
38+
- **WHEN** 完成迁移
39+
- **THEN** JSX SHALL 输出 `data-watched={watched ? '' : undefined}`
40+
- **AND** className 中 SHALL 用 `data-[watched]:text-library-fg-3` 形式表达 watched 态样式
41+
42+
### 需求: 视觉与行为完全一致
43+
44+
迁移前后,所有 Tier 1 / Tier 2 / Tier 3 组件的视觉表现与交互行为 MUST 完全一致,亮色与暗色模式下均无可察觉差异。
45+
46+
#### 场景: 亮色 / 暗色双模式视觉一致
47+
48+
- **GIVEN** 任意已迁移的组件
49+
- **WHEN** 在亮色模式下截图比对,并在暗色模式下截图比对
50+
- **THEN** 颜色 / 间距 / 字号 / 阴影 / 圆角 SHALL 与迁移前一致
51+
- **AND** hover / focus / 选中状态 SHALL 与迁移前一致
52+
53+
### 需求: 长 className 通过 cn() 拆分
54+
55+
当单个元素的 utility 字符串超过约 80 字符或包含明显分组逻辑时,MUST 用 `cn()`(项目已有的工具函数)拆分为多行或多个语义片段,确保可读性。
56+
57+
#### 场景: 复杂卡片样式分组
58+
59+
- **GIVEN** PosterCard 根元素需要表达:基础形状 + 卡片背景 + hover 反馈 + 过渡动画
60+
- **WHEN** 迁移这部分 className
61+
- **THEN** JSX SHALL 用 `cn('base utilities', 'hover utilities', 'transition utilities')` 形式分组
62+
- **AND** 单行 className 字符串 SHALL NOT 超过约 80 字符
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## 目的
2+
3+
明确哪些样式表达必须保留 bespoke CSS,以及保留的 CSS 如何组织在仓库中,确保「Tailwind 优先 + CSS 兜底」的规则可被后续 AI / 协作者机械遵守。
4+
5+
### 需求: 必须保留 CSS 的场景被显式列出
6+
7+
CLAUDE.md 中 MUST 增加「样式规范」一节,明确以下场景应保留 bespoke CSS 而非强行 Tailwind 化:
8+
9+
- `@keyframes``cubic-bezier(...)` 缓动曲线
10+
- `::before / ::after` 伪元素(含 mask / overlay / gradient)
11+
- `:has()` / `:is()` / 多层后代选择器
12+
- `::-webkit-scrollbar-*` 系列
13+
- 包含 `linear-gradient` / `radial-gradient` / `color-mix` 的复合背景
14+
- 同元素多个 `filter` / `backdrop-filter` 叠加
15+
16+
#### 场景: 文档规则可被后续读者执行
17+
18+
- **GIVEN** CLAUDE.md 已包含「样式规范」一节
19+
- **WHEN** 协作者 / AI 准备添加新的样式
20+
- **THEN** 该读者 SHALL 能根据列表直接判断「这条规则该用 Tailwind 还是 .css」
21+
- **AND** 不需要再询问或猜测
22+
23+
### 需求: 保留的 CSS 必须 co-located 在组件目录
24+
25+
保留的 bespoke CSS MUST 拆分到组件同级目录,文件名与组件文件一致(如 `Hero.tsx``Hero.css` 同目录),并由组件文件直接 `import './Hero.css'``styles/library.css` 仅保留跨组件的全局 token / scrim / shell 级规则。
26+
27+
#### 场景: 拆分后的目录结构
28+
29+
- **GIVEN** Hero 组件保留了 `::after` 渐变 + keyframes
30+
- **WHEN** 完成迁移
31+
- **THEN** `src/renderer/src/page/library/Hero.css` SHALL 存在并只包含 Hero 相关规则
32+
- **AND** `Hero.tsx` SHALL 在文件顶部 `import './Hero.css'`
33+
- **AND** `styles/library.css` 中原 Hero 相关规则 SHALL 已被删除
34+
35+
#### 场景: 全局规则保留在 styles 目录
36+
37+
- **GIVEN** `--library-bg` 等真值声明、`.library-shell` 的根容器规则
38+
- **WHEN** 完成迁移
39+
- **THEN** 这些 SHALL 仍留在 `styles/library.css`
40+
- **AND** `styles/library.css` 总行数 SHALL 显著小于迁移前(预期 1154 → 约 300 行以内)
41+
42+
### 需求: 类名前缀冲突防护
43+
44+
保留的 bespoke class 名 MUST 继续使用 `library-* / sidebar-* / app-header-*` 前缀,避免与 Tailwind utility 或 shadcn class 命名冲突。
45+
46+
#### 场景: 新增 bespoke class 命名
47+
48+
- **GIVEN** Hero.css 需要新增一个伪元素叠加层
49+
- **WHEN** 给该层命名
50+
- **THEN** 类名 SHALL 以 `library-hero-` 开头
51+
- **AND** SHALL NOT 与 Tailwind utility class 名冲突
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
## 目的
2+
3+
把私域 CSS 变量(`--library-*` / `--sidebar-*` / `--app-header-*`)桥接到 Tailwind 的 `@theme` 词典,使其可作为 utility class 在 JSX 中直接使用,同时保留主题切换(dark mode)所需的真值覆盖层。
4+
5+
### 需求: Tailwind 必须能识别 library / sidebar / app-header 命名空间下的 token
6+
7+
桥接完成后,项目内任意 JSX MUST 能通过 Tailwind utility 直接引用桥接过的 token。`@theme` 仅做映射登记,token 真值 MUST 留在 `:root / .dark` 内,确保主题切换链路不被破坏。
8+
9+
#### 场景: 颜色 token 注册并通过 utility 使用
10+
11+
- **GIVEN** `tailwind.css``@theme {}` 块声明 `--color-library-bg-2: var(--library-bg-2)`
12+
- **AND** `:root``.dark` 仍各自声明 `--library-bg-2` 真值
13+
- **WHEN** JSX 写 `className="bg-library-bg-2"`
14+
- **THEN** 编译后的 CSS SHALL 将该元素背景设为当前主题对应的 `--library-bg-2`
15+
- **AND** 切换 `.dark` class 时背景 SHALL 立即更新为暗色真值
16+
17+
#### 场景: 阴影 / 半径 / 尺寸 token 同样被识别
18+
19+
- **GIVEN** `@theme` 内同时注册 `--shadow-library-card``--radius-library-card``--spacing-rail-card-w`
20+
- **WHEN** JSX 写 `className="shadow-library-card rounded-library-card w-rail-card-w"`
21+
- **THEN** Tailwind SHALL 编译生成对应 utility class
22+
- **AND** 视觉结果 SHALL 与原 `.library-poster-card` 规则一致
23+
24+
### 需求: 不适合登记的 token 必须保留 CSS var 形式
25+
26+
非简单原子(渐变 / `color-mix` / 复合滤镜 / 多层叠加背景)不应强行 utility 化。这类 token MUST 保留为 CSS var,在需要使用时 JSX 通过 `className="[background:var(--library-poster-shade)]"` 或在 bespoke `.css` 中使用。
27+
28+
#### 场景: 渐变 token 不被错误注册
29+
30+
- **GIVEN** 一个值为 `linear-gradient(180deg, transparent, rgba(0,0,0,0.85))` 的变量 `--library-poster-shade`
31+
- **WHEN** 设计 token 注册表
32+
- **THEN** `--color-library-poster-shade` SHALL NOT 出现在 `@theme` 中(因为不是单一颜色)
33+
- **AND** 该变量仍 SHALL 保留在 `:root / .dark` 中供 CSS 或 arbitrary value 使用
34+
35+
### 需求: 主题切换链路不变
36+
37+
桥接前后,dark mode 切换的行为 MUST 完全一致:切换 `.dark` class 即触发所有桥接 token 的视觉更新。
38+
39+
#### 场景: dark mode 切换不破坏 utility 显示
40+
41+
- **GIVEN** 用户当前在亮色模式,页面渲染使用 `bg-library-bg-2`
42+
- **WHEN** 切换到暗色模式(`.dark` class 应用)
43+
- **THEN** 该元素的背景 SHALL 立即更新为 `:root.dark` 下声明的 `--library-bg-2` 真值
44+
- **AND** 无需重新构建或刷新页面

0 commit comments

Comments
 (0)