feat: make viewer zoom responsive

This commit is contained in:
user
2026-07-16 11:31:08 +02:00
parent a3ea8a0627
commit da06a2b81c
2 changed files with 70 additions and 8 deletions
+67 -8
View File
@@ -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 = (
'<aside class="inspector"><h2>Opis kodu</h2><div id="tree-inspector">'
@@ -3008,7 +3067,7 @@ applyViewerScale();
</head>
<body class="{'resource-header-enabled' if use_resource_header else ''}">
<script type="application/json" id="tree-data">{tree_data}</script>
<nav class="topbar"><div class="topbar-inner"><details><summary>Spis treści</summary><div class="toc-links">{toc}</div></details><output class="viewer-zoom-status" id="viewer-zoom-status" aria-live="polite" title="Alt + rolka: płótno; Shift + rolka: zawartość">Płótno 100% · Zawartość 100%</output></div></nav>
<nav class="topbar"><div class="topbar-inner"><details><summary>Spis treści</summary><div class="toc-links">{toc}</div></details><div class="viewer-zoom-controls"><output class="viewer-zoom-status" id="viewer-zoom-status" aria-live="polite" title="Alt + rolka: płótno; Shift + rolka: zawartość">Płótno 100% · Zawartość 100%</output><button class="viewer-zoom-reset" id="viewer-zoom-reset" type="button">Reset zoom</button></div></div></nav>
<main class="paper">
<article class="sheet front-sheet">
<div class="sheet-content">