feat(presenter): preserve slide layout + draggable splitters

Two user-reported issues fixed:

1. Slide layout broken in preview (content stuck to top, not centered)
   Root cause: iframe CSS was forcing .slide { display:block !important }
   which overrode base.css's .slide { display:flex; justify-content:center }
   Fix: Do NOT override .slide or .deck styling in iframe. Let host CSS
   (base.css + theme + scoped) handle flex centering, padding, etc.
   Only override .is-active visibility and hide notes/chrome.

2. Users can now resize preview regions
   - Horizontal splitter between current (left) and right column
   - Vertical splitter between next preview (top) and notes (bottom)
   - Splitters show blue hover state, drag updates flex sizes
   - reScale() called on every drag frame to keep iframe 1:1 accurate
   - Min sizes enforced (200px width, 80-100px height) to prevent collapse

Verified with headless Chrome: cover slide now correctly vertically
centered in the preview, matching audience view pixel-perfect.
This commit is contained in:
lewis 2026-04-17 23:05:48 +08:00
parent 647a908eab
commit 0fc41be493
1 changed files with 81 additions and 15 deletions

View File

@ -190,16 +190,22 @@
// at 1920x1080 so vw/vh/clamp all resolve exactly like the audience view.
// We inject via contentDocument.write() so there's ZERO HTML escaping issues.
// Template is a JS string (not embedded HTML attribute), so quotes stay raw.
// IMPORTANT: Do NOT override .slide or .deck styling. The original
// base.css + theme + scoped CSS handles flex centering, padding, etc.
// We only: (1) reset margins, (2) force the single slide to be visible
// (.is-active), (3) hide the speaker notes and runtime chrome.
const iframeDocTemplate = '<!DOCTYPE html>'
+ '<html ' + htmlAttrs + '>'
+ '<head><meta charset="utf-8">'
+ styleSheets
+ '<style>'
+ 'html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:var(--bg,#0d1117)}'
+ '.deck{position:relative;width:100vw;height:100vh;overflow:hidden}'
+ '.slide{position:absolute!important;inset:0!important;width:100vw!important;height:100vh!important;opacity:1!important;transform:none!important;display:block!important;overflow:hidden!important}'
+ 'html,body{margin:0;padding:0;overflow:hidden}'
+ '/* Keep .slide and .deck styling from host CSS untouched */'
+ '/* But ensure the one slide we render is visible */'
+ '.slide{opacity:1!important;transform:none!important;pointer-events:auto!important}'
+ '/* Hide elements that should not appear in preview */'
+ '.notes,aside.notes,.speaker-notes{display:none!important}'
+ '.progress-bar,.notes-overlay,.overview,.deck-header,.deck-footer,.slide-number{display:none!important}'
+ '.progress-bar,.notes-overlay,.overview{display:none!important}'
+ '</style></head>'
+ '<body class="' + bodyClasses + '">'
+ '<div class="deck">%%SLIDE_HTML%%</div>'
@ -239,27 +245,29 @@
+ ' .pv-hint { font-size: 11px; color: #484f58; margin-left: auto; }\n'
+ '</style>\n'
+ '</head>\n<body>\n'
+ '<div class="pv-grid">\n'
+ ' <div class="pv-current-wrap">\n'
+ '<div class="pv-main">\n'
+ ' <div class="pv-left" id="pv-left" style="flex:1.4 1 0">\n'
+ ' <div class="pv-label">CURRENT</div>\n'
+ ' <div class="pv-stage" id="pv-current"><iframe id="iframe-cur"></iframe></div>\n'
+ ' </div>\n'
+ ' <div class="pv-right">\n'
+ ' <div class="pv-next-wrap">\n'
+ ' <div class="pv-hsplit" id="pv-hsplit" title="拖动调整左右宽度"></div>\n'
+ ' <div class="pv-right" id="pv-right" style="flex:1 1 0">\n'
+ ' <div class="pv-next-wrap" id="pv-next-wrap" style="flex:0 0 38%">\n'
+ ' <div class="pv-label">NEXT</div>\n'
+ ' <div class="pv-stage pv-next-stage" id="pv-next"><iframe id="iframe-nxt"></iframe></div>\n'
+ ' </div>\n'
+ ' <div class="pv-notes">\n'
+ ' <div class="pv-vsplit" id="pv-vsplit" title="拖动调整上下高度"></div>\n'
+ ' <div class="pv-notes" id="pv-notes-wrap" style="flex:1 1 0">\n'
+ ' <div class="pv-label">SPEAKER SCRIPT · 逐字稿</div>\n'
+ ' <div class="pv-notes-body" id="pv-notes"></div>\n'
+ ' </div>\n'
+ ' </div>\n'
+ ' <div class="pv-bar">\n'
+ '</div>\n'
+ '<div class="pv-bar">\n'
+ ' <div class="pv-timer" id="pv-timer">00:00</div>\n'
+ ' <div class="pv-count" id="pv-count">1 / ' + total + '</div>\n'
+ ' <div class="pv-title" id="pv-title"></div>\n'
+ ' <div class="pv-hint">← → 翻页 · R 重置计时 · Esc 关闭</div>\n'
+ ' </div>\n'
+ ' <div class="pv-hint">← → 翻页 · R 重置计时 · 拖动分隔线调整区域 · Esc 关闭</div>\n'
+ '</div>\n'
+ '<script>\n'
+ '(function(){\n'
@ -355,6 +363,64 @@
+ ' });\n'
+ '\n'
+ ' window.addEventListener("resize", reScale);\n'
+ '\n'
+ ' /* ===== Draggable splitters ===== */\n'
+ ' (function initSplitters(){\n'
+ ' var hsplit = document.getElementById("pv-hsplit");\n'
+ ' var vsplit = document.getElementById("pv-vsplit");\n'
+ ' var pvLeft = document.getElementById("pv-left");\n'
+ ' var pvRight = document.getElementById("pv-right");\n'
+ ' var pvMain = document.querySelector(".pv-main");\n'
+ ' var pvNextWrap = document.getElementById("pv-next-wrap");\n'
+ ' var pvNotesWrap = document.getElementById("pv-notes-wrap");\n'
+ '\n'
+ ' /* Horizontal splitter: left / right columns */\n'
+ ' hsplit.addEventListener("mousedown", function(e){\n'
+ ' e.preventDefault();\n'
+ ' document.body.classList.add("pv-dragging");\n'
+ ' var mainRect = pvMain.getBoundingClientRect();\n'
+ ' function onMove(ev){\n'
+ ' var x = ev.clientX - mainRect.left - 10;\n'
+ ' var totalW = mainRect.width - 20 - 8;\n'
+ ' var leftW = Math.max(200, Math.min(totalW - 200, x));\n'
+ ' var rightW = totalW - leftW;\n'
+ ' pvLeft.style.flex = "0 0 " + leftW + "px";\n'
+ ' pvRight.style.flex = "0 0 " + rightW + "px";\n'
+ ' reScale();\n'
+ ' }\n'
+ ' function onUp(){\n'
+ ' document.removeEventListener("mousemove", onMove);\n'
+ ' document.removeEventListener("mouseup", onUp);\n'
+ ' document.body.classList.remove("pv-dragging");\n'
+ ' }\n'
+ ' document.addEventListener("mousemove", onMove);\n'
+ ' document.addEventListener("mouseup", onUp);\n'
+ ' });\n'
+ '\n'
+ ' /* Vertical splitter: next preview / notes */\n'
+ ' vsplit.addEventListener("mousedown", function(e){\n'
+ ' e.preventDefault();\n'
+ ' document.body.classList.add("pv-dragging-v");\n'
+ ' var rightRect = pvRight.getBoundingClientRect();\n'
+ ' function onMove(ev){\n'
+ ' var y = ev.clientY - rightRect.top;\n'
+ ' var totalH = rightRect.height - 8;\n'
+ ' var nextH = Math.max(80, Math.min(totalH - 100, y));\n'
+ ' var notesH = totalH - nextH;\n'
+ ' pvNextWrap.style.flex = "0 0 " + nextH + "px";\n'
+ ' pvNotesWrap.style.flex = "0 0 " + notesH + "px";\n'
+ ' reScale();\n'
+ ' }\n'
+ ' function onUp(){\n'
+ ' document.removeEventListener("mousemove", onMove);\n'
+ ' document.removeEventListener("mouseup", onUp);\n'
+ ' document.body.classList.remove("pv-dragging-v");\n'
+ ' }\n'
+ ' document.addEventListener("mousemove", onMove);\n'
+ ' document.addEventListener("mouseup", onUp);\n'
+ ' });\n'
+ ' })();\n'
+ '\n'
+ ' /* Wait for iframes to be ready, then render */\n'
+ ' function initWhenReady() {\n'
+ ' if (iframeCur.contentDocument && iframeNxt.contentDocument) {\n'