diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 7812b5b6b6..fa20d41683 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -70,6 +70,10 @@ async function copyPdfText(page: Page, preview: Locator, expected: string): Prom const text = preview.locator('[data-pdf-text] span:not(.markedContent)').filter({ hasText: expected }).first() await text.waitFor({ state: 'visible' }) await expect.poll(() => text.evaluate(node => getComputedStyle(node).userSelect)).toBe('text') + await expect.poll(() => text.evaluate(node => ({ + background: getComputedStyle(node, '::selection').backgroundColor, + color: getComputedStyle(node, '::selection').color, + }))).toEqual({ background: 'color(srgb 0.231373 0.509804 0.964706 / 0.4)', color: 'rgba(0, 0, 0, 0)' }) await text.click({ clickCount: 3 }) await expect.poll(() => page.evaluate(() => window.getSelection()?.toString().trim())).toBe(expected) await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: new URL(page.url()).origin }) @@ -938,6 +942,7 @@ else process.exit(1); await page.emulateMedia({ colorScheme }) await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe(colorScheme === 'dark' ? '' : null) await expectPdfPageSpacing(preview) + await copyPdfText(page, preview, 'Selectable PDF text') await successShot(page, `pdf-spacing-${colorScheme}`) } expect(await body.evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true) @@ -1033,14 +1038,16 @@ else process.exit(1); expect(forward).toContain('THREE TASKS') expect(forward).not.toContain('REFLECTION') expect(forward).not.toContain('AFTER TABLE') + expect(await page.evaluate(() => window.getSelection()?.toString())).toBe(forward) const backward = await drag(priority, start) expect(backward).toContain('THREE TASKS') expect(backward).not.toContain('REFLECTION') expect(backward).not.toContain('AFTER TABLE') + expect(await page.evaluate(() => window.getSelection()?.toString())).toBe(backward) expect(await selectionLayer.locator('br').first().evaluate(node => getComputedStyle(node, '::selection').backgroundColor)) .toBe('rgba(0, 0, 0, 0)') await successShot(page, 'pdf-drag-selection') - sections.push('## PDF drag selection\n\n- Table selection: forward and backward drags exclude later sections\n- Line-break highlight: transparent') + sections.push('## PDF drag selection\n\n- Table selection: forward and backward drags exclude later sections\n- Line-break highlight: transparent\n- Text selection: translucent blue in light and dark themes; canvas text remains visible\n- Mouse release preserves the selected text') const pngResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/workspaceFiles/readBytes' && (response.request().postDataJSON() as { payload: { args: { path: string } } }).payload.args.path === 'tiny.png') @@ -1584,6 +1591,7 @@ describe.skipIf(MODE === 'record')('web e2e: Host Office preview', () => { await expect.poll(() => preview.locator('[data-pdf-preview]').evaluate(node => getComputedStyle(node).backgroundColor)) .toBe(colorScheme === 'dark' ? 'rgb(21, 21, 23)' : 'rgb(235, 238, 242)') await expectPdfPageSpacing(preview) + await copyPdfText(page, preview, '中文文档') await successShot(page, `office-background-${colorScheme}`) } await (await revealDocumentZoom(page, preview)).click() @@ -1695,6 +1703,7 @@ describe.skipIf(MODE === 'record')('web e2e: Host Office preview', () => { await compareOrRefreshGolden(fileURLToPath(new URL('./expected/office-font-notice.md', import.meta.url)), [ '# Office font warning', '', '- Document preparation: centered 28px spinner with visible rendering status in both themes', + '- Text selection: translucent blue with transparent overlay text in both themes; Chinese text copies unchanged', '- Document backdrop: cool light grey in light mode; matte black in dark mode', '- Word and PowerPoint paper layout: 12px page gaps and outer backdrop insets', '- Warning precedes reload in the same toolbar: true', @@ -1719,6 +1728,7 @@ describe.skipIf(MODE === 'record')('web e2e: Host Office preview', () => { await officeZoom.click() await page.getByRole('menuitem', { name: '150%', exact: true }).click() await expectPdfResolution(canvas) + if (extension === 'pptx') await copyPdfText(page, preview, '中文文档') if (['doc', 'ppt'].includes(extension)) expect(await warning.count()).toBe(0) if (extension === 'pptx') { await preview.locator('[data-document-zoom-scrollport]').evaluate((node) => { diff --git a/apps/web/tests/expected/office-font-notice.md b/apps/web/tests/expected/office-font-notice.md index 5f5690ae83..bb4a3fc2d0 100644 --- a/apps/web/tests/expected/office-font-notice.md +++ b/apps/web/tests/expected/office-font-notice.md @@ -1,6 +1,7 @@ # Office font warning - Document preparation: centered 28px spinner with visible rendering status in both themes +- Text selection: translucent blue with transparent overlay text in both themes; Chinese text copies unchanged - Document backdrop: cool light grey in light mode; matte black in dark mode - Word and PowerPoint paper layout: 12px page gaps and outer backdrop insets - Warning precedes reload in the same toolbar: true diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index b08c3bdec3..de4c4d2265 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -21,8 +21,8 @@ en: 949156aa02ec8b07 zh: d3675754e2a37b5d /deepseek-ai-dsh-client-ui-sidebar-documentpreview/how-it-reads: - en: 5d8bf2e43d9b6dd0 - zh: b0d2d1d5a6f520db + en: 5cdbd4a218315e70 + zh: d5054f541e0e0275 /deepseek-ai-dsh-client-ui-sidebar-documentpreview/excel-preview: en: b99b0e0b8efc2853 zh: 4327352d1334fc6b diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index e07dc0835c..d420c2f34e 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -63,7 +63,7 @@ PNG, JPEG, GIF, WebP, BMP, ICO, and SVG render through Blob URLs in an `` s Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its localized labels. PDF and converted Office previews use a cool light-grey background in light mode and a matte-black background in dark mode, with subtle page shadows and original document colors. PDF and image previews share a floating control with **Fit width**, 25%, 50%, 100%, 150%, and 200% choices plus 25% incremental zoom across the 25%–400% fixed range. Fit width is the default and tracks pane resizing; 100% means the PDF's 96-DPI page size or the image's intrinsic CSS-pixel size. On fine pointers, the control starts hidden, slides up when the pointer enters the bottom 72 CSS pixels, and slides down after a 420 ms leave delay; hover, keyboard focus, an open menu, and an active gesture keep it visible, while devices without hover keep it visible continuously. Chromium maps a macOS trackpad pinch to the same live, pointer-anchored path as Ctrl+wheel; the control displays the gesture percentage, and the settled value survives body remounts until the tab closes. HTML previews do not expose zoom controls. After zoom settles or fit width changes, nearby PDF pages redraw at the resolved scale multiplied by the device pixel ratio. The previous bitmap and selectable text stay visible until the replacement bitmap is ready; offscreen pages refresh when they approach the viewport. -Initial reads and renderer preparation, including Office conversion, share a centered 28px ongoing `StateDot` with the visible localized status “Rendering document...”. The status exposes the same accessible name and respects reduced-motion preferences. Additional text pages keep a compact inline spinner while loaded content stays visible. The PDF body loads its package-local `client.pdf.js` chunk only when a PDF preview mounts; PDF.js, its Worker source, and embedded support data stay out of the startup `client.js`. PDF and converted Word/PowerPoint pages form a vertical sequence with 12px page gaps and a 12px outer inset showing the theme-specific document background; fit width reserves this inset, and pages render lazily near the viewport; an unrendered page holds its place as a quiet 3:4 placeholder block. PDF.js’s official TextLayerBuilder manages selection boundaries and normalized copying over an aligned text layer. Its companion styles keep blank line breaks unhighlighted; alignment accounts for PDF page units, page rotation, and viewport resizing, and page disposal cancels both layers. Image-only PDFs contain no selectable text. Code previews show a tertiary-colored language label and a copy icon with a tooltip; wrapping remains in the document toolbar. They show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill; its banner is adjacent to a full-height inner scrollport, so both scrollbars begin below the copy control. +Initial reads and renderer preparation, including Office conversion, share a centered 28px ongoing `StateDot` with the visible localized status “Rendering document...”. The status exposes the same accessible name and respects reduced-motion preferences. Additional text pages keep a compact inline spinner while loaded content stays visible. The PDF body loads its package-local `client.pdf.js` chunk only when a PDF preview mounts; PDF.js, its Worker source, and embedded support data stay out of the startup `client.js`. PDF and converted Word/PowerPoint pages form a vertical sequence with 12px page gaps and a 12px outer inset showing the theme-specific document background; fit width reserves this inset, and pages render lazily near the viewport; an unrendered page holds its place as a quiet 3:4 placeholder block. PDF.js’s official TextLayerBuilder manages selection boundaries and normalized copying over an aligned text layer. Selections use translucent blue in both themes while the canvas retains the document text; blank line breaks remain unhighlighted; alignment accounts for PDF page units, page rotation, and viewport resizing, and page disposal cancels both layers. Image-only PDFs contain no selectable text. Code previews show a tertiary-colored language label and a copy icon with a tooltip; wrapping remains in the document toolbar. They show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill; its banner is adjacent to a full-height inner scrollport, so both scrollbars begin below the copy control. ## Excel preview diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 9c067d0841..77e3cc79a8 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -63,7 +63,7 @@ PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `` 静态 共享文案来自 `sidebarDocumentPreview`;各内置渲染器拥有自己的本地化标签。PDF 与转换后的 Office 预览在浅色模式下使用冷调浅灰底色,在深色模式下使用哑黑底色,页面带有轻微阴影并保留文档原色。PDF 与图片预览共用浮动控件,提供**适应宽度**、25%、50%、100%、150% 和 200% 选项,并在 25%–400% 固定范围内按 25% 逐级缩放。适应宽度是默认模式并跟随面板尺寸;100% 表示 PDF 的 96 DPI 页面尺寸或图片的固有 CSS 像素尺寸。在精细指针设备上,控件初始隐藏;指针进入底部 72 个 CSS 像素时向上滑入,离开 420 ms 后向下退出;控件悬停、键盘焦点、菜单展开和手势进行期间保持显示,无 hover 的设备则始终显示。Chromium 将 macOS 触控板捏合映射到与 Ctrl+滚轮相同的实时指针锚定路径;控件同步显示手势比例,静止后的值在正文重新挂载后继续保留,直至 tab 关闭。HTML 预览不显示缩放控件。缩放稳定或适应宽度发生变化后,视口附近的 PDF 页面按最终缩放比例乘以设备像素比重绘。新位图完成前保留原有画面和可选文字层;屏幕外的页面在接近视口时更新。 -首次读取及渲染器准备(包括 Office 转换)共用居中的 28px ongoing `StateDot`,下方显示本地化状态文字“文档渲染中...”。状态向辅助技术提供相同的名称,并遵循减少动态效果偏好。追加文本页时使用紧凑的行内转圈,同时保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 `client.pdf.js` chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 `client.js`。PDF 与转换后的 Word/PowerPoint 页面组成纵向序列,页间距与四周留白均为 12px,露出当前主题的文档背景色;适应宽度会预留此留白,页面在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。配套样式不高亮空白换行;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览使用 tertiary 色的语言名称和带提示的复制图标;换行仍由文档工具栏控制。默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。 +首次读取及渲染器准备(包括 Office 转换)共用居中的 28px ongoing `StateDot`,下方显示本地化状态文字“文档渲染中...”。状态向辅助技术提供相同的名称,并遵循减少动态效果偏好。追加文本页时使用紧凑的行内转圈,同时保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 `client.pdf.js` chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 `client.js`。PDF 与转换后的 Word/PowerPoint 页面组成纵向序列,页间距与四周留白均为 12px,露出当前主题的文档背景色;适应宽度会预留此留白,页面在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。两种主题均使用半透明蓝色选区,画布保留文档文字,空白换行不高亮;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览使用 tertiary 色的语言名称和带提示的复制图标;换行仍由文档工具栏控制。默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。 ## Excel 预览 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/pdf/PdfBody.module.css b/packages/client/ui-sidebar-documentpreview/src/client/pdf/PdfBody.module.css index a5de55af20..5caa32ed0f 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/pdf/PdfBody.module.css +++ b/packages/client/ui-sidebar-documentpreview/src/client/pdf/PdfBody.module.css @@ -111,7 +111,7 @@ } .text ::selection { - background: var(--dsw-alias-interactive-bg-hover-accent); + background: var(--dsw-alias-bg-document-selection); color: transparent; } diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index 7ade0ae00d..e4f824eddd 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -30,8 +30,8 @@ en: 8fbfdb050e880f9b zh: dd201f049036c5c4 /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets: - en: cb63405e3ab1f395 - zh: 2a40c303286c8ea0 + en: 3c3b40e770dec2b1 + zh: 654c80a5bf4d85fa /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding: en: bf76e7c34a331595 zh: 5accd0260ef7c61e diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index b0f2c7510d..24d20328a6 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -55,7 +55,7 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou `base.css` owns the shared radius scale and settings-card material aliases. The material aliases resolve on `body`, alongside the active palette. Follow [Web styling](../../../docs/web-styling.md#corner-radii-and-settings-cards) when choosing component radii. -`src/styles/` holds eight sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `focus.css`, `onboarding.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `design-platform.css` also owns the code-diff fill aliases and their static alpha palette entries, plus the `--dsw-alias-file-diff-*` code, gutter, and marker palette for file comparisons; `shiki.css` owns syntax colors. +`src/styles/` holds eight sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `focus.css`, `onboarding.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `--dsw-alias-bg-document-selection` uses blue-500 at 40% opacity in both themes for selections over original document colors. `design-platform.css` also owns the code-diff fill aliases and their static alpha palette entries, plus the `--dsw-alias-file-diff-*` code, gutter, and marker palette for file comparisons; `shiki.css` owns syntax colors. [`focus.css`](src/styles/focus.css) provides a `:focus-visible` fallback that names the ring colour through `var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))` and the standard width through `--dsw-focus-ring-width`, never the outline style — so a control that disables its outline stays paintless, and one that declares no ring keeps the standard geometry instead of Chromium's `auto 1px`. The theme resolves this blue to `#4176E6` in light mode and `#7AAAFF` in dark mode. Component outlines and focus-ring shadows use the same colour expression, including rings on descendants and pseudo-elements. `--dsw-focus-ring-width` (2px) is the standard width; dense tables and toolbars may keep 1px, and offsets remain component-owned. diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index b1c54a6608..52825fee30 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -55,7 +55,7 @@ kind: "package-reference" `base.css` 持有共享圆角尺度与设置卡片材质别名。材质别名在 `body` 上随当前色板解析。组件圆角选择遵循 [Web 样式参考](../../../docs/web-styling.zh.md#corner-radii-and-settings-cards)。 -`src/styles/` 下有八张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`focus.css`、`onboarding.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。`design-platform.css` 还负责代码差异底色的别名及其静态透明度色阶,以及文件对比所用的 `--dsw-alias-file-diff-*` 代码区、行号区和标记配色;`shiki.css` 负责语法颜色。 +`src/styles/` 下有八张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`focus.css`、`onboarding.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。`--dsw-alias-bg-document-selection` 在两种主题中均使用 40% 不透明度的 blue-500,供保留文档原色的选区使用。`design-platform.css` 还负责代码差异底色的别名及其静态透明度色阶,以及文件对比所用的 `--dsw-alias-file-diff-*` 代码区、行号区和标记配色;`shiki.css` 负责语法颜色。 [`focus.css`](src/styles/focus.css) 提供 `:focus-visible` 兜底:通过 `var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))` 声明焦点环颜色,并通过 `--dsw-focus-ring-width` 声明标准宽度,但不声明轮廓样式——因此禁用轮廓的控件仍然不绘制,而没有自有焦点环的控件保持标准几何,而不是落到 Chromium 的 `auto 1px`。主题将该蓝色解析为浅色模式的 `#4176E6` 和深色模式的 `#7AAAFF`。组件轮廓与焦点环阴影使用同一颜色表达式,包括后代和伪元素上的环。`--dsw-focus-ring-width`(2px)是标准宽度;密集表格与工具栏可以保留 1px,offset 仍由组件决定。 diff --git a/packages/client/ui-theme/src/styles/design-platform.css b/packages/client/ui-theme/src/styles/design-platform.css index 3a52fa25a0..27b5b908f0 100644 --- a/packages/client/ui-theme/src/styles/design-platform.css +++ b/packages/client/ui-theme/src/styles/design-platform.css @@ -166,6 +166,7 @@ body[data-ds-dark-theme] { body { --dsw-alias-bg-base: var(--dsw-static-neutral-bluish-00); --dsw-alias-bg-document-preview: var(--dsw-static-neutral-bluish-100); + --dsw-alias-bg-document-selection: color-mix(in srgb, var(--dsw-static-blue-500) 40%, transparent); --dsw-alias-label-document-preview: var(--dsw-static-neutral-bluish-700); --dsw-alias-bg-layer-1: var(--dsw-static-neutral-bluish-00); --dsw-alias-bg-layer-2: var(--dsw-static-neutral-bluish-00); @@ -277,6 +278,7 @@ body { body[data-ds-dark-theme] { --dsw-alias-bg-base: var(--dsw-static-neutral-bluish-950); --dsw-alias-bg-document-preview: var(--dsw-static-neutral-bluish-950); + --dsw-alias-bg-document-selection: color-mix(in srgb, var(--dsw-static-blue-500) 40%, transparent); --dsw-alias-label-document-preview: var(--dsw-static-neutral-bluish-300); --dsw-alias-bg-layer-1: var(--dsw-static-neutral-bluish-875); --dsw-alias-bg-layer-2: var(--dsw-static-neutral-bluish-850); diff --git a/snapshots/web/document-preview/document.expected.md b/snapshots/web/document-preview/document.expected.md index e81f834d08..b1093c40d5 100644 --- a/snapshots/web/document-preview/document.expected.md +++ b/snapshots/web/document-preview/document.expected.md @@ -60,6 +60,8 @@ - Table selection: forward and backward drags exclude later sections - Line-break highlight: transparent +- Text selection: translucent blue in light and dark themes; canvas text remains visible +- Mouse release preserves the selected text ## Image zoom