Преглед изворни кода

fix(sidebar): synchronize collapsed rail motion

_Kerman пре 1 месец
родитељ
комит
de97ea0ba8

+ 6 - 0
.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md
+2026-08-12-collapsed-sidebar-shared-entry-motion.md: c5bc18973db693cf9ba60800fbcf7720593dbad5
+2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md: b87ef6322d548061cc2e29c0e8f9896098c09041

+ 33 - 0
.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md

@@ -0,0 +1,33 @@
+# Agent Note: Collapsed sidebar upper controls share one entry motion
+
+Status: implemented
+Archived: 2026-08-12
+
+English | [中文](2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md)
+
+## Problem
+
+The collapsed sidebar rail renders four upper controls owned by two packages: the shell owns the toggle and New Session, while the workspace region owns add and search. Their opacity timing matched, but their geometry did not. Right-aligned controls moved with the narrowing column while left-aligned controls stayed fixed, so add appeared slower than search even under the same fade.
+
+The bottom settings control has a different role. It is pinned to the rail foot and must not join the upper controls' horizontal entry.
+
+## Decision
+
+At the rail settle point, the four upper 36px controls start from one left-anchored layout and share one `150ms` animation from `translateX(49px)` to their final 10px inset. The shell applies the translation to its toggle and New Session seats and once to the workspace region, so add and search inherit the same path without nested transforms. Opacity uses the same animation timeline.
+
+The settings seat uses a separate opacity-only keyframe with the same duration and easing. A page that starts collapsed renders the rail without an entry animation, and reduced-motion mode disables both keyframes.
+
+## Alternatives considered
+
+**Keep every rail control fixed at its final inset.** This removes the mismatch, but it also removes the requested horizontal entry from the four upper controls.
+
+**Animate each workspace button independently.** This would duplicate shell timing inside `ui-workspace` and could apply both a region and child transform. Translating the registered region once keeps animation ownership in the sidebar shell.
+
+**Translate the settings control with the upper controls.** Rejected because settings is a bottom-pinned foot action, not part of the upper control sequence.
+
+## Consequences
+
+- Toggle, New Session, add, and search follow the same horizontal coordinates throughout collapse.
+- Settings fades at its final horizontal coordinate.
+- Static collapsed renders retain their final geometry without startup motion.
+- Style tests pin the shared animation assignments, translation distance, base anchors, and settings exception.

+ 33 - 0
.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 收起侧栏的上方控件共用同一进入动画
+
+Status: implemented
+Archived: 2026-08-12
+
+[English](2026-08-12-collapsed-sidebar-shared-entry-motion.md) | 中文
+
+## Problem
+
+收起侧栏轨道的四个上方控件由两个包渲染:外壳持有侧栏切换与新建会话,Workspace 区域持有添加和搜索。它们的透明度时序相同,但几何行为不同。右对齐控件会随栏变窄而移动,左对齐控件则保持不动,因此添加即使使用相同淡入,视觉上仍比搜索慢。
+
+底部设置控件承担不同角色。它固定在轨道页脚,不能参与上方控件的横向进入。
+
+## Decision
+
+轨道落位时,四个 36px 上方控件从同一个左对齐布局开始,共用一段 `150ms` 动画,从 `translateX(49px)` 移动到最终 10px 内边距。外壳把位移分别应用于侧栏切换、新建会话,并只对 Workspace 区域应用一次,因此添加与搜索会继承同一路径,不产生嵌套变换。透明度使用同一条动画时间线。
+
+设置控件使用时长与缓动相同、但只改变透明度的独立关键帧。页面初始即为收起状态时不会播放进入动画;减少动态效果模式会禁用两段关键帧。
+
+## Alternatives considered
+
+**把每个轨道控件固定在最终内边距。** 这能消除不一致,但也会移除四个上方控件所需的横向进入效果。
+
+**分别为每个 Workspace 按钮添加动画。** 这会在 `ui-workspace` 中重复外壳时序,还可能同时应用区域与子控件变换。只移动一次已注册区域,可以让动画继续由侧栏外壳持有。
+
+**让设置控件随上方控件一起移动。** 不予采纳,因为设置是固定在底部的页脚操作,不属于上方控件序列。
+
+## Consequences
+
+- 侧栏切换、新建会话、添加与搜索在整个收起过程中使用相同横坐标。
+- 设置在最终横坐标上淡入。
+- 静态收起渲染保持最终几何,不播放启动动画。
+- 样式测试固定共用动画分配、位移距离、基础锚点与设置例外。

