From c192c855d09c5653eba8c97b000eed37162cc2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E6=B5=A9?= Date: Thu, 6 Aug 2026 22:15:15 +0800 Subject: [PATCH] Improve presenter preview layout and scaling --- assets/template-swiss.html | 35 +++++++++++++++++++++++------ assets/template.html | 35 +++++++++++++++++++++++------ references/checklist.md | 4 +++- references/presenter-mode.md | 4 +++- scripts/validate-presenter-mode.mjs | 5 +++++ 5 files changed, 67 insertions(+), 16 deletions(-) diff --git a/assets/template-swiss.html b/assets/template-swiss.html index 33d3648..684f1be 100644 --- a/assets/template-swiss.html +++ b/assets/template-swiss.html @@ -566,7 +566,7 @@ body.ppt-preview{pointer-events:none} body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint, body.ppt-presenter>.bg{visibility:hidden} - #ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:1000px;min-height:680px;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} + #ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} body.ppt-presenter #ppt-presenter{display:grid;grid-template-rows:auto 1fr auto} .ppt-presenter-top{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px 24px;border-bottom:1px solid #2b2e35;background:#15181d} .ppt-presenter-brand{font-weight:650;letter-spacing:.02em}.ppt-presenter-meta{display:flex;align-items:center;gap:12px;color:#aeb4be;font-size:14px} @@ -576,17 +576,21 @@ .ppt-sync[data-state="connecting"]::before{background:#f0c766}.ppt-sync[data-state="unsynced"]::before,.ppt-sync[data-state="blocked"]::before{background:#ff9e64}.ppt-sync[data-state="disconnected"]::before{background:#ff6b72} .ppt-ui-btn{appearance:none;border:1px solid #3a3e48;background:#20242a;color:#f6f7f8;border-radius:8px;padding:9px 13px;font:600 14px/1 Inter,"Noto Sans SC",sans-serif;cursor:pointer} .ppt-ui-btn:hover{background:#2a2f37;border-color:#525864}.ppt-ui-btn:focus-visible{outline:2px solid #84a8ff;outline-offset:2px}.ppt-ui-btn:disabled{opacity:.28;cursor:not-allowed} - .ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(320px,.75fr);gap:18px;min-height:0;padding:18px 24px} - .ppt-preview-stack{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,.42fr);gap:16px;min-width:0;align-items:start} + .ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(300px,.75fr);gap:18px;min-height:0;padding:18px 24px} + .ppt-preview-stack{display:grid;grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px);gap:16px;min-width:0;min-height:0} .ppt-stage-card,.ppt-notes-card{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden} + .ppt-stage-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0} .ppt-stage-label{display:flex;justify-content:space-between;align-items:center;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34} - .ppt-frame{display:block;width:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86} + .ppt-frame-viewport{display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b} + .ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86} .ppt-notes-card{display:grid;grid-template-rows:auto 1fr;min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-title{font-size:20px;font-weight:650;margin-bottom:7px}.ppt-notes-sub{display:flex;align-items:center;justify-content:space-between;gap:10px;color:#9fa6b1;font-size:13px} .ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif} .ppt-saved{color:#72df99}.ppt-presenter-bottom{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;padding:16px 24px;border-top:1px solid #2b2e35;background:#15181d} .ppt-timer{display:flex;align-items:center;gap:9px}.ppt-clock{min-width:72px;font:600 24px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-controls{display:flex;align-items:center;gap:8px}.ppt-controls .ppt-ui-btn{min-width:72px}.ppt-controls .ppt-primary{background:#f2f4f6;color:#111318;border-color:#f2f4f6}.ppt-controls .ppt-primary:hover{background:#fff}.ppt-page-state{text-align:right;color:#aeb4be;font-size:14px}.ppt-page-state strong{display:block;color:#fff;font-size:18px;margin-bottom:3px} #overview{z-index:110!important} - @media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(300px,.75fr)}.ppt-preview-stack{grid-template-columns:1fr}.ppt-preview-stack .ppt-stage-card:last-child{display:none}} + @media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.55fr) minmax(280px,.72fr)}} + @media(max-width:900px){.ppt-presenter-top{padding:12px 16px}.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-presenter-bottom{gap:8px;padding:12px 16px}.ppt-controls{gap:5px}.ppt-controls .ppt-ui-btn{min-width:58px;padding:8px 10px}.ppt-page-state{display:none}} + @media(max-height:700px){.ppt-presenter-top{padding-top:11px;padding-bottom:11px}.ppt-presenter-main{padding-top:12px;padding-bottom:12px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(96px,18vh,126px);gap:12px}.ppt-stage-label{padding:8px 11px}.ppt-notes-head{padding:12px 14px}.ppt-presenter-bottom{padding-top:10px;padding-bottom:10px}} /* ESC 索引页: 动画元素强制可见 (覆盖 motion-ready 的 opacity:0) */ #overview [data-anim]{opacity:1!important;transform:none!important} @@ -1572,7 +1576,7 @@ window.__pptPresenter=(()=>{ const slideId=i=>slides[i]?.dataset.slideId||`slide-${String(i+1).padStart(2,'0')}`; let mode='deck',session=query.get('session')||Math.random().toString(36).slice(2),audienceWindow=null; let channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0; - let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17); + let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17),presenterFrameObserver=null; let elapsed=0,timerRunning=false,timerStartedAt=0,lowPowerBeforePresenter=window.__lowPowerMode; const syncKey=()=>`guizang-ppt-sync:${location.pathname}:${session}`; const channelName=()=>`guizang-ppt:${location.pathname}:${session}`; @@ -1592,11 +1596,24 @@ window.__pptPresenter=(()=>{ url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1)); return url.href; } + function fitFrame(frame){ + const viewport=frame?.closest('.ppt-frame-viewport'); + if(!viewport)return; + const availableWidth=viewport.clientWidth,availableHeight=viewport.clientHeight; + if(!availableWidth||!availableHeight)return; + const width=Math.min(availableWidth,availableHeight*(16/9)); + frame.style.width=`${width}px`; + frame.style.height=`${width*(9/16)}px`; + } + function fitPresenterFrames(){ + fitFrame(document.getElementById('ppt-current-frame')); + fitFrame(document.getElementById('ppt-next-frame')); + } function mountShell(){ if(document.getElementById('ppt-presenter'))return; document.body.insertAdjacentHTML('beforeend',`
演讲者视图
观众屏 · 未连接
-
当前页
下一页
+
当前页
下一页
已保存
00:00
`); @@ -1612,7 +1629,10 @@ window.__pptPresenter=(()=>{ document.getElementById('ppt-note-smaller').onclick=()=>setNoteSize(noteSize-1); document.getElementById('ppt-note-larger').onclick=()=>setNoteSize(noteSize+1); document.getElementById('ppt-note-editor').addEventListener('input',saveNote); + if('ResizeObserver'in window){presenterFrameObserver=new ResizeObserver(()=>requestAnimationFrame(fitPresenterFrames));presenterFrameObserver.observe(document.querySelector('.ppt-preview-stack'));} + else addEventListener('resize',fitPresenterFrames); setNoteSize(noteSize,false); + requestAnimationFrame(fitPresenterFrames); } function updateShell(){ if(mode!=='presenter')return;mountShell(); @@ -1630,6 +1650,7 @@ window.__pptPresenter=(()=>{ document.getElementById('ppt-first').disabled=idx===0;document.getElementById('ppt-prev').disabled=idx===0;document.getElementById('ppt-next').disabled=idx===total-1; document.getElementById('ppt-last').textContent=idx===total-1?'重新开始':'尾页'; if(renderedNoteId!==id){renderedNoteId=id;editor.value=localStorage.getItem(noteStorageKey(id))??generatedNote(n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';} + requestAnimationFrame(fitPresenterFrames); updateSyncStatus(); } function saveNote(){ diff --git a/assets/template.html b/assets/template.html index 3ecc0af..d0e4a37 100644 --- a/assets/template.html +++ b/assets/template.html @@ -210,7 +210,7 @@ body.ppt-preview{pointer-events:none} body.ppt-presenter>#deck,body.ppt-presenter>#nav,body.ppt-presenter>#hint, body.ppt-presenter>.bg{visibility:hidden} - #ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:1000px;min-height:680px;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} + #ppt-presenter{position:fixed;inset:0;z-index:90;display:none;min-width:0;min-height:0;overflow:hidden;background:#101216;color:#f5f6f7;font-family:Inter,"Noto Sans SC",sans-serif} body.ppt-presenter #ppt-presenter{display:grid;grid-template-rows:auto 1fr auto} .ppt-presenter-top{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:18px 24px;border-bottom:1px solid #2b2e35;background:#15181d} .ppt-presenter-brand{font-weight:650;letter-spacing:.02em}.ppt-presenter-meta{display:flex;align-items:center;gap:12px;color:#aeb4be;font-size:14px} @@ -220,17 +220,21 @@ .ppt-sync[data-state="connecting"]::before{background:#f0c766}.ppt-sync[data-state="unsynced"]::before,.ppt-sync[data-state="blocked"]::before{background:#ff9e64}.ppt-sync[data-state="disconnected"]::before{background:#ff6b72} .ppt-ui-btn{appearance:none;border:1px solid #3a3e48;background:#20242a;color:#f6f7f8;border-radius:8px;padding:9px 13px;font:600 14px/1 Inter,"Noto Sans SC",sans-serif;cursor:pointer} .ppt-ui-btn:hover{background:#2a2f37;border-color:#525864}.ppt-ui-btn:focus-visible{outline:2px solid #84a8ff;outline-offset:2px}.ppt-ui-btn:disabled{opacity:.28;cursor:not-allowed} - .ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(320px,.75fr);gap:18px;min-height:0;padding:18px 24px} - .ppt-preview-stack{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,.42fr);gap:16px;min-width:0;align-items:start} + .ppt-presenter-main{display:grid;grid-template-columns:minmax(0,1.75fr) minmax(300px,.75fr);gap:18px;min-height:0;padding:18px 24px} + .ppt-preview-stack{display:grid;grid-template-rows:minmax(0,1fr) clamp(132px,22vh,210px);gap:16px;min-width:0;min-height:0} .ppt-stage-card,.ppt-notes-card{background:#171a20;border:1px solid #2b2f37;border-radius:12px;overflow:hidden} + .ppt-stage-card{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:0} .ppt-stage-label{display:flex;justify-content:space-between;align-items:center;padding:11px 14px;color:#aeb4be;font-size:13px;border-bottom:1px solid #292d34} - .ppt-frame{display:block;width:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86} + .ppt-frame-viewport{display:grid;place-items:center;min-width:0;min-height:0;overflow:hidden;background:#08090b} + .ppt-frame{display:block;width:0;height:0;max-width:100%;max-height:100%;aspect-ratio:16/9;border:0;background:#08090b}.ppt-next-frame{opacity:.86} .ppt-notes-card{display:grid;grid-template-rows:auto 1fr;min-height:0}.ppt-notes-head{padding:16px 18px;border-bottom:1px solid #292d34}.ppt-notes-title{font-size:20px;font-weight:650;margin-bottom:7px}.ppt-notes-sub{display:flex;align-items:center;justify-content:space-between;gap:10px;color:#9fa6b1;font-size:13px} .ppt-notes-tools{display:flex;align-items:center;gap:6px}.ppt-note-editor{width:100%;height:100%;min-height:0;resize:none;border:0;outline:0;background:#171a20;color:#edf0f3;padding:18px;font:400 var(--ppt-note-size,17px)/1.72 Inter,"Noto Sans SC",sans-serif} .ppt-saved{color:#72df99}.ppt-presenter-bottom{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:20px;padding:16px 24px;border-top:1px solid #2b2e35;background:#15181d} .ppt-timer{display:flex;align-items:center;gap:9px}.ppt-clock{min-width:72px;font:600 24px/1 var(--mono,monospace);font-variant-numeric:tabular-nums}.ppt-controls{display:flex;align-items:center;gap:8px}.ppt-controls .ppt-ui-btn{min-width:72px}.ppt-controls .ppt-primary{background:#f2f4f6;color:#111318;border-color:#f2f4f6}.ppt-controls .ppt-primary:hover{background:#fff}.ppt-page-state{text-align:right;color:#aeb4be;font-size:14px}.ppt-page-state strong{display:block;color:#fff;font-size:18px;margin-bottom:3px} #overview{z-index:110!important} - @media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(300px,.75fr)}.ppt-preview-stack{grid-template-columns:1fr}.ppt-preview-stack .ppt-stage-card:last-child{display:none}} + @media(max-width:1200px){.ppt-presenter-main{grid-template-columns:minmax(0,1.55fr) minmax(280px,.72fr)}} + @media(max-width:900px){.ppt-presenter-top{padding:12px 16px}.ppt-presenter-main{grid-template-columns:minmax(0,1.45fr) minmax(250px,.72fr);gap:12px;padding:12px 16px}.ppt-preview-stack{gap:12px;grid-template-rows:minmax(0,1fr) clamp(104px,20vh,150px)}.ppt-presenter-bottom{gap:8px;padding:12px 16px}.ppt-controls{gap:5px}.ppt-controls .ppt-ui-btn{min-width:58px;padding:8px 10px}.ppt-page-state{display:none}} + @media(max-height:700px){.ppt-presenter-top{padding-top:11px;padding-bottom:11px}.ppt-presenter-main{padding-top:12px;padding-bottom:12px}.ppt-preview-stack{grid-template-rows:minmax(0,1fr) clamp(96px,18vh,126px);gap:12px}.ppt-stage-label{padding:8px 11px}.ppt-notes-head{padding:12px 14px}.ppt-presenter-bottom{padding-top:10px;padding-bottom:10px}} /* ============================================================ ============ LAYOUTS API · 面向 agent 的类(v2)============ @@ -747,7 +751,7 @@ window.__pptPresenter=(()=>{ const slideId=i=>slides[i]?.dataset.slideId||`slide-${String(i+1).padStart(2,'0')}`; let mode='deck',session=query.get('session')||Math.random().toString(36).slice(2),audienceWindow=null; let channel=null,transportReady=false,popupBlocked=false,audienceOpenedAt=0,lastAckAt=0,lastAckIndex=-1,lastAckSeq=-1,seq=0; - let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17); + let renderedNoteId='',saveTimer=0,noteSize=Number(localStorage.getItem('guizang-ppt-note-size')||17),presenterFrameObserver=null; let elapsed=0,timerRunning=false,timerStartedAt=0,lowPowerBeforePresenter=window.__lowPowerMode; const syncKey=()=>`guizang-ppt-sync:${location.pathname}:${session}`; const channelName=()=>`guizang-ppt:${location.pathname}:${session}`; @@ -767,11 +771,24 @@ window.__pptPresenter=(()=>{ url.searchParams.set('mode',targetMode);url.searchParams.set('session',session);url.searchParams.set('slide',String(i+1)); return url.href; } + function fitFrame(frame){ + const viewport=frame?.closest('.ppt-frame-viewport'); + if(!viewport)return; + const availableWidth=viewport.clientWidth,availableHeight=viewport.clientHeight; + if(!availableWidth||!availableHeight)return; + const width=Math.min(availableWidth,availableHeight*(16/9)); + frame.style.width=`${width}px`; + frame.style.height=`${width*(9/16)}px`; + } + function fitPresenterFrames(){ + fitFrame(document.getElementById('ppt-current-frame')); + fitFrame(document.getElementById('ppt-next-frame')); + } function mountShell(){ if(document.getElementById('ppt-presenter'))return; document.body.insertAdjacentHTML('beforeend',`
演讲者视图
观众屏 · 未连接
-
当前页
下一页
+
当前页
下一页
已保存
00:00
`); @@ -787,7 +804,10 @@ window.__pptPresenter=(()=>{ document.getElementById('ppt-note-smaller').onclick=()=>setNoteSize(noteSize-1); document.getElementById('ppt-note-larger').onclick=()=>setNoteSize(noteSize+1); document.getElementById('ppt-note-editor').addEventListener('input',saveNote); + if('ResizeObserver'in window){presenterFrameObserver=new ResizeObserver(()=>requestAnimationFrame(fitPresenterFrames));presenterFrameObserver.observe(document.querySelector('.ppt-preview-stack'));} + else addEventListener('resize',fitPresenterFrames); setNoteSize(noteSize,false); + requestAnimationFrame(fitPresenterFrames); } function updateShell(){ if(mode!=='presenter')return;mountShell(); @@ -805,6 +825,7 @@ window.__pptPresenter=(()=>{ document.getElementById('ppt-first').disabled=idx===0;document.getElementById('ppt-prev').disabled=idx===0;document.getElementById('ppt-next').disabled=idx===total-1; document.getElementById('ppt-last').textContent=idx===total-1?'重新开始':'尾页'; if(renderedNoteId!==id){renderedNoteId=id;editor.value=localStorage.getItem(noteStorageKey(id))??generatedNote(n);editor.scrollTop=0;document.getElementById('ppt-saved').textContent='已保存';} + requestAnimationFrame(fitPresenterFrames); updateSyncStatus(); } function saveNote(){ diff --git a/references/checklist.md b/references/checklist.md index e9a97a3..5098909 100644 --- a/references/checklist.md +++ b/references/checklist.md @@ -14,6 +14,8 @@ **做法**: - 演讲入口只放在右下角现有快捷控制区,显示 `P 演讲模式`。 +- 演讲者主体只做“左侧预览 + 右侧备注”两栏;当前页在上、下一页在下,避免形成挤压当前页的三栏布局。 +- 两个预览 iframe 必须严格保持 `16:9`,容器不足时等比缩小并留边,不得裁切或拉伸;小屏优先压缩下一页预览高度。 - 演讲者控制顺序固定为 `首页 / 上一页 / 总览 / 下一页 / 尾页`;尾页时最后一个按钮变为“重新开始”。 - 观众屏必须显示软件链路状态:`连接中 / 已同步 / 未同步 / 未连接 / 弹窗被拦截`,并始终提供“重新打开观众屏”。 - 每页必须有稳定、唯一的 `data-slide-id`;`SPEAKER_NOTES` 数量、顺序和 ID 必须逐页一致。 @@ -26,7 +28,7 @@ node /scripts/validate-presenter-mode.mjs path/to/index.html ``` -浏览器里关闭观众窗口,确认状态变化;再点击“重新打开观众屏”,确认它恢复到演讲者当前页。 +浏览器里关闭观众窗口,确认状态变化;再点击“重新打开观众屏”,确认它恢复到演讲者当前页。缩小浏览器窗口再检查一次:当前页与下一页仍上下排列,两个 iframe 的 `width / height` 仍约等于 `16 / 9`,且没有超出预览容器。 ### 0-S. Swiss locked mode:正文页必须来自原始 22P diff --git a/references/presenter-mode.md b/references/presenter-mode.md index 4a6b3a6..cbcb888 100644 --- a/references/presenter-mode.md +++ b/references/presenter-mode.md @@ -63,6 +63,8 @@ window.__SPEAKER_NOTES__ = SPEAKER_NOTES; - 普通 deck 的右下角控制区显示 `P 演讲模式`,不单独悬浮一个抢眼按钮。 - 点击后当前窗口进入演讲者视图,并打开独立观众屏。 +- 演讲者主体保持两栏:左侧预览、右侧备注;左侧“当前页在上、下一页在下”,不要再把两个预览左右并排成第三栏。 +- 当前页与下一页 iframe 必须始终保持严格 `16:9`。空间不足时在预览容器内留边并整体等比缩小,不得裁切、压扁或让页面内部文字重新排版;小屏优先缩小下一页预览,把空间留给当前页。 - 控制顺序固定为:`首页 / 上一页 / 总览 / 下一页 / 尾页`;到尾页后“尾页”变为“重新开始”。 - 总览卡片显示页码、标题和当前页高亮,点击缩略图直接跳页;`ESC` 同样切换总览。 - 计时器由用户显式开始,可暂停、继续、重置;不要进入模式就自动计时。 @@ -90,4 +92,4 @@ node /scripts/validate-presenter-mode.mjs path/to/index.html node /scripts/validate-presenter-mode.mjs path/to/index.html --target-minutes 30 ``` -浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页、总览跳页、首页/尾页、尾页重新开始、观众窗口关闭后的状态变化、重新打开后恢复同步、备注保存与重排后不串页。 +浏览器实测至少包含:进入演讲模式、弹窗允许/拦截、前后翻页、总览跳页、首页/尾页、尾页重新开始、观众窗口关闭后的状态变化、重新打开后恢复同步、备注保存与重排后不串页。至少在一组常用尺寸和一组小屏尺寸下检查当前页/下一页为上下排列,两个 iframe 宽高比均为 `16:9`,且没有超出各自容器。 diff --git a/scripts/validate-presenter-mode.mjs b/scripts/validate-presenter-mode.mjs index 73fdd43..bd4965a 100644 --- a/scripts/validate-presenter-mode.mjs +++ b/scripts/validate-presenter-mode.mjs @@ -78,6 +78,10 @@ else{ const runtimeChecks=[ ['right-bottom presenter entry','id="ppt-presenter-btn"'], ['presenter shell','id="ppt-presenter"'], + ['vertical preview stack','.ppt-preview-stack{display:grid;grid-template-rows:minmax(0,1fr)'], + ['16:9 preview viewport','class="ppt-frame-viewport"'], + ['proportional preview fitter','fitPresenterFrames'], + ['exact 16:9 fitting','availableHeight*(16/9)'], ['overview control','id="ppt-grid"'], ['first-page control','id="ppt-first"'], ['last/restart control','id="ppt-last"'], @@ -88,6 +92,7 @@ const runtimeChecks=[ ['storage fallback','__guizangPptSync'], ]; for(const [label,needle]of runtimeChecks)if(!html.includes(needle))errors.push(`Presenter runtime missing ${label}.`); +if(/\.ppt-preview-stack\{[^}]*grid-template-columns/.test(html))errors.push('Presenter previews must stack vertically; remove grid-template-columns from .ppt-preview-stack.'); const planned=speakerNotes.reduce((sum,n)=>sum+(Number(n?.minutes)||0),0); if(Number.isFinite(targetMinutes)&&targetMinutes>0&&planned>targetMinutes*.9)errors.push(`Planned ${planned.toFixed(1)} min exceeds the 90% speaking budget (${(targetMinutes*.9).toFixed(1)} of ${targetMinutes} min).`);