/* global React */const { useEffect, useRef, useState } = React;// ============================================================// DESIGN TOKENS — pinned to 1920×1080 slide canvas// ===================
88c1242 verified | // ============================================================ | |
| // SLIDE CANVAS — Presentation Kit | |
| // Vanilla JS Slide Engine + Demo Slides | |
| // ============================================================ | |
| // ---------------------------------------------------------- | |
| // PRIMITIVE HELPERS — Generate HTML strings | |
| // ---------------------------------------------------------- | |
| const TYPE = { | |
| eyebrow: 24, micro: 24, body: 32, bodyLg: 38, small: 26, | |
| kicker: 44, title: 88, titleLg: 120, display: 200, mega: 280, | |
| }; | |
| const C = { | |
| orange: '#FF4D00', black: '#000000', white: '#FFFFFF', | |
| white20: 'rgba(255,255,255,0.20)', white10: 'rgba(255,255,255,0.10)', | |
| black20: 'rgba(0,0,0,0.20)', black10: 'rgba(0,0,0,0.10)', | |
| }; | |
| function metaBar(left, right, extraStyle = '') { | |
| return `<div class="meta-bar" style="${extraStyle}"> | |
| <span>${left}</span> | |
| <span class="meta-right">${right}</span> | |
| </div>`; | |
| } | |
| function bottomMeta(left, right, fg = C.white) { | |
| return `<div class="bottom-meta" style="color:${fg}"> | |
| <span>${left}</span><span>${right}</span> | |
| </div>`; | |
| } | |
| function display(text, size = 'title', color = 'inherit', extraStyle = '') { | |
| return `<div class="display ${size}" style="color:${color};${extraStyle}">${text}</div>`; | |
| } | |
| function bodyText(text, size = 'size-body', extraStyle = '') { | |
| return `<p class="body-text ${size}" style="${extraStyle}">${text}</p>`; | |
| } | |
| function mono(text, size = 'size-eyebrow', extraStyle = '') { | |
| return `<span class="mono ${size}" style="${extraStyle}">${text}</span>`; | |
| } | |
| function idx(n, color = C.orange, size = TYPE.eyebrow) { | |
| return `<span class="idx" style="font-size:${size}px;color:${color}">${String(n).padStart(2,'0')}</span>`; | |
| } | |
| function marquee(text, dir = 'left', bg = C.black, fg = C.white, skew = true, fontSize = '10vw') { | |
| const items = Array(6).fill(text); | |
| const itemsHtml = items.map(t => | |
| `<span class="marquee-item" style="font-size:${fontSize};color:${fg}">${t}<span class="diamond">◆</span></span>` | |
| ).join(''); | |
| return `<div class="marquee-strip ${skew ? 'skew' : ''}" style="background:${bg}"> | |
| <div class="marquee-track dir-${dir}">${itemsHtml}${itemsHtml}</div> | |
| </div>`; | |
| } | |
| function badge(text, size = 180, fg = C.white, spinning = true) { | |
| const r = size / 2 - 18; | |
| const chars = text; | |
| const repeated = `${chars} · ${chars} · ${chars} · `; | |
| return `<div class="badge ${spinning ? 'spinning' : ''}" style="width:${size}px;height:${size}px"> | |
| <svg viewBox="0 0 ${size} ${size}"> | |
| <defs> | |
| <path id="bp-${size}" d="M ${size/2},${size/2} m -${r},0 a ${r},${r} 0 1,1 ${r*2},0 a ${r},${r} 0 1,1 -${r*2},0"/> | |
| </defs> | |
| <circle cx="${size/2}" cy="${size/2}" r="${r+10}" fill="none" stroke="${fg}" stroke-width="2"/> | |
| <text fill="${fg}" font-family="'Space Mono',monospace" font-size="24" letter-spacing="4"> | |
| <textPath href="#bp-${size}" startOffset="0%">${repeated}</textPath> | |
| </text> | |
| </svg> | |
| </div>`; | |
| } | |
| function arrowSvg(size = 40, color = 'currentColor', rotate = 0) { | |
| return `<svg class="arrow-svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter" style="transform:rotate(${rotate}deg)"> | |
| <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | |
| </svg>`; | |
| } | |
| // ---------------------------------------------------------- | |
| // SLIDE DATA | |
| // ---------------------------------------------------------- | |
| const slides = [ | |
| // ---- SLIDE 0: Title / Cover ---- | |
| { | |
| bg: C.orange, | |
| fg: C.black, | |
| html: ` | |
| <div style="flex:1;display:flex;flex-direction:column;justify-content:center;position:relative"> | |
| ${metaBar('SLIDE CANVAS · 01', 'PRESENTATION KIT')} | |
| <div class="anim-fade-in-up anim-delay-1">${display('SLIDE', 'titleLg', C.black)}</div> | |
| <div class="anim-fade-in-up anim-delay-2" style="margin-top:8px">${display('CANVAS', 'mega', C.black)}</div> | |
| <div style="margin-top:40px;max-width:680px" class="anim-fade-in-up anim-delay-3"> | |
| ${bodyText('A design system for building bold, high-impact presentation slides. Primitives, tokens, and animations — ready to compose.', 'size-bodyLg')} | |
| </div> | |
| <div style="position:absolute;right:0;top:50%;transform:translateY(-50%)" class="anim-fade-in anim-delay-4"> | |
| ${badge('SLIDE CANVAS · PRESENTATION KIT · ', 220, C.black, true)} | |
| </div> | |
| </div> | |
| ${bottomMeta('2024 · VANILLA JS', 'DESIGN TOKENS + PRIMITIVES', C.black)} | |
| ` | |
| }, | |
| // ---- SLIDE 1: Marquee Divider ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| noPad: true, | |
| html: ` | |
| <div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 var(--sp-pxLg)"> | |
| ${display('DESIGN', 'display', C.white, 'margin-bottom:16px;')} | |
| ${display('TOKENS', 'mega', C.orange)} | |
| </div> | |
| ${marquee('DESIGN TOKENS · TYPE · SPACE · COLOR · FONT', 'left', C.orange, C.black, true, '6vw')} | |
| ` | |
| }, | |
| // ---- SLIDE 2: Design Tokens Overview ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| html: ` | |
| ${metaBar('TOKENS · 02', 'TYPOGRAPHY + SPACE + COLOR')} | |
| <div class="feature-grid anim-scale-in anim-delay-1"> | |
| <div class="feature-card"> | |
| <div class="card-idx">${idx(1)} ${mono('TYPE SCALE')}</div> | |
| <div class="card-title">Typography</div> | |
| <div class="card-body">10-step type scale from eyebrow (24px) to mega (280px). Display faces for impact, body faces for readability.</div> | |
| <div style="margin-top:auto;display:flex;gap:8px;flex-wrap:wrap"> | |
| <span style="font-size:14px;color:${C.orange};font-family:var(--f-mono)">24</span> | |
| <span style="font-size:18px;color:${C.orange};font-family:var(--f-mono)">32</span> | |
| <span style="font-size:24px;color:${C.orange};font-family:var(--f-mono)">44</span> | |
| <span style="font-size:32px;color:${C.orange};font-family:var(--f-mono)">88</span> | |
| <span style="font-size:40px;color:${C.orange};font-family:var(--f-mono)">120</span> | |
| </div> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="card-idx">${idx(2)} ${mono('SPACING')}</div> | |
| <div class="card-title">Space</div> | |
| <div class="card-body">Consistent spacing tokens for padding, gaps, and layout rhythm. Slide padding, item gaps, and title spacing all tokenized.</div> | |
| <div style="margin-top:auto;display:flex;flex-direction:column;gap:6px"> | |
| <div style="display:flex;align-items:center;gap:12px"><div style="width:32px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">32px</span></div> | |
| <div style="display:flex;align-items:center;gap:12px"><div style="width:56px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">56px</span></div> | |
| <div style="display:flex;align-items:center;gap:12px"><div style="width:80px;height:6px;background:${C.orange};border-radius:3px"></div><span style="font-family:var(--f-mono);font-size:14px;opacity:0.5">120px</span></div> | |
| </div> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="card-idx">${idx(3)} ${mono('COLOR')}</div> | |
| <div class="card-title">Color</div> | |
| <div class="card-body">Brand orange as the singular accent. Black, white, and transparency steps for layering and depth.</div> | |
| <div style="margin-top:auto;display:flex;gap:8px"> | |
| <div style="width:40px;height:40px;border-radius:8px;background:${C.orange}" title="#FF4D00"></div> | |
| <div style="width:40px;height:40px;border-radius:8px;background:${C.black};border:1px solid rgba(255,255,255,0.2)" title="#000000"></div> | |
| <div style="width:40px;height:40px;border-radius:8px;background:${C.white}" title="#FFFFFF"></div> | |
| <div style="width:40px;height:40px;border-radius:8px;background:rgba(255,255,255,0.20)" title="20%"></div> | |
| <div style="width:40px;height:40px;border-radius:8px;background:rgba(255,255,255,0.10)" title="10%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| ` | |
| }, | |
| // ---- SLIDE 3: Stats ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| html: ` | |
| ${metaBar('IMPACT · 03', 'BY THE NUMBERS')} | |
| <div style="flex:1;display:flex;flex-direction:column;justify-content:center"> | |
| <div class="stat-row anim-fade-in-up anim-delay-1" style="margin-bottom:64px"> | |
| <div class="stat-item"> | |
| <div class="stat-number" style="color:${C.orange}">10</div> | |
| <div class="stat-label">Type Scale Steps</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number">8</div> | |
| <div class="stat-label">Spacing Tokens</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number" style="color:${C.orange}">6</div> | |
| <div class="stat-label">Color Tokens</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number">9</div> | |
| <div class="stat-label">UI Primitives</div> | |
| </div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-3" style="display:flex;flex-direction:column;gap:20px"> | |
| <div style="display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);text-transform:uppercase;letter-spacing:-0.02em"><span>Design Coverage</span><span style="color:${C.orange}">92%</span></div> | |
| <div class="progress-bar"><div class="progress-fill" style="width:92%"></div></div> | |
| <div style="display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);text-transform:uppercase;letter-spacing:-0.02em;margin-top:16px"><span>Token Adoption</span><span style="color:${C.orange}">87%</span></div> | |
| <div class="progress-bar"><div class="progress-fill" style="width:87%"></div></div> | |
| </div> | |
| </div> | |
| ${bottomMeta('03 · IMPACT', 'DATA-DRIVEN DESIGN')} | |
| ` | |
| }, | |
| // ---- SLIDE 4: Timeline ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| html: ` | |
| ${metaBar('ROADMAP · 04', 'EVOLUTION')} | |
| <div class="two-col"> | |
| <div style="display:flex;flex-direction:column;gap:24px" class="anim-fade-in-up anim-delay-1"> | |
| ${display('THE', 'kicker')} | |
| ${display('ROAD-', 'titleLg')} | |
| ${display('MAP', 'titleLg', C.orange)} | |
| ${bodyText('A phased approach to building a complete design system for high-impact presentations.', 'size-body')} | |
| </div> | |
| <div class="timeline anim-fade-in-up anim-delay-2"> | |
| <div class="timeline-item"> | |
| <div class="timeline-year">PHASE 1</div> | |
| <div class="timeline-content"> | |
| <div class="timeline-title">Tokens & Primitives</div> | |
| <div class="timeline-desc">Define color, type, spacing, and build base UI components.</div> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-year">PHASE 2</div> | |
| <div class="timeline-content"> | |
| <div class="timeline-title">Slide Templates</div> | |
| <div class="timeline-desc">Compose primitives into reusable slide layouts: title, split, data, quote.</div> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-year">PHASE 3</div> | |
| <div class="timeline-content"> | |
| <div class="timeline-title">Motion & Transitions</div> | |
| <div class="timeline-desc">Add entrance animations, slide transitions, and micro-interactions.</div> | |
| </div> | |
| </div> | |
| <div class="timeline-item"> | |
| <div class="timeline-year">PHASE 4</div> | |
| <div class="timeline-content"> | |
| <div class="timeline-title">Export & Publish</div> | |
| <div class="timeline-desc">Generate PDFs, share links, and present fullscreen from the browser.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ` | |
| }, | |
| // ---- SLIDE 5: Primitives Showcase ---- | |
| { | |
| bg: C.orange, | |
| fg: C.black, | |
| html: ` | |
| ${metaBar('PRIMITIVES · 05', 'BUILDING BLOCKS', 'border-color:currentColor')} | |
| <div style="flex:1;display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-gap)"> | |
| <div class="anim-fade-in-up anim-delay-1" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">METABAR</div> | |
| <div style="border-bottom:2px solid currentColor;padding-bottom:12px;display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);letter-spacing:-0.02em;text-transform:uppercase"><span>SECTION · 05</span><span style="opacity:0.55">STATUS</span></div> | |
| <div style="border-bottom:2px solid currentColor;padding-bottom:12px;display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:var(--t-eyebrow);letter-spacing:-0.02em;text-transform:uppercase"><span>SECTION · 06</span><span style="opacity:0.55">META</span></div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-2" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">DISPLAY</div> | |
| <div class="display kicker">KICKER 44</div> | |
| <div class="display title">TITLE 88</div> | |
| <div class="display titleLg">BIG 120</div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-3" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">BADGE</div> | |
| <div style="display:flex;gap:24px;align-items:center;margin-top:8px"> | |
| ${badge('PRIMITIVES · ', 140, C.black, true)} | |
| ${badge('DEMO · ', 110, C.black, true)} | |
| </div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-4" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">BODY TEXT</div> | |
| <p class="body-text size-body">Body text at 32px for comfortable reading on large canvases.</p> | |
| <p class="body-text size-small" style="opacity:0.65">Small body at 26px for secondary content and captions.</p> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-5" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">INDEX + ARROW</div> | |
| <div style="display:flex;align-items:center;gap:16px;font-size:var(--t-title)"> | |
| <span class="idx" style="color:${C.black}">01</span> ${arrowSvg(40, C.black, 0)} | |
| </div> | |
| <div style="display:flex;align-items:center;gap:16px;font-size:var(--t-title)"> | |
| <span class="idx" style="color:${C.black}">02</span> ${arrowSvg(40, C.black, 0)} | |
| </div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-6" style="display:flex;flex-direction:column;gap:16px"> | |
| <div class="mono size-eyebrow" style="color:rgba(0,0,0,0.5)">MONO LABEL</div> | |
| <div class="mono size-eyebrow">UPPERCASE MONO 24</div> | |
| <div class="mono size-micro" style="opacity:0.55">MICRO LABEL 24</div> | |
| </div> | |
| </div> | |
| ${bottomMeta('05 · PRIMITIVES', '9 COMPONENTS', C.black)} | |
| ` | |
| }, | |
| // ---- SLIDE 6: Quote ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| html: ` | |
| <div style="flex:1;display:flex;align-items:center"> | |
| <div class="quote-block anim-fade-in-up anim-delay-1"> | |
| <div style="font-family:var(--f-display);font-size:var(--t-display);color:${C.orange};line-height:0.88;letter-spacing:-0.04em">"</div> | |
| <div class="quote-text">DESIGN IS<br/>NOT JUST WHAT<br/>IT LOOKS LIKE.</div> | |
| <div class="quote-attr">— STEVE JOBS · QUOTE SLIDE PRIMITIVE</div> | |
| </div> | |
| </div> | |
| ${bottomMeta('06 · PHILOSOPHY', 'QUOTE PRIMITIVE')} | |
| ` | |
| }, | |
| // ---- SLIDE 7: Image + Text Split ---- | |
| { | |
| bg: C.black, | |
| fg: C.white, | |
| html: ` | |
| ${metaBar('SHOWCASE · 07', 'IMAGE + TEXT')} | |
| <div class="two-col" style="flex:1"> | |
| <div class="anim-fade-in-up anim-delay-1"> | |
| ${display('VISUAL', 'titleLg')} | |
| ${display('IMPACT', 'titleLg', C.orange)} | |
| <div style="margin-top:24px"> | |
| ${bodyText('Full-bleed images paired with bold typography create slides that command attention and communicate with clarity.', 'size-body')} | |
| </div> | |
| <div style="margin-top:32px;display:flex;align-items:center;gap:12px;color:${C.orange}"> | |
| ${mono('EXPLORE MORE')} ${arrowSvg(24, C.orange, 0)} | |
| </div> | |
| </div> | |
| <div class="img-block anim-scale-in anim-delay-2" style="height:100%;max-height:580px"> | |
| <img src="http://static.photos/abstract/1024x576/42" alt="Abstract visual" loading="lazy" /> | |
| </div> | |
| </div> | |
| ${bottomMeta('07 · SHOWCASE', 'SPLIT LAYOUT')} | |
| ` | |
| }, | |
| // ---- SLIDE 8: Marquee + End ---- | |
| { | |
| bg: C.orange, | |
| fg: C.black, | |
| noPad: true, | |
| html: ` | |
| ${marquee('SLIDE CANVAS · PRESENTATION KIT · DESIGN SYSTEM · ', 'right', C.black, C.orange, true, '8vw')} | |
| <div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;background:${C.orange};padding:0 var(--sp-pxLg)"> | |
| <div class="end-badge-container"> | |
| <div class="anim-scale-in anim-delay-1"> | |
| ${badge('END · SLIDE CANVAS · PRESENTATION KIT · FIN · ', 260, C.black, true)} | |
| </div> | |
| </div> | |
| <div class="anim-fade-in-up anim-delay-2" style="text-align:center;margin-top:40px"> | |
| ${display('THANK YOU', 'titleLg', C.black)} | |
| </div> | |
| <div class="mono size-eyebrow anim-fade-in anim-delay-3" style="margin-top:16px;opacity:0.55;color:${C.black}"> | |
| BUILT WITH DESIGN TOKENS + PRIMITIVES | |
| </div> | |
| </div> | |
| ${marquee('THANK YOU · GRAZIE · MERCI · DANKE · ARIGATO · ', 'left', C.black, C.orange, true, '6vw')} | |
| ` | |
| }, | |
| ]; | |
| // ---------------------------------------------------------- | |
| // SLIDE ENGINE | |
| // ---------------------------------------------------------- | |
| let currentSlide = 0; | |
| let isTransitioning = false; | |
| let overviewOpen = false; | |
| function renderSlides() { | |
| const container = document.getElementById('slide-container'); | |
| container.innerHTML = slides.map((s, i) => { | |
| const padClass = s.noPad ? 'no-pad' : ''; | |
| return `<div class="slide ${padClass} ${i === 0 ? 'active' : ''}" data-slide="${i}" | |
| style="background:${s.bg};color:${s.fg}"> | |
| ${s.html} | |
| </div>`; | |
| }).join(''); | |
| } | |
| function goToSlide(n, animate = true) { | |
| if (isTransitioning || n < 0 || n >= slides.length) return; | |
| isTransitioning = true; | |
| const allSlides = document.querySelectorAll('.slide'); | |
| const prev = allSlides[currentSlide]; | |
| const next = allSlides[n]; | |
| if (prev) prev.classList.remove('active'); | |
| if (next) next.classList.add('active'); | |
| currentSlide = n; | |
| updateIndicator(); | |
| setTimeout(() => { isTransitioning = false; }, animate ? 500 : 0); | |
| } | |
| function nextSlide() { goToSlide(currentSlide + 1); } | |
| function prevSlide() { goToSlide(currentSlide - 1); } | |
| function updateIndicator() { | |
| const el = document.getElementById('slide-indicator'); | |
| el.textContent = `${String(currentSlide + 1).padStart(2, '0')} / ${String(slides.length).padStart(2, '0')}`; | |
| } | |
| // ---------------------------------------------------------- | |
| // OVERVIEW | |
| // ---------------------------------------------------------- | |
| function toggleOverview() { | |
| overviewOpen = !overviewOpen; | |
| const modal = document.getElementById('overview-modal'); | |
| if (overviewOpen) { | |
| buildOverview(); | |
| modal.classList.remove('hidden'); | |
| modal.classList.add('flex'); | |
| } else { | |
| modal.classList.add('hidden'); | |
| modal.classList.remove('flex'); | |
| } | |
| } | |
| function buildOverview() { | |
| const grid = document.getElementById('overview-grid'); | |
| grid.innerHTML = slides.map((s, i) => { | |
| const padClass = s.noPad ? 'no-pad' : ''; | |
| return `<div class="overview-card" onclick="overviewGo(${i})" title="Slide ${i+1}"> | |
| <div class="slide ${padClass}" style="background:${s.bg};color:${s.fg};position:relative;pointer-events:none;transform:scale(1);opacity:1;padding:24px 32px 20px;font-size:40%;overflow:hidden;aspect-ratio:16/9"> | |
| <div style="transform:scale(0.25);transform-origin:top left;width:400%;height:400%;pointer-events:none"> | |
| ${s.html} | |
| </div> | |
| </div> | |
| <div class="overview-label">${String(i+1).padStart(2,'0')}</div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| function overviewGo(n) { | |
| toggleOverview(); | |
| setTimeout(() => goToSlide(n), 200); | |
| } | |
| // ---------------------------------------------------------- | |
| // FULLSCREEN | |
| // ---------------------------------------------------------- | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(() => {}); | |
| } else { | |
| document.exitFullscreen().catch(() => {}); | |
| } | |
| } | |
| // ---------------------------------------------------------- | |
| // KEYBOARD | |
| // ---------------------------------------------------------- | |
| document.addEventListener('keydown', (e) => { | |
| if (overviewOpen) { | |
| if (e.key === 'Escape') toggleOverview(); | |
| return; | |
| } | |
| switch (e.key) { | |
| case 'ArrowRight': | |
| case 'ArrowDown': | |
| case ' ': | |
| case 'PageDown': | |
| e.preventDefault(); | |
| nextSlide(); | |
| break; | |
| case 'ArrowLeft': | |
| case 'ArrowUp': | |
| case 'PageUp': | |
| e.preventDefault(); | |
| prevSlide(); | |
| break; | |
| case 'Home': | |
| e.preventDefault(); | |
| goToSlide(0); | |
| break; | |
| case 'End': | |
| e.preventDefault(); | |
| goToSlide(slides.length - 1); | |
| break; | |
| case 'f': | |
| case 'F': | |
| toggleFullscreen(); | |
| break; | |
| case 'Escape': | |
| if (document.fullscreenElement) toggleFullscreen(); | |
| break; | |
| case 'o': | |
| case 'O': | |
| toggleOverview(); | |
| break; | |
| } | |
| }); | |
| // Touch support | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| document.addEventListener('touchstart', (e) => { | |
| touchStartX = e.changedTouches[0].screenX; | |
| touchStartY = e.changedTouches[0].screenY; | |
| }, { passive: true }); | |
| document.addEventListener('touchend', (e) => { | |
| const dx = e.changedTouches[0].screenX - touchStartX; | |
| const dy = e.changedTouches[0].screenY - touchStartY; | |
| if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) { | |
| if (dx < 0) nextSlide(); | |
| else prevSlide(); | |
| } | |
| }, { passive: true }); | |
| // Hide key hint after first navigation | |
| let hintHidden = false; | |
| function hideHint() { | |
| if (hintHidden) return; | |
| hintHidden = true; | |
| const hint = document.getElementById('key-hint'); | |
| if (hint) hint.style.opacity = '0'; | |
| } | |
| const origNext = nextSlide; | |
| const origPrev = prevSlide; | |
| // Override to also hide hint | |
| window.nextSlide = function() { hideHint(); origNext(); }; | |
| window.prevSlide = function() { hideHint(); origPrev(); }; | |
| // ---------------------------------------------------------- | |
| // INIT | |
| // ---------------------------------------------------------- | |
| function init() { | |
| renderSlides(); | |
| updateIndicator(); | |
| // Auto-hide hint after 6s | |
| setTimeout(() => { hideHint(); }, 6000); | |
| } | |
| document.addEventListener('DOMContentLoaded', init); |