From da06a2b81c2ff915522783ee399c621d3c6a0801 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 16 Jul 2026 11:31:08 +0200 Subject: [PATCH] feat: make viewer zoom responsive --- scripts/check_repository.py | 3 ++ tools/render_card.py | 75 +++++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/scripts/check_repository.py b/scripts/check_repository.py index 3ba958d..55f50cd 100644 --- a/scripts/check_repository.py +++ b/scripts/check_repository.py @@ -175,6 +175,9 @@ def check_section_assets() -> None: "--viewer-content-scale", "event.altKey", "event.shiftKey", + 'id="viewer-zoom-reset"', + "viewerCanvasMaximum", + "viewerAutoFit", ): if marker not in html: raise AssertionError(f"HTML nie zawiera obsługi zoomu viewera: {marker!r}") diff --git a/tools/render_card.py b/tools/render_card.py index 12b62eb..7884e3b 100644 --- a/tools/render_card.py +++ b/tools/render_card.py @@ -2888,9 +2888,10 @@ def render_main_html(data: dict) -> str: css = r""" :root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1} *{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25} -.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:8px 14px;font-family:system-ui,-apple-system,Segoe UI,sans-serif} -.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.toc-links a{font-size:13px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:4px;padding:3px 7px}.viewer-zoom-status{flex:none;font:600 12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap} -.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--viewer-content-scale));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm} +.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 10px;font-family:system-ui,-apple-system,Segoe UI,sans-serif} +.topbar-inner{max-width:1120px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:12px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:12px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:11px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 10px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px} +.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;height:297mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:hidden;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm} +@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}} .pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap} .hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px} .meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace} @@ -2951,6 +2952,11 @@ document.addEventListener('click', event => { const viewerRoot = document.documentElement; const viewerPaper = document.querySelector('.paper'); const viewerZoomStatus = document.getElementById('viewer-zoom-status'); +const viewerZoomReset = document.getElementById('viewer-zoom-reset'); +const viewerSheets = Array.from(document.querySelectorAll('.sheet')).map(sheet => ({ + sheet, + content: sheet.querySelector('.sheet-content') +})).filter(item => item.content); let viewerCanvasScale = 1; let viewerContentScale = 1; function clampViewerScale(value, minimum, maximum){ @@ -2960,12 +2966,45 @@ function nextViewerScale(current, delta, minimum, maximum){ const boundedDelta = Math.min(120, Math.max(-120, delta)); return clampViewerScale(current * Math.exp(-boundedDelta * 0.001), minimum, maximum); } +function viewerCanvasMaximum(){ + if(!viewerPaper || !viewerSheets.length){ return 1; } + const paperStyle = getComputedStyle(viewerPaper); + const availableWidth = viewerPaper.clientWidth + - parseFloat(paperStyle.paddingLeft || 0) + - parseFloat(paperStyle.paddingRight || 0); + const renderedWidth = viewerSheets[0].sheet.getBoundingClientRect().width; + const baseWidth = renderedWidth / Math.max(viewerCanvasScale, 0.001); + return Math.max(0.1, availableWidth / Math.max(baseWidth, 1)); +} +function updateViewerAutoFit(){ + for(const {sheet, content} of viewerSheets){ + const widthFit = sheet.clientWidth / Math.max(content.scrollWidth, 1); + const heightFit = sheet.clientHeight / Math.max(content.scrollHeight, 1); + content.dataset.viewerAutoFit = Math.min(1, widthFit, heightFit).toFixed(4); + } +} function applyViewerScale(){ viewerRoot.style.setProperty('--viewer-canvas-scale', viewerCanvasScale.toFixed(3)); viewerRoot.style.setProperty('--viewer-content-scale', viewerContentScale.toFixed(3)); - if(viewerZoomStatus){ - viewerZoomStatus.textContent = `Płótno ${Math.round(viewerCanvasScale * 100)}% · Zawartość ${Math.round(viewerContentScale * 100)}%`; + let usesAutoFit = false; + for(const {content} of viewerSheets){ + const autoFit = Number(content.dataset.viewerAutoFit || 1); + usesAutoFit ||= autoFit < 0.999; + content.style.setProperty('--sheet-effective-scale', (autoFit * viewerContentScale).toFixed(4)); } + if(viewerZoomStatus){ + viewerZoomStatus.textContent = `Płótno ${Math.round(viewerCanvasScale * 100)}% · Treść ${Math.round(viewerContentScale * 100)}%${usesAutoFit ? ' · auto-fit' : ''}`; + } +} +function refreshViewerLayout(){ + updateViewerAutoFit(); + viewerCanvasScale = Math.min(viewerCanvasScale, viewerCanvasMaximum()); + applyViewerScale(); +} +let viewerRefreshFrame = 0; +function scheduleViewerRefresh(){ + cancelAnimationFrame(viewerRefreshFrame); + viewerRefreshFrame = requestAnimationFrame(refreshViewerLayout); } if(viewerPaper){ viewerPaper.addEventListener('wheel', event => { @@ -2975,14 +3014,34 @@ if(viewerPaper){ if(!delta){ return; } event.preventDefault(); if(event.altKey){ - viewerCanvasScale = nextViewerScale(viewerCanvasScale, delta, 0.4, 2.0); + const maximum = viewerCanvasMaximum(); + viewerCanvasScale = nextViewerScale( + viewerCanvasScale, + delta, + Math.min(0.25, maximum), + maximum + ); }else{ viewerContentScale = nextViewerScale(viewerContentScale, delta, 0.5, 2.0); } applyViewerScale(); }, {passive:false}); } -applyViewerScale(); +if(viewerZoomReset){ + viewerZoomReset.addEventListener('click', () => { + viewerCanvasScale = Math.min(1, viewerCanvasMaximum()); + viewerContentScale = 1; + refreshViewerLayout(); + }); +} +window.addEventListener('resize', scheduleViewerRefresh); +window.visualViewport?.addEventListener('resize', scheduleViewerRefresh); +if('ResizeObserver' in window){ + const viewerResizeObserver = new ResizeObserver(scheduleViewerRefresh); + if(viewerPaper){ viewerResizeObserver.observe(viewerPaper); } + for(const {content} of viewerSheets){ viewerResizeObserver.observe(content); } +} +refreshViewerLayout(); """ inspector_html = ( '