Back to skills
SkillHub ClubDesign ProductFull StackDesignerTesting

presentation-generator

Generate interactive HTML presentations with neobrutalism styling, ASCII art decorations, and Agency brand colors. Outputs HTML (interactive with navigation), PNG (individual slides via Playwright), and PDF. References brand-agency skill for colors and typography. Use when creating presentations, slide decks, pitch materials, or visual summaries.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
45
Hot score
91
Updated
March 20, 2026
Overall rating
C3.4
Composite score
3.4
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install glebis-claude-skills-presentation-generator

Repository

glebis/claude-skills

Skill path: presentation-generator

Generate interactive HTML presentations with neobrutalism styling, ASCII art decorations, and Agency brand colors. Outputs HTML (interactive with navigation), PNG (individual slides via Playwright), and PDF. References brand-agency skill for colors and typography. Use when creating presentations, slide decks, pitch materials, or visual summaries.

Open repository

Best for

Primary workflow: Design Product.

Technical facets: Full Stack, Designer, Testing.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: glebis.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install presentation-generator into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/glebis/claude-skills before adding presentation-generator to shared team environments
  • Use presentation-generator for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: presentation-generator
description: Generate interactive HTML presentations with neobrutalism styling, ASCII art decorations, and Agency brand colors. Outputs HTML (interactive with navigation), PNG (individual slides via Playwright), and PDF. References brand-agency skill for colors and typography. Use when creating presentations, slide decks, pitch materials, or visual summaries.
---

# Presentation Generator

## Overview

Create stunning presentations in neobrutalism style with Agency brand colors. Generate interactive HTML presentations with smooth scrolling navigation, export individual slides as PNG, or create PDF documents.

**Output formats:**
- **HTML** - Interactive presentation with navigation dots, keyboard support, smooth scrolling
- **PNG** - Individual slide images via Playwright (1920x1080)
- **PDF** - Multi-page document via Playwright

## Quick Start

### 1. Create presentation from JSON/YAML content

```bash
node scripts/generate-presentation.js --input content.json --output presentation.html
```

### 2. Export to PNG slides

```bash
node scripts/export-slides.js presentation.html --format png --output ./slides/
```

### 3. Export to PDF

```bash
node scripts/export-slides.js presentation.html --format pdf --output presentation.pdf
```

## Brand Integration

This skill references `brand-agency` for consistent styling:

### Colors (from brand-agency)

| Color | Hex | Usage |
|-------|-----|-------|
| Primary (Orange) | `#e85d04` | Title slides, CTAs, accents |
| Secondary (Yellow) | `#ffd60a` | Highlights, accent slides |
| Accent (Blue) | `#3a86ff` | Info slides, links |
| Success (Green) | `#38b000` | Positive content |
| Error (Red) | `#d62828` | Warnings, emphasis |
| Foreground | `#000000` | Text, borders |
| Background | `#ffffff` | Light slides |

### Typography

- **Headings**: Geist ExtraBold (800)
- **Body**: EB Garamond
- **Code/ASCII**: Geist Mono

## Slide Types

### 1. Title Slide (`--title`)
Full-screen title with subtitle, colored background (primary/secondary/accent/dark).

### 2. Content Slide (`--content`)
Heading + body text + optional bullet list.

### 3. Two-Column Slide (`--two-col`)
Split layout for comparisons, text + image, before/after.

### 4. Code Slide (`--code`)
Dark background, syntax-highlighted code block with title.

### 5. Stats Slide (`--stats`)
Big numbers with labels (e.g., "14 templates | 4 formats | 1 skill").

### 6. Task Grid Slide (`--grid`)
Grid of cards with numbers, titles, descriptions.

### 7. ASCII Art Slide (`--ascii`)
Decorative slide with ASCII box-drawing characters.

### 8. Image Slide (`--image`)
Full-bleed or contained image with optional caption.

## ASCII Decorations

Use ASCII box-drawing characters for tech aesthetic:

