deepseek-harness 侧栏收起动画修复:四个上方控件共用同一进入动画的实现解析
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
在 deepseek-harness 的客户端界面中,侧栏(Sidebar)从展开态收起到 56px 窄轨道时,需要播放一段「滑入 + 淡入」的进入动画。本指南以仓库中已归档的 Agent Note(收起侧栏的上方控件共用同一进入动画)为线索,结合dsh-client-ui-sidebar与dsh-client-ui-workspace两个包的源码与测试,完整讲解这次修复的问题背景、150ms 共用动画的设计参数、跨包控件的动画归属划分、设置控件的例外处理,以及样式测试如何锁定这套动画契约。读完本文,你将掌握在「外壳持有动画、区域子控件继承同一路径」的架构约束下,如何设计一套视觉一致且可测试的侧栏轨道进入动画。
问题背景:透明度一致,几何却不一致
在修复前,收起侧栏轨道(rail)的上方四个控件由两个不同的包渲染:
| 控件 | 归属包 | 对齐方式 |
|---|---|---|
| 侧栏切换(Toggle) | dsh-client-ui-sidebar(外壳) | 右对齐 |
| 新建会话(New Session) | dsh-client-ui-sidebar(外壳) | 左对齐 |
| 添加工作区(Add) | dsh-client-ui-workspace(Workspace 区域) | 右对齐 |
| 搜索(Search) | dsh-client-ui-workspace(Workspace 区域) | 左对齐 |
它们各自使用了相同的透明度淡入时序,但几何行为并不一致:右对齐的控件会随栏变窄而移动,左对齐的控件则保持不动。结果就是——即使添加与搜索使用相同的淡入曲线,添加在视觉上仍比搜索「慢半拍」,因为它的起点在轨道之外更远的位置。四个控件的透明度时间线相同、位移路径却各自为政,这种不一致正是本次修复要消除的对象。
值得注意的是底部设置(Settings)控件的角色差异:它固定在轨道页脚,承担的是独立的「页脚操作」职责,不属于上方控件序列,因此不能参与上方控件的横向进入动画。
决策:一个左对齐起点 + 一段共用动画
修复的核心决策可以概括为三点:
- 统一的起点:轨道落位时,四个 36px 上方控件从同一个左对齐布局开始(
.collapsed .logoRow的justify-content: flex-start、.collapsed .newSession的align-self: flex-start提供了统一的基础锚点)。 - 一段共用动画:四者共用一段
150ms动画,从translateX(49px)移动到最终的 10px 内边距;透明度使用同一条动画时间线。 - 动画归属只在外壳:外壳(shell)把位移分别应用于自己的侧栏切换、新建会话,并且只对 Workspace 区域应用一次(
translate整个regionArea),因此添加与搜索会继承同一条位移路径,不会产生嵌套变换(nested transform)。
设计参数速查
| 参数 | 取值 | 说明 |
|---|---|---|
| 动画时长 | 150ms | 与展开态内容 150ms 淡出(COLLAPSE_SETTLE_MS)衔接 |
| 起始位移 | translateX(49px) | 从原轨道右边缘起滑 |
| 结束位置 | 10px 内边距 | .root.collapsed的padding: 18px 10px 6px |
| 控件尺寸 | 36px × 36px | 居中于 56px 轨道 |
| 缓动 | var(--ds-ease-in-out) | 与外壳其他动画共用同一缓动变量 |
| 填充模式 | backwards | 动画开始前先应用from帧,避免延迟闪烁 |
源码实现剖析
外壳:只在「实时收起」时挂载动画类
SidebarRoot.tsx中,外壳通过三个状态变量协同控制收起流程(SidebarRoot.tsx):
settled:展开态内容在 150ms 淡出结束后卸载(COLLAPSE_SETTLE_MS = 150,与宽内容淡出时长严格匹配);wide:!collapsed || !settled,即收起过程中宽内容仍保持挂载并原位淡出,栏宽冻结为展开宽度,由滑动中的轨道列裁剪它,中途不触发任何回流;everWide:记录本次挂载是否经历过展开态。这是「冷启动不播放动画」的关键——!wide && everWide.current && css.railIn意味着只有从展开实时收起到轨道(live collapse)时才挂上railIn类;页面初次加载即处于收起态时,轨道直接以最终几何静态渲染,没有延迟隐藏的图标。
外层根节点还有fading(收起第一阶段的整体淡出)与quietBars(指针不在列内时隐藏滚动条)两个类,分别对应文档描述的「收起 = 滑入 + 交叉淡入」模型与滚动条指针提示行为。
样式:一条 keyframe,三个选择器,一个例外
动画契约全部收敛在 SidebarRoot.module.css:
.railIn .iconButton, .railIn .newSession, .railIn .regionArea { animation: rail-in 150ms var(--ds-ease-in-out) backwards; } .railIn .footArea { animation: rail-fade-in 150ms var(--ds-ease-in-out) backwards; } @keyframes rail-in { from { opacity: 0; transform: translateX(49px); } } @keyframes rail-fade-in { from { opacity: 0; } }这段代码精确落实了文档决策:
rail-in同时作用于三个选择器:.iconButton(侧栏切换)、.newSession(新建会话)、.regionArea(整个 Workspace 浏览区域,内含添加与搜索两个 36px 控件)。外壳只对regionArea应用一次变换,子控件继承同一路径,避免了「区域变换 + 子控件变换」叠加成嵌套 transform 的隐患;footArea单独使用rail-fade-in:设置控件使用与上方控件完全相同的时长(150ms)与缓动(--ds-ease-in-out),但只改变透明度,保持横向坐标固定——这正是「设置在最终横坐标上淡入」的实现;- 上方控件还通过
.collapsed .logoRow { justify-content: flex-start; }与.collapsed .newSession { align-self: flex-start; width: 36px; }(SidebarRoot.module.css)保证四个控件共享同一左对齐基础锚点,使translateX(49px)对所有控件产生一致的视觉起点。
动态效果偏好与冷启动
CSS 文件末尾的@media (prefers-reduced-motion: reduce)块(SidebarRoot.module.css)同时禁用了.wide、.fading > *以及两段 rail 关键帧的动画与过渡,完整覆盖「减少动态效果模式会禁用两段关键帧」的决策要求。而「静态收起渲染不播放启动动画」则由前文所述的everWide逻辑在 React 层兜底。
跨包协作:区域为何只移动一次
Workspace 区域的渲染方是dsh-client-ui-workspace的WorkspaceBrowser,它在 rail 态渲染搜索与添加工件图标(IconSearchOutline16、IconProjectAddOutline16),且明确注明这两个图标「作为 36px 控件走外壳的共用轨道进入路径,各自通过 owner share 请求展开」(WorkspaceBrowser.tsx)。
外壳与区域之间的边界由 slot 契约约束(contract/slots.ts):外壳声明sidebar.workspaces槽位,并把wide布尔值(是否渲染完整浏览器)与expandSidebar(rail 图标点击时请求展开)通过SidebarSectionOwnerProps传给区域(contract/slots.ts)。这一设计使得动画的时序、位移、缓动完全由外壳单点持有,区域只需提供自身内容,不感知任何动画参数——这正是「把动画拥有权留在侧栏外壳」的架构意图。
备选方案与取舍
文档记录了三条被评估后否决的替代路径,理解它们有助于把握最终方案的边界:
- 把每个轨道控件固定在最终内边距。方案最简单,能彻底消除不一致,但代价是移除四个上方控件本应具备的横向进入效果——属于「为了修复而砍掉体验」。
- 分别为每个 Workspace 按钮(添加、搜索)添加动画。这会把外壳的时序复制进
ui-workspace包,造成时序散落两处、难以维护;更危险的是可能同时应用区域变换与子控件变换,产生嵌套 transform。对已注册的regionArea只移动一次,则动画拥有权仍由外壳持有。 - 让设置控件随上方控件一起移动。被否决的原因是设置属于「底部固定的页脚操作」,语义上不属于上方控件序列,强行纳入反而破坏其固定页脚的视觉定位。
测试保障:用样式测试锁死动画契约
这套动画契约不是口头约定,而是由 sidebar-styles.client.spec.ts 逐条锁定的。测试直接读取SidebarRoot.module.css源码文本,解析出精确选择器的声明后进行断言,重点包括:
- 共用动画分配:
.railIn .iconButton、.railIn .newSession、.railIn .regionArea三者的animation必须完全等于rail-in 150ms var(--ds-ease-in-out) backwards(sidebar-styles.client.spec.ts); - 位移距离:
@keyframes rail-in的from帧必须包含opacity: 0; transform: translateX(49px)(sidebar-styles.client.spec.ts); - 设置例外:
.railIn .footArea的动画必须是独立的rail-fade-in 150ms var(--ds-ease-in-out) backwards,且rail-fade-in的from帧只含opacity: 0(sidebar-styles.client.spec.ts); - 基础锚点:
.collapsed .logoRow的justify-content: flex-start、.collapsed .newSession的align-self: flex-start与width: 36px(sidebar-styles.client.spec.ts)。
只要有人修改 CSS 中的动画时长、位移距离或选择器归属,这些断言就会立刻失败,从机制上防止「透明度时序相同、几何行为不同」的问题再次回归。
影响与总结
本次修复的最终效果,正如文档 Consequences 一节所述:
- 侧栏切换、新建会话、添加与搜索在整个收起过程中使用相同的横坐标,视觉运动完全一致;
- 设置在最终横坐标上原地淡入,保持页脚操作的固定语义;
- 静态收起渲染(冷启动)保持最终几何,不播放启动动画;
- 样式测试固定了共用动画分配、位移距离、基础锚点与设置例外四项契约。
从工程视角看,这是一次典型的「跨包动画一致性」修复:动画的时序参数集中在dsh-client-ui-sidebar外壳的 SidebarRoot.module.css 中单点持有,Workspace 区域通过 slot 契约以整体regionArea身份继承同一位移路径,设置控件以独立淡入关键帧保留例外语义,冷启动与prefers-reduced-motion两条边界路径各有明确的处理机制,最终由样式测试锁死全部关键参数。这一模式同样适用于其他需要「多个包渲染的控件共享同一进入动画」的 UI 场景。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考