소스 검색

WIP: 增加 DOCX 正文左右留白

[gstack-context]
Decisions: DOCX 页面适配预览宽度,正文层使用 24-36px 响应式左右内边距并限制 Flex 最小宽度,避免留白撑宽页面。
Remaining: 重跑 Vitest、完整 Playwright 并做最终差异复审。
Tried: 仅调整包装层没有解决正文贴边;仅给 article 加 padding 会把真实页面由 912px 撑到 982px。
Skill: /investigate
[/gstack-context]
samien 2 달 전
부모
커밋
d04669cc09
2개의 변경된 파일과 16개의 추가작업 그리고 6개의 파일을 삭제
  1. 1 1
      src/client/components/library/PreviewInspector.vue
  2. 15 5
      tests/e2e/workspace.spec.ts

+ 1 - 1
src/client/components/library/PreviewInspector.vue

@@ -123,7 +123,7 @@ watch(() => props.item.id, loadRichPreview, { immediate: true, flush: 'post' })
 .preview-inspector { display: flex; height: 100%; min-height: 0; flex-direction: column; overflow: hidden; }
 header { display: flex; min-height: 56px; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--line); } header > strong { min-width: 0; flex: 1; overflow: hidden; font-size: 12px; font-weight: 630; text-overflow: ellipsis; white-space: nowrap; }.preview-header-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 3px; }.preview-header-actions .icon-button { display: grid; width: 32px; height: 32px; place-items: center; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; text-decoration: none; }.preview-header-actions .icon-button:hover { background: var(--surface-soft); color: var(--text-primary); }
 .preview-stage { display: grid; min-height: 0; flex: 1 1 auto; place-items: center; overflow: hidden; background: var(--surface-soft); touch-action: pan-y; }.preview-stage img, .preview-stage video { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }.preview-stage audio { width: calc(100% - 34px); }.preview-stage object { width: 100%; height: 100%; border: 0; }.preview-text { width: 100%; height: 100%; box-sizing: border-box; padding: 20px 28px 28px; overflow: auto; background: var(--surface-raised); }.preview-text pre { margin: 0; color: var(--text-secondary); font: 10px/1.65 var(--font-code); white-space: pre-wrap; overflow-wrap: anywhere; }
