From 4db6ffb367cfa43faec6aadfde40e219fb970533 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 11 Dec 2025 09:42:20 +0000 Subject: [PATCH] update --- package.json | 2 +- pnpm-lock.yaml | 27 +- ts_web/elements/document.ts | 50 +++- ts_web/elements/page.ts | 16 +- ts_web/elements/viewer.ts | 571 ++++++++++++++++++++++++++++++++++-- 5 files changed, 629 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index e6f2782..1c97156 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@git.zone/tsbuild": "^3.1.2", "@git.zone/tsbundle": "^2.6.3", "@git.zone/tstest": "^3.1.3", - "@git.zone/tswatch": "^2.3.7", + "@git.zone/tswatch": "^2.3.9", "@push.rocks/projectinfo": "^5.0.2" }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4d8e39..83d3db1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,8 +67,8 @@ importers: specifier: ^3.1.3 version: 3.1.3(@push.rocks/smartserve@1.4.0)(socks@2.8.7)(typescript@5.9.3) '@git.zone/tswatch': - specifier: ^2.3.7 - version: 2.3.7(@tiptap/pm@2.27.1) + specifier: ^2.3.9 + version: 2.3.9(@tiptap/pm@2.27.1) '@push.rocks/projectinfo': specifier: ^5.0.2 version: 5.0.2 @@ -283,6 +283,9 @@ packages: '@cloudflare/workers-types@4.20251210.0': resolution: {integrity: sha512-EGf2lWqeVO48LjDYFl1peSbi/AvQFDJ1vj+etwRAGqLjGWgq+R1fwFfLCjXr7tMsX8aHykE17XpCAVuroKpZoQ==} + '@cloudflare/workers-types@4.20251211.0': + resolution: {integrity: sha512-e87o6KbelCz7dnI5ngrGT2ca15vJZ+COb2eqJ52iDHmOaujyC6aYZ71e2vor8X6V9T6tcDElC5sAqPR93j09EA==} + '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} @@ -506,8 +509,8 @@ packages: resolution: {integrity: sha512-t+/cKV21JHK8X7NGAmihs5M/eMm+V+jn4R5rzfwGG97WJFAcP5qE1Os9VYtyZw3tx/NZXA2yA4abo/ELluTuRA==} hasBin: true - '@git.zone/tswatch@2.3.7': - resolution: {integrity: sha512-cykbA1rwsh88H5k1/irkVKpecU9vJKOFuSZmcPYWdo2UJQN4IweLFU+IeWwiFHaHR3QqKpR1wyiKcKEGbf7mrQ==} + '@git.zone/tswatch@2.3.9': + resolution: {integrity: sha512-lm3rwkeLXrT8arsQYTTnLSobyXYio+Q70vciBTflpf2Sf4I9fd4QH/89EmKSLysJso2Gnrz63brLzTYCtbdlQQ==} hasBin: true '@happy-dom/global-registrator@15.11.7': @@ -1185,8 +1188,8 @@ packages: '@push.rocks/smartversion@3.0.5': resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==} - '@push.rocks/smartwatch@6.2.1': - resolution: {integrity: sha512-mVLf3hFc9L73douQ6w3kRFTt2qSwM2cSncxByfSSxk9pFrEwUL0/+Ff4hw5OMg5lgaowFa5FGRJsXzZAVaZgEw==} + '@push.rocks/smartwatch@6.2.3': + resolution: {integrity: sha512-M7rMLdcO423JIF7PbMnqy730h4seAx8lXkP3d7yGhIXep2jizPP+KlkdbdkBdaVp7YupcFZiTnu2HY66SKVtpQ==} engines: {node: '>=20.0.0'} '@push.rocks/smartxml@2.0.0': @@ -4331,7 +4334,7 @@ snapshots: '@api.global/typedrequest': 3.2.5 '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.4.0) - '@cloudflare/workers-types': 4.20251210.0 + '@cloudflare/workers-types': 4.20251211.0 '@design.estate/dees-catalog': 3.3.0(@tiptap/pm@2.27.1) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.2.2 @@ -4357,7 +4360,7 @@ snapshots: '@push.rocks/smartsitemap': 2.0.4 '@push.rocks/smartstream': 3.2.5 '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartwatch': 6.2.1 + '@push.rocks/smartwatch': 6.2.3 '@push.rocks/taskbuffer': 3.5.0 '@push.rocks/webrequest': 4.0.1 '@push.rocks/webstore': 2.0.20 @@ -4900,6 +4903,8 @@ snapshots: '@cloudflare/workers-types@4.20251210.0': {} + '@cloudflare/workers-types@4.20251211.0': {} + '@configvault.io/interfaces@1.0.17': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 @@ -5225,7 +5230,7 @@ snapshots: - utf-8-validate - vue - '@git.zone/tswatch@2.3.7(@tiptap/pm@2.27.1)': + '@git.zone/tswatch@2.3.9(@tiptap/pm@2.27.1)': dependencies: '@api.global/typedserver': 7.11.1(@tiptap/pm@2.27.1) '@git.zone/tsbundle': 2.6.3 @@ -5238,7 +5243,7 @@ snapshots: '@push.rocks/smartlog': 3.1.10 '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartshell': 3.3.0 - '@push.rocks/smartwatch': 6.2.1 + '@push.rocks/smartwatch': 6.2.3 '@push.rocks/taskbuffer': 3.5.0 transitivePeerDependencies: - '@nuxt/kit' @@ -6534,7 +6539,7 @@ snapshots: '@types/semver': 7.7.1 semver: 7.7.3 - '@push.rocks/smartwatch@6.2.1': + '@push.rocks/smartwatch@6.2.3': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartenv': 6.0.0 diff --git a/ts_web/elements/document.ts b/ts_web/elements/document.ts index 2528435..4390653 100644 --- a/ts_web/elements/document.ts +++ b/ts_web/elements/document.ts @@ -87,6 +87,12 @@ export class DeDocument extends DeesElement { accessor documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings; + @property({ type: Number }) + accessor zoomLevel: number = null; // null = auto-fit, otherwise percentage (e.g., 100 = 100%) + + @property({ type: Number }) + accessor pageGap: number = 16; // pixels between pages + constructor() { super(); domtools.DomTools.setupDomTools(); @@ -223,6 +229,7 @@ export class DeDocument extends DeesElement { if (!this.printMode) { const betweenPagesSpacerDiv = document.createElement("div"); betweenPagesSpacerDiv.classList.add("betweenPagesSpacer"); + betweenPagesSpacerDiv.style.height = `${this.pageGap}px`; documentContainer.appendChild(betweenPagesSpacerDiv); } } @@ -249,10 +256,25 @@ export class DeDocument extends DeesElement { if ( changedProperties.has("viewHeight") || - changedProperties.has("viewWidth") + changedProperties.has("viewWidth") || + changedProperties.has("zoomLevel") ) { this.adjustDePageScaling(); } + + if (changedProperties.has("pageGap")) { + this.updatePageGaps(); + } + } + + /** + * Update page gap spacing without re-rendering document + */ + private updatePageGaps(): void { + const spacers = this.shadowRoot.querySelectorAll(".betweenPagesSpacer"); + spacers.forEach((spacer: HTMLElement) => { + spacer.style.height = `${this.pageGap}px`; + }); } private adjustDePageScaling() { @@ -263,8 +285,11 @@ export class DeDocument extends DeesElement { // Find all DePage instances within this DeDocument const pages = this.shadowRoot.querySelectorAll("dedocument-page"); - // Update each DePage instance's viewHeight and viewWidth + // Update each DePage instance's viewHeight, viewWidth, and zoomLevel pages.forEach((page: DePage) => { + // Pass manual zoom level if set + page.manualZoomLevel = this.zoomLevel; + if (this.viewHeight) { page.viewHeight = this.viewHeight; } @@ -273,4 +298,25 @@ export class DeDocument extends DeesElement { } }); } + + /** + * Set zoom level manually. Pass null to return to auto-fit mode. + * @param level - Zoom percentage (e.g., 100 for 100%) or null for auto-fit + */ + public setZoomLevel(level: number | null): void { + this.zoomLevel = level; + this.adjustDePageScaling(); + } + + /** + * Get the current effective zoom percentage + */ + public getEffectiveZoom(): number { + if (this.zoomLevel !== null) { + return this.zoomLevel; + } + // Calculate auto-fit zoom percentage + const scale = this.viewWidth / plugins.shared.A4_WIDTH; + return Math.round(scale * 100); + } } diff --git a/ts_web/elements/page.ts b/ts_web/elements/page.ts index e1605b8..39ae161 100644 --- a/ts_web/elements/page.ts +++ b/ts_web/elements/page.ts @@ -67,6 +67,9 @@ export class DePage extends DeesElement { accessor documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings; + @property({ type: Number }) + accessor manualZoomLevel: number = null; // null = auto-fit, otherwise percentage + constructor() { super(); domtools.DomTools.setupDomTools(); @@ -246,7 +249,8 @@ export class DePage extends DeesElement { super.updated(changedProperties); if ( changedProperties.has("viewHeight") || - changedProperties.has("viewWidth") + changedProperties.has("viewWidth") || + changedProperties.has("manualZoomLevel") ) { this.adjustScaling(); } @@ -259,15 +263,21 @@ export class DePage extends DeesElement { if (!scaleWrapper) return; let scale = 1; - if (this.viewHeight) { + + // If manual zoom is set, use it directly + if (this.manualZoomLevel !== null) { + scale = this.manualZoomLevel / 100; + } else if (this.viewHeight) { + // Auto-fit to height scale = this.viewHeight / plugins.shared.A4_HEIGHT; } else if (this.viewWidth) { + // Auto-fit to width scale = this.viewWidth / plugins.shared.A4_WIDTH; } + scaleWrapper.style.transform = `scale(${scale})`; // Adjust the outer dimensions so they match the scaled content - this.style.width = `${plugins.shared.A4_WIDTH * scale}px`; this.style.height = `${plugins.shared.A4_HEIGHT * scale}px`; } diff --git a/ts_web/elements/viewer.ts b/ts_web/elements/viewer.ts index b4cd135..8d660e8 100644 --- a/ts_web/elements/viewer.ts +++ b/ts_web/elements/viewer.ts @@ -7,8 +7,13 @@ import { customElement, html, property, + state, + type TemplateResult, } from "@design.estate/dees-element"; import { demoFunc } from "./viewer.demo.js"; +import { DeDocument } from "./document.js"; + +import * as deesCatalog from "@design.estate/dees-catalog"; declare global { interface HTMLElementTagNameMap { @@ -16,6 +21,32 @@ declare global { } } +type TZoomPreset = "auto" | "fit-width" | "fit-page" | number; + +interface ISpacingPreset { + label: string; + value: number; + icon: string; +} + +const ZOOM_PRESETS: { label: string; value: TZoomPreset }[] = [ + { label: "Auto Fit", value: "auto" }, + { label: "Fit Width", value: "fit-width" }, + { label: "Fit Page", value: "fit-page" }, + { label: "50%", value: 50 }, + { label: "75%", value: 75 }, + { label: "100%", value: 100 }, + { label: "125%", value: 125 }, + { label: "150%", value: 150 }, + { label: "200%", value: 200 }, +]; + +const SPACING_PRESETS: ISpacingPreset[] = [ + { label: "Compact", value: 8, icon: "lucide:minimize2" }, + { label: "Normal", value: 16, icon: "lucide:minus" }, + { label: "Comfortable", value: 32, icon: "lucide:maximize2" }, +]; + @customElement("dedocument-viewer") export class DeDocumentViewer extends DeesElement { // DEMO @@ -34,74 +65,574 @@ export class DeDocumentViewer extends DeesElement { }) accessor documentSettings: plugins.shared.interfaces.IDocumentSettings; + @state() + accessor zoomMode: TZoomPreset = "auto"; + + @state() + accessor zoomLevel: number = null; // null = auto, otherwise percentage + + @state() + accessor pageGap: number = 16; + + @state() + accessor showZoomDropdown: boolean = false; + + @state() + accessor displayZoom: number = 100; + public static styles = [ cssManager.defaultStyles, css` + :host { + --toolbar-height: 40px; + --toolbar-bg: ${cssManager.bdTheme("rgba(250, 250, 250, 0.95)", "rgba(24, 24, 24, 0.95)")}; + --toolbar-border: ${cssManager.bdTheme("rgba(0, 0, 0, 0.1)", "rgba(255, 255, 255, 0.1)")}; + --button-hover: ${cssManager.bdTheme("rgba(0, 0, 0, 0.08)", "rgba(255, 255, 255, 0.08)")}; + --button-active: ${cssManager.bdTheme("rgba(0, 0, 0, 0.12)", "rgba(255, 255, 255, 0.15)")}; + --text-primary: ${cssManager.bdTheme("#1a1a1a", "#e5e5e5")}; + --text-secondary: ${cssManager.bdTheme("#666", "#999")}; + --accent-color: ${cssManager.bdTheme("#0066cc", "#4d9fff")}; + --dropdown-bg: ${cssManager.bdTheme("#fff", "#1e1e1e")}; + --dropdown-shadow: ${cssManager.bdTheme("0 4px 16px rgba(0,0,0,0.15)", "0 4px 16px rgba(0,0,0,0.4)")}; + --slider-track: ${cssManager.bdTheme("#ddd", "#444")}; + --slider-fill: ${cssManager.bdTheme("#0066cc", "#4d9fff")}; + } + .maincontainer { position: relative; height: 100%; width: 100%; background: ${cssManager.bdTheme("#eeeeeb", "#111")}; } + .controls { + position: absolute; top: 0px; right: 0px; left: 0px; - position: absolute; - height: 32px; - width: 100%; - background: ${cssManager.bdTheme("#eeeeeb", "#111111ee")}; - box-shadow: 0px 2px 8px 0px #000000; + height: var(--toolbar-height); + background: var(--toolbar-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--toolbar-border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 12px; + z-index: 100; + box-sizing: border-box; } + + .controls__section { + display: flex; + align-items: center; + gap: 4px; + } + + .controls__divider { + width: 1px; + height: 20px; + background: var(--toolbar-border); + margin: 0 8px; + } + + .controls__button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + background: transparent; + border-radius: 6px; + cursor: pointer; + color: var(--text-primary); + transition: background 0.15s ease; + } + + .controls__button:hover { + background: var(--button-hover); + } + + .controls__button:active { + background: var(--button-active); + } + + .controls__button--active { + background: var(--button-active); + color: var(--accent-color); + } + + .controls__button dees-icon { + font-size: 16px; + } + + .zoom-control { + position: relative; + display: flex; + align-items: center; + gap: 2px; + } + + .zoom-dropdown-trigger { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border: 1px solid var(--toolbar-border); + background: transparent; + border-radius: 6px; + cursor: pointer; + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + min-width: 80px; + justify-content: space-between; + transition: all 0.15s ease; + } + + .zoom-dropdown-trigger:hover { + background: var(--button-hover); + border-color: var(--text-secondary); + } + + .zoom-dropdown-trigger dees-icon { + font-size: 12px; + opacity: 0.6; + transition: transform 0.2s ease; + } + + .zoom-dropdown-trigger--open dees-icon { + transform: rotate(180deg); + } + + .zoom-dropdown { + position: absolute; + top: calc(100% + 4px); + left: 50%; + transform: translateX(-50%); + background: var(--dropdown-bg); + border: 1px solid var(--toolbar-border); + border-radius: 8px; + box-shadow: var(--dropdown-shadow); + min-width: 140px; + padding: 4px; + z-index: 1000; + opacity: 0; + visibility: hidden; + transform: translateX(-50%) translateY(-8px); + transition: all 0.2s ease; + } + + .zoom-dropdown--visible { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); + } + + .zoom-dropdown__item { + display: flex; + align-items: center; + padding: 8px 12px; + border: none; + background: transparent; + width: 100%; + text-align: left; + cursor: pointer; + border-radius: 4px; + color: var(--text-primary); + font-size: 13px; + transition: background 0.1s ease; + } + + .zoom-dropdown__item:hover { + background: var(--button-hover); + } + + .zoom-dropdown__item--active { + color: var(--accent-color); + font-weight: 500; + } + + .zoom-slider-container { + display: flex; + align-items: center; + gap: 8px; + padding: 0 4px; + } + + .zoom-slider { + width: 80px; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: var(--slider-track); + border-radius: 2px; + outline: none; + cursor: pointer; + } + + .zoom-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + background: var(--slider-fill); + border-radius: 50%; + cursor: pointer; + transition: transform 0.15s ease; + } + + .zoom-slider::-webkit-slider-thumb:hover { + transform: scale(1.15); + } + + .zoom-slider::-moz-range-thumb { + width: 14px; + height: 14px; + background: var(--slider-fill); + border: none; + border-radius: 50%; + cursor: pointer; + } + + .spacing-control { + display: flex; + align-items: center; + gap: 4px; + } + + .spacing-label { + font-size: 12px; + color: var(--text-secondary); + margin-right: 4px; + } + + .spacing-slider-container { + display: flex; + align-items: center; + gap: 6px; + } + + .spacing-slider { + width: 60px; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: var(--slider-track); + border-radius: 2px; + outline: none; + cursor: pointer; + } + + .spacing-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + background: var(--slider-fill); + border-radius: 50%; + cursor: pointer; + } + + .spacing-slider::-moz-range-thumb { + width: 12px; + height: 12px; + background: var(--slider-fill); + border: none; + border-radius: 50%; + cursor: pointer; + } + + .spacing-value { + font-size: 11px; + color: var(--text-secondary); + min-width: 28px; + text-align: right; + } + .controlsShadow { position: absolute; - top: 32px; + top: var(--toolbar-height); right: 0px; left: 0px; height: 16px; - width: 100%; - position: absolute; - background: linear-gradient(to bottom, #111111cc 0%, #11111100 100%); + background: ${cssManager.bdTheme( + "linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, transparent 100%)", + "linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, transparent 100%)" + )}; + pointer-events: none; + z-index: 99; } .viewport { position: absolute; padding: 16px; - padding-top: 48px; + padding-top: calc(var(--toolbar-height) + 16px); padding-bottom: 0px; top: 0px; left: 0px; right: 0px; bottom: 0px; overflow-y: scroll; + overflow-x: auto; overscroll-behavior: contain; } + + .viewport--centered { + display: flex; + flex-direction: column; + align-items: center; + } `, ]; - public render = () => { + private documentRef: DeDocument | null = null; + + public render(): TemplateResult { return html`
-
+
${this.letterData ? html` ` : html``}
-
+
+ ${this.renderZoomControls()} + ${this.renderSpacingControls()} +
+
`; - }; + } - public updated = ( - changedProperties: Map - ) => { - super.updated(changedProperties); - if (changedProperties.has("letterData")) { + private renderZoomControls(): TemplateResult { + return html` +
+ + + + +
+ +
+ ${ZOOM_PRESETS.map( + (preset) => html` + + ` + )} +
+
+ + + + +
+ + +
+ +
+
+ `; + } + + private renderSpacingControls(): TemplateResult { + return html` +
+ Spacing + + + ${SPACING_PRESETS.map( + (preset) => html` + + ` + )} + +
+ + +
+ + ${this.pageGap}px +
+
+ `; + } + + private getZoomDisplayText(): string { + if (this.zoomMode === "auto") return "Auto"; + if (this.zoomMode === "fit-width") return "Fit Width"; + if (this.zoomMode === "fit-page") return "Fit Page"; + return `${this.displayZoom}%`; + } + + private isActivePreset(value: TZoomPreset): boolean { + if (typeof value === "string") { + return this.zoomMode === value; } - }; + return this.zoomMode === value || (typeof this.zoomMode === "number" && this.zoomMode === value); + } + + private toggleZoomDropdown(): void { + this.showZoomDropdown = !this.showZoomDropdown; + + if (this.showZoomDropdown) { + // Close dropdown when clicking outside + const closeHandler = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (!target.closest(".zoom-control")) { + this.showZoomDropdown = false; + document.removeEventListener("click", closeHandler); + } + }; + setTimeout(() => document.addEventListener("click", closeHandler), 0); + } + } + + private handleZoomPreset(value: TZoomPreset): void { + this.showZoomDropdown = false; + this.zoomMode = value; + + if (value === "auto") { + this.zoomLevel = null; + this.updateDisplayZoom(); + } else if (value === "fit-width") { + // Fit width will be handled by auto scaling + this.zoomLevel = null; + this.updateDisplayZoom(); + } else if (value === "fit-page") { + // Calculate zoom to fit entire page in viewport + this.calculateFitPage(); + } else { + this.zoomLevel = value; + this.displayZoom = value; + } + } + + private handleZoomStep(delta: number): void { + const current = this.zoomLevel ?? this.displayZoom; + const newZoom = Math.min(400, Math.max(25, current + delta)); + this.zoomLevel = newZoom; + this.displayZoom = newZoom; + this.zoomMode = newZoom; + } + + private handleZoomSlider(e: Event): void { + const value = parseInt((e.target as HTMLInputElement).value, 10); + this.zoomLevel = value; + this.displayZoom = value; + this.zoomMode = value; + } + + private handleSpacingPreset(value: number): void { + this.pageGap = value; + } + + private handleSpacingSlider(e: Event): void { + this.pageGap = parseInt((e.target as HTMLInputElement).value, 10); + } + + private calculateFitPage(): void { + const viewport = this.shadowRoot?.querySelector(".viewport"); + if (!viewport) return; + + const viewportHeight = viewport.clientHeight - 32; // Account for padding + const viewportWidth = viewport.clientWidth - 32; + + const scaleByHeight = viewportHeight / plugins.shared.A4_HEIGHT; + const scaleByWidth = viewportWidth / plugins.shared.A4_WIDTH; + const scale = Math.min(scaleByHeight, scaleByWidth); + + this.zoomLevel = Math.round(scale * 100); + this.displayZoom = this.zoomLevel; + } + + private updateDisplayZoom(): void { + // Update display zoom based on current auto scale + requestAnimationFrame(() => { + const doc = this.shadowRoot?.querySelector("dedocument-dedocument") as DeDocument; + if (doc) { + this.displayZoom = doc.getEffectiveZoom(); + } + }); + } + + public updated( + changedProperties: Map + ): void { + super.updated(changedProperties); + + if (changedProperties.has("letterData")) { + // Update display zoom after document renders + setTimeout(() => this.updateDisplayZoom(), 100); + } + } + + public firstUpdated(): void { + // Update display zoom initially + setTimeout(() => this.updateDisplayZoom(), 200); + + // Handle viewport resize to update display zoom in auto mode + const viewport = this.shadowRoot?.querySelector(".viewport"); + if (viewport) { + const resizeObserver = new ResizeObserver(() => { + if (this.zoomMode === "auto" || this.zoomMode === "fit-width") { + this.updateDisplayZoom(); + } + }); + resizeObserver.observe(viewport); + this.registerGarbageFunction(() => resizeObserver.disconnect()); + } + } }