feat: add independent viewer zoom controls

This commit is contained in:
user
2026-07-16 10:42:28 +02:00
parent 7bb32400d5
commit a3ea8a0627
2 changed files with 62 additions and 9 deletions
+9
View File
@@ -169,6 +169,15 @@ def check_section_assets() -> None:
raise AssertionError("HTML zawiera surowe środowisko verbatim")
if "<pre><code>stemctl test demo</code></pre>" not in html:
raise AssertionError("HTML nie zawiera wyrenderowanego bloku verbatim")
for marker in (
'class="sheet-content"',
"--viewer-canvas-scale",
"--viewer-content-scale",
"event.altKey",
"event.shiftKey",
):
if marker not in html:
raise AssertionError(f"HTML nie zawiera obsługi zoomu viewera: {marker!r}")
if not copied_asset.is_file():
raise AssertionError(f"asset nie został skopiowany: {copied_asset}")
+53 -9
View File
@@ -2843,24 +2843,28 @@ def render_main_html(data: dict) -> str:
)
sections_html.append(
f'<article class="sheet section-sheet" id="{html_escape(html_id("section-" + str(index)))}">'
f'<div class="sheet-content">'
f'{render_page_header_html(data, page_number, total_pages) if use_resource_header else ""}'
f'<aside class="pdf-margin left">{left_margin}</aside>'
f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}'
f'{heading}'
f'{render_step_map_html(section, data)}{content}</section>'
f'<aside class="pdf-margin right">{right_margin}</aside>'
f'{render_page_footer_html(data, page_number, total_pages) if use_resource_header else ""}</article>'
f'{render_page_footer_html(data, page_number, total_pages) if use_resource_header else ""}'
f'</div></article>'
)
if bibliography_html:
bibliography_page = len(data.get("sections", [])) + 2
sections_html.append(
f'<article class="sheet section-sheet" id="bibliografia-i-zrodla">'
f'<div class="sheet-content">'
f'{render_page_header_html(data, bibliography_page, total_pages) if use_resource_header else ""}'
f'<aside class="pdf-margin left"></aside>'
f'<section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ""}'
f'<h2>Bibliografia i źródła</h2>{bibliography_html}</section>'
f'<aside class="pdf-margin right"></aside>'
f'{render_page_footer_html(data, bibliography_page, total_pages) if use_resource_header else ""}</article>'
f'{render_page_footer_html(data, bibliography_page, total_pages) if use_resource_header else ""}'
f'</div></article>'
)
scope = data.get("front_page_scope", {})
@@ -2882,11 +2886,11 @@ def render_main_html(data: dict) -> str:
dictionary_page = total_pages
css = r"""
:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000}
: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 details{max-width:1120px;margin:0 auto}.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}
.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:13mm 21.5mm 15mm}.resource-header-enabled .sheet{padding-top:5mm}.sheet>.page-resource-header{margin-bottom:3mm}
.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}
.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}
@@ -2904,8 +2908,8 @@ figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;m
.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em}
code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #aaa;margin:2mm 0 4mm}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:10px;padding:8px 11px;background:#f3f3f3;border-bottom:1px solid #aaa}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#777}.task-block,.task-exercise{margin:10px 12px;padding:0 0 10px;border-bottom:1px solid #ddd}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#666}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{counter-reset:block-step;margin:5px 0 0 24px!important}.block-steps li{margin:7px 0}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace}.task-exercise{border:1px solid #777;border-left:4px solid #111;padding:9px 11px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:8px;padding-top:6px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:7px 0 0}.task-flow>footer{padding:8px 11px;border-top:1px solid #aaa;font-size:12px}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none}
.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)}
@media(max-width:900px){.sheet{display:block;min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}}
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet{padding:0}.sheet>.page-resource-header,.sheet>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,figure{break-inside:avoid}}
@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:static}.viewer-zoom-status{font-size:10px}}
@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,figure{break-inside:avoid}}
"""
css += page_resource_header_css(data)
if use_resource_header:
@@ -2942,6 +2946,43 @@ document.addEventListener('click', event => {
inspector.innerHTML = `<div class="tree-card"><h3>${display}</h3><p>${tip}</p></div>`;
}
});
"""
viewer_js = r"""
const viewerRoot = document.documentElement;
const viewerPaper = document.querySelector('.paper');
const viewerZoomStatus = document.getElementById('viewer-zoom-status');
let viewerCanvasScale = 1;
let viewerContentScale = 1;
function clampViewerScale(value, minimum, maximum){
return Math.min(maximum, Math.max(minimum, value));
}
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 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)}%`;
}
}
if(viewerPaper){
viewerPaper.addEventListener('wheel', event => {
const target = event.target instanceof Element ? event.target.closest('.sheet') : null;
if(!target || (!event.altKey && !event.shiftKey) || event.ctrlKey){ return; }
const delta = event.deltaY || event.deltaX;
if(!delta){ return; }
event.preventDefault();
if(event.altKey){
viewerCanvasScale = nextViewerScale(viewerCanvasScale, delta, 0.4, 2.0);
}else{
viewerContentScale = nextViewerScale(viewerContentScale, delta, 0.5, 2.0);
}
applyViewerScale();
}, {passive:false});
}
applyViewerScale();
"""
inspector_html = (
'<aside class="inspector"><h2>Opis kodu</h2><div id="tree-inspector">'
@@ -2952,6 +2993,7 @@ document.addEventListener('click', event => {
)
if not show_tree_inspector:
js = ""
js = viewer_js + js
return f"""<!doctype html>
<html lang="pl">
<head>
@@ -2966,9 +3008,10 @@ document.addEventListener('click', event => {
</head>
<body class="{'resource-header-enabled' if use_resource_header else ''}">
<script type="application/json" id="tree-data">{tree_data}</script>
<nav class="topbar"><details><summary>Spis treści</summary><div class="toc-links">{toc}</div></details></nav>
<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>
<main class="paper">
<article class="sheet front-sheet">
<div class="sheet-content">
{render_page_header_html(data, 1, total_pages) if use_resource_header else ''}
<aside class="pdf-margin left">{front_left}</aside>
<section class="pdf-main">
@@ -2976,9 +3019,10 @@ document.addEventListener('click', event => {
</section>
<aside class="pdf-margin right">{front_right}</aside>
{render_page_footer_html(data, 1, total_pages) if use_resource_header else ''}
</div>
</article>
{''.join(sections_html)}
<article class="sheet dictionary-sheet">{render_page_header_html(data, dictionary_page, total_pages) if use_resource_header else ''}<aside class="pdf-margin left"></aside><section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ''}<h2>Słownik WE/EN/EK/KW</h2>{render_learning_tree_overview_html(data)}</section><aside class="pdf-margin right"></aside>{render_page_footer_html(data, dictionary_page, total_pages) if use_resource_header else ''}</article>
<article class="sheet dictionary-sheet"><div class="sheet-content">{render_page_header_html(data, dictionary_page, total_pages) if use_resource_header else ''}<aside class="pdf-margin left"></aside><section class="pdf-main card-section">{render_running_header_html(card) if not use_resource_header else ''}<h2>Słownik WE/EN/EK/KW</h2>{render_learning_tree_overview_html(data)}</section><aside class="pdf-margin right"></aside>{render_page_footer_html(data, dictionary_page, total_pages) if use_resource_header else ''}</div></article>
</main>
{inspector_html}
<script>{js}</script>