diff --git a/.gitignore b/.gitignore index 37e0c34..d57d635 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules dist/ *.local +*.tsbuildinfo # mimosa 扫描钩子的本地缓存(scripts/hooks 写入,不入库) .mimosa diff --git a/src/previewfit.ts b/src/previewfit.ts new file mode 100644 index 0000000..ba975f5 --- /dev/null +++ b/src/previewfit.ts @@ -0,0 +1,50 @@ +// docx 分页纸面按预览栏宽度等比缩小。只缩不放:栏宽够放下整页时保持 100%。 +// gutter 与 styles.css 里 .docx-wrapper 的左右 padding(12px + 12px)对齐。 + +export function computeFitScale(containerWidth: number, pageWidth: number, gutter: number): number { + if (!Number.isFinite(containerWidth) || !Number.isFinite(pageWidth) || pageWidth <= 0) return 1 + if (!Number.isFinite(gutter)) return 1 + const available = containerWidth - gutter + if (!(available > 0)) return 1 + return Math.min(1, available / pageWidth) +} + +const PAGE_GUTTER_PX = 24 + +/** host 是 docx-preview 的渲染根(内含 .docx-wrapper);el 是用来量栏宽的元素。 + * 拖动分隔条时 el 变宽,ResizeObserver 重算。返回的函数卸下监听并清掉行内缩放。 */ +export function fitPagesToWidth(host: HTMLElement, el: HTMLElement): () => void { + const apply = () => { + const wrapper = host.querySelector('.docx-wrapper') + const page = host.querySelector('section.docx') + if (!wrapper || !page) return + const scale = computeFitScale(el.clientWidth, page.offsetWidth, PAGE_GUTTER_PX) + wrapper.style.transformOrigin = 'top left' + wrapper.style.transform = scale < 1 ? `scale(${scale})` : '' + const nextHeight = `${Math.round(wrapper.offsetHeight * scale)}px` + if (host.style.height !== nextHeight) host.style.height = nextHeight + host.style.overflow = 'hidden' + host.style.minHeight = '0' + } + apply() + if (typeof ResizeObserver === 'undefined') { + return () => clearFit(host) + } + const observer = new ResizeObserver(() => apply()) + observer.observe(el) + return () => { + observer.disconnect() + clearFit(host) + } +} + +function clearFit(host: HTMLElement) { + const wrapper = host.querySelector('.docx-wrapper') + if (wrapper) { + wrapper.style.transform = '' + wrapper.style.transformOrigin = '' + } + host.style.height = '' + host.style.overflow = '' + host.style.minHeight = '' +} diff --git a/src/previewsetup.ts b/src/previewsetup.ts index bf2e268..9c7595f 100644 --- a/src/previewsetup.ts +++ b/src/previewsetup.ts @@ -4,6 +4,7 @@ import { configureFilePreviewLoaders, configureLinkPreviewFetcher, renderMarkdown, setSafeHTML, stripUnsafeLinks } from '@ui' import { auth } from './auth' import type { LinkPreviewData } from '@ui' +import { fitPagesToWidth } from './previewfit' const fetchBuf = async (url: string | undefined): Promise => { const r = await fetch(url!) @@ -43,7 +44,7 @@ const docxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?: el.innerHTML = '' await renderAsync(buf, el, el, { className: 'docx', inWrapper: true, breakPages: true }) stripUnsafeLinks(el) - return + return fitPagesToWidth(el, el) } catch (err) { console.error('[preview:docx] page render failed, falling back', source.name, err) } diff --git a/src/styles.css b/src/styles.css index 04b2012..3016203 100644 --- a/src/styles.css +++ b/src/styles.css @@ -203,8 +203,8 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .preview-pane-body { flex: 1; min-height: 0; overflow: auto; } .preview-pane-body .ui-fp { height: 100%; } .preview-pane-body .ui-fp-pdf { height: 100%; min-height: 480px; border: none; } -.docx-wrapper { background: var(--bg-inset) !important; padding: 12px; } -.docx-wrapper > section.docx { background: var(--bg) !important; color: var(--text); box-shadow: var(--shadow-md); } +.docx-wrapper { background: var(--bg-inset) !important; padding: 12px; display: flex; flex-direction: column; align-items: flex-start !important; } +.docx-wrapper > section.docx { background: var(--paper) !important; color: var(--paper-ink); box-shadow: var(--shadow-md); } /* 过程段内联组(流式/关闭自动折叠时,按原位渲染的过程容器) */ .part-group { margin: 2px 0; } diff --git a/test/previewfit.test.ts b/test/previewfit.test.ts new file mode 100644 index 0000000..31a9806 --- /dev/null +++ b/test/previewfit.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { computeFitScale } from '../src/previewfit' + +describe('computeFitScale', () => { + it('keeps the page at 100% when the column is wider than the page', () => { + expect(computeFitScale(1100, 794, 24)).toBe(1) + expect(computeFitScale(818, 794, 24)).toBe(1) + }) + + it('scales the page down to the column width after subtracting the gutter', () => { + expect(computeFitScale(420, 794, 24)).toBeCloseTo((420 - 24) / 794) + expect(computeFitScale(794, 794, 24)).toBeCloseTo((794 - 24) / 794) + }) + + it('does not invert or zoom when the width is unusable', () => { + expect(computeFitScale(0, 794, 24)).toBe(1) + expect(computeFitScale(20, 794, 24)).toBe(1) + expect(computeFitScale(420, 0, 24)).toBe(1) + expect(computeFitScale(Number.NaN, 794, 24)).toBe(1) + }) +}) diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo deleted file mode 100644 index e2a2ab1..0000000 --- a/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/outbox.ts","./src/passwords.ts","./src/pluginHost.ts","./src/previewsetup.ts","./src/previewtabs.ts","./src/projectmeta.ts","./src/runstate.test.ts","./src/runstate.ts","./src/scrollsync.ts","./src/store.ts","./src/tasklist.ts","./src/theme.ts","./src/toolkind.ts","./src/workspaces.ts","./src/components/AdminUsersModal.tsx","./src/components/AdminZone.tsx","./src/components/AgentTracker.tsx","./src/components/ArtifactsPanel.tsx","./src/components/BorrowPanels.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/PreviewPane.tsx","./src/components/ProcessTimeline.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/TopBar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./src/components/adminSections.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/design-guard.test.ts","./test/html-sinks.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/notice.test.ts","./test/outbox.test.ts","./test/previewtabs.test.ts","./test/scrollsync.test.ts","./test/send.test.ts","./test/tasklist.test.ts","./test/theme.test.ts","./test/toolkind.test.ts","./test/workspaces.test.ts"],"version":"5.9.3"} \ No newline at end of file