From a9fd4de2827c69602de3247dcd94e771fbfef2e3 Mon Sep 17 00:00:00 2001 From: Saafo <46736350+Saafo@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:23:11 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix=20macOS=20drawer=20backgroun?= =?UTF-8?q?d=20and=20native=20close=20hit=20area?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/tests/sidebar-drawer.e2e.ts | 55 +++++++++++++++++++ packages/client/ui-layout/README.i18n.yaml | 4 +- packages/client/ui-layout/README.md | 2 +- packages/client/ui-layout/README.zh.md | 2 +- .../ui-layout/src/client/AppFrame.module.css | 8 +-- .../src/client/SidebarRoot.module.css | 8 +-- packages/client/web/src/base.css | 5 ++ 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/apps/web/tests/sidebar-drawer.e2e.ts b/apps/web/tests/sidebar-drawer.e2e.ts index c360eb86f2..eed37f5fb7 100644 --- a/apps/web/tests/sidebar-drawer.e2e.ts +++ b/apps/web/tests/sidebar-drawer.e2e.ts @@ -21,6 +21,61 @@ async function assertCompactRail(sidebar: Locator): Promise { })).toEqual({ fits: true, animations: [] }) } +it.each(['light', 'dark'] as const)('keeps the macOS drawer opaque and disables covered window drag regions in %s mode', async (colorScheme) => { + const scaffold = await launchWebScaffold({ + extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), + }) + try { + const browser = await chromium.launch() + try { + const page = await browser.newPage({ viewport: { width: 520, height: 680 }, colorScheme, locale: 'en-US' }) + await page.goto(scaffold.authenticatedUrl) + const opener = page.getByRole('button', { name: 'Open sidebar', exact: true }) + await opener.waitFor() + await page.evaluate(() => { document.documentElement.dataset.platform = 'darwin' }) + await opener.click() + const drawer = page.getByRole('dialog', { name: 'Navigation', exact: true }) + await drawer.waitFor() + const column = page.locator('[data-slot="sidebar"]').locator('..') + const background = await column.evaluate((el) => { + const style = getComputedStyle(el) + return { color: style.backgroundColor, image: style.backgroundImage } + }) + expect(background.image).toBe('none') + expect(background.color).toMatch(/^rgb\(/) + expect(await drawer.getByRole('button', { name: 'New session', exact: true }).last().evaluate((el) => { + const actual = getComputedStyle(el).backgroundColor + const root = document.documentElement + const platform = root.dataset.platform + delete root.dataset.platform + try { + return actual === getComputedStyle(el).backgroundColor + } finally { + root.dataset.platform = platform + } + })).toBe(true) + const inactiveDragRows = page.locator('[inert] [data-window-drag]') + expect(await inactiveDragRows.count()).toBeGreaterThan(0) + expect(await inactiveDragRows.evaluateAll(rows => rows.every(row => getComputedStyle(row).getPropertyValue('app-region') === 'no-drag'))) + .toBe(true) + const collapse = drawer.getByRole('button', { name: 'Collapse sidebar', exact: true }) + await collapse.click() + await drawer.waitFor({ state: 'hidden' }) + await page.setViewportSize({ width: 1200, height: 800 }) + const sidebar = page.locator('[data-slot="sidebar"]').locator('..') + await expect.poll(() => sidebar.evaluate(el => getComputedStyle(el).backgroundImage)).not.toBe('none') + const activeDragRows = page.locator('[data-window-drag]:not([inert] [data-window-drag])') + expect(await activeDragRows.count()).toBeGreaterThan(0) + expect(await activeDragRows.evaluateAll(rows => rows.every(row => getComputedStyle(row).getPropertyValue('app-region') === 'drag'))) + .toBe(true) + } finally { + await browser.close() + } + } finally { + await scaffold.close() + } +}) + it('hides a collapsed rail immediately when it becomes a drawer', async () => { const scaffold = await launchWebScaffold({ extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)), diff --git a/packages/client/ui-layout/README.i18n.yaml b/packages/client/ui-layout/README.i18n.yaml index 75bda0d492..86ed6cdb41 100644 --- a/packages/client/ui-layout/README.i18n.yaml +++ b/packages/client/ui-layout/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-layout/use-this-package: - en: 762116247685afd7 - zh: cfb0767cb6c7e923 + en: e09b6b2a6b826cae + zh: 81834846a602b1b3 /deepseek-ai-dsh-client-ui-layout/use-this-package/window-chrome-seat: en: 7eac53b38d0777fe zh: 7f9422dff671ba50 diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index 3063101d5c..fb87273a7f 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -31,7 +31,7 @@ The root slot composes the sidebar, main content, and right column. The sidebar When the browser reports a folded device in landscape and the frame is at least 768px wide, the expanded sidebar occupies half the frame, aligning its divider with the center fold. Its resize handle is hidden; unfolding restores the saved width. Fold changes at a stable viewport width use the existing track transition; reduced motion disables it. Browsers without the device-posture media feature keep the ordinary layout. -The drawer mask fades with the panel’s transition duration and easing on both open and close; reduced motion disables both transitions. Entering drawer mode from an already collapsed sidebar hides the drawer immediately; an expanded sidebar keeps its closing slide. The drawer has no edge divider, and its frame does not scroll. The frame fills the shell’s layout viewport, retaining its height while the keyboard opens. Drawer backing and mask cover the shell’s full height, including `shell.bottom`, above fullscreen and floating right panels. Browser-owned status and address bars remain outside the shell; the layout adds no synthetic safe-area insets. +The drawer mask fades with the panel’s transition duration and easing on both open and close; reduced motion disables both transitions. Entering drawer mode from an already collapsed sidebar hides the drawer immediately; an expanded sidebar keeps its closing slide. The drawer has no edge divider, and its frame does not scroll. On macOS, the drawer uses the same opaque background as Web; window vibrancy applies only to the ordinary sidebar. The frame fills the shell’s layout viewport, retaining its height while the keyboard opens. Drawer backing and mask cover the shell’s full height, including `shell.bottom`, above fullscreen and floating right panels. Browser-owned status and address bars remain outside the shell; the layout adds no synthetic safe-area insets. Global panels occupy the root-scoped `main` keyed slot; `conversation` is the reserved key for the Conversation. `ctx.layout.selectPanel(id)` selects a registered panel, and `null` selects the Conversation without changing the current Session. The Plugins page contributes one of the shipped global panels. diff --git a/packages/client/ui-layout/README.zh.md b/packages/client/ui-layout/README.zh.md index 005bf6d538..725b34a38c 100644 --- a/packages/client/ui-layout/README.zh.md +++ b/packages/client/ui-layout/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" 当浏览器报告设备处于横屏半折叠状态,且框架宽度不低于 768px 时,展开的侧边栏占据一半宽度,分隔线与屏幕中央的折痕对齐。此时隐藏拖拽手柄;完全展开后恢复保存的宽度。视口宽度不变时,折叠状态切换复用现有的列宽过渡;减少动态效果时禁用该过渡。不支持 device-posture 媒体特性的浏览器继续使用常规布局。 -抽屉遮罩使用与面板相同的时长和缓动,在打开与关闭时渐变透明度;减少动态效果时两者均不播放动画。从已收起的侧栏切换到抽屉布局时,抽屉立即隐藏;已展开的侧栏保留收起动画。抽屉边缘不绘制分隔线,外层框架不滚动。框架铺满外壳的布局视口,在键盘抬起时保留原有高度。抽屉背景和遮罩覆盖外壳完整高度,包括 `shell.bottom`,并显示在全屏和浮动右侧面板之上。浏览器管理的状态栏和地址栏位于外壳之外;布局不添加模拟安全区边距。 +抽屉遮罩使用与面板相同的时长和缓动,在打开与关闭时渐变透明度;减少动态效果时两者均不播放动画。从已收起的侧栏切换到抽屉布局时,抽屉立即隐藏;已展开的侧栏保留收起动画。抽屉边缘不绘制分隔线,外层框架不滚动。macOS 抽屉与 Web 使用相同的不透明背景,毛玻璃仅用于普通侧栏。框架铺满外壳的布局视口,在键盘抬起时保留原有高度。抽屉背景和遮罩覆盖外壳完整高度,包括 `shell.bottom`,并显示在全屏和浮动右侧面板之上。浏览器管理的状态栏和地址栏位于外壳之外;布局不添加模拟安全区边距。 全局面板占据 root 作用域的 `main` keyed slot;`conversation` 是为会话界面保留的 key。`ctx.layout.selectPanel(id)` 选中已注册面板,`null` 则选中会话界面,但不改变当前会话。Plugins 页面是默认组合中的一个全局面板。 diff --git a/packages/client/ui-layout/src/client/AppFrame.module.css b/packages/client/ui-layout/src/client/AppFrame.module.css index 3be77665ef..0840483b53 100644 --- a/packages/client/ui-layout/src/client/AppFrame.module.css +++ b/packages/client/ui-layout/src/client/AppFrame.module.css @@ -120,7 +120,7 @@ --dsh-frame-chrome-top: 0px; } -:global([data-platform='darwin']) .sidebarCol { +:global([data-platform='darwin']) .frame:not([data-sidebar-drawer]) .sidebarCol { /* A blue wash over the top of the translucent tint, gone by a third of the column; a faint blue-grey-violet rises through the bottom third; and a faint blue cast is mixed into the tint itself. Every layer stays @@ -139,7 +139,7 @@ /* Dark drops the blue cast (the light-mode wash reads purple over dark fills) and thins the fill so more wallpaper shows; the washes stay. */ -:global([data-platform='darwin']) :global([data-ds-dark-theme]) .sidebarCol { +:global([data-platform='darwin']) :global([data-ds-dark-theme]) .frame:not([data-sidebar-drawer]) .sidebarCol { background: linear-gradient( to bottom, @@ -156,8 +156,8 @@ them with a 90%-opaque fill of the same token in both themes (the dark selector repeats so this outranks its thinner fill). */ @media (prefers-reduced-transparency: reduce) { - :global([data-platform='darwin']) .sidebarCol, - :global([data-platform='darwin']) :global([data-ds-dark-theme]) .sidebarCol { + :global([data-platform='darwin']) .frame:not([data-sidebar-drawer]) .sidebarCol, + :global([data-platform='darwin']) :global([data-ds-dark-theme]) .frame:not([data-sidebar-drawer]) .sidebarCol { background: color-mix(in srgb, var(--dsw-specific-sidebar-fill) 90%, transparent); } } diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css index 410d7af8a3..8d877324a5 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css @@ -444,22 +444,22 @@ /* macOS: the themed elevated fill reads as a foreign slab on the vibrancy sidebar; a white wash sits in the glass like native macOS sidebar controls. Dark keeps the same white ink at lower alpha. */ -:global([data-platform='darwin']) .newSession { +:global([data-platform='darwin']) :where(.root:not([role='dialog'])) .newSession { background: rgb(255 255 255 / 0.55); } @media (hover: hover) { - :global([data-platform='darwin']) .newSession:hover { + :global([data-platform='darwin']) :where(.root:not([role='dialog'])) .newSession:hover { background: rgb(255 255 255 / 0.3); } } -:global([data-platform='darwin']) :global([data-ds-dark-theme]) .newSession { +:global([data-platform='darwin']) :global([data-ds-dark-theme]) :where(.root:not([role='dialog'])) .newSession { background: rgb(255 255 255 / 0.15); } @media (hover: hover) { - :global([data-platform='darwin']) :global([data-ds-dark-theme]) .newSession:hover { + :global([data-platform='darwin']) :global([data-ds-dark-theme]) :where(.root:not([role='dialog'])) .newSession:hover { background: rgb(255 255 255 / 0.2); } } diff --git a/packages/client/web/src/base.css b/packages/client/web/src/base.css index af74988cf1..6cbd165efc 100644 --- a/packages/client/web/src/base.css +++ b/packages/client/web/src/base.css @@ -56,6 +56,11 @@ html[data-platform='darwin'] [data-window-drag] { -webkit-app-region: drag; } +/* Inactive chrome must not cover a foreground drawer's controls with native drag geometry. */ +html[data-platform='darwin'] [inert] [data-window-drag] { + -webkit-app-region: no-drag; +} + /* One-frame app-region pulse that forces Electron to recollect the window's drag rects (electron#32341): a change to a computed app-region value is what dirties them. The shell's window drag watcher (window-drag/recall.ts) sets this