fix(sidebar): synchronize collapsed rail motion
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 在此注册触发行和设置面板。
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user