```
Frames:   ┌─────┐  ╔═════╗  ┏━━━━━┓
          │     │  ║     ║  ┃     ┃
          └─────┘  ╚═════╝  ┗━━━━━┛

Lines:    ─ │ ═ ║ ━ ┃ ━━━ ───

Arrows:   → ← ↑ ↓ ▶ ◀ ▲ ▼

Shapes:   ● ○ ■ □ ▲ △ ★ ☆ ◆ ◇

Blocks:   █ ▓ ▒ ░
```

## Content Format

### JSON format:

```json
{
  "title": "Presentation Title",
  "footer": "Company / Date",
  "slides": [
    {
      "type": "title",
      "bg": "primary",
      "title": "Main Title",
      "subtitle": "Subtitle text"
    },
    {
      "type": "content",
      "title": "Section Title",
      "body": "Introduction paragraph",
      "bullets": ["Point 1", "Point 2", "Point 3"]
    },
    {
      "type": "code",
      "title": "Code Example",
      "language": "javascript",
      "code": "const x = 42;"
    },
    {
      "type": "stats",
      "items": [
        {"value": "14", "label": "templates"},
        {"value": "4", "label": "formats"},
        {"value": "∞", "label": "possibilities"}
      ]
    }
  ]
}
```

### YAML format:

```yaml
title: Presentation Title
footer: Company / Date
slides:
  - type: title
    bg: primary
    title: Main Title
    subtitle: Subtitle text

  - type: content
    title: Section Title
    body: Introduction paragraph
    bullets:
      - Point 1
      - Point 2
```

## Interactive Features

Generated HTML includes:

- **Navigation dots** - Fixed right sidebar with clickable dots
- **Keyboard navigation** - Arrow keys, Page Up/Down, Home/End
- **Smooth scrolling** - CSS scroll-snap and smooth behavior
- **Intersection Observer** - Active slide highlighting
- **Responsive** - Works on various screen sizes (optimized for 16:9)

## Usage Examples

### Create workshop summary:

```bash
# Generate from today's session
node scripts/generate-presentation.js \
  --title "Claude Code Lab — Day Summary" \
  --footer "29.11.2025" \
  --slides slides-content.json \
  --output workshop-summary.html
```

### Quick presentation from markdown:

```bash
# Convert markdown outline to presentation
node scripts/md-to-slides.js notes.md --output presentation.html
```

### Batch export:

```bash
# Export all slides as PNGs
node scripts/export-slides.js presentation.html --format png --output ./export/

# Result: slide-01.png, slide-02.png, etc.
```

## File Structure

```
presentation-generator/
├── SKILL.md              # This file
├── templates/
│   ├── base.html         # Base HTML template
│   ├── slides/           # Slide type partials
│   │   ├── title.html
│   │   ├── content.html
│   │   ├── code.html
│   │   ├── stats.html
│   │   ├── two-col.html
│   │   ├── grid.html
│   │   └── ascii.html
│   └── styles.css        # Neobrutalism styles
├── scripts/
│   ├── generate-presentation.js  # Main generator
│   ├── export-slides.js          # PNG/PDF export
│   └── md-to-slides.js           # Markdown converter
└── output/               # Generated files
```

## Dependencies

- Node.js 18+
- Playwright (`npm install playwright`)

## Tips

1. **Use ASCII sparingly** - Great for tech/dev presentations, can feel dated otherwise
2. **Stick to brand colors** - Don't mix custom colors, use the 5-color palette
3. **Big text on title slides** - h1 should be 4-5rem minimum
4. **One idea per slide** - Neobrutalism works best with focused content
5. **Test interactivity** - Always preview HTML before exporting


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/generate-presentation.js

