diff --git a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts index 4661481de8..846bc41cac 100644 --- a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts @@ -5402,21 +5402,42 @@ export class PresentationEditor extends EventEmitter { */ #applyZoom() { if (this.#isSemanticFlowMode()) { - // Semantic mode: fill the container with fluid widths, no zoom scaling. - this.#viewportHost.style.width = '100%'; + const zoom = this.#layoutOptions.zoom ?? 1; + + // Semantic mode: fluid widths with optional zoom scaling. this.#viewportHost.style.minWidth = ''; this.#viewportHost.style.minHeight = ''; - this.#viewportHost.style.transform = ''; - this.#painterHost.style.width = '100%'; - this.#painterHost.style.minHeight = ''; - this.#painterHost.style.transformOrigin = ''; - this.#painterHost.style.transform = ''; + if (zoom === 1) { + this.#viewportHost.style.width = '100%'; + this.#viewportHost.style.transform = ''; - this.#selectionOverlay.style.width = '100%'; - this.#selectionOverlay.style.height = '100%'; - this.#selectionOverlay.style.transformOrigin = ''; - this.#selectionOverlay.style.transform = ''; + this.#painterHost.style.width = '100%'; + this.#painterHost.style.minHeight = ''; + this.#painterHost.style.transformOrigin = ''; + this.#painterHost.style.transform = ''; + + this.#selectionOverlay.style.width = '100%'; + this.#selectionOverlay.style.height = '100%'; + this.#selectionOverlay.style.transformOrigin = ''; + this.#selectionOverlay.style.transform = ''; + } else { + // Scale content while keeping fluid layout: set unscaled width to + // container/zoom so the reflowed content visually fills the container + // after the CSS transform enlarges it. + this.#viewportHost.style.width = `${100 / zoom}%`; + this.#viewportHost.style.transform = ''; + + this.#painterHost.style.width = '100%'; + this.#painterHost.style.minHeight = ''; + this.#painterHost.style.transformOrigin = 'top left'; + this.#painterHost.style.transform = `scale(${zoom})`; + + this.#selectionOverlay.style.width = '100%'; + this.#selectionOverlay.style.height = '100%'; + this.#selectionOverlay.style.transformOrigin = 'top left'; + this.#selectionOverlay.style.transform = `scale(${zoom})`; + } return; } diff --git a/packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.zoom.test.ts b/packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.zoom.test.ts index c423cfdb63..4277ae130c 100644 --- a/packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.zoom.test.ts +++ b/packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.zoom.test.ts @@ -983,4 +983,78 @@ describe('PresentationEditor - Zoom Functionality', () => { } }); }); + + describe('semantic flow mode zoom', () => { + let semanticEditor: PresentationEditor; + + afterEach(() => { + if (semanticEditor) { + semanticEditor.destroy(); + } + }); + + it('should apply CSS transform when zoom is set in semantic mode', () => { + semanticEditor = new PresentationEditor({ + element: container, + documentId: 'test-doc-semantic-zoom', + pageSize: { w: 612, h: 792 }, + layoutEngineOptions: { flowMode: 'semantic' }, + }); + + semanticEditor.setZoom(1.5); + + const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement; + const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement; + const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement; + + expect(painterHost?.style.transform).toBe('scale(1.5)'); + expect(painterHost?.style.transformOrigin).toBe('top left'); + + expect(selectionOverlay?.style.transform).toBe('scale(1.5)'); + expect(selectionOverlay?.style.transformOrigin).toBe('top left'); + + // Viewport width should be narrowed to compensate for scale + expect(viewportHost?.style.width).toBe(`${100 / 1.5}%`); + }); + + it('should clear transforms when zoom is reset to 1 in semantic mode', () => { + semanticEditor = new PresentationEditor({ + element: container, + documentId: 'test-doc-semantic-zoom-reset', + pageSize: { w: 612, h: 792 }, + layoutEngineOptions: { flowMode: 'semantic' }, + }); + + semanticEditor.setZoom(2); + semanticEditor.setZoom(1); + + const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement; + const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement; + const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement; + + expect(painterHost?.style.transform).toBe(''); + expect(painterHost?.style.transformOrigin).toBe(''); + expect(selectionOverlay?.style.transform).toBe(''); + expect(selectionOverlay?.style.transformOrigin).toBe(''); + expect(viewportHost?.style.width).toBe('100%'); + }); + + it('should keep fluid width on all elements in semantic mode', () => { + semanticEditor = new PresentationEditor({ + element: container, + documentId: 'test-doc-semantic-zoom-fluid', + pageSize: { w: 612, h: 792 }, + layoutEngineOptions: { flowMode: 'semantic' }, + }); + + semanticEditor.setZoom(0.75); + + const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement; + const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement; + + // painterHost and selectionOverlay keep 100% width (viewport is narrowed instead) + expect(painterHost?.style.width).toBe('100%'); + expect(selectionOverlay?.style.width).toBe('100%'); + }); + }); }); diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index 768ac985c0..a2ec917188 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -1255,12 +1255,32 @@ const handlePdfSelectionRaw = ({ selectionBounds, documentId, page }) => { watch( () => activeZoom.value, (zoom) => { + const zoomFactor = (zoom ?? 100) / 100; + if (proxy.$superdoc.config.useLayoutEngine !== false) { - PresentationEditor.setGlobalZoom((zoom ?? 100) / 100); + PresentationEditor.setGlobalZoom(zoomFactor); + } else { + // Web layout without layout engine — apply CSS transform directly + // to non-PDF sub-document containers so zoom works for PM fallback rendering. + // PDF documents are excluded because pdfViewer.updateScale() handles their zoom + // separately below; applying both would result in double-zoom. + const subDocs = layers.value?.querySelectorAll('.superdoc__sub-document'); + subDocs?.forEach((el) => { + if (el.querySelector('.sd-pdf-viewer')) return; + if (zoomFactor === 1) { + el.style.transformOrigin = ''; + el.style.transform = ''; + el.style.width = ''; + } else { + el.style.transformOrigin = 'top left'; + el.style.transform = `scale(${zoomFactor})`; + el.style.width = `${100 / zoomFactor}%`; + } + }); } const pdfViewer = getPDFViewer(); - pdfViewer?.updateScale((zoom ?? 100) / 100); + pdfViewer?.updateScale(zoomFactor); nextTick(() => { updateWhiteboardPageSizes();