+ 3 - 0
.agents/notes/archived/manifest.json

@@ -97,6 +97,9 @@
     "bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml": "sha256:859c4399f9a017a68ba89552fdafa05e73c0599d94cee9551c84ea5b749a14f3",
     "bug-fix/2026-08-10-web-favicon-dark-mode.md": "sha256:4d17e247abd76ae3aed5fb4e075fd66a2838292f89f7021c82a79fe37ed905e6",
     "bug-fix/2026-08-10-web-favicon-dark-mode.zh.md": "sha256:7bbff8a3b7061c127afcc75cd2a8043b02a999b78c0180edd8f7e4807fcfe71d",
+    "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml": "sha256:3ce4f6e39e173fc304bf64deca9c95bcddc1dbb492e065ca8c267a7a40788588",
+    "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md": "sha256:7b169aa4543edfc965de5a8b7b9e60aa9d9d5218693cd0b57908e2d482280723",
+    "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md": "sha256:88db36c698800bf55c3c7531d6f92665576d978c29c15ff7d74215fb93376cb1",
     "feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
     "feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
     "feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",

+ 2 - 2
packages/client/ui-sidebar/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-sidebar/README.md
-README.md: 9974118f69901de985e012e1b62f95a0bcee64c2
-README.zh.md: 11b0aa142cf62626ab6105e2c405d506e35349b0
+README.md: 7cc4fe0a722fe5f8cf0e983a0e3fdb30cd31bb64
+README.zh.md: 96b6f60f871758c126e6c387c6ce3f0993039af0

+ 2 - 0
packages/client/ui-sidebar/README.md

