Merge pull request #448 from chendefine/fix/panel-host-scroll-jump
fix(shell): keep an overflow:hidden fallback beneath clip (old-engine compat) + guard the cascade
This commit is contained in:
@@ -752,7 +752,7 @@ ctx.effect(() =>
|
||||
|
||||
- **面板表面**:右/底面板背景 = `var(--dsw-alias-bg-layer-1)`。**绝不消费 `--dsw-specific-sidebar-fill`**(宿主左导航专属,皮肤按左导航语义覆盖它,面板消费会失去填充)。换面板表面 = 覆写 `--dsw-alias-bg-layer-1`。
|
||||
- **终端/编辑器表面**:`effectiveTokenValue` 读 `--dsw-alias-bg-base`——`transparent` 与 alpha < 0.9 的半透明值回退不透明底色(文字不叠背景画,issue #90);≥ 0.9 放行。
|
||||
- **根锚点**:宿主 div 带 `data-dsh-better-sidebar`(append 到 body);其内**面板宿主层** `[data-dsh-panel-host]`(`fixed; inset:0; z-25; pointer-events:none`,v0.13.1+),面板/开关簇 absolute 定位,免疫中间层 transform 劫持;页面级 transform 触发 `data-dsh-panel-host-degraded` 降级。皮肤作用域覆盖限定在 `[data-dsh-better-sidebar]` 内。
|
||||
- **根锚点**:宿主 div 带 `data-dsh-better-sidebar`(append 到 body);其内**面板宿主层** `[data-dsh-panel-host]`(`fixed; inset:0; z-25; pointer-events:none; overflow:hidden+clip`,v0.13.1+),面板/开关簇 absolute 定位,免疫中间层 transform 劫持;页面级 transform 触发 `data-dsh-panel-host-degraded` 降级。`overflow` 级联是**契约**(`hidden` 兜底 + `clip` 收尾,`tests/panel-host-css.spec.ts` 守护):`hidden` 盒子仍是滚动容器,脚本滚动或浏览器 scroll-into-view 修正(焦点移入视口外区域、嵌套 iframe/工作台加载时抢焦点、面板滑出动画中 focus() 落点)会沿最近可滚祖先滚走整层——面板与开关簇集体偏离视口角(computed left/right 仍"正确",偏移藏在盒子自身 scroll offset 里);`clip` 裁剪语义相同但不产生滚动盒,任何路径都滚不动这层。皮肤作用域覆盖限定在 `[data-dsh-better-sidebar]` 内。
|
||||
- **布局变量**(`<html>` 上,面板打开时有效):`--dsh-sidebar-width` / `--dsh-sidebar-height`。右面板宽度 = AppFrame 的 `padding-right` 预留(新版 `#root [data-dsh-frame]` / rc.8 `#root > [data-slot="root"] > div` 双锚点),AppFrame border box 保持完整桌面视口宽度(Harness 以此判定桌面/窄屏布局,避免插件面板展开误入窄屏);AppFrame 的 details 拖拽手柄按同一变量向左平移贴合列边缘。底部面板仍走 centerCol `margin-bottom`;centerCol 锚点 = **JS 标注**(禁止 `nth-child`):侧栏 shell 的定位器给测得的 centerCol 节点打 `[data-dsh-center-col]` 标签(`Sidebar.tsx` locate,节点更换/HMR 时随 ref 迁移),`layout.css` 用 `#root [data-dsh-center-col]` 选中(`drag-layout.e2e.ts` 断言恰一节点且为 `[data-slot="conversation"]` 的父级;frame 宽度与桌面 Session Log 由 `desktop-layout.e2e.ts` 断言)。
|
||||
- **桌面信号与标题栏**(v0.14.1+ 四方案模型 `SidebarPrefs.titleBarScheme`,唯一决策点 `src/client/titlebar-strip.ts` 纯函数):
|
||||
- 壳信号(只读,不自动触发修改):URL `dsh-desktop-mode` / `dsh-desktop-platform` / 可选 `dsh-desktop-titlebar-inset`(0–120 clamp)。
|
||||
|
||||
@@ -48,7 +48,18 @@
|
||||
hidden box stays a PROGRAMMATICALLY scrollable container, and a
|
||||
scrollIntoView fired from panel content (the file tree's "Show in
|
||||
folder" reveal) scrolled the host itself — shifting the whole panel,
|
||||
tab bar included, out of the viewport with no way to scroll back. */
|
||||
tab bar included, out of the viewport with no way to scroll back.
|
||||
The same class covers any scroll-into-view fixup — focus moving into
|
||||
an off-viewport region, a nested iframe/workbench claiming focus
|
||||
while it loads — so the guard belongs on the layer itself, not per
|
||||
call site. The `hidden` declaration BEFORE `clip` is an old-engine
|
||||
fallback, not dead style: engines without `clip` support (Safari < 16)
|
||||
drop the `clip` line, which would leave this box `overflow: visible`
|
||||
— no clipping at all, and the page-draggable regression above
|
||||
returns. With `hidden` first those engines keep the clipping (plus
|
||||
its pre-clip scrollability) while every clip-aware engine applies
|
||||
the later `clip`. */
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* The unified panel host layer must clip WITHOUT being a scroll container —
|
||||
* the guard for the "panels / toggle cluster jumped away from the viewport
|
||||
* corner" bug class.
|
||||
*
|
||||
* `overflow: hidden` only clips: the box stays a scroll container, so any
|
||||
* script scroll or the browser's native scroll-into-view fixup (focus moving
|
||||
* into an off-viewport region, a nested workbench/iframe claiming focus
|
||||
* while it loads, focus() landing during a panel's slide-out transition)
|
||||
* walks up to the nearest scrollable ancestor — this layer — and scrolls it,
|
||||
* dragging every panel plus the toggle cluster off the corner while the
|
||||
* computed `left`/`right` still read correct (the offset hides in the box's
|
||||
* own scroll offset). `overflow: clip` declared AFTER `hidden` keeps the
|
||||
* `hidden` fallback for engines without clip support and removes the
|
||||
* scrollability everywhere else; the cascade order is part of the contract.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const css = readFileSync('src/client/sidebar.module.css', 'utf8')
|
||||
|
||||
/** Body of the `:global([data-dsh-panel-host])` rule (up to the un-indented `}`). */
|
||||
const hostRule = css.match(/:global\(\[data-dsh-panel-host\]\) \{([\s\S]*?)\n\}/)?.[1]
|
||||
|
||||
/** Strip /* ... */ comment blocks so prose mentions don't count as declarations. */
|
||||
const stripComments = (cssText: string): string => cssText.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
|
||||
describe('panel host layer css', () => {
|
||||
it('declares the clip cascade: hidden first, clip last', () => {
|
||||
expect(hostRule, 'the [data-dsh-panel-host] rule must exist').toBeDefined()
|
||||
const declarations = stripComments(hostRule!).match(/overflow\s*:\s*\w+/g) ?? []
|
||||
expect(declarations).toEqual(['overflow: hidden', 'overflow: clip'])
|
||||
})
|
||||
|
||||
it('stays the fixed viewport layer (containing block + BFC guarantee)', () => {
|
||||
expect(hostRule).toMatch(/position:\s*fixed/)
|
||||
expect(hostRule).toMatch(/inset:\s*0/)
|
||||
})
|
||||
|
||||
it('does not reintroduce overflow on the degraded (absolute) layer', () => {
|
||||
const degraded = css.match(
|
||||
/:global\(\[data-dsh-panel-host\]\[data-dsh-panel-host-degraded\]\) \{([\s\S]*?)\n\}/,
|
||||
)?.[1]
|
||||
expect(degraded, 'the degraded-mode rule must exist').toBeDefined()
|
||||
expect(degraded).not.toMatch(/overflow\s*:/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user