diff --git a/SKILL.md b/SKILL.md index 37d3073d..24fae99c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -457,6 +457,7 @@ Before saving, verify all 8 spec rules pass. Fix any that fail. Save the HTML fi 3. **Summarize** — Tell the user: - File location, style name, slide count - Navigation: Arrow keys, Space, scroll/swipe, click nav dots + - Public functions: `goTo(index)`, `next()`, `prev()`, and `replayActiveSlide()` to restart the current slide's entry animations - Speaker notes: Open DevTools (F12), detach to separate window — notes appear in console on each slide change - How to customize: `:root` CSS variables for colors, font link for typography, `.reveal` class for animations - If inline editing was enabled: Hover top-left corner or press E to enter edit mode, click any text to edit, Ctrl+S to save diff --git a/assets/slides-runtime.js b/assets/slides-runtime.js index 9af41234..b1da115b 100644 --- a/assets/slides-runtime.js +++ b/assets/slides-runtime.js @@ -39,7 +39,25 @@ function updateUI() { document.querySelectorAll('.slide-nav-dot').forEach((d,i) => d.classList.toggle('active', i===current)); } +// True when an overlay owns navigation. Authors mark custom overlays with +// data-blocks-nav; the skill's own lightbox is covered with no wiring. +function navBlocked() { + return !!(document.querySelector('.diagram-lightbox.active') || + document.querySelector('[data-blocks-nav]:not([hidden])')); +} + +// Restart the staggered entry animations on the current slide. Standalone so +// the next, prev, keyboard, and dot contract stays untouched. +function replayActiveSlide() { + const s = slides[current]; + if (!s) return; + s.classList.remove('active'); + void s.offsetWidth; // force reflow so the re-add restarts the animations + s.classList.add('active'); +} + function goTo(index) { + if (navBlocked()) return; if (index<0 || index>=total || index===current) return; const prev = current; current = index; @@ -66,18 +84,44 @@ document.addEventListener('keydown', (e) => { if (e.key==='End') { e.preventDefault(); goTo(total-1); } }); -let touchStart = 0; -document.addEventListener('touchstart', (e) => { touchStart=e.touches[0].clientX; }); +// True when the target sits inside an element that can scroll under the +// pointer, so wheel and swipe deltas belong to the content, not the deck. +function inScrollable(target) { + let el = target; + while (el && el !== document.body) { + if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) return true; + el = el.parentElement; + } + return false; +} + +let touchX = 0, touchY = 0, touchT = 0; +document.addEventListener('touchstart', (e) => { + touchX = e.touches[0].clientX; + touchY = e.touches[0].clientY; + touchT = Date.now(); +}); document.addEventListener('touchend', (e) => { - const diff = touchStart - e.changedTouches[0].clientX; - if (Math.abs(diff)>50) { diff>0 ? next() : prev(); } + if (e.changedTouches.length > 1) return; // ignore multi-touch gestures + // Let interactive and flippable targets keep their own gestures. + if (e.target.closest('.flip-card, a, button, input, textarea, select')) return; + const dx = e.changedTouches[0].clientX - touchX; + const dy = e.changedTouches[0].clientY - touchY; + if (Math.abs(dx) <= Math.abs(dy)) return; // require horizontal dominance + if (Math.abs(dx) < 50) return; // require a deliberate distance + if (Date.now() - touchT > 600) return; // reject slow drags + dx < 0 ? next() : prev(); }); let wheelCD = false; document.addEventListener('wheel', (e) => { + if (inScrollable(e.target)) return; // let scrollable content keep the delta + if (Math.abs(e.deltaX) < 10 && Math.abs(e.deltaY) < 10) return; // ignore noise if (wheelCD) return; wheelCD = true; setTimeout(() => wheelCD=false, 600); - if (e.deltaY>0||e.deltaX>0) next(); else prev(); + // Navigate on the dominant axis only, so a diagonal scroll never reverses. + const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY; + delta > 0 ? next() : prev(); }, {passive:true}); updateUI(); diff --git a/references/html-template.md b/references/html-template.md index d48d56c1..c396aa00 100644 --- a/references/html-template.md +++ b/references/html-template.md @@ -126,7 +126,25 @@ Every generated HTML file **must** comply with these rules: var current = 0; var total = slides.length; + // True when an overlay owns navigation. Authors mark custom overlays + // with data-blocks-nav; the skill's own lightbox is covered with no wiring. + function navBlocked() { + return !!(document.querySelector('.diagram-lightbox.active') || + document.querySelector('[data-blocks-nav]:not([hidden])')); + } + + // Restart the staggered entry animations on the current slide. Standalone + // so the next, prev, keyboard, and dot contract stays untouched. + function replayActiveSlide() { + var s = slides[current]; + if (!s) return; + s.classList.remove('active'); + void s.offsetWidth; // force reflow so the re-add restarts the animations + s.classList.add('active'); + } + function goTo(index) { + if (navBlocked()) return; if (index < 0 || index >= total || index === current) return; current = index; slides.forEach(function(s, i) { @@ -163,20 +181,46 @@ Every generated HTML file **must** comply with these rules: if (e.key === 'End') { e.preventDefault(); goTo(total - 1); } }); + // True when the target sits inside an element that can scroll under the + // pointer, so wheel and swipe deltas belong to the content, not the deck. + function inScrollable(target) { + var el = target; + while (el && el !== document.body) { + if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) return true; + el = el.parentElement; + } + return false; + } + // Touch/swipe navigation - var touchStart = 0; - document.addEventListener('touchstart', function(e) { touchStart = e.touches[0].clientX; }); + var touchX = 0, touchY = 0, touchT = 0; + document.addEventListener('touchstart', function(e) { + touchX = e.touches[0].clientX; + touchY = e.touches[0].clientY; + touchT = Date.now(); + }); document.addEventListener('touchend', function(e) { - var diff = touchStart - e.changedTouches[0].clientX; - if (Math.abs(diff) > 50) { diff > 0 ? next() : prev(); } + if (e.changedTouches.length > 1) return; // ignore multi-touch gestures + // Let interactive and flippable targets keep their own gestures. + if (e.target.closest('.flip-card, a, button, input, textarea, select')) return; + var dx = e.changedTouches[0].clientX - touchX; + var dy = e.changedTouches[0].clientY - touchY; + if (Math.abs(dx) <= Math.abs(dy)) return; // require horizontal dominance + if (Math.abs(dx) < 50) return; // require a deliberate distance + if (Date.now() - touchT > 600) return; // reject slow drags + dx < 0 ? next() : prev(); }); // Mouse wheel navigation var wheelCD = false; document.addEventListener('wheel', function(e) { + if (inScrollable(e.target)) return; // let scrollable content keep the delta + if (Math.abs(e.deltaX) < 10 && Math.abs(e.deltaY) < 10) return; // ignore noise if (wheelCD) return; wheelCD = true; setTimeout(function() { wheelCD = false; }, 600); - if (e.deltaY > 0 || e.deltaX > 0) next(); else prev(); + // Navigate on the dominant axis only, so a diagonal scroll never reverses. + var delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY; + delta > 0 ? next() : prev(); }, {passive: true}); // Nav dots (generate) @@ -247,6 +291,7 @@ Every presentation must include these **global functions**: 1. **`goTo(index)`** — Navigate to slide at index. Toggle `.active` class. 2. **`next()`** — Go to next slide. 3. **`prev()`** — Go to previous slide. +4. **`replayActiveSlide()`** - Restart the entry animations on the current slide without changing slides. Removes `.active`, forces a reflow, then re-adds `.active`. These must be plain functions on `window`, not inside an IIFE, class, or module. diff --git a/references/presentation-layer.md b/references/presentation-layer.md index b74a593f..347eac60 100644 --- a/references/presentation-layer.md +++ b/references/presentation-layer.md @@ -95,6 +95,19 @@ The presentation must support: Build navigation inline following the pattern in [html-template.md](html-template.md). Generate `goTo()`, `next()`, `prev()` as global functions, plus speaker notes console logging on each slide change. +### Public functions + +| Function | Purpose | +|----------|---------| +| `goTo(index)` | Navigate to the slide at `index`. Toggles the `.active` class and updates the chrome. | +| `next()` | Go to the next slide. | +| `prev()` | Go to the previous slide. | +| `replayActiveSlide()` | Restart the entry animations on the current slide without changing slides. Removes `.active`, forces a reflow, then re-adds `.active`. Useful after a slide returns from an overlay or a rotation. | + +Navigation is suppressed while an overlay is open. `goTo()` returns early when a `.diagram-lightbox.active` element exists or any element carries `data-blocks-nav` and is not hidden, so arrow keys, the wheel, and swipes do not move the slide behind a modal. Mark custom overlays with `data-blocks-nav` to opt into the same guard. + +The wheel and swipe handlers ignore deltas that originate inside a scrollable element, so wide code blocks and tall cards keep their own scroll. Wheel navigation reads the dominant axis only, and swipe navigation requires horizontal dominance, a minimum distance of about 50px, and a short gesture duration before it advances. + ## Generator Meta Tag Every presentation must include in `
`: diff --git a/testing/fixtures/deck-overlay-and-codeblock.html b/testing/fixtures/deck-overlay-and-codeblock.html new file mode 100644 index 00000000..637a0a0f --- /dev/null +++ b/testing/fixtures/deck-overlay-and-codeblock.html @@ -0,0 +1,173 @@ + + + + + + +Overlay open. Navigation is blocked.
+ +