-.preview-office { width: 100%; height: 100%; overflow: auto; background: #e6e6e4; color: #111; }.preview-office :deep(.docx-wrapper) { width: 100%; min-width: 0; box-sizing: border-box; align-items: flex-start; padding: 18px; background: #e6e6e4; }.preview-office :deep(.docx) { margin-inline: auto; margin-bottom: 14px; box-shadow: 0 4px 18px rgba(0,0,0,.12); }
+.preview-office { width: 100%; height: 100%; overflow: auto; background: #e6e6e4; color: #111; }.preview-office :deep(.docx-wrapper) { width: 100%; min-width: 0; box-sizing: border-box; align-items: flex-start; padding: 18px; background: #e6e6e4; }.preview-office :deep(.docx) { width: 100%; min-width: 0; max-width: 100%; margin-inline: auto; margin-bottom: 14px; box-shadow: 0 4px 18px rgba(0,0,0,.12); }.preview-office :deep(.docx > article) { width: 100%; min-width: 0; box-sizing: border-box; padding-inline: clamp(24px, 3vw, 36px); }
 .preview-pdf { display: flex; width: 100%; height: 100%; box-sizing: border-box; align-items: center; flex-direction: column; gap: 12px; padding: 12px; overflow: auto; background: #dadad7; }.preview-pdf :deep(.pdf-page) { flex: 0 0 auto; overflow: hidden; background: #fff; box-shadow: 0 3px 14px rgba(0,0,0,.15); }.preview-pdf :deep(canvas) { display: block; }
 .preview-sheet { width: 100%; height: 100%; overflow: auto; background: var(--surface-raised); }.preview-sheet table { min-width: 100%; border-collapse: collapse; font-size: 9px; }.preview-sheet td { min-width: 72px; max-width: 240px; padding: 6px 8px; overflow: hidden; border: 1px solid var(--line); color: var(--text-secondary); text-overflow: ellipsis; white-space: nowrap; }.preview-sheet tr:first-child td { position: sticky; top: 0; background: var(--surface-soft); color: var(--text-primary); font-weight: 600; }
 .preview-link { display: flex; width: calc(100% - 34px); flex-direction: column; align-items: center; color: var(--text-secondary); text-decoration: none; text-align: center; }.preview-link > span, .preview-unavailable > span { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 12px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-raised); }.preview-link strong { max-width: 100%; overflow: hidden; color: var(--text-primary); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }.preview-link small { margin-top: 5px; color: var(--text-muted); font-size: 9px; }

+ 15 - 5
tests/e2e/workspace.spec.ts

@@ -158,7 +158,7 @@ test('previews an octet-stream Markdown file from the file library', async ({ pa
   await expect(emptyDialog.locator('.preview-unavailable')).toHaveCount(0)
 })
 
-test('keeps the left edge reachable for a wide DOCX preview', async ({ page }) => {
+test('keeps DOCX text inset without horizontal clipping', async ({ page }) => {
   await page.setViewportSize({ width: 620, height: 760 })
   await page.route('**/api/app/files**', async route => {
     const url = new URL(route.request().url())
@@ -204,8 +204,10 @@ test('keeps the left edge reachable for a wide DOCX preview', async ({ page }) =
   const geometry = await office.evaluate(element => {
     const wrapper = element.querySelector<HTMLElement>('.docx-wrapper')!
     const pageElement = element.querySelector<HTMLElement>('section.docx')!
+    const article = pageElement.querySelector<HTMLElement>('article')!
     const officeRect = element.getBoundingClientRect()
     const pageRect = pageElement.getBoundingClientRect()
+    const articleStyle = getComputedStyle(article)
     return {
       officeLeft: officeRect.left,
       pageLeft: pageRect.left,
@@ -213,14 +215,20 @@ test('keeps the left edge reachable for a wide DOCX preview', async ({ page }) =
       clientWidth: element.clientWidth,
       scrollWidth: element.scrollWidth,
       wrapperWidth: wrapper.getBoundingClientRect().width,
+      articlePaddingLeft: Number.parseFloat(articleStyle.paddingLeft),
+      articlePaddingRight: Number.parseFloat(articleStyle.paddingRight),
+      articleMinWidth: articleStyle.minWidth,
     }
   })
-  expect(geometry.pageWidth).toBeGreaterThan(geometry.clientWidth)
+  expect(geometry.pageWidth).toBeLessThanOrEqual(geometry.clientWidth)
   expect(geometry.pageLeft).toBeGreaterThanOrEqual(geometry.officeLeft)
-  expect(geometry.scrollWidth).toBeGreaterThanOrEqual(geometry.pageWidth)
+  expect(geometry.scrollWidth).toBeLessThanOrEqual(geometry.clientWidth + 1)
   expect(Math.abs(geometry.wrapperWidth - geometry.clientWidth)).toBeLessThan(1)
+  expect(geometry.articlePaddingLeft).toBeGreaterThanOrEqual(24)
+  expect(geometry.articlePaddingRight).toBeGreaterThanOrEqual(24)
+  expect(geometry.articleMinWidth).toBe('0px')
 
-  await page.setViewportSize({ width: 1200, height: 760 })
+  await page.setViewportSize({ width: 930, height: 760 })
   const centered = await office.evaluate(element => {
     const pageElement = element.querySelector<HTMLElement>('section.docx')!
     const officeRect = element.getBoundingClientRect()
@@ -233,7 +241,9 @@ test('keeps the left edge reachable for a wide DOCX preview', async ({ page }) =
     }
   })
   expect(centered.pageWidth).toBeLessThan(centered.clientWidth)
-  expect(Math.abs(centered.leftGap - centered.rightGap)).toBeLessThan(1)
+  expect(centered.leftGap).toBeGreaterThanOrEqual(18)
+  expect(centered.rightGap).toBeGreaterThanOrEqual(18)
+  expect(Math.abs(centered.leftGap - centered.rightGap)).toBeLessThanOrEqual(12)
 })
 
 test('keeps pins first and loads the next session page at the list bottom', async ({ page }) => {