@@ -8,6 +8,8 @@ New Session starts the runtime's page-local frontend Session Intent. The runtime
 
 `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared `sidebar.workspaces` and `sidebar.settings` child slots, and injected `startSession` plus sidebar-toggle callbacks. There is no plugin store.
 
+During a live collapse, the shell holds the expanded content at its current width while it fades out for 150ms. The four upper controls—the shell toggle and New Session plus add and search rendered through `sidebar.workspaces`—then share one 150ms fade and 49px leftward translation into the 56px rail, ending with the layout's 300ms column slide; every 36px control box follows the same path to the rail's 10px left inset. The bottom-pinned `sidebar.settings` control shares the fade timing but has no horizontal translation. A page that starts collapsed renders the rail statically, and reduced-motion mode disables both transitions.
+
 Scrollbars in the column are a pointer affordance: the shell rebinds ui-theme's [scrollbar indirection](../ui-theme/README.md) to `transparent` whenever the pointer is outside it, and keeps the thumb drawn for 2s after the pointer leaves, so a list nobody is pointing at carries no bar. The reservation that keeps rows from moving belongs to the scrolling region ([ui-workspace](../ui-workspace/README.md)), so revealing a thumb never reflows.
 
 The foot is the `sidebar.settings` seat: the sidebar renders only the bottom-pinned layout slot and shares its column state (`wide`); ui-settings registers the trigger row and settings panel there.

+ 2 - 0
packages/client/ui-sidebar/README.zh.md

@@ -8,6 +8,8 @@ New Session 会启动运行时的页面局部前端 Session Intent。运行时
 
 `SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 和 `useWorkspaces` 钩子、已声明的 `sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的 `startSession` 与侧边栏切换回调。这里没有插件 store。
 
+实时收起时,外壳会把展开内容固定在当前宽度,并用 150ms 将其淡出。随后,上方四个控件——外壳的侧栏切换与新建会话,以及通过 `sidebar.workspaces` 渲染的添加和搜索——共用一次 150ms 的淡入和 49px 左移,在布局的 300ms 栏滑动结束时一起进入 56px 轨道;每个 36px 控件盒都会沿同一条路径到达轨道左侧 10px 的内边距。固定在底部的 `sidebar.settings` 控件只共用淡入时序,不发生横向位移。页面初始即为收起状态时会静态渲染轨道;减少动态效果模式会禁用两段过渡。
+
 栏内的滚动条是一种指针可供性:只要指针不在栏内,外壳就把 ui-theme 的[滚动条间接层](../ui-theme/README.md)重新绑定为 `transparent`;指针离开后滑块再保留 2 秒,因此没人指向的列表不会带着滚动条。避免行位移的空间预留属于滚动区域本身([ui-workspace](../ui-workspace/README.md)),所以显示滑块不会引起重排。
 
 页脚承载 `sidebar.settings`:侧边栏只渲染固定在底部的布局 slot,并共享其栏状态(`wide`);ui-settings 在此注册触发行和设置面板。

+ 25 - 10
packages/client/ui-sidebar/src/client/SidebarRoot.module.css

@@ -58,24 +58,35 @@
   from { opacity: 0; }
 }
 
-/* Rail controls hold hidden while the column slides shut, then fade in over
-   the slide's tail: .railIn applies at settle (150ms into the 0.3s AppFrame
-   track transition), so a 100ms delay + 150ms fade starts just before the
-   slide ends (250ms) and finishes at 400ms; `backwards` keeps them at
-   opacity 0 through the delay. Only a live collapse gets .railIn — a
-   refresh straight into the collapsed state renders statically. */
+/* At the 150ms rail settle, the four upper controls enter from the former
+   rail right edge over the remaining 150ms of the AppFrame track transition.
+   The bottom-pinned settings seat shares their opacity timeline but stays
+   horizontally fixed. Only a live collapse gets .railIn; a cold collapsed
+   render stays static. */
 .railIn .iconButton,
 .railIn .newSession,
+.railIn .regionArea {
+  animation: rail-in 150ms var(--ds-ease-in-out) backwards;
+}
+
 .railIn .footArea {
-  animation: rail-in 150ms var(--ds-ease-in-out) 100ms backwards;
+  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; }
 }
 
-/* Logo row (figma pad (4,8,4,8)): brand left, panel toggle right-anchored —
-   the toggle is the rail's expand control and slides in with the right edge. */
+/* Logo row (figma pad (4,8,4,8)): expanded keeps the panel toggle at the
+   right edge; the rail gives every control the same base left anchor before
+   the shared entry translation is applied. */
 .logoRow {
   flex: none;
   display: flex;
@@ -93,6 +104,7 @@
   height: 36px;
   padding: 0;
   margin-bottom: 12px;
+  justify-content: flex-start;
 }
 
 /* Brand group (figma I133:7632): the full wordmark rides the text ink
@@ -185,6 +197,8 @@
 }
 
 .collapsed .newSession {
+  align-self: flex-start;
+  width: 36px;
   height: 36px;
   padding: 0;
   margin: 0 0 12px;
@@ -239,7 +253,8 @@
   .fading > *,
   .railIn .iconButton,
   .railIn .newSession,
-  .railIn .footArea {
+  .railIn .footArea,
+  .railIn .regionArea {
     transition: none;
     animation: none;
   }

+ 7 - 6
packages/client/ui-sidebar/src/client/SidebarRoot.tsx

@@ -2,12 +2,13 @@
  * Sidebar shell: column geometry only. Collapse is a slide plus crossfade:
  * content freezes at its expanded width (inline style) and fades out in place
  * while the sliding column (AppFrame grid tracks) clips it — nothing reflows
- * mid-slide. At settle the wide-only content unmounts and the control rows
- * snap to the 56px rail (one icon each, same top-down order) fading in as the
- * slide ends. The workspace/session browsing region between the New Session
- * button and the foot is the `sidebar.workspaces` registrant's, and the foot
- * is the `sidebar.settings` registrant's; the shell hands them the wide flag
- * (plus an expand request callback for the browser).
+ * mid-slide. At settle the wide-only content unmounts and the four upper
+ * controls enter the 56px rail from the same horizontal offset (one icon each,
+ * same top-down order) on one fade that ends with the slide. The bottom-pinned
+ * settings control only fades. The workspace/session browsing region between
+ * the New Session button and the foot is the `sidebar.workspaces` registrant's,
+ * and the foot is the `sidebar.settings` registrant's; the shell hands them the
+ * wide flag (plus an expand request callback for the browser).
  *
  * The column also owns whether the scroll regions nested in it draw a
  * scrollbar at all: the shell tracks the pointer and rebinds ui-theme's

+ 26 - 2
packages/client/ui-sidebar/tests/sidebar-styles.client.spec.ts

@@ -1,4 +1,4 @@
-/** Sidebar shell inset contract shared with the nested workspace browser. */
+/** Sidebar shell style contracts shared with its slot-owned controls. */
 import { readFileSync } from 'node:fs'
 import { fileURLToPath } from 'node:url'
 import { describe, expect, it } from 'vitest'
@@ -25,7 +25,7 @@ function declarations(selector: string): Map<string, string> | undefined {
   return undefined
 }
 
-describe('SidebarRoot.module.css inset', () => {
+describe('SidebarRoot.module.css', () => {
   it('shares and cancels the wide shell trailing padding structurally', () => {
     const root = declarations('.root')
     expect(root?.get('--dsh-sidebar-inline-padding')).toBe('12px')
@@ -39,4 +39,28 @@ describe('SidebarRoot.module.css inset', () => {
     expect(declarations('.collapsed .regionArea')?.get('padding-left')).toBe('0')
     expect(declarations('.collapsed .regionArea')?.get('margin-right')).toBe('0')
   })
+
+  it('moves the four upper controls while the settings seat only fades', () => {
+    const animation = 'rail-in 150ms var(--ds-ease-in-out) backwards'
+    for (const selector of [
+      '.railIn .iconButton',
+      '.railIn .newSession',
+      '.railIn .regionArea',
+    ]) {
+      expect(declarations(selector)?.get('animation')).toBe(animation)
+    }
+    expect(declarations('.railIn .footArea')?.get('animation')).toBe(
+      'rail-fade-in 150ms var(--ds-ease-in-out) backwards',
+    )
+    expect(css).toMatch(
+      /@keyframes rail-in\s*\{\s*from\s*\{\s*opacity: 0;\s*transform: translateX\(49px\);\s*}\s*}/,
+    )
+    expect(css).toMatch(/@keyframes rail-fade-in\s*\{\s*from\s*\{\s*opacity: 0;\s*}\s*}/)
+  })
+
+  it('gives shell rail controls the same base anchor for their shared translation', () => {
+    expect(declarations('.collapsed .logoRow')?.get('justify-content')).toBe('flex-start')
+    expect(declarations('.collapsed .newSession')?.get('align-self')).toBe('flex-start')
+    expect(declarations('.collapsed .newSession')?.get('width')).toBe('36px')
+  })
 })

+ 2 - 2
packages/client/ui-workspace/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
-README.md: 9d7d4d77cc064146f1fdaed615509215c64308fc
-README.zh.md: ca35d7cd2e7ff176f4ea40d1e9a3a6d1a7457462
+README.md: 8878aa49dcccd60ddcde5f0a9563bbfbd969c9c0
+README.zh.md: c83f1c53d5471dd8d52b933163a573eb98a70c2e

Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
packages/client/ui-workspace/README.md


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
packages/client/ui-workspace/README.zh.md


+ 3 - 1
packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css

@@ -240,11 +240,13 @@
 
 /* Rail variant (own .rail class from the wide owner prop — the region never
    reads the shell's class names): the two icon controls stack as 36x36
-   circles matching the shell's rail rhythm. */
+   circles matching the shell's rail rhythm. Both use the rail's shared base
+   left anchor so the outer shell can translate the whole region uniformly. */
 .rail .sectionHeader {
   gap: 0;
   padding-left: 0;
   margin-bottom: 12px;
+  justify-content: flex-start;
 }
 
 .rail .headerActions {

+ 5 - 5
packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx

@@ -3,11 +3,11 @@
  * `sidebar.workspaces` hole: section header (title + view options + add
  * workspace), search, the grouped tree or flat list, and the workspace
  * dialogs. Wide state renders the full browser; rail state renders the two
- * region icons (search / add workspace), each requesting shell expansion
- * through the owner share. Adding is the header button's one action, so it
- * raises the directory flow with no menu in between; the flow and its error
- * dialog live in WorkspacePicker (same package — direct composition, no slot
- * between them).
+ * region icons (search / add workspace) as 36px controls on the shell's shared
+ * rail entry path, each requesting expansion through the owner share. Adding is the header
+ * button's one action, so it raises the directory flow with no menu in
+ * between; the flow and its error dialog live in WorkspacePicker (same
+ * package — direct composition, no slot between them).
  */
 import { useEffect, useMemo, useRef, useState } from 'react'
 import clsx from 'clsx'

+ 6 - 0
packages/client/ui-workspace/tests/browser-styles.client.spec.ts

@@ -107,4 +107,10 @@ describe('WorkspaceBrowser.module.css list', () => {
     expect(rowDeclarations('.sessionRow.selected')?.get('background'))
       .toBe('var(--dsw-alias-interactive-bg-hover)')
   })
+
+  it('pins both rail controls to the shared left anchor during the column slide', () => {
+    expect(declarations('.rail .sectionHeader')?.get('justify-content')).toBe('flex-start')
+    expect(declarations('.rail .iconButton')?.get('width')).toBe('36px')
+    expect(declarations('.rail .search')?.get('width')).toBe('36px')
+  })
 })

Неке датотеке нису приказане због велике количине промена