
Presentation Generator
- 292 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
presentation-generator is an Office & Documents skill that drafts slide decks for pitch, roadmap, customer, and investor narratives with structured outlines, speaker notes, and export-ready presentation content for devel
About
presentation-generator is an Office & Documents skill from glebis/claude-skills that drafts slide decks for pitch, roadmap, customer, and investor narratives. The skill produces structured outlines, speaker notes, and export-ready presentation content so developers and technical leads can communicate product direction without starting from a blank deck template. Developers reach for presentation-generator when they need a coherent narrative arc—problem, solution, milestones, and ask—formatted for slides before a customer review, roadmap sync, or investor conversation during active product build cycles.
- Slide outline generation
- Speaker notes
- Pitch and roadmap decks
- Narrative structuring
- Export-ready content
Presentation Generator by the numbers
- 292 all-time installs (skills.sh)
- Ranked #191 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill presentation-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 292 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
How do you draft a technical pitch slide deck?
Draft slide decks—pitch, roadmap, customer, and investor narratives—with structured outlines, speaker notes, and export-ready presentation content.
Who is it for?
Developers and technical leads who need fast structured slide narratives for pitch, roadmap, customer, or investor meetings.
Skip if: Teams requiring pixel-perfect designed slide templates, brand-locked PowerPoint masters, or automated chart generation from live analytics data.
When should I use this skill?
Drafting pitch, roadmap, customer, or investor slide decks that need structured outlines and speaker notes before a stakeholder meeting.
What you get
Structured slide outlines, speaker notes, narrative sections, and export-ready presentation content for pitch, roadmap, customer, or investor decks.
- slide deck outline
- speaker notes
- export-ready presentation content
Files
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
node scripts/generate-presentation.js --input content.json --output presentation.html2. Export to PNG slides
node scripts/export-slides.js presentation.html --format png --output ./slides/3. Export to PDF
node scripts/export-slides.js presentation.html --format pdf --output presentation.pdfBrand 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:
{
"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:
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 2Interactive 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:
# 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.htmlQuick presentation from markdown:
# Convert markdown outline to presentation
node scripts/md-to-slides.js notes.md --output presentation.htmlBatch export:
# 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 filesDependencies
- 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
{
"name": "presentation-generator",
"description": "Generate interactive HTML presentations with neobrutalism styling, ASCII art decorations, and Agency brand colors. Outpu",
"author": {
"name": "Gleb Kalinin"
},
"repository": "https://github.com/glebis/claude-skills",
"license": "MIT"
}#!/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 };
#!/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>` : ''}
<div class="code-container">
<button class="copy-btn" onclick="copyCode(this)" aria-label="Copy code">
<svg class="copy-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" style="display:none;">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="copy-text">Copy</span>
</button>
<pre style="max-width: 900px;"><code>${highlightCode(slide.code, slide.language)}</code></pre>
</div>
${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>`,
// Repo slide - GitHub repository link
repo: (slide, index, total) => `
<section id="slide-${index + 1}" class="slide slide--dark centered">
${slide.label ? `<div class="label" style="color: var(--color-secondary);">${slide.label}</div>` : ''}
<div style="margin-top: 2rem;">
<svg viewBox="0 0 24 24" width="80" height="80" fill="var(--color-background)">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</div>
${slide.title ? `<h2 style="margin-top: 1.5rem; color: var(--color-background);">${slide.title}</h2>` : ''}
${slide.body ? `<p style="color: rgba(255,255,255,0.8); font-size: 1.3rem; margin-top: 1rem;">${slide.body}</p>` : ''}
<a href="${slide.url}" target="_blank" rel="noopener noreferrer" class="repo-link" style="
display: inline-block;
margin-top: 2rem;
padding: 1rem 2rem;
background: var(--color-background);
color: var(--color-primary);
text-decoration: none;
font-family: var(--font-mono);
font-weight: bold;
font-size: 1.2rem;
border-radius: 8px;
border: 3px solid var(--color-secondary);
transition: transform 0.2s, box-shadow 0.2s;
" onmouseover="this.style.transform='translateY(-2px)';this.style.boxShadow='0 8px 0 var(--color-secondary)';" onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
${slide.url}
</a>
<div class="slide-number" style="color: rgba(255,255,255,0.5);">${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${item.target ? ' clickable' : ''}"${item.target ? ` onclick="navigateToSlide('${item.target.replace(/'/g, "\\'")}')"` : ''}>
<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 (preserve $ for JSON regex patterns)
function escapeHtml(str, preserveDollar = false) {
if (!str) return '';
if (preserveDollar) {
// For JSON - don't escape $ to preserve regex patterns
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, ''');
}
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Helper: basic syntax highlighting
function highlightCode(code, language) {
if (!code) return '';
const isJson = language === 'json';
// For JSON, preserve $ to avoid breaking regex patterns in strings
let escaped = escapeHtml(code, isJson);
if (isJson) {
// For JSON: only highlight strings (skip comments to avoid // in URLs being matched)
escaped = escaped.replace(/(".*?")/g, '<span class="code-string">$1</span>');
return escaped;
}
// For other languages: process in correct order
// 1. Strings FIRST - so comments don't match inside strings
escaped = escaped.replace(/(".*?"|'.*?'|`.*?`)/g, '<span class="code-string">$1</span>');
// 2. Comments AFTER strings (// and # won't match inside strings now)
escaped = escaped.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
escaped = escaped.replace(/(#.*$)/gm, '<span class="code-comment">$1</span>');
// 3. 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>');
});
// 4. 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 };
<!DOCTYPE html>
<html lang="{{lang}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<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=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Geist:wght@400;800&family=Geist+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
<style>
{{styles}}
/* Animation initial states */
.slide .anim-fade {
opacity: 0;
}
.slide .anim-slide-up {
opacity: 0;
transform: translateY(40px);
}
.slide .anim-slide-left {
opacity: 0;
transform: translateX(-40px);
}
.slide .anim-slide-right {
opacity: 0;
transform: translateX(40px);
}
.slide .anim-scale {
opacity: 0;
transform: scale(0.8);
}
.slide .anim-bounce {
opacity: 0;
transform: translateY(-20px);
}
/* Stats counter animation */
.stat-value[data-value] {
opacity: 0;
}
/* Stagger children animation helper */
.anim-stagger > * {
opacity: 0;
transform: translateY(20px);
}
/* Hide all animatable elements by default - they will be revealed by JS */
.slide .label, .slide .badge, .slide h1, .slide h2, .slide h3,
.slide .subtitle, .slide p:not(.stat-label), .slide .task-card, .slide .card, .slide .box,
.slide .stat, .slide .stat-value, .slide pre, .slide .terminal,
.slide .ascii-box, .slide .ascii-border, .slide ul li, .slide ol li,
.slide .tag, .slide .btn, .slide .footer, .slide .slide-number,
.slide .two-col > div, .slide .color-swatch, .slide .svg-demo svg {
opacity: 0;
}
/* Copy button styles */
.code-container {
position: relative;
}
.copy-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
color: #fff;
font-family: var(--font-mono);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
}
.copy-btn.copied {
background: rgba(46, 204, 113, 0.3);
border-color: rgba(46, 204, 113, 0.6);
}
.copy-icon, .check-icon {
flex-shrink: 0;
}
/* Clickable task cards */
.task-card.clickable {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.task-card.clickable:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="nav">
{{navigation}}
</nav>
<!-- Slides -->
{{slides}}
<script>
// Animation configurations for different element types
const animationConfigs = {
// Labels and badges - quick fade
label: {
selector: '.label, .badge',
animation: {
opacity: [0, 1],
translateY: [-10, 0],
duration: 400,
easing: 'easeOutQuad'
},
delay: 100
},
// Main headings - slide up with bounce
h1: {
selector: 'h1',
animation: {
opacity: [0, 1],
translateY: [60, 0],
duration: 800,
easing: 'easeOutExpo'
},
delay: 200
},
// Secondary headings
h2: {
selector: 'h2',
animation: {
opacity: [0, 1],
translateY: [40, 0],
duration: 700,
easing: 'easeOutExpo'
},
delay: 300
},
// Subtitles and body text
subtitle: {
selector: '.subtitle, p:not(.task-desc):not(.stat-label)',
animation: {
opacity: [0, 1],
translateY: [30, 0],
duration: 600,
easing: 'easeOutQuad'
},
delay: 500
},
// Cards - staggered scale up
cards: {
selector: '.task-card, .card, .box',
animation: {
opacity: [0, 1],
scale: [0.9, 1],
translateY: [20, 0],
duration: 500,
easing: 'easeOutBack'
},
delay: 600,
stagger: 100
},
// Stats - counter effect
stats: {
selector: '.stat',
animation: {
opacity: [0, 1],
scale: [0.5, 1],
duration: 600,
easing: 'easeOutBack'
},
delay: 400,
stagger: 150
},
// Code blocks - slide in
code: {
selector: 'pre, .terminal',
animation: {
opacity: [0, 1],
translateX: [-30, 0],
duration: 600,
easing: 'easeOutQuad'
},
delay: 700
},
// ASCII boxes
ascii: {
selector: '.ascii-box, .ascii-border',
animation: {
opacity: [0, 1],
duration: 800,
easing: 'easeInOutQuad'
},
delay: 500
},
// Lists - staggered items
listItems: {
selector: 'ul li, ol li',
animation: {
opacity: [0, 1],
translateX: [-20, 0],
duration: 400,
easing: 'easeOutQuad'
},
delay: 700,
stagger: 80
},
// Tags
tags: {
selector: '.tag',
animation: {
opacity: [0, 1],
scale: [0.8, 1],
duration: 300,
easing: 'easeOutQuad'
},
delay: 900,
stagger: 50
},
// Buttons
buttons: {
selector: '.btn',
animation: {
opacity: [0, 1],
translateY: [20, 0],
duration: 500,
easing: 'easeOutBack'
},
delay: 1000
},
// Footer
footer: {
selector: '.footer, .slide-number',
animation: {
opacity: [0, 0.7],
duration: 600,
easing: 'easeOutQuad'
},
delay: 1200
},
// Two-col children
columns: {
selector: '.two-col > div',
animation: {
opacity: [0, 1],
translateX: (el, i) => [i === 0 ? -30 : 30, 0],
duration: 600,
easing: 'easeOutQuad'
},
delay: 500,
stagger: 200
},
// Color swatches
swatches: {
selector: '.color-swatch',
animation: {
opacity: [0, 1],
scale: [0.8, 1],
duration: 400,
easing: 'easeOutBack'
},
delay: 800,
stagger: 80
},
// SVG demos
svgs: {
selector: '.svg-demo svg',
animation: {
opacity: [0, 1],
rotate: [-10, 0],
scale: [0.9, 1],
duration: 500,
easing: 'easeOutBack'
},
delay: 700,
stagger: 100
}
};
// Animate a slide when it becomes visible
function animateSlide(slide) {
// Skip if already animated
if (slide.dataset.animated === 'true') return;
slide.dataset.animated = 'true';
// Run animations for each element type
Object.values(animationConfigs).forEach(config => {
const elements = slide.querySelectorAll(config.selector);
if (elements.length === 0) return;
anime({
targets: elements,
...config.animation,
delay: config.stagger
? anime.stagger(config.stagger, { start: config.delay })
: config.delay
});
});
// Special: animate stat counters
const statValues = slide.querySelectorAll('.stat-value');
statValues.forEach((el, i) => {
const finalValue = el.textContent;
const isNumber = /^\d+$/.test(finalValue);
if (isNumber) {
el.textContent = '0';
anime({
targets: el,
innerHTML: [0, parseInt(finalValue)],
round: 1,
duration: 1500,
delay: 400 + (i * 150),
easing: 'easeOutExpo'
});
}
anime({
targets: el,
opacity: [0, 1],
scale: [0.5, 1],
duration: 600,
delay: 400 + (i * 150),
easing: 'easeOutBack'
});
});
}
// Reset slide animations (for re-triggering)
function resetSlideAnimations(slide) {
slide.dataset.animated = 'false';
// Reset all animatable elements
Object.values(animationConfigs).forEach(config => {
const elements = slide.querySelectorAll(config.selector);
elements.forEach(el => {
el.style.opacity = '0';
el.style.transform = '';
});
});
}
// Navigation highlighting and animation triggering
const dots = document.querySelectorAll('.nav-dot');
const slides = document.querySelectorAll('.slide');
// Only use IntersectionObserver for interactive mode (not video recording)
if (!window.videoRecordingMode) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
dots.forEach(dot => {
dot.classList.toggle('active', dot.getAttribute('href') === '#' + id);
});
// Trigger animations when slide becomes visible
animateSlide(entry.target);
}
});
}, { threshold: 0.5 });
slides.forEach(slide => observer.observe(slide));
// Animate first slide immediately
setTimeout(() => {
if (slides[0]) animateSlide(slides[0]);
}, 300);
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const currentSlide = [...slides].findIndex(s => {
const rect = s.getBoundingClientRect();
return rect.top >= -100 && rect.top <= 100;
});
if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') {
e.preventDefault();
if (currentSlide < slides.length - 1) {
slides[currentSlide + 1].scrollIntoView({ behavior: 'smooth' });
}
}
if (e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault();
if (currentSlide > 0) {
slides[currentSlide - 1].scrollIntoView({ behavior: 'smooth' });
}
}
if (e.key === 'Home') {
e.preventDefault();
slides[0].scrollIntoView({ behavior: 'smooth' });
}
if (e.key === 'End') {
e.preventDefault();
slides[slides.length - 1].scrollIntoView({ behavior: 'smooth' });
}
// 'R' to replay current slide animations
if (e.key === 'r' || e.key === 'R') {
if (currentSlide >= 0) {
resetSlideAnimations(slides[currentSlide]);
setTimeout(() => animateSlide(slides[currentSlide]), 100);
}
}
});
// Copy code functionality
function copyCode(btn) {
const codeContainer = btn.closest('.code-container');
const codeElement = codeContainer.querySelector('code');
const text = codeElement.textContent;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('copied');
btn.querySelector('.copy-icon').style.display = 'none';
btn.querySelector('.check-icon').style.display = 'block';
btn.querySelector('.copy-text').textContent = 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.querySelector('.copy-icon').style.display = 'block';
btn.querySelector('.check-icon').style.display = 'none';
btn.querySelector('.copy-text').textContent = 'Copy';
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
}
// Navigate to slide by title
function navigateToSlide(title) {
// Find slide with matching h2 title
const slides = document.querySelectorAll('.slide');
for (const slide of slides) {
const heading = slide.querySelector('h2');
if (heading && heading.textContent.trim() === title) {
slide.scrollIntoView({ behavior: 'smooth' });
return;
}
}
console.warn('Slide not found:', title);
}
</script>
</body>
</html>
/* Presentation Generator - Neobrutalism Styles */
/* References brand-agency skill for colors and typography */
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Geist:wght@400;800&family=Geist+Mono:wght@400;500;700&display=swap');
:root {
/* Colors from brand-agency */
--color-background: #ffffff;
--color-foreground: #000000;
--color-primary: #e85d04;
--color-secondary: #ffd60a;
--color-accent: #3a86ff;
--color-success: #38b000;
--color-error: #d62828;
--color-muted: #e5e5e5;
/* Typography */
--font-body: 'EB Garamond', Georgia, serif;
--font-heading: 'Geist', Arial, sans-serif;
--font-mono: 'Geist Mono', 'Courier New', monospace;
/* Shadows */
--shadow: 4px 4px 0px 0px #000000;
--shadow-sm: 2px 2px 0px 0px #000000;
--shadow-lg: 8px 8px 0px 0px #000000;
/* Slide dimensions */
--slide-width: 100vw;
--slide-height: 100vh;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
}
body {
font-family: var(--font-body);
background: var(--color-background);
color: var(--color-foreground);
font-size: 18px;
line-height: 1.6;
overflow-x: hidden;
}
/* ============================================
SLIDE BASE
============================================ */
.slide {
min-height: var(--slide-height);
width: var(--slide-width);
padding: 60px 80px;
display: flex;
flex-direction: column;
justify-content: center;
border-bottom: 3px solid var(--color-foreground);
scroll-snap-align: start;
position: relative;
}
/* Slide backgrounds */
.slide--light {
background: var(--color-background);
color: var(--color-foreground);
}
.slide--dark {
background: var(--color-foreground);
color: var(--color-background);
}
.slide--primary {
background: var(--color-primary);
color: var(--color-background);
}
.slide--secondary {
background: var(--color-secondary);
color: var(--color-foreground);
}
.slide--accent {
background: var(--color-accent);
color: var(--color-background);
}
.slide--success {
background: var(--color-success);
color: var(--color-background);
}
.slide--error {
background: var(--color-error);
color: var(--color-background);
}
.slide--muted {
background: var(--color-muted);
color: var(--color-foreground);
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 800;
letter-spacing: -0.02em;
}
h1 {
font-size: 4.5rem;
line-height: 1.1;
margin-bottom: 1.5rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1.5rem;
}
h3 {
font-size: 1.8rem;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1.8rem;
opacity: 0.9;
}
.label {
font-family: var(--font-mono);
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.8;
margin-bottom: 0.5rem;
}
.highlight {
background: var(--color-secondary);
padding: 0 0.3rem;
color: var(--color-foreground);
}
/* ============================================
CARDS & BOXES
============================================ */
.card {
background: var(--color-background);
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
padding: 2rem;
margin-bottom: 1.5rem;
}
.card--primary {
border-color: var(--color-primary);
}
.card--no-shadow {
box-shadow: none;
}
.box {
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
padding: 1.5rem;
}
/* ============================================
CODE BLOCKS
============================================ */
code {
font-family: var(--font-mono);
background: var(--color-foreground);
color: var(--color-secondary);
padding: 0.2rem 0.4rem;
border: 2px solid var(--color-foreground);
}
pre {
font-family: var(--font-mono);
background: var(--color-foreground);
color: var(--color-background);
padding: 1.5rem;
border: 3px solid var(--color-foreground);
overflow-x: auto;
font-size: 0.95rem;
line-height: 1.5;
box-shadow: var(--shadow);
}
pre code {
background: transparent;
border: none;
padding: 0;
color: inherit;
}
/* Syntax highlighting */
.code-comment { color: #888; }
.code-string { color: var(--color-secondary); }
.code-keyword { color: var(--color-accent); }
.code-function { color: var(--color-success); }
.code-number { color: var(--color-error); }
/* ============================================
LISTS
============================================ */
ul, ol {
margin-left: 1.5rem;
margin-top: 1rem;
}
li {
margin-bottom: 0.75rem;
}
li::marker {
color: var(--color-primary);
}
/* ============================================
TAGS / BADGES
============================================ */
.tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border: 2px solid var(--color-foreground);
margin-right: 0.5rem;
margin-top: 0.75rem;
}
.tag--primary { background: var(--color-primary); color: white; }
.tag--secondary { background: var(--color-secondary); color: black; }
.tag--accent { background: var(--color-accent); color: white; }
.tag--success { background: var(--color-success); color: white; }
.tag--error { background: var(--color-error); color: white; }
.badge {
display: inline-block;
font-family: var(--font-mono);
font-weight: 700;
font-size: 1rem;
padding: 0.5rem 1rem;
background: var(--color-foreground);
color: var(--color-secondary);
letter-spacing: 0.1em;
box-shadow: var(--shadow-sm);
}
/* ============================================
BUTTONS
============================================ */
.btn {
display: inline-block;
font-family: var(--font-heading);
font-weight: 800;
font-size: 1rem;
padding: 1rem 2rem;
background: var(--color-primary);
color: white;
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
text-decoration: none;
cursor: pointer;
transition: transform 0.1s;
}
.btn:hover {
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0px 0px #000000;
}
.btn--secondary {
background: var(--color-secondary);
color: var(--color-foreground);
}
.btn--dark {
background: var(--color-foreground);
color: var(--color-background);
}
/* ============================================
LAYOUTS
============================================ */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
}
.three-col {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.four-col {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
}
.task-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
margin-top: 2rem;
}
.centered {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
/* ============================================
TASK CARDS
============================================ */
.task-card {
background: var(--color-background);
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
padding: 1.5rem;
transition: transform 0.1s;
}
.task-card:hover {
transform: translate(-2px, -2px);
box-shadow: 6px 6px 0px 0px #000000;
}
.task-number {
font-family: var(--font-heading);
font-weight: 800;
font-size: 3rem;
color: var(--color-primary);
line-height: 1;
}
.task-title {
font-family: var(--font-heading);
font-weight: 800;
font-size: 1.3rem;
margin: 0.5rem 0;
}
.task-desc {
font-size: 1rem;
color: #444;
}
/* ============================================
STATS
============================================ */
.stats-row {
display: flex;
gap: 3rem;
justify-content: center;
align-items: baseline;
margin: 2rem 0;
}
.stat {
text-align: center;
}
.stat-value {
font-family: var(--font-heading);
font-weight: 800;
font-size: 5rem;
line-height: 1;
color: inherit;
}
.stat-label {
font-family: var(--font-mono);
font-size: 1rem;
opacity: 0.8;
margin-top: 0.5rem;
}
/* ============================================
ASCII ART
============================================ */
.ascii-box {
font-family: var(--font-mono);
font-size: 1rem;
line-height: 1.4;
white-space: pre;
background: var(--color-muted);
padding: 1.5rem;
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
}
.ascii-border {
font-family: var(--font-mono);
font-size: 1.2rem;
color: inherit;
opacity: 0.5;
letter-spacing: 0.2em;
}
.ascii-decoration {
font-family: var(--font-mono);
font-size: 2rem;
opacity: 0.6;
}
/* ============================================
COLOR SWATCHES
============================================ */
.color-row {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.color-swatch {
width: 80px;
height: 80px;
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow-sm);
display: flex;
align-items: flex-end;
padding: 0.25rem;
}
.color-swatch span {
font-family: var(--font-mono);
font-size: 0.65rem;
background: white;
padding: 0.1rem 0.2rem;
}
/* ============================================
NAVIGATION
============================================ */
.nav {
position: fixed;
right: 2rem;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 100;
}
.nav-dot {
width: 12px;
height: 12px;
background: var(--color-muted);
border: 2px solid var(--color-foreground);
cursor: pointer;
transition: background 0.2s;
}
.nav-dot:hover,
.nav-dot.active {
background: var(--color-primary);
}
/* ============================================
FOOTER
============================================ */
.footer {
font-family: var(--font-mono);
font-size: 0.9rem;
margin-top: auto;
padding-top: 2rem;
opacity: 0.7;
}
.slide-number {
position: absolute;
bottom: 2rem;
right: 2rem;
font-family: var(--font-mono);
font-size: 0.9rem;
opacity: 0.5;
}
/* ============================================
SVG & IMAGES
============================================ */
.svg-demo {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
margin-top: 1.5rem;
}
.svg-demo svg {
filter: drop-shadow(4px 4px 0px #000000);
}
.image-container {
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
overflow: hidden;
}
.image-container img {
display: block;
width: 100%;
height: auto;
}
/* ============================================
TERMINAL STYLE
============================================ */
.terminal {
background: var(--color-foreground);
border: 3px solid var(--color-foreground);
box-shadow: var(--shadow);
font-family: var(--font-mono);
color: var(--color-background);
}
.terminal-header {
background: var(--color-muted);
padding: 0.5rem 1rem;
display: flex;
gap: 0.5rem;
}
.terminal-btn {
width: 12px;
height: 12px;
border-radius: 50%;
}
.terminal-btn--close { background: var(--color-error); }
.terminal-btn--minimize { background: var(--color-secondary); }
.terminal-btn--maximize { background: var(--color-success); }
.terminal-content {
padding: 1.5rem;
font-size: 0.9rem;
line-height: 1.6;
}
.terminal-prompt {
color: var(--color-success);
}
.terminal-output {
color: var(--color-muted);
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 1200px) {
.slide {
padding: 40px 60px;
}
h1 {
font-size: 3.5rem;
}
h2 {
font-size: 2.5rem;
}
.two-col {
gap: 2rem;
}
}
@media (max-width: 900px) {
.slide {
padding: 30px 40px;
}
.two-col,
.task-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 2.5rem;
}
.stat-value {
font-size: 3rem;
}
.nav {
display: none;
}
}
/* ============================================
PRINT / PDF EXPORT
============================================ */
@media print {
.slide {
page-break-after: always;
min-height: 100vh;
border-bottom: none;
}
.nav {
display: none;
}
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
Related skills
FAQ
What deck types does presentation-generator support?
presentation-generator supports pitch, roadmap, customer, and investor slide decks, producing structured outlines, speaker notes, and export-ready presentation content for each narrative type.
What outputs does presentation-generator deliver?
presentation-generator delivers structured slide outlines, per-slide speaker notes, and export-ready presentation content organized for stakeholder meetings without requiring a blank-template start.