Merge pull request #398 from SparkElf/fix/desktop-sidebar-layout-mode
fix: preserve desktop shell layout with side card
This commit is contained in:
@@ -753,7 +753,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]` 内。
|
||||
- **布局变量**(`<html>` 上,面板打开时有效):`--dsh-sidebar-width` / `--dsh-sidebar-height`。宿主推挤 = `#root` margin/width calc + centerCol `margin-bottom`;锚点 = **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"]` 的父级)。
|
||||
- **布局变量**(`<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)。
|
||||
- **strip 取值链**:⓪ `web` 方案强制 0;① `navigator.windowControlsOverlay` 真实几何(`wco.ts` 订阅 `geometrychange`,**为 0 也权威**,`visible=false` 幽灵 API 视为缺失);② URL inset;③ 壳预设 `stripFor`(仅 `preset`);④ 手动 `titleBarStripPx`(仅 `custom`);⑤ 0。驱动 `body[data-dsh-title-bar-compat]` + `--dsh-title-bar-strip`。
|
||||
|
||||
@@ -259,3 +259,4 @@ document.body
|
||||
- 本地 Node 24 跑 `test:mount` 需 `node --expose-internals` 启动 dsh CLI(HMR 服务要求,CI Node 22 无此问题)——本地用 DSH_CMD 包装,不改仓库脚本。
|
||||
- **CR #232 修复(2026-08-19)**:① 降级同步改为按**未修正几何**判定(跟踪自身补偿量,祖先 transform 消失才退出循环);② chunk 重验证成为 `loadChunk` 屏障(重验证挂起期间不提供缓存,杜绝 HMR 竞态陈旧 exports);③ 键盘 inset 公式补 `visualViewport.offsetTop` 并监听 `scroll`。
|
||||
- **z-index 40→25(2026-08-22)**:设计初稿的"30–99 无任何 DSH 元素"(源自 #52 修复记录)低估了官方 ui-cordis 的动态插件面板(fixed z-index 30,挂 `sidebar.footer.action` 槽)——better-sidebar 底部面板曾盖住其清单/审批面。下调宿主层至 25(AppFrame overlayLayer 20 之上、ui-cordis 30 与 DSH 浮层栈 100+ 之下),折叠按钮簇 45 保留为 host 内部层叠;`AGENTS.md` §8 与 `2026-08-15-skin-compat-design.md` 已同步。
|
||||
- **桌面布局判定修正(2026-08-26)**:`#root` 的 width/margin 推挤会缩小 AppFrame 的 border box,触发 Harness `<1024px` 窄屏判定,使桌面左侧栏自动折叠、Session Log 退化为 fixed icon,并挤乱后台任务操作区。右面板宽度改为 AppFrame 内部 `padding-right`(新版 `[data-dsh-frame]` 与 rc.8 root-slot direct child 双锚点),Frame 与 `#root` 保持视口宽度;details 拖拽手柄按同一变量平移。`desktop-layout.e2e.ts` 守护 frame 宽度、桌面 Session Log 文本按钮与面板不重叠,`drag-layout.e2e.ts` 继续守护拖拽 1:1。
|
||||
|
||||
@@ -316,7 +316,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// session header's right-aligned utilities (the "Session log" download
|
||||
// capsule) must yield. layout.css keys off this body attribute to push the
|
||||
// header's right padding out past the cluster. Only the CLOSED panel needs
|
||||
// it — an open panel already squeezes `#root` left, moving the header clear.
|
||||
// it — an open panel reserves AppFrame padding, moving the header clear.
|
||||
const collapsed = state === undefined || !state.panelOpen
|
||||
useEffect(() => {
|
||||
if (collapsed) document.body.setAttribute('data-dsh-sidebar-collapsed', '')
|
||||
@@ -703,9 +703,9 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// from the AppFrame's center column DOM (the parent of the
|
||||
// [data-slot="conversation"] wrapper — layout.css's center column) so the
|
||||
// bottom panel tracks the column's real
|
||||
// horizontal edges — including the animated margin-right push while the
|
||||
// right panel opens/closes; a frame that never appears keeps the initial
|
||||
// zero-size fallback (the panel renders at 0 width until measured).
|
||||
// horizontal edges — including the animated AppFrame padding reservation
|
||||
// while the right panel opens/closes; a frame that never appears keeps the
|
||||
// initial zero-size fallback (the panel renders at 0 width until measured).
|
||||
// The rect lives in a REF (not state): the open/close transition resizes
|
||||
// the center column EVERY frame for its duration, and reacting per frame
|
||||
// with setState re-renders the whole Sidebar (every mounted tab) at
|
||||
@@ -1708,8 +1708,8 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
// Direct from the center column's measured right edge: the bottom
|
||||
// panel spans ONLY the center column, ending exactly at the
|
||||
// details column's left edge (the details column sits between the
|
||||
// center and the right panel, and the right panel's margin-right
|
||||
// push is already baked into centerRect.right).
|
||||
// center and the right panel, and the right panel's reserved frame
|
||||
// padding is already baked into centerRect.right).
|
||||
right: window.innerWidth - centerRectRef.current.right,
|
||||
// The seam against the open right panel needs its own hairline
|
||||
// (the right panel's border-left alone is covered by this panel's
|
||||
|
||||
+44
-32
@@ -1,38 +1,42 @@
|
||||
/**
|
||||
* Layout push: when a panel is open it OCCUPIES the layout instead of
|
||||
* floating over it — the app shell (#root, the AppFrame three-column grid)
|
||||
* gives up space. Only the center column is flexible (1fr), so the right
|
||||
* panel's width squeeze (margin-right on #root) lands exactly on the
|
||||
* conversation output and the input bar, like a VSCode sidebar.
|
||||
* Layout push: an open panel occupies layout instead of floating over it.
|
||||
* The right panel reserves padding inside the AppFrame three-column grid,
|
||||
* so the app sidebar, conversation, and details column all remain to its
|
||||
* left while the frame's border-box stays viewport-wide. Harness measures
|
||||
* that border-box to choose desktop versus narrow presentation; preserving
|
||||
* it prevents a desktop window from entering narrow mode merely because the
|
||||
* plugin panel opened.
|
||||
*
|
||||
* The width rides `calc(100% - var(...))` instead of a bare margin on a
|
||||
* full-width box: some desktop shells (DSH Desktop, #208) set #root to
|
||||
* width:100%, where a margin would overflow the viewport additively —
|
||||
* the calc keeps the box at exactly 100% minus the push in every shell.
|
||||
* Width and margin transition in lockstep (same variable, same duration
|
||||
* and easing), so expand/collapse animates the content width exactly as
|
||||
* the bare-margin version did.
|
||||
* AppFrame positions its details drag handle from the measured border-box.
|
||||
* Move that handle left by the same reserved width so it stays on the details
|
||||
* column edge. The ordinary sidebar handle remains at its native coordinate.
|
||||
*
|
||||
* The bottom panel squeezes ONLY the center column — it must not cover the
|
||||
* app's own left sidebar or the right panel. The anchor is the frame's
|
||||
* center grid item via its stable data attribute ([data-pane="conversation"]
|
||||
* on the centerCol inside [data-dsh-frame]), not a positional path: the
|
||||
* frame's child order also contains an overlay layer and drag handles
|
||||
* (verified against a live DSH 0.1.x page), so nth-child is brittle. A
|
||||
* stretched grid item shrinks by its margins, so the conversation content
|
||||
* (output + input bar) lifts without touching the sidebars.
|
||||
* The bottom panel squeezes only the conversation column. The anchor is the
|
||||
* [data-dsh-center-col] tag the Sidebar shell's locator writes on the
|
||||
* frame's measured center grid item (Sidebar.tsx locate — not a positional
|
||||
* path: the frame also contains an overlay layer and drag handles). A
|
||||
* stretched grid item shrinks by its bottom margin, so the conversation
|
||||
* output and composer lift without touching either sidebar.
|
||||
*
|
||||
* The sizes ride CSS variables updated by the Sidebar shell (0 while
|
||||
* collapsed); expand/collapse animates both the margins and the panel
|
||||
* slides on the same theme duration. Drags disable the transition so the
|
||||
* layout tracks the pointer.
|
||||
* Sizes ride CSS variables updated by the Sidebar shell (0 while collapsed).
|
||||
* Expand/collapse uses the host theme duration; drags disable transitions so
|
||||
* every reserved edge tracks the pointer.
|
||||
*/
|
||||
#root {
|
||||
margin-right: var(--dsh-sidebar-width, 0px);
|
||||
width: calc(100% - var(--dsh-sidebar-width, 0px));
|
||||
/* Current shells expose [data-dsh-frame]; rc.8-era shells expose the same
|
||||
AppFrame only as the root slot's direct child. */
|
||||
#root [data-dsh-frame],
|
||||
#root > [data-slot="root"] > div {
|
||||
box-sizing: border-box;
|
||||
padding-right: var(--dsh-sidebar-width, 0px);
|
||||
transition: padding-right var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
#root [data-dsh-frame] > [data-side="details"],
|
||||
#root > [data-slot="root"] > div > [data-side="details"] {
|
||||
transform: translateX(calc(0px - var(--dsh-sidebar-width, 0px)));
|
||||
transition:
|
||||
margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
left var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
/* The AppFrame's center column. The Sidebar shell's locator (Sidebar.tsx
|
||||
@@ -69,8 +73,13 @@ body[data-dsh-sidebar-collapsed] [data-slot="conversation.session.header"] > hea
|
||||
padding-right: 78px;
|
||||
}
|
||||
|
||||
body[data-dsh-sidebar-dragging] #root,
|
||||
body[data-dsh-sidebar-dragging] #root [data-dsh-center-col] {
|
||||
body[data-dsh-sidebar-dragging] #root [data-dsh-frame],
|
||||
body[data-dsh-sidebar-dragging] #root > [data-slot="root"] > div,
|
||||
body[data-dsh-sidebar-dragging] #root [data-dsh-frame] > [data-side="details"],
|
||||
body[data-dsh-sidebar-dragging] #root > [data-slot="root"] > div > [data-side="details"],
|
||||
body[data-dsh-sidebar-dragging] #root [data-dsh-center-col],
|
||||
#root [data-dsh-frame][data-dragging] > [data-side="details"],
|
||||
#root > [data-slot="root"] > div[data-dragging] > [data-side="details"] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -94,7 +103,10 @@ body[data-dsh-sidebar-dragging] #root [data-dsh-center-col] {
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#root,
|
||||
#root [data-dsh-frame],
|
||||
#root > [data-slot="root"] > div,
|
||||
#root [data-dsh-frame] > [data-side="details"],
|
||||
#root > [data-slot="root"] > div > [data-side="details"],
|
||||
#root [data-dsh-center-col] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/** Desktop layout regression: opening the right panel must not make Harness enter narrow mode. */
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
import { PAGE_URL } from './host'
|
||||
|
||||
const WORKSPACE_PATH = process.env.DSH_E2E_DESKTOP_WORKSPACE
|
||||
?? (process.env.DSH_E2E_WORKSPACE === undefined
|
||||
? join(homedir(), 'dsh-e2e-desktop-layout-workspace')
|
||||
: `${process.env.DSH_E2E_WORKSPACE}-desktop-layout`)
|
||||
|
||||
async function dismissOnboarding(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(() => page.getByRole('button', { name: /^(Continue|Configure later|继续|稍后配置)$/ }).count(), { timeout: 60_000 })
|
||||
.toBeGreaterThan(0)
|
||||
for (let round = 0; round < 8; round++) {
|
||||
let dismissed = false
|
||||
for (const name of [/^(Continue|继续)$/, /^(Configure later|稍后配置)$/]) {
|
||||
const button = page.getByRole('button', { name }).first()
|
||||
if ((await button.count()) === 0) continue
|
||||
await button.click()
|
||||
dismissed = true
|
||||
await expect(button).toBeHidden()
|
||||
}
|
||||
if (!dismissed) return
|
||||
}
|
||||
throw new Error('onboarding takeovers did not settle after visible dismissal')
|
||||
}
|
||||
|
||||
test.beforeAll(() => {
|
||||
mkdirSync(WORKSPACE_PATH, { recursive: true })
|
||||
})
|
||||
|
||||
test('right panel keeps desktop session actions in their header positions', async ({ page }) => {
|
||||
const pageErrors: string[] = []
|
||||
const consoleErrors: string[] = []
|
||||
const requestFailures: string[] = []
|
||||
const badResponses: string[] = []
|
||||
page.on('pageerror', error => pageErrors.push(error.stack ?? error.message))
|
||||
page.on('console', message => { if (message.type() === 'error') consoleErrors.push(message.text()) })
|
||||
page.on('requestfailed', request => requestFailures.push(`${request.method()} ${request.url()} ${request.failure()?.errorText ?? 'unknown'}`))
|
||||
page.on('response', response => { if (response.status() >= 400) badResponses.push(`${response.status()} ${response.url()}`) })
|
||||
|
||||
await page.goto(PAGE_URL, { waitUntil: 'domcontentloaded' })
|
||||
await dismissOnboarding(page)
|
||||
|
||||
await page.getByRole('button', { name: /^(Choose workspace|选择工作区)$/ }).click()
|
||||
const picker = page.getByRole('dialog', { name: /^(Select Workspace Directory|选择工作区目录)$/ })
|
||||
await expect(picker).toBeVisible()
|
||||
await picker.getByRole('button', { name: /^(Edit path|编辑路径)$/ }).click()
|
||||
const pathInput = picker.getByRole('textbox', { name: /^(Edit path|编辑路径)$/ })
|
||||
await pathInput.fill(WORKSPACE_PATH)
|
||||
await pathInput.press('Enter')
|
||||
await expect(pathInput).toBeHidden()
|
||||
await picker.getByRole('button', { name: /^(Open|打开)$/ }).click()
|
||||
|
||||
// Target the ACTIVE composer by its accessible name. DSH 0.1.2-alpha hosts
|
||||
// keep an INERT `data-composer-input` "Choose workspace" textbox (with
|
||||
// contenteditable="false") later in DOM order than the real composer — a
|
||||
// bare `getByRole('textbox').last()` resolved to that one and fill() threw
|
||||
// ("not an <input>, <textarea> or [contenteditable] element").
|
||||
const composer = page.getByRole('textbox', { name: /^(Describe what you want to build|描述你想要构建)/ })
|
||||
await expect(composer).toBeVisible()
|
||||
await composer.fill('Create a desktop side-card layout test session.')
|
||||
await page.getByRole('button', { name: /^(Send message|发送消息)$/ }).click()
|
||||
|
||||
const root = page.locator('#root')
|
||||
const frame = page.locator('#root [data-dsh-frame], #root > [data-slot="root"] > div').first()
|
||||
const appSidebarExpanded = root.getByRole('button', { name: /^(Collapse sidebar|收起侧边栏)$/ })
|
||||
const sidebar = page.locator('[data-dsh-better-sidebar]')
|
||||
const sessionLog = page.getByRole('button', { name: 'Session log', exact: true })
|
||||
const sessionLogLabel = sessionLog.getByText('Session log', { exact: true })
|
||||
await expect(sessionLogLabel, 'the desktop session-log action starts as a text button').toBeVisible({ timeout: 30_000 })
|
||||
await expect(appSidebarExpanded, 'the desktop frame starts with its app sidebar expanded').toBeVisible()
|
||||
const beforeRoot = await root.boundingBox()
|
||||
const beforeFrame = await frame.boundingBox()
|
||||
expect(beforeRoot).not.toBeNull()
|
||||
expect(beforeFrame).not.toBeNull()
|
||||
|
||||
await sidebar.getByRole('button', { name: /^(Expand sidebar|展开侧边栏)$/ }).click()
|
||||
const panel = page.locator('[data-dsh-panel]:not([data-dsh-bottom-panel])')
|
||||
await expect(panel).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const center = page.locator('#root [data-dsh-frame] > [data-pane="conversation"], #root :has(> [data-slot="conversation"])').first()
|
||||
const [centerBox, panelBox] = await Promise.all([center.boundingBox(), panel.boundingBox()])
|
||||
if (centerBox === null || panelBox === null) return Number.POSITIVE_INFINITY
|
||||
return Math.abs(centerBox.x + centerBox.width - panelBox.x)
|
||||
}, { timeout: 30_000 })
|
||||
.toBeLessThanOrEqual(2)
|
||||
|
||||
const afterRoot = await root.boundingBox()
|
||||
const afterFrame = await frame.boundingBox()
|
||||
expect(afterRoot).not.toBeNull()
|
||||
expect(afterFrame).not.toBeNull()
|
||||
expect(Math.abs(afterRoot!.width - beforeRoot!.width), 'opening the plugin panel must not shrink #root').toBeLessThanOrEqual(1)
|
||||
expect(Math.abs(afterFrame!.width - beforeFrame!.width), 'opening the plugin panel must not shrink AppFrame').toBeLessThanOrEqual(1)
|
||||
await expect(appSidebarExpanded, 'a desktop window must not enter the host narrow layout').toBeVisible()
|
||||
await expect(sessionLogLabel, 'the session-log action must keep its desktop label').toBeVisible()
|
||||
expect(await sessionLog.evaluate(element => getComputedStyle(element).position), 'the session-log action must stay in the header flow').not.toBe('fixed')
|
||||
|
||||
const [sessionLogBox, panelBox] = await Promise.all([sessionLog.boundingBox(), panel.boundingBox()])
|
||||
expect(sessionLogBox).not.toBeNull()
|
||||
expect(panelBox).not.toBeNull()
|
||||
expect(sessionLogBox!.width, 'the desktop session-log action must keep its full button width').toBeGreaterThanOrEqual(110)
|
||||
expect(sessionLogBox!.x + sessionLogBox!.width, 'the session-log action must not overlap the plugin panel chrome').toBeLessThanOrEqual(panelBox!.x - 8)
|
||||
expect(pageErrors).toEqual([])
|
||||
expect(consoleErrors).toEqual([])
|
||||
expect(requestFailures).toEqual([])
|
||||
expect(badResponses).toEqual([])
|
||||
})
|
||||
@@ -2,23 +2,23 @@
|
||||
* Drag-layout lane: the width drag must track the app shell 1:1 — the
|
||||
* regression test for issue #92 ("主会话框左右抖动").
|
||||
*
|
||||
* The layout push squeezes `#root` via `margin-right: var(--dsh-sidebar-width)`
|
||||
* (layout.css), and layout.css disables that margin's transition while a drag
|
||||
* is live via `body[data-dsh-sidebar-dragging]`. If the transition stays
|
||||
* active during the drag (or the conversation lags the panel edge), the
|
||||
* The layout push reserves `padding-right: var(--dsh-sidebar-width)` inside
|
||||
* AppFrame (layout.css), and layout.css disables that padding's transition
|
||||
* while a drag is live via `body[data-dsh-sidebar-dragging]`. If the transition
|
||||
* stays active during the drag (or the conversation lags the panel edge), the
|
||||
* conversation visibly shakes at pointer cadence. This spec drives a real
|
||||
* pointer drag on the width strip while a requestAnimationFrame sampler
|
||||
* records, per frame:
|
||||
*
|
||||
* - the strip's x (the panel edge),
|
||||
* - the conversation column's right edge (`#root`'s margin push lands
|
||||
* - the conversation column's right edge (AppFrame's reserved padding lands
|
||||
* exactly there),
|
||||
* - whether `body[data-dsh-sidebar-dragging]` is set,
|
||||
* - `#root`'s computed transition property/duration.
|
||||
* - AppFrame's computed transition property/duration.
|
||||
*
|
||||
* Then it asserts the drag contract:
|
||||
* 1. the dragging attribute is present during the drag;
|
||||
* 2. `#root`'s transition is disabled (none) during the drag;
|
||||
* 2. AppFrame's transition is disabled (none) during the drag;
|
||||
* 3. the conversation edge follows the panel edge monotonically (no
|
||||
* oscillation) and 1:1 (total travel within a rounding epsilon).
|
||||
*
|
||||
@@ -162,15 +162,16 @@ test('width drag tracks the shell 1:1 with transitions disabled (issue #92)', as
|
||||
: [...host.querySelectorAll<HTMLElement>('*')]
|
||||
.filter(el => getComputedStyle(el).cursor === 'col-resize')
|
||||
.sort((a, b) => a.getBoundingClientRect().x - b.getBoundingClientRect().x)[0]
|
||||
// The conversation column: the grid item the layout push squeezes (the
|
||||
// same nth-child(2) layout.css targets for the vertical push; its right
|
||||
// edge is where the width push lands).
|
||||
const center = document.querySelector('#root > div[data-slot="root"] > div > div:nth-child(2)')
|
||||
const root = document.querySelector('#root') as HTMLElement
|
||||
// The conversation column is the grid item whose right edge meets the
|
||||
// AppFrame padding reservation and the plugin panel.
|
||||
const center = document.querySelector('#root [data-dsh-frame] > [data-pane="conversation"]')
|
||||
?? document.querySelector('#root :has(> [data-slot="conversation"])')
|
||||
const frame = (document.querySelector('#root [data-dsh-frame]')
|
||||
?? document.querySelector('#root > [data-slot="root"] > div')) as HTMLElement
|
||||
const loop = (): void => {
|
||||
const s = strip?.getBoundingClientRect() ?? { left: 0 }
|
||||
const c = center?.getBoundingClientRect() ?? { left: 0, right: 0 }
|
||||
const cs = getComputedStyle(root)
|
||||
const cs = getComputedStyle(frame)
|
||||
samples.push({
|
||||
t: performance.now(),
|
||||
stripX: s.left,
|
||||
@@ -212,15 +213,15 @@ test('width drag tracks the shell 1:1 with transitions disabled (issue #92)', as
|
||||
// The conversation-column selector must have matched (the push lands on it).
|
||||
expect(last!.convoRight, 'the conversation edge must have moved with the drag').toBeLessThan(first!.convoRight - 40)
|
||||
|
||||
// Contract 1 + 2: while dragging, the body attribute is set and #root's
|
||||
// margin transition is disabled (computed `transition: none` reads as
|
||||
// Contract 1 + 2: while dragging, the body attribute is set and AppFrame's
|
||||
// padding transition is disabled (computed `transition: none` reads as
|
||||
// transition-property "none" with 0s duration; the non-dragging rule would
|
||||
// compute to "margin-right" with the theme duration).
|
||||
// compute to "padding-right" with the theme duration).
|
||||
const draggingSamples = samples.filter(s => s.dragging)
|
||||
expect(draggingSamples.length).toBeGreaterThan(5)
|
||||
for (const sample of draggingSamples) {
|
||||
expect(sample.transitionProperty, 'the margin transition must be off while dragging').toBe('none')
|
||||
expect(sample.transitionDuration, 'the margin transition must be off while dragging').toBe('0s')
|
||||
expect(sample.transitionProperty, 'the padding transition must be off while dragging').toBe('none')
|
||||
expect(sample.transitionDuration, 'the padding transition must be off while dragging').toBe('0s')
|
||||
}
|
||||
|
||||
// Contract 3: monotonic, 1:1 tracking. During a leftward drag both the
|
||||
@@ -552,9 +553,9 @@ test('bottom panel never flashes full-width after a width drag release (issue #2
|
||||
// cleanup and setup phases — React can yield a render frame in that gap,
|
||||
// so the browser painted the push-less layout (center column full width),
|
||||
// the drag-end measure cached that full-width rect, and the bottom panel
|
||||
// rendered full-width while #root's margin transition animated back to
|
||||
// the new width. Every post-release frame must keep the two edges glued
|
||||
// at the committed width: no full-width frame, no drift, no margin
|
||||
// rendered full-width while AppFrame's padding transition animated back
|
||||
// to the new width. Every post-release frame must keep the two edges glued
|
||||
// at the committed width: no full-width frame, no drift, no padding
|
||||
// animation.
|
||||
await page.evaluate(() => {
|
||||
const samples: Array<{ t: number; bottomRight: number; colRight: number; varW: string; dragging: boolean; iw: number }> = []
|
||||
|
||||
Reference in New Issue
Block a user