From 407d7a1475818c84840c3373bd808b6e421e8774 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 5 Oct 2026 23:08:46 +0800 Subject: [PATCH 1/2] fix(client): keep the image lightbox clear of the Windows caption --- apps/web/tests/markdown-images.e2e.ts | 13 ++++++++++++- packages/client/ui-primitives/README.i18n.yaml | 4 ++-- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../ui-primitives/src/ImageLightbox.module.css | 10 +++++----- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/web/tests/markdown-images.e2e.ts b/apps/web/tests/markdown-images.e2e.ts index 122abebc73..4031ed8562 100644 --- a/apps/web/tests/markdown-images.e2e.ts +++ b/apps/web/tests/markdown-images.e2e.ts @@ -289,7 +289,18 @@ describe('web e2e: Markdown image rendering', () => { await page.getByRole('button', { name: `View full image: ${WORKSPACE_ALT}`, exact: true }).click() const lightbox = page.getByRole('dialog', { name: 'Image preview', exact: true }) await lightbox.waitFor() - await lightbox.getByRole('button', { name: 'Close image preview' }).click() + const lightboxMask = lightbox.locator(':scope > [aria-hidden="true"]') + const lightboxClose = lightbox.getByRole('button', { name: 'Close image preview' }) + for (const windows of [true, false]) { + await page.evaluate((value) => { + document.documentElement.toggleAttribute('data-windows-titlebar', value) + document.documentElement.style.setProperty('--dsh-windows-titlebar-height', value ? '40px' : '') + }, windows) + // The Windows caption stays unpainted and the close control sits below it. + expect((await lightboxMask.boundingBox())?.y).toBe(windows ? 40 : 0) + expect((await lightboxClose.boundingBox())?.y).toBe(windows ? 60 : 20) + } + await lightboxClose.click() await lightbox.waitFor({ state: 'detached' }) const link = page.getByRole('button', { name: 'View comparison', exact: true }) await link.hover() diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 2a527cf249..566395b051 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -24,8 +24,8 @@ en: f0e54915b896af5a zh: 2047520fd321be90 /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: d79e20306593c4bf - zh: 5c0f1100e2cd6fcc + en: 4a1a61bb2d64fe4c + zh: 1b0097fff440c2ef /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: 4aef65dea11de083 zh: abc22cd7c65cdf38 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 9ce34c7095..5f92aaec70 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -105,7 +105,7 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `JsonTree` clamps collapsed strings to `collapsedStringLines` (three by default). Expanded strings show raw text, retain sibling commas, and fit within the window and outer scrolling containers. Resize and ancestor-scroll events update that limit. Row copy feedback updates independently of JSON value rendering; pending clipboard writes cannot update a different row or an unmounted tree. -`ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. +`ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. Like `Modal`, its mask leaves the Windows caption unpainted, and its close control sits 20px below the caption. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. ### Localizing copy diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 92a8de27b0..e2c1b8f46f 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -105,7 +105,7 @@ kind: "package-library" `JsonTree` 把折叠字符串限制为 `collapsedStringLines` 行(默认三行)。展开后显示原始文本、保留同级逗号,并限制在窗口与外层滚动容器内;尺寸变化和祖先滚动事件会更新此限制。行复制反馈独立于 JSON 值渲染更新;尚未完成的剪贴板写入不会更新另一行或已卸载的树。 -`ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 +`ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。与 `Modal` 相同,其遮罩不覆盖 Windows 顶栏,关闭按钮位于顶栏下方 20px。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 ### 本地化文案 diff --git a/packages/client/ui-primitives/src/ImageLightbox.module.css b/packages/client/ui-primitives/src/ImageLightbox.module.css index 1907fe930f..d24769cefd 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.module.css +++ b/packages/client/ui-primitives/src/ImageLightbox.module.css @@ -4,13 +4,13 @@ z-index: 1000; display: grid; place-items: center; - padding: 40px; + padding: calc(40px + var(--dsh-frame-chrome-top, 0px)) 40px 40px; } -/* Shared dimming mask below the preview and its controls. */ +/* Shared dimming mask below the preview and its controls; like Modal, it leaves the Windows caption unpainted. */ .mask { position: absolute; - inset: 0; + inset: var(--dsh-frame-chrome-top, 0px) 0 0; background: var(--dsw-alias-bg-mask-1); backdrop-filter: var(--dsw-mask-blur); } @@ -18,7 +18,7 @@ .image { position: relative; max-width: min(100%, 1600px); - max-height: calc(100vh - 80px); + max-height: calc(100vh - 80px - var(--dsh-frame-chrome-top, 0px)); object-fit: contain; border-radius: var(--dsw-radius-lg); background: var(--dsw-specific-input-major); @@ -28,7 +28,7 @@ .close { position: fixed; - top: 20px; + top: calc(20px + var(--dsh-frame-chrome-top, 0px)); right: 20px; z-index: 1; display: grid; From e127ed2d6cd2b51cfc256fc6e7e808aedd3fcfc0 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 5 Oct 2026 23:55:51 +0800 Subject: [PATCH 2/2] refactor(client): position the lightbox in a stage below the Windows caption --- apps/web/tests/markdown-images.e2e.ts | 2 +- .../src/ImageLightbox.module.css | 24 ++++++++++++------- .../ui-primitives/src/ImageLightbox.tsx | 12 ++++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/web/tests/markdown-images.e2e.ts b/apps/web/tests/markdown-images.e2e.ts index 4031ed8562..390675a3d1 100644 --- a/apps/web/tests/markdown-images.e2e.ts +++ b/apps/web/tests/markdown-images.e2e.ts @@ -289,7 +289,7 @@ describe('web e2e: Markdown image rendering', () => { await page.getByRole('button', { name: `View full image: ${WORKSPACE_ALT}`, exact: true }).click() const lightbox = page.getByRole('dialog', { name: 'Image preview', exact: true }) await lightbox.waitFor() - const lightboxMask = lightbox.locator(':scope > [aria-hidden="true"]') + const lightboxMask = lightbox.locator(':scope > div > [aria-hidden="true"]') const lightboxClose = lightbox.getByRole('button', { name: 'Close image preview' }) for (const windows of [true, false]) { await page.evaluate((value) => { diff --git a/packages/client/ui-primitives/src/ImageLightbox.module.css b/packages/client/ui-primitives/src/ImageLightbox.module.css index d24769cefd..4cffa16c36 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.module.css +++ b/packages/client/ui-primitives/src/ImageLightbox.module.css @@ -1,16 +1,24 @@ +/* Full-viewport layer that blocks background clicks, including on the Windows caption. */ .backdrop { position: fixed; inset: 0; z-index: 1000; - display: grid; - place-items: center; - padding: calc(40px + var(--dsh-frame-chrome-top, 0px)) 40px 40px; } -/* Shared dimming mask below the preview and its controls; like Modal, it leaves the Windows caption unpainted. */ -.mask { +/* Painted area below the Windows caption; like Modal, the caption stays unpainted. */ +.stage { position: absolute; inset: var(--dsh-frame-chrome-top, 0px) 0 0; + display: flex; + align-items: center; + justify-content: center; + padding: 40px; +} + +/* Shared dimming mask below the preview and its controls. */ +.mask { + position: absolute; + inset: 0; background: var(--dsw-alias-bg-mask-1); backdrop-filter: var(--dsw-mask-blur); } @@ -18,7 +26,7 @@ .image { position: relative; max-width: min(100%, 1600px); - max-height: calc(100vh - 80px - var(--dsh-frame-chrome-top, 0px)); + max-height: 100%; object-fit: contain; border-radius: var(--dsw-radius-lg); background: var(--dsw-specific-input-major); @@ -27,8 +35,8 @@ } .close { - position: fixed; - top: calc(20px + var(--dsh-frame-chrome-top, 0px)); + position: absolute; + top: 20px; right: 20px; z-index: 1; display: grid; diff --git a/packages/client/ui-primitives/src/ImageLightbox.tsx b/packages/client/ui-primitives/src/ImageLightbox.tsx index 893ed2fe95..1cb3fe4a8c 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.tsx +++ b/packages/client/ui-primitives/src/ImageLightbox.tsx @@ -54,11 +54,13 @@ export function ImageLightbox({ src, alt, labels, onClose }: { aria-modal="true" aria-label={labels.dialog} > -