```javascript
#!/usr/bin/env node
/**
 * Presentation Generator
 *
 * Generates interactive HTML presentations from JSON/YAML content
 * with neobrutalism styling from brand-agency skill.
 */

const fs = require('fs');
const path = require('path');

// Template directory
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');

// Read CSS styles
const styles = fs.readFileSync(path.join(TEMPLATES_DIR, 'styles.css'), 'utf-8');

// Slide type renderers
const slideRenderers = {
  // Title slide - big title with optional subtitle
  title: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'primary'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    <h1>${slide.title}</h1>
    ${slide.subtitle ? `<p class="subtitle">${slide.subtitle}</p>` : ''}
    ${slide.footer ? `<div class="footer">${slide.footer}</div>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Content slide - heading + body + optional bullets
  content: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'light'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    <h2>${slide.title}</h2>
    ${slide.body ? `<p style="font-size: 1.3rem; max-width: 800px;">${slide.body}</p>` : ''}
    ${slide.bullets ? `
    <ul style="margin-top: 1.5rem; font-size: 1.2rem;">
      ${slide.bullets.map(b => `<li>${b}</li>`).join('\n      ')}
    </ul>` : ''}
    ${slide.tags ? `
    <div style="margin-top: 2rem;">
      ${slide.tags.map(t => `<span class="tag tag--${t.type || ''}">${t.text}</span>`).join('')}
    </div>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Two-column slide
  'two-col': (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'light'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    ${slide.title ? `<h2>${slide.title}</h2>` : ''}
    <div class="two-col" style="margin-top: 2rem;">
      <div>
        ${slide.left.title ? `<h3>${slide.left.title}</h3>` : ''}
        ${slide.left.body ? `<p>${slide.left.body}</p>` : ''}
        ${slide.left.bullets ? `
        <ul>
          ${slide.left.bullets.map(b => `<li>${b}</li>`).join('\n          ')}
        </ul>` : ''}
        ${slide.left.code ? `<pre><code>${escapeHtml(slide.left.code)}</code></pre>` : ''}
      </div>
      <div>
        ${slide.right.title ? `<h3>${slide.right.title}</h3>` : ''}
        ${slide.right.body ? `<p>${slide.right.body}</p>` : ''}
        ${slide.right.bullets ? `
        <ul>
          ${slide.right.bullets.map(b => `<li>${b}</li>`).join('\n          ')}
        </ul>` : ''}
        ${slide.right.code ? `<pre><code>${escapeHtml(slide.right.code)}</code></pre>` : ''}
        ${slide.right.ascii ? `<div class="ascii-box">${slide.right.ascii}</div>` : ''}
      </div>
    </div>
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Code slide - dark background with code block
  code: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--dark">
    ${slide.label ? `<div class="label" style="color: var(--color-success);">${slide.label}</div>` : ''}
    <h2>${slide.title}</h2>
    ${slide.description ? `<p style="opacity: 0.8; margin-bottom: 1.5rem;">${slide.description}</p>` : ''}
    <pre style="max-width: 900px;"><code>${highlightCode(slide.code, slide.language)}</code></pre>
    ${slide.tags ? `
    <div style="margin-top: 1.5rem;">
      ${slide.tags.map(t => `<span class="tag tag--${t.type || ''}">${t.text}</span>`).join('')}
    </div>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Stats slide - big numbers with labels
  stats: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'light'} centered">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    ${slide.title ? `<h2>${slide.title}</h2>` : ''}
    <div class="stats-row">
      ${slide.items.map(item => `
      <div class="stat">
        <div class="stat-value">${item.value}</div>
        <div class="stat-label">${item.label}</div>
      </div>`).join('')}
    </div>
    ${slide.subtitle ? `<p class="subtitle" style="margin-top: 2rem;">${slide.subtitle}</p>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Grid slide - task/feature cards
  grid: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'light'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    <h2>${slide.title}</h2>
    ${slide.body ? `<p style="font-size: 1.2rem; max-width: 700px;">${slide.body}</p>` : ''}
    <div class="task-grid">
      ${slide.items.map(item => `
      <div class="task-card">
        <div class="task-number">${item.number || ''}</div>
        <div class="task-title">${item.title}</div>
        <div class="task-desc">${item.desc || ''}</div>
        ${item.tags ? `
        <div>
          ${item.tags.map(t => `<span class="tag tag--${t.type || ''}">${t.text}</span>`).join('')}
        </div>` : ''}
      </div>`).join('')}
    </div>
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // ASCII art slide
  ascii: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'dark'}">
    ${slide.label ? `<div class="label" style="color: var(--color-secondary);">${slide.label}</div>` : ''}
    ${slide.title ? `<h2>${slide.title}</h2>` : ''}
    <div class="ascii-box" style="margin-top: 2rem; ${slide.bg === 'dark' ? 'background: rgba(255,255,255,0.1); color: var(--color-background);' : ''}">${slide.ascii}</div>
    ${slide.caption ? `<p style="margin-top: 1.5rem; font-family: var(--font-mono);">${slide.caption}</p>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Terminal slide
  terminal: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'muted'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    ${slide.title ? `<h2>${slide.title}</h2>` : ''}
    <div class="terminal" style="max-width: 800px; margin-top: 2rem;">
      <div class="terminal-header">
        <span class="terminal-btn terminal-btn--close"></span>
        <span class="terminal-btn terminal-btn--minimize"></span>
        <span class="terminal-btn terminal-btn--maximize"></span>
      </div>
      <div class="terminal-content">
        ${slide.lines.map(line => {
          if (line.type === 'prompt') {
            return `<div><span class="terminal-prompt">$ </span>${escapeHtml(line.text)}</div>`;
          } else if (line.type === 'output') {
            return `<div class="terminal-output">${escapeHtml(line.text)}</div>`;
          } else if (line.type === 'comment') {
            return `<div style="color: #888;"># ${escapeHtml(line.text)}</div>`;
          }
          return `<div>${escapeHtml(line.text || line)}</div>`;
        }).join('\n        ')}
      </div>
    </div>
    ${slide.note ? `<p style="margin-top: 1.5rem; font-family: var(--font-mono); opacity: 0.7;">${slide.note}</p>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Image slide
  image: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'light'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    ${slide.title ? `<h2>${slide.title}</h2>` : ''}
    <div class="image-container" style="max-width: ${slide.maxWidth || '800px'}; margin-top: 2rem;">
      <img src="${slide.src}" alt="${slide.alt || slide.title || ''}" />
    </div>
    ${slide.caption ? `<p style="margin-top: 1rem; font-family: var(--font-mono); font-size: 0.9rem; opacity: 0.7;">${slide.caption}</p>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Quote slide
  quote: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'secondary'} centered">
    <div class="ascii-border" style="font-size: 2rem; margin-bottom: 1rem;">╔══════════════════════════════════════╗</div>
    <blockquote style="font-size: 2rem; font-style: italic; max-width: 800px; line-height: 1.4;">
      "${slide.quote}"
    </blockquote>
    <div class="ascii-border" style="font-size: 2rem; margin-top: 1rem;">╚══════════════════════════════════════╝</div>
    ${slide.author ? `<p style="margin-top: 2rem; font-family: var(--font-mono);">— ${slide.author}</p>` : ''}
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`,

  // Comparison slide (before/after, pros/cons)
  comparison: (slide, index, total) => `
  <section id="slide-${index + 1}" class="slide slide--${slide.bg || 'muted'}">
    ${slide.label ? `<div class="label">${slide.label}</div>` : ''}
    <h2>${slide.title}</h2>
    <div class="two-col" style="margin-top: 2rem;">
      <div class="card" style="border-color: ${slide.leftColor || 'var(--color-error)'};">
        <h3 style="color: ${slide.leftColor || 'var(--color-error)'};">${slide.leftTitle || 'Before'}</h3>
        ${slide.left.map(item => `<p style="margin-top: 0.5rem;">- ${item}</p>`).join('')}
      </div>
      <div class="card" style="border-color: ${slide.rightColor || 'var(--color-success)'};">
        <h3 style="color: ${slide.rightColor || 'var(--color-success)'};">${slide.rightTitle || 'After'}</h3>
        ${slide.right.map(item => `<p style="margin-top: 0.5rem;">+ ${item}</p>`).join('')}
      </div>
    </div>
    <div class="slide-number">${index + 1} / ${total}</div>
  </section>`
};

// Helper: escape HTML
function escapeHtml(str) {
  if (!str) return '';
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// Helper: basic syntax highlighting
function highlightCode(code, language) {
  if (!code) return '';
  let escaped = escapeHtml(code);

  // Comments
  escaped = escaped.replace(/(\/\/.*$|#.*$)/gm, '<span class="code-comment">$1</span>');
  escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');

  // Strings
  escaped = escaped.replace(/(".*?"|'.*?'|`.*?`)/g, '<span class="code-string">$1</span>');

  // Keywords
  const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'class', 'extends', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'true', 'false', 'null', 'undefined'];
  keywords.forEach(kw => {
    escaped = escaped.replace(new RegExp(`\\b(${kw})\\b`, 'g'), '<span class="code-keyword">$1</span>');
  });

  // Numbers
  escaped = escaped.replace(/\b(\d+)\b/g, '<span class="code-number">$1</span>');

  return escaped;
}

// Generate navigation dots
function generateNavigation(slideCount) {
  let nav = '';
  for (let i = 1; i <= slideCount; i++) {
    nav += `    <a href="#slide-${i}" class="nav-dot${i === 1 ? ' active' : ''}"></a>\n`;
  }
  return nav;
}

// Main generator function
function generatePresentation(content, outputPath) {
  const { title, lang, footer, slides } = content;

  // Render all slides
  const renderedSlides = slides.map((slide, index) => {
    const renderer = slideRenderers[slide.type];
    if (!renderer) {
      console.warn(`Unknown slide type: ${slide.type}`);
      return '';
    }
    // Add global footer to slides if not specified
    if (!slide.footer && footer && slide.type === 'title') {
      slide.footer = footer;
    }
    return renderer(slide, index, slides.length);
  }).join('\n\n');

  // Generate navigation
  const navigation = generateNavigation(slides.length);

  // Read base template
  const baseTemplate = fs.readFileSync(path.join(TEMPLATES_DIR, 'base.html'), 'utf-8');

  // Replace placeholders
  const html = baseTemplate
    .replace('{{title}}', title || 'Presentation')
    .replace('{{lang}}', lang || 'en')
    .replace('{{styles}}', styles)
    .replace('{{navigation}}', navigation)
    .replace('{{slides}}', renderedSlides);

  // Write output
  fs.writeFileSync(outputPath, html);
  console.log(`Generated: ${outputPath}`);

  return outputPath;
}

// CLI handling
if (require.main === module) {
  const args = process.argv.slice(2);

  if (args.includes('--help') || args.includes('-h')) {
    console.log(`
Presentation Generator
======================

Usage:
  node generate-presentation.js --input content.json --output presentation.html
  node generate-presentation.js -i content.json -o presentation.html

Options:
  --input, -i   Input JSON/YAML file with presentation content
  --output, -o  Output HTML file path
  --help, -h    Show this help

Content format (JSON):
{
  "title": "Presentation Title",
  "lang": "en",
  "footer": "Company / Date",
  "slides": [
    { "type": "title", "bg": "primary", "title": "...", "subtitle": "..." },
    { "type": "content", "title": "...", "body": "...", "bullets": [...] },
    { "type": "code", "title": "...", "code": "...", "language": "javascript" },
    { "type": "stats", "items": [{ "value": "10", "label": "items" }] }
  ]
}

Slide types: title, content, two-col, code, stats, grid, ascii, terminal, image, quote, comparison
`);
    process.exit(0);
  }

  const inputIndex = args.findIndex(a => a === '--input' || a === '-i');
  const outputIndex = args.findIndex(a => a === '--output' || a === '-o');

  if (inputIndex === -1) {
    console.error('Error: --input is required');
    process.exit(1);
  }

  const inputPath = args[inputIndex + 1];
  const outputPath = outputIndex !== -1 ? args[outputIndex + 1] : inputPath.replace(/\.(json|yaml|yml)$/, '.html');

  // Read input
  let content;
  try {
    const inputContent = fs.readFileSync(inputPath, 'utf-8');
    if (inputPath.endsWith('.yaml') || inputPath.endsWith('.yml')) {
      // Simple YAML parsing (for basic cases)
      // For full YAML support, use js-yaml package
      console.error('YAML support requires js-yaml package. Please use JSON for now.');
      process.exit(1);
    } else {
      content = JSON.parse(inputContent);
    }
  } catch (err) {
    console.error(`Error reading input: ${err.message}`);
    process.exit(1);
  }

  generatePresentation(content, outputPath);
}

module.exports = { generatePresentation };

```

### scripts/export-slides.js

```javascript
#!/usr/bin/env node
/**
 * Export Slides
 *
 * Export presentation HTML to PNG slides, PDF, or video using Playwright.
 */

const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

async function exportSlides(htmlPath, options = {}) {
  const { format = 'png', output, width = 1920, height = 1080, duration = 6 } = options;

  if (!fs.existsSync(htmlPath)) {
    console.error(`File not found: ${htmlPath}`);
    process.exit(1);
  }

  const absolutePath = path.resolve(htmlPath);

  const browser = await chromium.launch();
  const context = await browser.newContext({
    viewport: { width, height },
    deviceScaleFactor: 1,
  });

  const page = await context.newPage();
  await page.goto(`file://${absolutePath}`, { waitUntil: 'networkidle' });

  // Wait for fonts to load
  await page.waitForTimeout(1500);

  if (format === 'pdf') {
    // Export as PDF
    const pdfPath = output || htmlPath.replace('.html', '.pdf');
    await page.pdf({
      path: pdfPath,
      width: `${width}px`,
      height: `${height}px`,
      printBackground: true,
      preferCSSPageSize: true,
    });
    console.log(`Exported PDF: ${pdfPath}`);

  } else if (format === 'png') {
    // Export individual slides as PNGs
    const outputDir = output || path.dirname(htmlPath);
    const baseName = path.basename(htmlPath, '.html');

    if (!fs.existsSync(outputDir)) {
      fs.mkdirSync(outputDir, { recursive: true });
    }

    // Get all slides
    const slides = await page.$$('.slide');
    console.log(`Found ${slides.length} slides`);

    for (let i = 0; i < slides.length; i++) {
      // Scroll to slide
      await page.evaluate((index) => {
        const slides = document.querySelectorAll('.slide');
        slides[index].scrollIntoView({ behavior: 'instant' });
      }, i);

      await page.waitForTimeout(300);

      // Screenshot the slide
      const slideNum = String(i + 1).padStart(2, '0');
      const pngPath = path.join(outputDir, `${baseName}-slide-${slideNum}.png`);

      await page.screenshot({
        path: pngPath,
        clip: {
          x: 0,
          y: i * height,
          width: width,
          height: height,
        },
      });

      console.log(`Exported: ${pngPath}`);
    }

  } else if (format === 'video' || format === 'webm') {
    // Export as video with auto-scrolling slides
    await browser.close();

    const videoDir = path.dirname(output || htmlPath.replace('.html', '.webm'));
    if (!fs.existsSync(videoDir)) {
      fs.mkdirSync(videoDir, { recursive: true });
    }

    const videoBrowser = await chromium.launch();

    // First, create a prep page WITHOUT video recording to pre-load fonts
    const prepContext = await videoBrowser.newContext({
      viewport: { width, height },
      deviceScaleFactor: 1,
    });

    const prepPage = await prepContext.newPage();
    await prepPage.goto(`file://${absolutePath}`, { waitUntil: 'networkidle' });
    await prepPage.waitForTimeout(2000); // Wait for fonts

    const slideCount = await prepPage.evaluate(() => {
      return document.querySelectorAll('.slide').length;
    });

    console.log(`Recording video: ${slideCount} slides, ${duration}s per slide`);
    console.log(`Total duration: ~${slideCount * duration}s`);

    await prepPage.close();
    await prepContext.close();

    // Create the video recording context
    const videoContext = await videoBrowser.newContext({
      viewport: { width, height },
      deviceScaleFactor: 1,
      recordVideo: {
        dir: videoDir,
        size: { width, height },
      },
    });

    const videoPage = await videoContext.newPage();

    // Set video recording mode flag before loading
    await videoPage.addInitScript(() => {
      window.videoRecordingMode = true;
    });

    // Load presentation - elements are hidden by CSS in the template itself
    await videoPage.goto(`file://${absolutePath}`, { waitUntil: 'networkidle' });

    // Set up for video mode
    await videoPage.evaluate(() => {
      // Disable scroll-snap for smooth programmatic scrolling
      document.documentElement.style.scrollSnapType = 'none';
      document.documentElement.style.scrollBehavior = 'auto';

      // Mark all slides as not animated
      document.querySelectorAll('.slide').forEach(slide => {
        slide.dataset.animated = 'false';
      });

      // Set first nav dot as active
      const dots = document.querySelectorAll('.nav-dot');
      if (dots[0]) dots[0].classList.add('active');
    });

    // Wait for fonts to fully load
    await videoPage.waitForTimeout(500);

    // Auto-scroll through slides with animation triggers
    for (let i = 0; i < slideCount; i++) {
      console.log(`Recording slide ${i + 1}/${slideCount}...`);

      // Scroll to slide using window.scrollTo for reliable positioning
      await videoPage.evaluate(({ index, viewportHeight }) => {
        window.scrollTo({
          top: index * viewportHeight,
          behavior: 'instant'
        });

        // Update navigation dots manually
        const dots = document.querySelectorAll('.nav-dot');
        dots.forEach((dot, j) => {
          dot.classList.toggle('active', j === index);
        });
      }, { index: i, viewportHeight: height });

      // Brief pause to ensure scroll completed
      await videoPage.waitForTimeout(200);

      // Trigger animations for current slide
      await videoPage.evaluate((index) => {
        const slides = document.querySelectorAll('.slide');
        const slide = slides[index];

        // Call the animateSlide function if available
        if (typeof animateSlide === 'function') {
          animateSlide(slide);
        }
      }, i);

      // Wait for animations to play + remaining slide duration
      await videoPage.waitForTimeout(duration * 1000);
    }

    // Small pause at the end
    await videoPage.waitForTimeout(1000);

    // Close page to finalize video
    await videoPage.close();

    // Get the recorded video path
    const video = videoPage.video();
    if (video) {
      const tempVideoPath = await video.path();
      const finalVideoPath = output || htmlPath.replace('.html', '.webm');

      // Move video to final destination
      fs.renameSync(tempVideoPath, finalVideoPath);
      console.log(`Exported video: ${finalVideoPath}`);
    }

    await videoBrowser.close();
    return;

  } else {
    console.error(`Unknown format: ${format}. Use 'png', 'pdf', or 'video'.`);
  }

  await browser.close();
}

