Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ When converting PowerPoint files:
2. **Open** — Use `open [filename].html` to launch in browser
3. **Summarize** — Tell the user:
- File location, style name, slide count
- Navigation: Arrow keys, Space, scroll/swipe, click nav dots
- Navigation: Arrow keys, Space, mouse wheel, click slide / nav dots (no scrollbar — transform-based deck)
- 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

Expand Down
179 changes: 129 additions & 50 deletions html-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,65 +91,142 @@ Reference architecture for generating slide presentations. Every presentation fo
</style>
</head>
<body>
<!-- Optional: Progress bar -->
<div class="progress-bar"></div>

<!-- Optional: Navigation dots -->
<nav class="nav-dots"><!-- Generated by JS --></nav>

<!-- Slides -->
<section class="slide title-slide">
<h1 class="reveal">Presentation Title</h1>
<p class="reveal">Subtitle or author</p>
</section>

<section class="slide">
<div class="slide-content">
<h2 class="reveal">Slide Title</h2>
<p class="reveal">Content...</p>
</div>
</section>

<!-- More slides... -->
<!-- Optional: Progress bar (position:fixed, z-index above .deck) -->
<div class="progress-bar" id="progressBar"></div>

<!-- Optional: Navigation dots (position:fixed, z-index above .deck) -->
<nav class="nav-dots" id="navDots"><!-- Generated by JS --></nav>

<!-- All slides live inside a single .deck container. JS moves the deck
via transform:translateY() to navigate. No scrollbar ever shows
(body has overflow:hidden), and the feel is true slide-by-slide
advancement rather than scrolling-that-snaps. -->
<div class="deck" id="deck">
<section class="slide title-slide">
<h1 class="reveal">Presentation Title</h1>
<p class="reveal">Subtitle or author</p>
</section>

<section class="slide">
<div class="slide-content">
<h2 class="reveal">Slide Title</h2>
<p class="reveal">Content...</p>
</div>
</section>

<!-- More slides... -->
</div><!-- /.deck -->

<script>
/* ===========================================
SLIDE PRESENTATION CONTROLLER
Transform-based navigation: JS sets `.deck` transform to
translateY(-idx * 100vh) and the CSS transition on `.deck`
handles the smooth animation.
=========================================== */
class SlidePresentation {
constructor() {
this.slides = document.querySelectorAll(".slide");
this.currentSlide = 0;
this.setupIntersectionObserver();
this.setupKeyboardNav();
this.setupTouchNav();
this.setupProgressBar();
this.slides = Array.from(document.querySelectorAll(".slide"));
this.deck = document.getElementById("deck");
this.dotsEl = document.getElementById("navDots");
this.progressEl = document.getElementById("progressBar");
this.current = 0;
this.locked = false;
this.setupNavDots();
this.setupKeyboardNav();
this.setupWheelNav();
this.setupClickNav();
this.activate(0); // initial slide
}

setupIntersectionObserver() {
// Add .visible class when slides enter viewport
// Triggers CSS animations efficiently
}

setupKeyboardNav() {
// Arrow keys, Space, Page Up/Down
}

setupTouchNav() {
// Touch/swipe support for mobile
// Mark a slide active: update dot highlight, progress bar, and
// trigger reveal animations by adding .visible to the slide.
// Replaces the older Intersection Observer pattern, which doesn't
// apply now that slides never enter/leave the viewport via scroll.
activate(idx) {
Array.from(this.dotsEl.children).forEach((d, i) =>
d.classList.toggle("active", i === idx)
);
this.progressEl.style.width =
((idx + 1) / this.slides.length * 100) + "%";
this.slides[idx].classList.add("visible");
}

setupProgressBar() {
// Update progress bar on scroll
// Move the deck. CSS transition on .deck handles the smooth motion;
// the timeout matches the transition duration so consecutive
// rapid-fire calls don't queue past the visible animation.
goTo(idx) {
idx = Math.max(0, Math.min(idx, this.slides.length - 1));
if (idx === this.current) return;
this.current = idx;
this.locked = true;
this.activate(idx);
this.deck.style.transform = `translateY(${-idx * 100}vh)`;
setTimeout(() => { this.locked = false; }, 550);
}

setupNavDots() {
// IMPORTANT: Always clear before building — if outerHTML was
// captured while dots were rendered, re-opening the file would
// append a duplicate set on top of the existing ones.
this.navDotsContainer.innerHTML = "";
// Generate and manage navigation dots
this.dotsEl.innerHTML = "";
this.slides.forEach((_, i) => {
const dot = document.createElement("button");
dot.className = "nav-dot";
dot.setAttribute("aria-label", `Slide ${i + 1}`);
dot.addEventListener("click", (e) => {
e.stopPropagation();
this.goTo(i);
});
this.dotsEl.appendChild(dot);
});
}

setupKeyboardNav() {
document.addEventListener("keydown", (e) => {
if (["BUTTON","INPUT","TEXTAREA"].includes(e.target.tagName)) return;
if (["ArrowDown","ArrowRight","PageDown"," "].includes(e.key)) {
e.preventDefault(); this.goTo(this.current + 1);
} else if (["ArrowUp","ArrowLeft","PageUp"].includes(e.key)) {
e.preventDefault(); this.goTo(this.current - 1);
} else if (e.key === "Home") {
e.preventDefault(); this.goTo(0);
} else if (e.key === "End") {
e.preventDefault(); this.goTo(this.slides.length - 1);
}
});
}

// Debounced wheel navigation. Accumulate deltaY across a rapid
// burst; advance once the threshold is hit, then lock until the
// current transition finishes.
setupWheelNav() {
let accum = 0, lastTime = 0;
window.addEventListener("wheel", (e) => {
e.preventDefault();
const now = Date.now();
if (now - lastTime > 250) accum = 0;
lastTime = now;
if (this.locked) return;
accum += e.deltaY;
if (Math.abs(accum) > 60) {
const dir = accum > 0 ? 1 : -1;
accum = 0;
this.goTo(this.current + dir);
}
}, { passive: false });
}

// Click on a slide background advances to the next slide.
// Clicks on interactive elements (buttons, links, nav dots) are
// excluded so they keep their own behaviour.
setupClickNav() {
this.slides.forEach((slide, idx) => {
slide.addEventListener("click", (e) => {
if (e.target.closest("button, a, input, [role='button'], .nav-dot, .nav-dots")) return;
this.goTo(idx + 1);
});
});
}
}

Expand All @@ -164,15 +241,17 @@ Reference architecture for generating slide presentations. Every presentation fo
Every presentation must include:

1. **SlidePresentation Class** — Main controller with:
- Keyboard navigation (arrows, space, page up/down)
- Touch/swipe support
- Mouse wheel navigation
- Progress bar updates
- Navigation dots

2. **Intersection Observer** — For scroll-triggered animations:
- Add `.visible` class when slides enter viewport
- Trigger CSS transitions efficiently
- Keyboard navigation (arrows, space, page up/down, Home / End)
- Mouse wheel navigation (debounced — accumulate deltaY, advance once threshold is hit)
- Click navigation (click on slide background → next; clicks on buttons / links / nav dots are excluded via `e.target.closest()`)
- Progress bar + navigation dots updates
- Transform-based slide motion: `this.deck.style.transform = translateY(-idx * 100vh)`. The smooth animation comes from the CSS `transition` on `.deck`.

2. **`activate(idx)` method** — replaces the older Intersection Observer pattern:
- Adds `.visible` class to the active slide (triggers reveal animations)
- Updates the active dot
- Updates the progress bar
- Called from `goTo()` and once at construction time for the initial slide

3. **Optional Enhancements** (match to chosen style):
- Custom cursor with trail
Expand Down
57 changes: 38 additions & 19 deletions viewport-base.css
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
/* ===========================================
VIEWPORT FITTING: MANDATORY BASE STYLES
Include this ENTIRE file in every presentation.
These styles ensure slides fit exactly in the viewport.

Navigation uses a TRANSFORM-based slide deck (not scroll-based). All
slides live inside <div class="deck">; JS sets the deck's
`transform: translateY(-idx * 100vh)` to navigate between slides.

Why transform instead of scroll:
- body has `overflow: hidden` so no scrollbar ever shows — important
in fullscreen presentation mode where the right-edge scrollbar is
a constant visual distraction
- The "slide advances" feels like an actual slide deck, not a long
webpage that happens to snap to multiples of 100vh
- Trackpad fine-scrolls can't land between two slides
=========================================== */

/* 1. Lock html/body to viewport */
/* 1. Lock html/body — no scrolling, no scrollbar */
html, body {
height: 100%;
overflow-x: hidden;
overflow: hidden;
}

html {
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
/* 2. The deck container holds all slides in a vertical stack
(total height = N × 100vh). JS sets `transform: translateY(-idx*100vh)`
to navigate. The CSS transition gives the smooth slide motion. */
.deck {
position: fixed;
top: 0;
left: 0;
width: 100vw;
transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}

/* 2. Each slide = exact viewport height */
/* 3. Each slide = exact viewport height */
.slide {
width: 100vw;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for mobile browsers */
overflow: hidden; /* CRITICAL: Prevent ANY overflow */
scroll-snap-align: start;
display: flex;
flex-direction: column;
position: relative;
}

/* 3. Content container with flex for centering */
/* 4. Content container with flex for centering */
.slide-content {
flex: 1;
display: flex;
Expand All @@ -38,7 +55,7 @@ html {
padding: var(--slide-padding);
}

/* 4. ALL typography uses clamp() for responsive scaling */
/* 5. ALL typography uses clamp() for responsive scaling */
:root {
/* Titles scale from mobile to desktop */
--title-size: clamp(1.5rem, 5vw, 4rem);
Expand All @@ -55,13 +72,13 @@ html {
--element-gap: clamp(0.25rem, 1vw, 1rem);
}

/* 5. Cards/containers use viewport-relative max sizes */
/* 6. Cards/containers use viewport-relative max sizes */
.card, .container, .content-box {
max-width: min(90vw, 1000px);
max-height: min(80vh, 700px);
}

/* 6. Lists auto-scale with viewport */
/* 7. Lists auto-scale with viewport */
.feature-list, .bullet-list {
gap: clamp(0.4rem, 1vh, 1rem);
}
Expand All @@ -71,14 +88,14 @@ html {
line-height: 1.4;
}

/* 7. Grids adapt to available space */
/* 8. Grids adapt to available space */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
gap: clamp(0.5rem, 1.5vw, 1rem);
}

/* 8. Images constrained to viewport */
/* 9. Images constrained to viewport */
img, .image-container {
max-width: 100%;
max-height: min(50vh, 400px);
Expand Down Expand Up @@ -139,15 +156,17 @@ img, .image-container {

/* ===========================================
REDUCED MOTION
Respect user preferences
Respect user preferences. Disable the deck transition for users who
prefer no motion — slides change instantly without losing the
transform-based "no scrollbar" benefit.
=========================================== */
@media (prefers-reduced-motion: reduce) {
.deck {
transition: none !important;
}

*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.2s !important;
}

html {
scroll-behavior: auto;
}
}