diff --git a/SKILL.md b/SKILL.md index 144572e..fd9b211 100644 --- a/SKILL.md +++ b/SKILL.md @@ -43,9 +43,12 @@ See [references/presenter-mode.md](references/presenter-mode.md) for the full au 2. **每页 150–300 字** — 2–3 分钟/页的节奏 3. **用口语,不用书面语** — "因此"→"所以","该方案"→"这个方案" -All full-deck templates technically support the S key presenter view (it's built into `runtime.js`), but only `presenter-mode-reveal` is designed from the ground up around the feature with proper example 逐字稿 on every slide. +All full-deck templates support the S key presenter view (it's built into `runtime.js`). **S opens a separate popup window** — the original page stays as the audience view, and the popup shows current/next slide (CSS scale at 1920×1080 design resolution, pixel-perfect) + large speaker script + timer. The two windows sync navigation via BroadcastChannel. -Keyboard in presenter mode: `S` toggle · `T` cycle theme · `← →` navigate · `R` reset timer · `Esc` close. +Only `presenter-mode-reveal` is designed from the ground up around the feature with proper example 逐字稿 on every slide. + +Keyboard in presenter window: `← →` navigate (syncs audience) · `R` reset timer · `Esc` close popup. +Keyboard in audience window: `S` open presenter · `T` cycle theme · `← →` navigate (syncs presenter) · `F` fullscreen · `O` overview. ## Before you author anything — ALWAYS ask or recommend @@ -194,9 +197,9 @@ capture, runtime.js exposes `#/N` deep-links, and render.sh iterates 1..N. ``` ← → Space PgUp PgDn Home End navigate F fullscreen -S presenter view (current + next + script + timer) -N quick notes drawer (bottom, legacy) -R reset timer (only in presenter view) +S open presenter window (new popup: current + next + script + timer) +N quick notes drawer (bottom overlay) +R reset timer (in presenter window) O slide overview grid T cycle themes (reads data-themes attr) A cycle demo animation on current slide diff --git a/assets/base.css b/assets/base.css index 6efa679..b8c1107 100644 --- a/assets/base.css +++ b/assets/base.css @@ -132,45 +132,10 @@ h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px} .overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)} /* ================= PRESENTER VIEW ================= */ -/* Split-view presenter mode: current slide + next preview + speaker script + timer. - * Toggle with S key. Hides the audience deck, shows this view only to presenter. */ -.presenter-view{position:fixed;inset:0;z-index:80;display:none; - background:#0a0e1a;color:#e6edf3;font-family:var(--font-sans); - grid-template-columns:1.35fr 1fr;gap:20px;padding:20px} -.presenter-view.open{display:grid} -body.presenter-mode .deck, -body.presenter-mode .progress-bar, -body.presenter-mode .deck-header, -body.presenter-mode .deck-footer, -body.presenter-mode .notes-overlay{display:none!important} -.pv-label{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:#8b949e; - margin-bottom:8px;font-weight:600} -.pv-main{display:flex;flex-direction:column;min-height:0} -.pv-stage{flex:1;background:var(--bg,#0d1117);border:1px solid rgba(255,255,255,.08); - border-radius:14px;overflow:hidden;position:relative} -.pv-stage .slide{position:absolute;inset:0;transform:none!important;opacity:1!important; - width:100%;height:100%} -.pv-side{display:flex;flex-direction:column;gap:14px;min-height:0} -.pv-next{flex:0 0 30%;display:flex;flex-direction:column;min-height:0} -.pv-next-stage{border:1px solid rgba(255,255,255,.06);border-radius:10px;opacity:.85} -.pv-next .pv-end{display:flex;align-items:center;justify-content:center;height:100%; - font-size:18px;color:#8b949e;letter-spacing:.12em} -.pv-notes{flex:1;display:flex;flex-direction:column;min-height:0; - background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.06); - border-radius:12px;padding:16px 20px} -.pv-notes-body{flex:1;overflow:auto;font-size:18px;line-height:1.75;color:#d0d7de; - padding-right:6px;font-family:var(--font-sans)} -.pv-notes-body p{margin:0 0 .8em 0} -.pv-notes-body strong{color:#f0883e} -.pv-notes-body em{color:#58a6ff;font-style:normal} -.pv-empty{color:#484f58;font-style:italic} -.pv-bar{display:flex;align-items:center;gap:16px;padding:10px 14px; - background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06); - border-radius:10px;font-size:13px;color:#8b949e} -.pv-timer{font-family:var(--font-mono,monospace);font-size:22px;font-weight:700;color:#3fb950; - letter-spacing:.04em} -.pv-count{font-weight:600;color:#e6edf3} -.pv-hint{margin-left:auto;font-size:11px;color:#6e7681;letter-spacing:.08em} +/* Presenter view opens in a separate popup window (S key). + * All presenter styles are self-contained in the popup HTML generated by runtime.js. + * The audience window (this file) is NOT affected — it stays as normal deck view. + * Only the .notes class below is needed to hide speaker notes from audience. */ /* ================= UTILITY ================= */ .hidden{display:none!important} diff --git a/assets/runtime.js b/assets/runtime.js index e706d3f..1afea73 100644 --- a/assets/runtime.js +++ b/assets/runtime.js @@ -4,8 +4,10 @@ * Features: * ← → / space / PgUp PgDn / Home End navigation * F fullscreen - * S presenter mode (split view: current + next + notes + timer) - * N quick notes overlay (bottom drawer, legacy) + * S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer) + * The original window stays as audience view, synced via BroadcastChannel. + * Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout. + * N quick notes overlay (bottom drawer) * O slide overview grid * T cycle themes (reads data-themes on or
) * A cycle demo animation on current slide @@ -32,7 +34,15 @@ let idx = 0; const total = slides.length; - // progress bar + /* ===== BroadcastChannel for presenter sync ===== */ + const CHANNEL_NAME = 'html-ppt-presenter-' + (location.pathname + location.search); + let bc; + try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; } + + // Are we running inside the presenter popup? + const isPresenterWindow = location.hash.indexOf('__presenter__') !== -1; + + /* ===== progress bar ===== */ let bar = document.querySelector('.progress-bar'); if (!bar) { bar = document.createElement('div'); @@ -42,7 +52,7 @@ } const barFill = bar.querySelector('span'); - // notes overlay + /* ===== notes overlay (N key) ===== */ let notes = document.querySelector('.notes-overlay'); if (!notes) { notes = document.createElement('div'); @@ -50,7 +60,7 @@ document.body.appendChild(notes); } - // overview + /* ===== overview grid (O key) ===== */ let overview = document.querySelector('.overview'); if (!overview) { overview = document.createElement('div'); @@ -67,7 +77,8 @@ document.body.appendChild(overview); } - function go(n){ + /* ===== navigation ===== */ + function go(n, fromRemote){ n = Math.max(0, Math.min(total-1, n)); slides.forEach((s,i) => { s.classList.toggle('is-active', i===n); @@ -77,13 +88,17 @@ barFill.style.width = ((n+1)/total*100)+'%'; const numEl = document.querySelector('.slide-number'); if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); } - // notes (bottom overlay, legacy N key) + + // notes (bottom overlay) const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes'); notes.innerHTML = note ? note.innerHTML : ''; - // presenter view live update - renderPresenter(n); + // hash - if (location.hash !== '#/'+(n+1)) history.replaceState(null,'','#/'+(n+1)); + const hashTarget = '#/'+(n+1); + if (location.hash !== hashTarget && !isPresenterWindow) { + history.replaceState(null,'', hashTarget); + } + // re-trigger entry animations slides[n].querySelectorAll('[data-anim]').forEach(el => { const a = el.getAttribute('data-anim'); @@ -91,6 +106,7 @@ void el.offsetWidth; el.classList.add('anim-'+a); }); + // counter-up slides[n].querySelectorAll('.counter').forEach(el => { const target = parseFloat(el.getAttribute('data-to')||el.textContent); @@ -105,102 +121,235 @@ } requestAnimationFrame(tick); }); + + // Broadcast to other window (audience ↔ presenter) + if (!fromRemote && bc) { + bc.postMessage({ type: 'go', idx: n }); + } + } + + /* ===== listen for remote navigation ===== */ + if (bc) { + bc.onmessage = function(e) { + if (e.data && e.data.type === 'go' && typeof e.data.idx === 'number') { + go(e.data.idx, true); + } + }; } function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); } function toggleOverview(force){ overview.classList.toggle('open', force!==undefined?force:!overview.classList.contains('open')); } - // ========== PRESENTER MODE ========== - // Split view: current slide (left) + next slide thumb + large speaker notes + timer - let presenter = document.querySelector('.presenter-view'); - if (!presenter) { - presenter = document.createElement('div'); - presenter.className = 'presenter-view'; - presenter.innerHTML = '' - + '