// CLI handling
if (require.main === module) {
  const args = process.argv.slice(2);

  if (args.includes('--help')) {
    console.log(`
Export Slides
=============

Export presentation HTML to PNG slides, PDF, or video.

Usage:
  node export-slides.js presentation.html --format png --output ./slides/
  node export-slides.js presentation.html --format pdf --output output.pdf
  node export-slides.js presentation.html --format video --output output.webm

Options:
  --format, -f    Output format: png, pdf, or video (default: png)
  --output, -o    Output path (directory for PNG, file for PDF/video)
  --width, -w     Slide width in pixels (default: 1920)
  --height        Slide height in pixels (default: 1080)
  --duration, -d  Seconds per slide for video (default: 6)
  --help          Show this help

Examples:
  node export-slides.js deck.html -f png -o ./export/
  node export-slides.js deck.html -f pdf -o deck.pdf
  node export-slides.js deck.html -f video -o deck.webm -d 5
`);
    process.exit(0);
  }

  const htmlPath = args.find(a => !a.startsWith('-'));
  if (!htmlPath) {
    console.error('Error: HTML file path is required');
    process.exit(1);
  }

  const formatIndex = args.findIndex(a => a === '--format' || a === '-f');
  const outputIndex = args.findIndex(a => a === '--output' || a === '-o');
  const widthIndex = args.findIndex(a => a === '--width' || a === '-w');
  const heightIndex = args.findIndex(a => a === '--height');
  const durationIndex = args.findIndex(a => a === '--duration' || a === '-d');

  const options = {
    format: formatIndex !== -1 ? args[formatIndex + 1] : 'png',
    output: outputIndex !== -1 ? args[outputIndex + 1] : null,
    width: widthIndex !== -1 ? parseInt(args[widthIndex + 1]) : 1920,
    height: heightIndex !== -1 ? parseInt(args[heightIndex + 1]) : 1080,
    duration: durationIndex !== -1 ? parseInt(args[durationIndex + 1]) : 6,
  };

  exportSlides(htmlPath, options).catch(console.error);
}

module.exports = { exportSlides };

```