Created
March 12, 2026 00:00
-
-
Save alexknowshtml/002e0825b1959d4e19123dc2b5742a84 to your computer and use it in GitHub Desktop.
Good Neighbors Hackathon Welcome Slides
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Good Neighbors Hackathon — Welcome</title> | |
| <!-- Fonts: Fraunces (display) + DM Mono (body) --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Fraunces:opsz,wght@9..144,400;9..144,700;9..144,900&display=swap" rel="stylesheet"> | |
| <style> | |
| /* =========================================== | |
| GOOD NEIGHBORS — CSS CUSTOM PROPERTIES | |
| =========================================== */ | |
| :root { | |
| --paper: #F7F3ED; | |
| --ink: #1C1C1C; | |
| --red: #E63946; | |
| --blue: #457B9D; | |
| --yellow: #F4A261; | |
| --green: #2A9D8F; | |
| --cream: #FAF6EE; | |
| --font-display: 'Fraunces', serif; | |
| --font-body: 'DM Mono', monospace; | |
| --title-size: clamp(2.5rem, 7vw, 6rem); | |
| --h2-size: clamp(1.5rem, 4vw, 3rem); | |
| --h3-size: clamp(1.1rem, 2.5vw, 1.75rem); | |
| --body-size: clamp(0.8rem, 1.5vw, 1.125rem); | |
| --small-size: clamp(0.7rem, 1.1vw, 0.875rem); | |
| --slide-padding: clamp(1.5rem, 5vw, 5rem); | |
| --content-gap: clamp(0.75rem, 2vw, 2rem); | |
| --element-gap: clamp(0.25rem, 1vw, 1rem); | |
| --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); | |
| --duration-normal: 0.6s; | |
| } | |
| /* =========================================== | |
| BASE STYLES | |
| =========================================== */ | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| /* --- VIEWPORT BASE (from viewport-base.css) --- */ | |
| html, body { | |
| height: 100%; | |
| overflow-x: hidden; | |
| } | |
| html { | |
| scroll-snap-type: y mandatory; | |
| scroll-behavior: smooth; | |
| } | |
| .slide { | |
| width: 100vw; | |
| height: 100vh; | |
| height: 100dvh; | |
| overflow: hidden; | |
| scroll-snap-align: start; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| .slide-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| max-height: 100%; | |
| overflow: hidden; | |
| padding: var(--slide-padding); | |
| } | |
| .card, .container, .content-box { | |
| max-width: min(90vw, 1000px); | |
| max-height: min(80vh, 700px); | |
| } | |
| .feature-list, .bullet-list { | |
| gap: clamp(0.4rem, 1vh, 1rem); | |
| } | |
| .feature-list li, .bullet-list li { | |
| font-size: var(--body-size); | |
| line-height: 1.4; | |
| } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); | |
| gap: clamp(0.5rem, 1.5vw, 1rem); | |
| } | |
| img, .image-container { | |
| max-width: 100%; | |
| max-height: min(50vh, 400px); | |
| object-fit: contain; | |
| } | |
| /* Responsive breakpoints */ | |
| @media (max-height: 700px) { | |
| :root { | |
| --slide-padding: clamp(0.75rem, 3vw, 2rem); | |
| --content-gap: clamp(0.4rem, 1.5vw, 1rem); | |
| --title-size: clamp(1.75rem, 5vw, 3.5rem); | |
| --h2-size: clamp(1.25rem, 3vw, 2rem); | |
| } | |
| } | |
| @media (max-height: 600px) { | |
| :root { | |
| --slide-padding: clamp(0.5rem, 2.5vw, 1.5rem); | |
| --content-gap: clamp(0.3rem, 1vw, 0.75rem); | |
| --title-size: clamp(1.5rem, 4.5vw, 2.5rem); | |
| --body-size: clamp(0.7rem, 1.2vw, 0.95rem); | |
| } | |
| .nav-dots, .keyboard-hint, .decorative { display: none; } | |
| } | |
| @media (max-height: 500px) { | |
| :root { | |
| --slide-padding: clamp(0.4rem, 2vw, 1rem); | |
| --title-size: clamp(1.25rem, 4vw, 2rem); | |
| --h2-size: clamp(1rem, 2.5vw, 1.5rem); | |
| --body-size: clamp(0.65rem, 1vw, 0.85rem); | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| :root { --title-size: clamp(1.75rem, 9vw, 3rem); } | |
| .grid { grid-template-columns: 1fr; } | |
| .sponsor-grid { grid-template-columns: 1fr !important; } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms !important; | |
| transition-duration: 0.2s !important; | |
| } | |
| html { scroll-behavior: auto; } | |
| } | |
| /* --- END VIEWPORT BASE --- */ | |
| body { | |
| font-family: var(--font-body); | |
| background: var(--paper); | |
| color: var(--ink); | |
| } | |
| /* =========================================== | |
| GRAIN TEXTURE OVERLAY | |
| Warm analog feel across all slides | |
| =========================================== */ | |
| .slide::after { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| pointer-events: none; | |
| opacity: 0.08; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); | |
| background-size: 150px; | |
| z-index: 1; | |
| } | |
| /* Ensure content sits above grain */ | |
| .slide-content, .slide > * { position: relative; z-index: 2; } | |
| /* =========================================== | |
| TYPOGRAPHY | |
| =========================================== */ | |
| h1, h2, h3 { | |
| font-family: var(--font-display); | |
| font-weight: 900; | |
| line-height: 1.1; | |
| letter-spacing: -0.02em; | |
| } | |
| h1 { font-size: var(--title-size); } | |
| h2 { font-size: var(--h2-size); } | |
| h3 { font-size: var(--h3-size); } | |
| p, li, span, .mono { | |
| font-family: var(--font-body); | |
| font-size: var(--body-size); | |
| line-height: 1.6; | |
| } | |
| .label { | |
| font-family: var(--font-body); | |
| font-size: var(--small-size); | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| } | |
| /* =========================================== | |
| COLOR ACCENT BLOCKS | |
| Flat, confident color markers | |
| =========================================== */ | |
| .accent-bar { | |
| width: clamp(3rem, 8vw, 6rem); | |
| height: clamp(4px, 0.5vh, 6px); | |
| border-radius: 2px; | |
| } | |
| .accent-red { background: var(--red); } | |
| .accent-blue { background: var(--blue); } | |
| .accent-yellow { background: var(--yellow); } | |
| .accent-green { background: var(--green); } | |
| /* =========================================== | |
| ANIMATIONS | |
| Warm, editorial — staggered reveals | |
| =========================================== */ | |
| .reveal { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| transition: opacity var(--duration-normal) var(--ease-out-expo), | |
| transform var(--duration-normal) var(--ease-out-expo); | |
| } | |
| .slide.visible .reveal { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .reveal:nth-child(1) { transition-delay: 0.1s; } | |
| .reveal:nth-child(2) { transition-delay: 0.2s; } | |
| .reveal:nth-child(3) { transition-delay: 0.3s; } | |
| .reveal:nth-child(4) { transition-delay: 0.4s; } | |
| .reveal:nth-child(5) { transition-delay: 0.5s; } | |
| .reveal:nth-child(6) { transition-delay: 0.6s; } | |
| /* =========================================== | |
| SLIDE-SPECIFIC STYLES | |
| =========================================== */ | |
| /* --- TITLE SLIDE --- */ | |
| .title-slide { | |
| background: var(--ink); | |
| color: var(--paper); | |
| align-items: center; | |
| text-align: center; | |
| } | |
| .title-slide .slide-content { | |
| align-items: center; | |
| gap: var(--content-gap); | |
| } | |
| .title-slide h1 { | |
| font-size: clamp(3rem, 9vw, 8rem); | |
| font-optical-sizing: auto; | |
| } | |
| .title-slide .event-date { | |
| color: var(--yellow); | |
| font-family: var(--font-body); | |
| font-size: clamp(1rem, 2vw, 1.5rem); | |
| font-weight: 500; | |
| } | |
| .title-slide .venue { | |
| color: var(--paper); | |
| opacity: 0.6; | |
| } | |
| /* --- SPONSOR SLIDE --- */ | |
| .sponsors-slide .slide-content { | |
| gap: var(--content-gap); | |
| } | |
| .sponsor-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: clamp(1rem, 3vw, 2.5rem); | |
| margin-top: var(--content-gap); | |
| } | |
| .sponsor-card { | |
| background: var(--cream); | |
| border-radius: clamp(8px, 1vw, 12px); | |
| padding: clamp(1rem, 2.5vw, 2rem); | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.25rem, 0.5vw, 0.5rem); | |
| } | |
| .sponsor-card.title-sponsor { | |
| grid-column: 1 / -1; | |
| border-left: clamp(4px, 0.5vw, 6px) solid var(--red); | |
| } | |
| .sponsor-card .sponsor-tier { | |
| color: var(--blue); | |
| } | |
| .sponsor-card h3 { | |
| font-family: var(--font-display); | |
| font-weight: 700; | |
| } | |
| .sponsor-card p { | |
| font-size: var(--small-size); | |
| opacity: 0.7; | |
| } | |
| /* --- WIFI SLIDE --- */ | |
| .wifi-slide { | |
| background: var(--blue); | |
| color: var(--paper); | |
| } | |
| .wifi-slide .slide-content { | |
| align-items: center; | |
| text-align: center; | |
| gap: clamp(1.5rem, 4vw, 3rem); | |
| } | |
| .wifi-slide h2 { | |
| font-size: clamp(2rem, 5vw, 4rem); | |
| } | |
| .wifi-box { | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.75rem, 2vw, 1.5rem); | |
| background: rgba(255,255,255,0.12); | |
| border-radius: clamp(12px, 2vw, 20px); | |
| padding: clamp(1.5rem, 4vw, 3rem) clamp(2rem, 6vw, 5rem); | |
| } | |
| .wifi-field { | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.15rem, 0.3vw, 0.25rem); | |
| } | |
| .wifi-field .label { | |
| opacity: 0.7; | |
| } | |
| .wifi-field .value { | |
| font-family: var(--font-display); | |
| font-size: clamp(1.5rem, 4vw, 3rem); | |
| font-weight: 700; | |
| letter-spacing: 0.05em; | |
| } | |
| /* --- CODE OF CONDUCT SLIDE --- */ | |
| .conduct-slide .slide-content { | |
| gap: var(--content-gap); | |
| } | |
| .conduct-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.6rem, 1.5vh, 1.2rem); | |
| margin-top: var(--element-gap); | |
| } | |
| .conduct-list li { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: clamp(0.5rem, 1vw, 1rem); | |
| } | |
| .conduct-list .dot { | |
| width: clamp(8px, 1vw, 12px); | |
| height: clamp(8px, 1vw, 12px); | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| margin-top: clamp(4px, 0.5vw, 8px); | |
| } | |
| /* --- SCHEDULE SLIDE --- */ | |
| .schedule-slide .slide-content { | |
| gap: var(--content-gap); | |
| } | |
| .schedule-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.4rem, 1vh, 0.8rem); | |
| } | |
| .schedule-list li { | |
| display: flex; | |
| align-items: baseline; | |
| gap: clamp(0.75rem, 2vw, 1.5rem); | |
| } | |
| .schedule-list .time { | |
| font-family: var(--font-body); | |
| font-weight: 500; | |
| font-size: var(--body-size); | |
| color: var(--blue); | |
| min-width: clamp(4rem, 8vw, 6rem); | |
| flex-shrink: 0; | |
| } | |
| .schedule-list .event { | |
| font-size: var(--body-size); | |
| } | |
| .schedule-list .event strong { | |
| font-weight: 500; | |
| } | |
| .schedule-divider { | |
| width: 100%; | |
| height: 1px; | |
| background: var(--ink); | |
| opacity: 0.1; | |
| margin: clamp(0.15rem, 0.3vh, 0.3rem) 0; | |
| } | |
| /* --- RULES SLIDE --- */ | |
| .rules-slide { | |
| background: var(--red); | |
| color: var(--paper); | |
| } | |
| .rules-slide .slide-content { | |
| gap: var(--content-gap); | |
| } | |
| .rules-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(0.6rem, 1.5vh, 1.2rem); | |
| } | |
| .rules-list li { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: clamp(0.5rem, 1vw, 1rem); | |
| } | |
| .rules-list .number { | |
| font-family: var(--font-display); | |
| font-weight: 900; | |
| font-size: clamp(1.25rem, 2.5vw, 2rem); | |
| opacity: 0.4; | |
| min-width: clamp(1.5rem, 3vw, 2.5rem); | |
| line-height: 1; | |
| } | |
| .rules-list .rule-text { | |
| padding-top: clamp(2px, 0.3vw, 4px); | |
| } | |
| /* --- CLOSING SLIDE --- */ | |
| .closing-slide { | |
| background: var(--green); | |
| color: var(--paper); | |
| text-align: center; | |
| } | |
| .closing-slide .slide-content { | |
| align-items: center; | |
| gap: var(--content-gap); | |
| } | |
| .closing-slide h1 { | |
| font-size: clamp(3rem, 10vw, 9rem); | |
| } | |
| .closing-slide p { | |
| opacity: 0.8; | |
| font-size: clamp(1rem, 2vw, 1.5rem); | |
| } | |
| /* =========================================== | |
| PROGRESS BAR | |
| =========================================== */ | |
| .progress-bar { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| height: 3px; | |
| background: var(--red); | |
| z-index: 100; | |
| transition: width 0.3s ease; | |
| } | |
| /* =========================================== | |
| NAV DOTS | |
| =========================================== */ | |
| .nav-dots { | |
| position: fixed; | |
| right: clamp(0.75rem, 2vw, 1.5rem); | |
| top: 50%; | |
| transform: translateY(-50%); | |
| display: flex; | |
| flex-direction: column; | |
| gap: clamp(6px, 0.8vh, 10px); | |
| z-index: 100; | |
| } | |
| .nav-dot { | |
| width: clamp(6px, 0.8vw, 10px); | |
| height: clamp(6px, 0.8vw, 10px); | |
| border-radius: 50%; | |
| background: var(--ink); | |
| opacity: 0.2; | |
| cursor: pointer; | |
| transition: opacity 0.3s ease, transform 0.3s ease; | |
| border: none; | |
| padding: 0; | |
| } | |
| .nav-dot.active { | |
| opacity: 0.8; | |
| transform: scale(1.3); | |
| } | |
| /* Invert dots on dark slides */ | |
| .slide.dark-bg ~ .nav-dots .nav-dot, | |
| .nav-dot.on-dark { | |
| background: var(--paper); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Progress bar --> | |
| <div class="progress-bar" id="progressBar"></div> | |
| <!-- Navigation dots --> | |
| <nav class="nav-dots" id="navDots" aria-label="Slide navigation"></nav> | |
| <!-- =========================================== | |
| SLIDE 1: TITLE | |
| =========================================== --> | |
| <section class="slide title-slide" data-theme="dark"> | |
| <div class="slide-content"> | |
| <span class="label reveal event-date">March 15, 2026</span> | |
| <h1 class="reveal">Good<br>Neighbors</h1> | |
| <span class="label reveal venue">Hackathon at Indy Hall</span> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 2: SPONSORS | |
| Thank the folks making this possible | |
| =========================================== --> | |
| <section class="slide sponsors-slide" data-theme="light"> | |
| <div class="slide-content"> | |
| <div class="accent-bar accent-yellow reveal"></div> | |
| <h2 class="reveal">Made possible by</h2> | |
| <div class="sponsor-grid"> | |
| <div class="sponsor-card title-sponsor reveal"> | |
| <span class="label sponsor-tier">Title Sponsor</span> | |
| <h3>Supabase</h3> | |
| <p>All projects built on Supabase</p> | |
| </div> | |
| <div class="sponsor-card reveal"> | |
| <span class="label sponsor-tier">Community</span> | |
| <h3>Resilient Coders</h3> | |
| </div> | |
| <div class="sponsor-card reveal"> | |
| <span class="label sponsor-tier">Breakfast</span> | |
| <h3>OmbuLabs</h3> | |
| </div> | |
| <div class="sponsor-card reveal"> | |
| <span class="label sponsor-tier">Sponsor</span> | |
| <h3>Guru</h3> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 3: WIFI | |
| Big, readable credentials | |
| =========================================== --> | |
| <section class="slide wifi-slide" data-theme="dark"> | |
| <div class="slide-content"> | |
| <h2 class="reveal">Get Connected</h2> | |
| <div class="wifi-box reveal"> | |
| <div class="wifi-field"> | |
| <span class="label">Network</span> | |
| <span class="value">Indy Hall</span> | |
| </div> | |
| <div class="wifi-field"> | |
| <span class="label">Password</span> | |
| <span class="value">coworking</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 4: CODE OF CONDUCT | |
| Be a good neighbor | |
| =========================================== --> | |
| <section class="slide conduct-slide" data-theme="light"> | |
| <div class="slide-content"> | |
| <div class="accent-bar accent-green reveal"></div> | |
| <h2 class="reveal">Be a Good Neighbor</h2> | |
| <ul class="conduct-list"> | |
| <li class="reveal"> | |
| <span class="dot accent-green"></span> | |
| <span>Treat everyone with respect regardless of experience level</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="dot accent-blue"></span> | |
| <span>Ask questions freely and help others when you can</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="dot accent-yellow"></span> | |
| <span>All skill levels welcome. Solo attendees welcome.</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="dot accent-red"></span> | |
| <span>Harassment of any kind means you're out. No warnings.</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="dot accent-green"></span> | |
| <span>Clean up after yourself. Leave it better than you found it.</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 5: SCHEDULE | |
| How the day works | |
| =========================================== --> | |
| <section class="slide schedule-slide" data-theme="light"> | |
| <div class="slide-content"> | |
| <div class="accent-bar accent-blue reveal"></div> | |
| <h2 class="reveal">How the Day Works</h2> | |
| <ul class="schedule-list"> | |
| <li class="reveal"> | |
| <span class="time">9:00am</span> | |
| <span class="event">Doors open — coffee & breakfast</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="time">10:00am</span> | |
| <span class="event"><strong>Kickoff</strong> — welcome, theme, team formation</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="time">10:30am</span> | |
| <span class="event">Teams formed, hacking begins</span> | |
| </li> | |
| <li class="reveal"><div class="schedule-divider"></div></li> | |
| <li class="reveal"> | |
| <span class="time">12:30pm</span> | |
| <span class="event">Lunch</span> | |
| </li> | |
| <li class="reveal"><div class="schedule-divider"></div></li> | |
| <li class="reveal"> | |
| <span class="time">6:00pm</span> | |
| <span class="event"><strong>Keyboards down</strong></span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="time">6:30pm</span> | |
| <span class="event"><strong>Demos & judging</strong></span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="time">7:30pm</span> | |
| <span class="event">Awards + social hour</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 6: THE RULES | |
| What you need to know | |
| =========================================== --> | |
| <section class="slide rules-slide" data-theme="dark"> | |
| <div class="slide-content"> | |
| <h2 class="reveal">The Rules</h2> | |
| <ul class="rules-list"> | |
| <li class="reveal"> | |
| <span class="number">01</span> | |
| <span class="rule-text">Every project must use <strong>Supabase</strong></span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="number">02</span> | |
| <span class="rule-text">Keyboards down at <strong>6:00pm sharp</strong></span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="number">03</span> | |
| <span class="rule-text">Every team demos — even if it's broken</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="number">04</span> | |
| <span class="rule-text">Teams of 2–5 people. Solo is fine too.</span> | |
| </li> | |
| <li class="reveal"> | |
| <span class="number">05</span> | |
| <span class="rule-text">Have fun. Ship something. Be a good neighbor.</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </section> | |
| <!-- =========================================== | |
| SLIDE 7: LET'S BUILD | |
| Closing hype slide | |
| =========================================== --> | |
| <section class="slide closing-slide" data-theme="dark"> | |
| <div class="slide-content"> | |
| <h1 class="reveal">Let's Build.</h1> | |
| <p class="reveal">Neighbors building together, not a startup pitch competition.</p> | |
| </div> | |
| </section> | |
| <script> | |
| /* =========================================== | |
| SLIDE PRESENTATION CONTROLLER | |
| Handles navigation, animations, progress | |
| =========================================== */ | |
| class SlidePresentation { | |
| constructor() { | |
| this.slides = document.querySelectorAll('.slide'); | |
| this.currentSlide = 0; | |
| this.isScrolling = false; | |
| this.scrollTimeout = null; | |
| this.setupIntersectionObserver(); | |
| this.setupKeyboardNav(); | |
| this.setupTouchNav(); | |
| this.setupWheelNav(); | |
| this.setupProgressBar(); | |
| this.setupNavDots(); | |
| this.updateNavDotColors(); | |
| } | |
| /* Trigger .visible class when slides enter viewport */ | |
| setupIntersectionObserver() { | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('visible'); | |
| const index = Array.from(this.slides).indexOf(entry.target); | |
| this.currentSlide = index; | |
| this.updateProgressBar(); | |
| this.updateNavDots(); | |
| this.updateNavDotColors(); | |
| } | |
| }); | |
| }, { threshold: 0.5 }); | |
| this.slides.forEach(slide => observer.observe(slide)); | |
| } | |
| /* Arrow keys, Space, Page Up/Down */ | |
| setupKeyboardNav() { | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown' || e.key === 'ArrowRight') { | |
| e.preventDefault(); | |
| this.goToSlide(this.currentSlide + 1); | |
| } else if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'ArrowLeft') { | |
| e.preventDefault(); | |
| this.goToSlide(this.currentSlide - 1); | |
| } else if (e.key === 'Home') { | |
| e.preventDefault(); | |
| this.goToSlide(0); | |
| } else if (e.key === 'End') { | |
| e.preventDefault(); | |
| this.goToSlide(this.slides.length - 1); | |
| } | |
| }); | |
| } | |
| /* Touch/swipe support */ | |
| setupTouchNav() { | |
| let startY = 0; | |
| document.addEventListener('touchstart', (e) => { | |
| startY = e.touches[0].clientY; | |
| }, { passive: true }); | |
| document.addEventListener('touchend', (e) => { | |
| const diff = startY - e.changedTouches[0].clientY; | |
| if (Math.abs(diff) > 50) { | |
| this.goToSlide(this.currentSlide + (diff > 0 ? 1 : -1)); | |
| } | |
| }, { passive: true }); | |
| } | |
| /* Mouse wheel with debounce */ | |
| setupWheelNav() { | |
| document.addEventListener('wheel', (e) => { | |
| if (this.isScrolling) return; | |
| this.isScrolling = true; | |
| if (e.deltaY > 0) { | |
| this.goToSlide(this.currentSlide + 1); | |
| } else if (e.deltaY < 0) { | |
| this.goToSlide(this.currentSlide - 1); | |
| } | |
| clearTimeout(this.scrollTimeout); | |
| this.scrollTimeout = setTimeout(() => { | |
| this.isScrolling = false; | |
| }, 800); | |
| }, { passive: true }); | |
| } | |
| goToSlide(index) { | |
| if (index < 0 || index >= this.slides.length) return; | |
| this.currentSlide = index; | |
| this.slides[index].scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| /* Progress bar at top */ | |
| setupProgressBar() { | |
| this.progressBar = document.getElementById('progressBar'); | |
| this.updateProgressBar(); | |
| } | |
| updateProgressBar() { | |
| if (!this.progressBar) return; | |
| const progress = ((this.currentSlide + 1) / this.slides.length) * 100; | |
| this.progressBar.style.width = progress + '%'; | |
| } | |
| /* Navigation dots on right side */ | |
| setupNavDots() { | |
| const container = document.getElementById('navDots'); | |
| if (!container) return; | |
| this.slides.forEach((_, i) => { | |
| const dot = document.createElement('button'); | |
| dot.className = 'nav-dot' + (i === 0 ? ' active' : ''); | |
| dot.setAttribute('aria-label', `Go to slide ${i + 1}`); | |
| dot.addEventListener('click', () => this.goToSlide(i)); | |
| container.appendChild(dot); | |
| }); | |
| this.navDots = container.querySelectorAll('.nav-dot'); | |
| } | |
| updateNavDots() { | |
| if (!this.navDots) return; | |
| this.navDots.forEach((dot, i) => { | |
| dot.classList.toggle('active', i === this.currentSlide); | |
| }); | |
| } | |
| /* Invert dot color on dark-themed slides */ | |
| updateNavDotColors() { | |
| if (!this.navDots) return; | |
| const currentTheme = this.slides[this.currentSlide]?.dataset.theme; | |
| this.navDots.forEach(dot => { | |
| dot.classList.toggle('on-dark', currentTheme === 'dark'); | |
| }); | |
| } | |
| } | |
| /* Initialize on load */ | |
| new SlidePresentation(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment