diff --git a/apps/web/tests/markdown-images.e2e.ts b/apps/web/tests/markdown-images.e2e.ts index 122abebc73..390675a3d1 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 > div > [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 fa11b9567d..06cd120754 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 d627534fdd..509fdd62fe 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -106,7 +106,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 82d0419260..1f458060ed 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -106,7 +106,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..4cffa16c36 100644 --- a/packages/client/ui-primitives/src/ImageLightbox.module.css +++ b/packages/client/ui-primitives/src/ImageLightbox.module.css @@ -1,9 +1,17 @@ +/* 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; +} + +/* 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; } @@ -18,7 +26,7 @@ .image { position: relative; max-width: min(100%, 1600px); - max-height: calc(100vh - 80px); + max-height: 100%; object-fit: contain; border-radius: var(--dsw-radius-lg); background: var(--dsw-specific-input-major); @@ -27,7 +35,7 @@ } .close { - position: fixed; + position: absolute; top: 20px; right: 20px; z-index: 1; 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} > -