Explorar el Código

feat: add Phase 6 — share & export (Vercel deploy + PDF export)

Two new sharing modes after presentation delivery:
- Deploy to Vercel for instant shareable URL (works on mobile)
- Export to PDF via Playwright screenshots (1920x1080 or --compact 1280x720)

Scripts handle edge cases: filenames with spaces, local image bundling,
URL-encoded paths, project name sanitization, and large file compression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zara Zhang hace 3 meses
padre
commit
fbf2c17fa4
Se han modificado 4 ficheros con 803 adiciones y 29 borrados
  1. 44 9
      README.md
  2. 123 20
      SKILL.md
  3. 218 0
      scripts/deploy.sh
  4. 418 0
      scripts/export-pdf.sh

+ 44 - 9
README.md

@@ -10,7 +10,6 @@ Here is a deck about the skill, made through the skill:
 
 https://github.com/user-attachments/assets/ef57333e-f879-432a-afb9-180388982478
 
-
 ### Key Features
 
 - **Zero Dependencies** — Single HTML files with inline CSS/JS. No npm, no build tools, no frameworks.
@@ -53,6 +52,7 @@ Then use it by typing `/frontend-slides` in Claude Code.
 ```
 
 The skill will:
+
 1. Ask about your content (slides, messages, images)
 2. Ask about the feeling you want (impressed? excited? calm?)
 3. Generate 3 visual style previews for you to compare
@@ -68,6 +68,7 @@ The skill will:
 ```
 
 The skill will:
+
 1. Extract all text, images, and notes from your PPT
 2. Show you the extracted content for confirmation
 3. Let you pick a visual style
@@ -76,18 +77,21 @@ The skill will:
 ## Included Styles
 
 ### Dark Themes
+
 - **Bold Signal** — Confident, high-impact, vibrant card on dark
 - **Electric Studio** — Clean, professional, split-panel
 - **Creative Voltage** — Energetic, retro-modern, electric blue + neon
 - **Dark Botanical** — Elegant, sophisticated, warm accents
 
 ### Light Themes
+
 - **Notebook Tabs** — Editorial, organized, paper with colorful tabs
 - **Pastel Geometry** — Friendly, approachable, vertical pills
 - **Split Pastel** — Playful, modern, two-color vertical split
 - **Vintage Editorial** — Witty, personality-driven, geometric shapes
 
 ### Specialty
+
 - **Neon Cyber** — Futuristic, particle backgrounds, neon glow
 - **Terminal Green** — Developer-focused, hacker aesthetic
 - **Swiss Modern** — Minimal, Bauhaus-inspired, geometric
@@ -97,14 +101,16 @@ The skill will:
 
 This skill uses **progressive disclosure** — the main `SKILL.md` is a concise map (~180 lines), with supporting files loaded on-demand only when needed:
 
-| File | Purpose | Loaded When |
-|------|---------|-------------|
-| `SKILL.md` | Core workflow and rules | Always (skill invocation) |
-| `STYLE_PRESETS.md` | 12 curated visual presets | Phase 2 (style selection) |
-| `viewport-base.css` | Mandatory responsive CSS | Phase 3 (generation) |
-| `html-template.md` | HTML structure and JS features | Phase 3 (generation) |
-| `animation-patterns.md` | CSS/JS animation reference | Phase 3 (generation) |
-| `scripts/extract-pptx.py` | PPT content extraction | Phase 4 (conversion) |
+| File                      | Purpose                        | Loaded When               |
+| ------------------------- | ------------------------------ | ------------------------- |
+| `SKILL.md`                | Core workflow and rules        | Always (skill invocation) |
+| `STYLE_PRESETS.md`        | 12 curated visual presets      | Phase 2 (style selection) |
+| `viewport-base.css`       | Mandatory responsive CSS       | Phase 3 (generation)      |
+| `html-template.md`        | HTML structure and JS features | Phase 3 (generation)      |
+| `animation-patterns.md`   | CSS/JS animation reference     | Phase 3 (generation)      |
+| `scripts/extract-pptx.py` | PPT content extraction         | Phase 4 (conversion)      |
+| `scripts/deploy.sh`       | Deploy to Vercel               | Phase 6 (sharing)         |
+| `scripts/export-pdf.sh`   | Export slides to PDF           | Phase 6 (sharing)         |
 
 This design follows [OpenAI's harness engineering](https://openai.com/index/harness-engineering/) principle: "give the agent a map, not a 1,000-page instruction manual."
 
@@ -120,10 +126,39 @@ This skill was born from the belief that:
 
 4. **Comments are kindness.** Code should explain itself to future-you (or anyone else who opens it).
 
+## Sharing Your Presentations
+
+After creating a presentation, the skill offers two ways to share it:
+
+### Deploy to a Live URL
+
+One command deploys your slides to a permanent, shareable URL that works on any device — phones, tablets, laptops:
+
+```bash
+bash scripts/deploy.sh ./my-deck/
+# or
+bash scripts/deploy.sh ./presentation.html
+```
+
+Uses [Vercel](https://vercel.com) (free tier). The skill walks you through signup and login if it's your first time.
+
+### Export to PDF
+
+Convert your slides to a PDF for email, Slack, Notion, or printing:
+
+```bash
+bash scripts/export-pdf.sh ./my-deck/index.html
+bash scripts/export-pdf.sh ./presentation.html ./output.pdf
+```
+
+Uses [Playwright](https://playwright.dev) to screenshot each slide at 1920×1080 and combine into a PDF. Installs automatically if needed. Animations are not preserved (it's a static snapshot).
+
 ## Requirements
 
 - [Claude Code](https://claude.ai/claude-code) CLI
 - For PPT conversion: Python with `python-pptx` library
+- For URL deployment: Node.js + Vercel account (free)
+- For PDF export: Node.js (Playwright installs automatically)
 
 ## Credits
 

+ 123 - 20
SKILL.md

@@ -19,12 +19,14 @@ Create zero-dependency, animation-rich HTML presentations that run entirely in t
 You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight.
 
 Focus on:
+
 - Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.
 - Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from IDE themes and cultural aesthetics for inspiration.
 - Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.
 - Backgrounds: Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.
 
 Avoid generic AI-generated aesthetics:
+
 - Overused font families (Inter, Roboto, Arial, system fonts)
 - Cliched color schemes (particularly purple gradients on white backgrounds)
 - Predictable layouts and component patterns
@@ -48,14 +50,14 @@ These invariants apply to EVERY slide in EVERY presentation:
 
 ### Content Density Limits Per Slide
 
-| Slide Type | Maximum Content |
-|------------|-----------------|
-| Title slide | 1 heading + 1 subtitle + optional tagline |
+| Slide Type    | Maximum Content                                           |
+| ------------- | --------------------------------------------------------- |
+| Title slide   | 1 heading + 1 subtitle + optional tagline                 |
 | Content slide | 1 heading + 4-6 bullet points OR 1 heading + 2 paragraphs |
-| Feature grid | 1 heading + 6 cards maximum (2x3 or 3x2) |
-| Code slide | 1 heading + 8-10 lines of code |
-| Quote slide | 1 quote (max 3 lines) + attribution |
-| Image slide | 1 heading + 1 image (max 60vh height) |
+| Feature grid  | 1 heading + 6 cards maximum (2x3 or 3x2)                  |
+| Code slide    | 1 heading + 8-10 lines of code                            |
+| Quote slide   | 1 quote (max 3 lines) + attribution                       |
+| Image slide   | 1 heading + 1 image (max 60vh height)                     |
 
 **Content exceeds limits? Split into multiple slides. Never cram, never scroll.**
 
@@ -98,6 +100,7 @@ Do you have content ready? Options: All content ready / Rough notes / Topic only
 
 **Question 4 — Inline Editing** (header: "Editing"):
 Do you need to edit text directly in the browser after generation? Options:
+
 - "Yes (Recommended)" — Can edit text in-browser, auto-save to localStorage, export file
 - "No" — Presentation only, keeps file smaller
 
@@ -110,6 +113,7 @@ If user has content, ask them to share it.
 If user selected "No images" → skip to Phase 2.
 
 If user provides an image folder:
+
 1. **Scan** — List all image files (.png, .jpg, .svg, .webp, etc.)
 2. **View each image** — Use the Read tool (Claude is multimodal)
 3. **Evaluate** — For each: what it shows, USABLE or NOT USABLE (with reason), what concept it represents, dominant colors
@@ -127,6 +131,7 @@ If user provides an image folder:
 ### Step 2.0: Style Path
 
 Ask how they want to choose (header: "Style"):
+
 - "Show me options" (recommended) — Generate 3 previews based on mood
 - "I know what I want" — Pick from preset list directly
 
@@ -136,6 +141,7 @@ Ask how they want to choose (header: "Style"):
 
 Ask (header: "Vibe", multiSelect: true, max 2):
 What feeling should the audience have? Options:
+
 - Impressed/Confident — Professional, trustworthy
 - Excited/Energized — Innovative, bold
 - Calm/Focused — Clear, thoughtful
@@ -145,12 +151,12 @@ What feeling should the audience have? Options:
 
 Based on mood, generate 3 distinct single-slide HTML previews showing typography, colors, animation, and overall aesthetic. Read [STYLE_PRESETS.md](STYLE_PRESETS.md) for available presets and their specifications.
 
-| Mood | Suggested Presets |
-|------|-------------------|
-| Impressed/Confident | Bold Signal, Electric Studio, Dark Botanical |
-| Excited/Energized | Creative Voltage, Neon Cyber, Split Pastel |
-| Calm/Focused | Notebook Tabs, Paper & Ink, Swiss Modern |
-| Inspired/Moved | Dark Botanical, Vintage Editorial, Pastel Geometry |
+| Mood                | Suggested Presets                                  |
+| ------------------- | -------------------------------------------------- |
+| Impressed/Confident | Bold Signal, Electric Studio, Dark Botanical       |
+| Excited/Energized   | Creative Voltage, Neon Cyber, Split Pastel         |
+| Calm/Focused        | Notebook Tabs, Paper & Ink, Swiss Modern           |
+| Inspired/Moved      | Dark Botanical, Vintage Editorial, Pastel Geometry |
 
 Save previews to `.claude-design/slide-previews/` (style-a.html, style-b.html, style-c.html). Each should be self-contained, ~50-100 lines, showing one animated title slide.
 
@@ -172,11 +178,13 @@ Generate the full presentation using content from Phase 1 (text, or text + curat
 If images were provided, the slide outline already incorporates them from Step 1.2. If not, CSS-generated visuals (gradients, shapes, patterns) provide visual interest — this is a fully supported first-class path.
 
 **Before generating, read these supporting files:**
+
 - [html-template.md](html-template.md) — HTML architecture and JS features
 - [viewport-base.css](viewport-base.css) — Mandatory CSS (include in full)
 - [animation-patterns.md](animation-patterns.md) — Animation reference for the chosen feeling
 
 **Key requirements:**
+
 - Single self-contained HTML file, all CSS/JS inline
 - Include the FULL contents of viewport-base.css in the `<style>` block
 - Use fonts from Fontshare or Google Fonts — never system fonts
@@ -208,12 +216,107 @@ When converting PowerPoint files:
 
 ---
 
+## Phase 6: Share & Export (Optional)
+
+After delivery, **ask the user:** _"Would you like to share this presentation? I can deploy it to a live URL (works on any device including phones) or export it as a PDF."_
+
+Options:
+
+- **Deploy to URL** — Shareable link that works on any device
+- **Export to PDF** — Universal file for email, Slack, print
+- **Both**
+- **No thanks**
+
+If the user declines, stop here. If they choose one or both, proceed below.
+
+### 6A: Deploy to a Live URL (Vercel)
+
+This deploys the presentation to Vercel — a free hosting platform. The link works on any device (phones, tablets, laptops) and stays live until the user takes it down.
+
+**If the user has never deployed before, guide them step by step:**
+
+1. **Check if Vercel CLI is installed** — Run `npx vercel --version`. If not found, install Node.js first (`brew install node` on macOS, or download from https://nodejs.org).
+
+2. **Check if user is logged in** — Run `npx vercel whoami`.
+   - If NOT logged in, explain: _"Vercel is a free hosting service. You need an account to deploy. Let me walk you through it:"_
+     - Step 1: Ask user to go to https://vercel.com/signup in their browser
+     - Step 2: They can sign up with GitHub, Google, email — whatever is easiest
+     - Step 3: Once signed up, run `vercel login` and follow the prompts (it opens a browser window to authorize)
+     - Step 4: Confirm login with `vercel whoami`
+   - Wait for the user to confirm they're logged in before proceeding.
+
+3. **Deploy** — Run the deploy script:
+
+   ```bash
+   bash scripts/deploy.sh <path-to-presentation>
+   ```
+
+   The script accepts either a folder (with index.html) or a single HTML file.
+
+4. **Share the URL** — Tell the user:
+   - The live URL (from the script output)
+   - That it works on any device — they can text it, Slack it, email it
+   - To take it down later: visit https://vercel.com/dashboard and delete the project
+   - The Vercel free tier is generous — they won't be charged
+
+**⚠ Deployment gotchas:**
+
+- **Local images/videos must travel with the HTML.** The deploy script auto-detects files referenced via `src="..."` in the HTML and bundles them. But if the presentation references files via CSS `background-image` or unusual paths, those may be missed. **Before deploying, verify:** open the deployed URL and check that all images load. If any are broken, the safest fix is to put the HTML and all its assets into a single folder and deploy the folder instead of a standalone HTML file.
+- **Prefer folder deployments when the presentation has many assets.** If the presentation lives in a folder with images alongside it (e.g., `my-deck/index.html` + `my-deck/logo.png`), deploy the folder directly: `bash scripts/deploy.sh ./my-deck/`. This is more reliable than deploying a single HTML file because the entire folder contents are uploaded as-is.
+- **Filenames with spaces work but can cause issues.** The script handles spaces in filenames, but Vercel URLs encode spaces as `%20`. If possible, avoid spaces in image filenames. If the user's images have spaces, the script handles it — but if images still break, renaming files to use hyphens instead of spaces is the fix.
+- **Redeploying updates the same URL.** Running the deploy script again on the same presentation overwrites the previous deployment. The URL stays the same — no need to share a new link.
+
+### 6B: Export to PDF
+
+This captures each slide as a screenshot and combines them into a PDF. Perfect for email attachments, embedding in documents, or printing.
+
+**Note:** Animations and interactivity are not preserved — the PDF is a static snapshot. This is normal and expected; mention it to the user so they're not surprised.
+
+1. **Run the export script:**
+
+   ```bash
+   bash scripts/export-pdf.sh <path-to-html> [output.pdf]
+   ```
+
+   If no output path is given, the PDF is saved next to the HTML file.
+
+2. **What happens behind the scenes** (explain briefly to the user):
+   - A headless browser opens the presentation at 1920×1080 (standard widescreen)
+   - It screenshots each slide one by one
+   - All screenshots are combined into a single PDF
+   - The script needs Playwright (a browser automation tool) — it will install automatically if missing
+
+3. **If Playwright installation fails:**
+   - The most common issue is Chromium not downloading. Run: `npx playwright install chromium`
+   - If that fails too, it may be a network/firewall issue. Ask the user to try on a different network.
+
+4. **Deliver the PDF** — The script auto-opens it. Tell the user:
+   - The file location and size
+   - That it works everywhere — email, Slack, Notion, Google Docs, print
+   - Animations are replaced by their final visual state (still looks great, just static)
+
+**⚠ PDF export gotchas:**
+
+- **First run is slow.** The script installs Playwright and downloads a Chromium browser (~150MB) into a temp directory. This happens once per run. Warn the user it may take 30-60 seconds the first time — subsequent exports within the same session are faster.
+- **Slides must use `class="slide"`.** The export script finds slides by querying `.slide` elements. If the presentation uses a different class name, the script will report "0 slides found" and fail. All presentations generated by this skill use `.slide`, so this only matters for externally-created HTML.
+- **Local images must be loadable via HTTP.** The script starts a local server and loads the HTML through it (so Google Fonts and relative image paths work). If images use absolute filesystem paths (e.g., `src="/Users/name/photo.png"`) instead of relative paths (e.g., `src="photo.png"`), they won't load. Generated presentations always use relative paths, but converted or user-provided decks might not — check and fix if needed.
+- **Local images appear in the PDF** as long as they are in the same directory as (or relative to) the HTML file. The export script serves the HTML's parent directory over HTTP, so relative paths like `src="photo.png"` resolve correctly — including filenames with spaces. If images still don't appear, check: (1) the image files actually exist at the referenced path, (2) the paths are relative, not absolute filesystem paths like `/Users/name/photo.png`.
+- **Large presentations produce large PDFs.** Each slide is captured as a full 1920×1080 PNG screenshot. An 18-slide deck can produce a ~20MB PDF. If the PDF exceeds 10MB, ask the user: _"The PDF is [size]. Would you like me to compress it? It'll look slightly less sharp but the file will be much smaller."_ If yes, re-run the export with the `--compact` flag:
+  ```bash
+  bash scripts/export-pdf.sh <path-to-html> [output.pdf] --compact
+  ```
+  This renders at 1280×720 instead of 1920×1080, typically cutting file size by 50-70% with minimal visual difference.
+
+---
+
 ## Supporting Files
 
-| File | Purpose | When to Read |
-|------|---------|-------------|
-| [STYLE_PRESETS.md](STYLE_PRESETS.md) | 12 curated visual presets with colors, fonts, and signature elements | Phase 2 (style selection) |
-| [viewport-base.css](viewport-base.css) | Mandatory responsive CSS — copy into every presentation | Phase 3 (generation) |
-| [html-template.md](html-template.md) | HTML structure, JS features, code quality standards | Phase 3 (generation) |
-| [animation-patterns.md](animation-patterns.md) | CSS/JS animation snippets and effect-to-feeling guide | Phase 3 (generation) |
-| [scripts/extract-pptx.py](scripts/extract-pptx.py) | Python script for PPT content extraction | Phase 4 (conversion) |
+| File                                               | Purpose                                                              | When to Read              |
+| -------------------------------------------------- | -------------------------------------------------------------------- | ------------------------- |
+| [STYLE_PRESETS.md](STYLE_PRESETS.md)               | 12 curated visual presets with colors, fonts, and signature elements | Phase 2 (style selection) |
+| [viewport-base.css](viewport-base.css)             | Mandatory responsive CSS — copy into every presentation              | Phase 3 (generation)      |
+| [html-template.md](html-template.md)               | HTML structure, JS features, code quality standards                  | Phase 3 (generation)      |
+| [animation-patterns.md](animation-patterns.md)     | CSS/JS animation snippets and effect-to-feeling guide                | Phase 3 (generation)      |
+| [scripts/extract-pptx.py](scripts/extract-pptx.py) | Python script for PPT content extraction                             | Phase 4 (conversion)      |
+| [scripts/deploy.sh](scripts/deploy.sh)             | Deploy slides to Vercel for instant sharing                          | Phase 6 (sharing)         |
+| [scripts/export-pdf.sh](scripts/export-pdf.sh)     | Export slides to PDF                                                 | Phase 6 (sharing)         |

+ 218 - 0
scripts/deploy.sh

@@ -0,0 +1,218 @@
+#!/usr/bin/env bash
+# deploy.sh — Deploy a slide deck to Vercel for instant sharing
+#
+# Usage:
+#   bash scripts/deploy.sh <path-to-slide-folder-or-html>
+#
+# Examples:
+#   bash scripts/deploy.sh ./my-pitch-deck/
+#   bash scripts/deploy.sh ./presentation.html
+#
+# What this does:
+#   1. Checks if Vercel CLI is installed (installs if not)
+#   2. Checks if user is logged in (guides through login if not)
+#   3. Deploys the slide deck to a public URL
+#   4. Prints the live URL
+#
+# The deployed URL is permanent and works on any device (mobile, tablet, desktop).
+# No server to maintain — Vercel hosts it for free.
+set -euo pipefail
+
+# ─── Colors ────────────────────────────────────────────────
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+CYAN='\033[0;36m'
+YELLOW='\033[1;33m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+info()  { echo -e "${CYAN}ℹ${NC} $*"; }
+ok()    { echo -e "${GREEN}✓${NC} $*"; }
+warn()  { echo -e "${YELLOW}⚠${NC} $*"; }
+err()   { echo -e "${RED}✗${NC} $*" >&2; }
+
+# ─── Input validation ─────────────────────────────────────
+
+if [[ $# -lt 1 ]]; then
+    err "Usage: bash scripts/deploy.sh <path-to-slide-folder-or-html>"
+    err ""
+    err "Examples:"
+    err "  bash scripts/deploy.sh ./my-pitch-deck/"
+    err "  bash scripts/deploy.sh ./presentation.html"
+    exit 1
+fi
+
+INPUT="$1"
+
+# If input is a single HTML file, create a temp directory with it as index.html
+if [[ -f "$INPUT" && "$INPUT" == *.html ]]; then
+    DEPLOY_DIR=$(mktemp -d)
+    cp "$INPUT" "$DEPLOY_DIR/index.html"
+    PARENT_DIR=$(dirname "$INPUT")
+
+    # Parse the HTML for local file references (src="...", url('...'), href="...")
+    # and copy any referenced local files into the deploy directory
+    grep -oE '(src|href|url\()["'"'"']?[^"'"'"'>)]+' "$INPUT" 2>/dev/null | \
+        sed "s/^src=//; s/^href=//; s/^url(//; s/[\"']//g" | \
+        grep -v '^http' | grep -v '^data:' | grep -v '^#' | grep -v '^/' | \
+        sort -u | while read -r ref; do
+            # Resolve the reference relative to the HTML file's directory
+            SOURCE_FILE="$PARENT_DIR/$ref"
+            if [[ -e "$SOURCE_FILE" ]]; then
+                # Preserve directory structure for nested paths (e.g., assets/img.png)
+                TARGET_DIR="$DEPLOY_DIR/$(dirname "$ref")"
+                mkdir -p "$TARGET_DIR"
+                cp -r "$SOURCE_FILE" "$TARGET_DIR/"
+            fi
+        done
+
+    # Also copy any assets/ folder if it exists (common convention)
+    if [[ -d "$PARENT_DIR/assets" ]]; then
+        cp -r "$PARENT_DIR/assets" "$DEPLOY_DIR/assets" 2>/dev/null || true
+    fi
+
+    CLEANUP_TEMP=true
+    info "Single HTML file detected — preparing for deployment..."
+elif [[ -d "$INPUT" ]]; then
+    # Verify the folder has an index.html
+    if [[ ! -f "$INPUT/index.html" ]]; then
+        err "Folder '$INPUT' does not contain an index.html file."
+        err "Make sure your presentation folder has an index.html."
+        exit 1
+    fi
+    DEPLOY_DIR="$INPUT"
+    CLEANUP_TEMP=false
+else
+    err "'$INPUT' is not a valid HTML file or directory."
+    exit 1
+fi
+
+# ─── Step 1: Check for Vercel CLI ─────────────────────────
+
+echo ""
+echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
+echo -e "${BOLD}║       Deploy Slides to Vercel         ║${NC}"
+echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
+echo ""
+
+if ! command -v npx &>/dev/null; then
+    err "Node.js is required but not installed."
+    err ""
+    err "Install Node.js:"
+    err "  macOS:   brew install node"
+    err "  or visit https://nodejs.org and download the installer"
+    exit 1
+fi
+
+info "Checking Vercel CLI..."
+
+# Check if vercel is available (either globally or via npx)
+if command -v vercel &>/dev/null; then
+    VERCEL_CMD="vercel"
+    ok "Vercel CLI found"
+elif npx --yes vercel --version &>/dev/null 2>&1; then
+    VERCEL_CMD="npx --yes vercel"
+    ok "Vercel CLI available via npx"
+else
+    info "Installing Vercel CLI..."
+    npm install -g vercel
+    VERCEL_CMD="vercel"
+    ok "Vercel CLI installed"
+fi
+
+# ─── Step 2: Check login status ───────────────────────────
+
+echo ""
+info "Checking Vercel login status..."
+
+# Try to check if logged in by running whoami
+if ! $VERCEL_CMD whoami &>/dev/null 2>&1; then
+    echo ""
+    warn "You're not logged in to Vercel yet."
+    echo ""
+    echo -e "${BOLD}To log in, run this command and follow the prompts:${NC}"
+    echo ""
+    echo "    vercel login"
+    echo ""
+    echo "If you don't have a Vercel account yet:"
+    echo "  1. Go to https://vercel.com/signup"
+    echo "  2. Sign up with GitHub, GitLab, email, or any method"
+    echo "  3. Come back here and run: vercel login"
+    echo "  4. Then re-run this deploy script"
+    echo ""
+
+    # Try interactive login
+    echo -e "${YELLOW}Attempting interactive login now...${NC}"
+    echo ""
+    $VERCEL_CMD login || {
+        err "Login failed. Please run 'vercel login' manually and try again."
+        [[ "$CLEANUP_TEMP" == "true" ]] && rm -rf "$DEPLOY_DIR"
+        exit 1
+    }
+    echo ""
+    ok "Logged in to Vercel!"
+fi
+
+VERCEL_USER=$($VERCEL_CMD whoami 2>/dev/null || echo "unknown")
+ok "Logged in as: $VERCEL_USER"
+
+# ─── Step 3: Deploy ───────────────────────────────────────
+
+echo ""
+info "Deploying slides..."
+echo ""
+
+# Deploy with sensible defaults:
+#   --yes: skip confirmation prompts
+#   --prod: deploy to production URL (not preview)
+#   --name: use the folder name as the project name
+DECK_NAME=$(basename "$DEPLOY_DIR")
+# If we used a temp dir, use the original filename without .html
+if [[ "$CLEANUP_TEMP" == "true" ]]; then
+    DECK_NAME=$(basename "$INPUT" .html)
+fi
+
+# Sanitize project name for Vercel:
+# - lowercase, replace spaces/special chars with hyphens
+# - collapse multiple hyphens, trim to 100 chars
+DECK_NAME=$(echo "$DECK_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-100)
+
+# Vercel uses the directory name as the project name, so rename the deploy
+# directory to the sanitized deck name (avoids deprecated --name flag)
+if [[ "$CLEANUP_TEMP" == "true" ]]; then
+    RENAMED_DIR="$(dirname "$DEPLOY_DIR")/$DECK_NAME"
+    mv "$DEPLOY_DIR" "$RENAMED_DIR"
+    DEPLOY_DIR="$RENAMED_DIR"
+fi
+
+DEPLOY_OUTPUT=$($VERCEL_CMD deploy "$DEPLOY_DIR" --yes --prod 2>&1) || {
+    err "Deployment failed:"
+    echo "$DEPLOY_OUTPUT"
+    [[ "$CLEANUP_TEMP" == "true" ]] && rm -rf "$DEPLOY_DIR"
+    exit 1
+}
+
+# Extract the URL from output
+DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -o 'https://[^ ]*' | tail -1)
+
+# ─── Step 4: Success ──────────────────────────────────────
+
+echo ""
+echo -e "${BOLD}════════════════════════════════════════${NC}"
+ok "Slides deployed successfully!"
+echo ""
+echo -e "  ${BOLD}Live URL:${NC}  $DEPLOY_URL"
+echo ""
+echo "  This URL works on any device — phones, tablets, laptops."
+echo "  Share it via Slack, email, text, or anywhere."
+echo ""
+echo -e "  ${CYAN}Tip:${NC} To take it down later, visit https://vercel.com/dashboard"
+echo -e "       and delete the project '${DECK_NAME}'."
+echo -e "${BOLD}════════════════════════════════════════${NC}"
+echo ""
+
+# ─── Cleanup ──────────────────────────────────────────────
+
+if [[ "$CLEANUP_TEMP" == "true" ]]; then
+    rm -rf "$DEPLOY_DIR"
+fi

+ 418 - 0
scripts/export-pdf.sh

@@ -0,0 +1,418 @@
+#!/usr/bin/env bash
+# export-pdf.sh — Export an HTML presentation to PDF
+#
+# Usage:
+#   bash scripts/export-pdf.sh <path-to-html> [output.pdf]
+#
+# Examples:
+#   bash scripts/export-pdf.sh ./my-deck/index.html
+#   bash scripts/export-pdf.sh ./presentation.html ./presentation.pdf
+#
+# What this does:
+#   1. Starts a local server to serve the HTML (fonts and assets need HTTP)
+#   2. Uses Playwright to screenshot each slide at 1920x1080
+#   3. Combines all screenshots into a single PDF
+#   4. Cleans up the server and temp files
+#
+# The PDF preserves colors, fonts, and layout — but not animations.
+# Perfect for email attachments, printing, or embedding in documents.
+set -euo pipefail
+
+# ─── Colors ────────────────────────────────────────────────
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+CYAN='\033[0;36m'
+YELLOW='\033[1;33m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+info()  { echo -e "${CYAN}ℹ${NC} $*"; }
+ok()    { echo -e "${GREEN}✓${NC} $*"; }
+warn()  { echo -e "${YELLOW}⚠${NC} $*"; }
+err()   { echo -e "${RED}✗${NC} $*" >&2; }
+
+# ─── Parse flags ──────────────────────────────────────────
+
+# Default resolution: 1920x1080 (full HD, ~1-2MB per slide)
+# Compact resolution: 1280x720 (HD, ~50-70% smaller files)
+VIEWPORT_W=1920
+VIEWPORT_H=1080
+COMPACT=false
+
+POSITIONAL=()
+for arg in "$@"; do
+    case $arg in
+        --compact)
+            COMPACT=true
+            VIEWPORT_W=1280
+            VIEWPORT_H=720
+            ;;
+        *)
+            POSITIONAL+=("$arg")
+            ;;
+    esac
+done
+set -- "${POSITIONAL[@]}"
+
+# ─── Input validation ─────────────────────────────────────
+
+if [[ $# -lt 1 ]]; then
+    err "Usage: bash scripts/export-pdf.sh <path-to-html> [output.pdf] [--compact]"
+    err ""
+    err "Examples:"
+    err "  bash scripts/export-pdf.sh ./my-deck/index.html"
+    err "  bash scripts/export-pdf.sh ./presentation.html ./slides.pdf"
+    err "  bash scripts/export-pdf.sh ./presentation.html --compact   # smaller file size"
+    exit 1
+fi
+
+INPUT_HTML="$1"
+if [[ ! -f "$INPUT_HTML" ]]; then
+    err "File not found: $INPUT_HTML"
+    exit 1
+fi
+
+# Resolve to absolute path
+INPUT_HTML=$(cd "$(dirname "$INPUT_HTML")" && pwd)/$(basename "$INPUT_HTML")
+
+# Output PDF path: use second argument or derive from input name
+if [[ $# -ge 2 ]]; then
+    OUTPUT_PDF="$2"
+else
+    OUTPUT_PDF="$(dirname "$INPUT_HTML")/$(basename "$INPUT_HTML" .html).pdf"
+fi
+
+# Resolve output to absolute path
+OUTPUT_DIR=$(dirname "$OUTPUT_PDF")
+mkdir -p "$OUTPUT_DIR"
+OUTPUT_PDF="$OUTPUT_DIR/$(basename "$OUTPUT_PDF")"
+
+echo ""
+echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
+echo -e "${BOLD}║       Export Slides to PDF            ║${NC}"
+echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
+echo ""
+
+# ─── Step 1: Check dependencies ───────────────────────────
+
+info "Checking dependencies..."
+
+if ! command -v npx &>/dev/null; then
+    err "Node.js is required but not installed."
+    err ""
+    err "Install Node.js:"
+    err "  macOS:   brew install node"
+    err "  or visit https://nodejs.org and download the installer"
+    exit 1
+fi
+
+ok "Node.js found"
+
+# ─── Step 2: Create the export script ─────────────────────
+
+# We use a temporary Node.js script with Playwright to:
+# 1. Start a local server (so fonts load correctly)
+# 2. Navigate to each slide
+# 3. Screenshot each slide at 1920x1080 (16:9 landscape)
+# 4. Combine into a single PDF
+
+TEMP_DIR=$(mktemp -d)
+TEMP_SCRIPT="$TEMP_DIR/export-slides.mjs"
+
+# Figure out which directory to serve (the folder containing the HTML)
+SERVE_DIR=$(dirname "$INPUT_HTML")
+HTML_FILENAME=$(basename "$INPUT_HTML")
+
+cat > "$TEMP_SCRIPT" << 'EXPORT_SCRIPT'
+// export-slides.mjs — Playwright script to export HTML slides to PDF
+//
+// How it works:
+// 1. Starts a local HTTP server (needed for fonts/assets to load)
+// 2. Opens the presentation in a headless browser at 1920x1080
+// 3. Counts the total number of slides
+// 4. Screenshots each slide one by one
+// 5. Generates a PDF with all slides as landscape pages
+
+import { chromium } from 'playwright';
+import { createServer } from 'http';
+import { readFileSync, existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
+import { join, extname, resolve } from 'path';
+import { execSync } from 'child_process';
+
+const SERVE_DIR = process.argv[2];
+const HTML_FILE = process.argv[3];
+const OUTPUT_PDF = process.argv[4];
+const SCREENSHOT_DIR = process.argv[5];
+const VP_WIDTH = parseInt(process.argv[6]) || 1920;
+const VP_HEIGHT = parseInt(process.argv[7]) || 1080;
+
+// ─── Simple static file server ────────────────────────────
+// (We need HTTP so that Google Fonts and relative assets load correctly)
+
+const MIME_TYPES = {
+  '.html': 'text/html',
+  '.css': 'text/css',
+  '.js': 'application/javascript',
+  '.json': 'application/json',
+  '.png': 'image/png',
+  '.jpg': 'image/jpeg',
+  '.jpeg': 'image/jpeg',
+  '.gif': 'image/gif',
+  '.svg': 'image/svg+xml',
+  '.webp': 'image/webp',
+  '.woff': 'font/woff',
+  '.woff2': 'font/woff2',
+  '.ttf': 'font/ttf',
+  '.eot': 'application/vnd.ms-fontobject',
+};
+
+const server = createServer((req, res) => {
+  // Decode URL-encoded characters (e.g., %20 → space) so filenames with spaces resolve correctly
+  const decodedUrl = decodeURIComponent(req.url);
+  let filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl);
+  try {
+    const content = readFileSync(filePath);
+    const ext = extname(filePath).toLowerCase();
+    res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
+    res.end(content);
+  } catch {
+    res.writeHead(404);
+    res.end('Not found');
+  }
+});
+
+// Find a free port
+const port = await new Promise((resolve) => {
+  server.listen(0, () => resolve(server.address().port));
+});
+
+console.log(`  Local server on port ${port}`);
+
+// ─── Screenshot each slide ────────────────────────────────
+
+const browser = await chromium.launch();
+const page = await browser.newPage({
+  viewport: { width: VP_WIDTH, height: VP_HEIGHT },
+});
+
+// Load the presentation
+await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle' });
+
+// Wait for fonts to load
+await page.evaluate(() => document.fonts.ready);
+
+// Extra wait for animations to settle on the first slide
+await page.waitForTimeout(1500);
+
+// Count slides
+const slideCount = await page.evaluate(() => {
+  return document.querySelectorAll('.slide').length;
+});
+
+console.log(`  Found ${slideCount} slides`);
+
+if (slideCount === 0) {
+  console.error('  ERROR: No .slide elements found in the presentation.');
+  console.error('  Make sure your HTML uses <div class="slide"> or <section class="slide">.');
+  await browser.close();
+  server.close();
+  process.exit(1);
+}
+
+// Screenshot each slide
+mkdirSync(SCREENSHOT_DIR, { recursive: true });
+const screenshotPaths = [];
+
+for (let i = 0; i < slideCount; i++) {
+  // Navigate to slide by simulating the presentation's navigation
+  // Most frontend-slides presentations use a currentSlide index and show/hide
+  await page.evaluate((index) => {
+    const slides = document.querySelectorAll('.slide');
+
+    // Try multiple navigation strategies used by frontend-slides:
+
+    // Strategy 1: Direct slide manipulation (most common in generated decks)
+    slides.forEach((slide, idx) => {
+      if (idx === index) {
+        slide.style.display = '';
+        slide.style.opacity = '1';
+        slide.style.visibility = 'visible';
+        slide.style.position = 'relative';
+        slide.style.transform = 'none';
+        slide.classList.add('active');
+      } else {
+        slide.style.display = 'none';
+        slide.classList.remove('active');
+      }
+    });
+
+    // Strategy 2: If there's a SlidePresentation class instance, use it
+    if (window.presentation && typeof window.presentation.goToSlide === 'function') {
+      window.presentation.goToSlide(index);
+    }
+
+    // Strategy 3: Scroll-based (some decks use scroll snapping)
+    slides[index]?.scrollIntoView({ behavior: 'instant' });
+  }, i);
+
+  // Wait for any slide transition animations to finish
+  await page.waitForTimeout(300);
+
+  // Wait for intersection observer animations to trigger
+  await page.waitForTimeout(200);
+
+  // Force all .reveal elements on the current slide to be visible
+  // (animations normally trigger on scroll/intersection, but we need them visible now)
+  await page.evaluate((index) => {
+    const slides = document.querySelectorAll('.slide');
+    const currentSlide = slides[index];
+    if (currentSlide) {
+      currentSlide.querySelectorAll('.reveal').forEach(el => {
+        el.style.opacity = '1';
+        el.style.transform = 'none';
+        el.style.visibility = 'visible';
+      });
+    }
+  }, i);
+
+  await page.waitForTimeout(100);
+
+  const screenshotPath = join(SCREENSHOT_DIR, `slide-${String(i + 1).padStart(3, '0')}.png`);
+  await page.screenshot({ path: screenshotPath, fullPage: false });
+  screenshotPaths.push(screenshotPath);
+  console.log(`  Captured slide ${i + 1}/${slideCount}`);
+}
+
+await browser.close();
+server.close();
+
+// ─── Combine screenshots into PDF ─────────────────────────
+// Use a second Playwright page to generate a PDF from the screenshots
+
+console.log('  Assembling PDF...');
+
+const browser2 = await chromium.launch();
+const pdfPage = await browser2.newPage();
+
+// Build an HTML page with all screenshots, one per page
+const imagesHtml = screenshotPaths.map((p) => {
+  const imgData = readFileSync(p).toString('base64');
+  return `<div class="page"><img src="data:image/png;base64,${imgData}" /></div>`;
+}).join('\n');
+
+const pdfHtml = `<!DOCTYPE html>
+<html>
+<head>
+<style>
+  * { margin: 0; padding: 0; }
+  @page { size: ${VP_WIDTH}px ${VP_HEIGHT}px; margin: 0; }
+  .page {
+    width: ${VP_WIDTH}px;
+    height: ${VP_HEIGHT}px;
+    page-break-after: always;
+    overflow: hidden;
+  }
+  .page:last-child { page-break-after: auto; }
+  img {
+    width: ${VP_WIDTH}px;
+    height: ${VP_HEIGHT}px;
+    display: block;
+    object-fit: contain;
+  }
+</style>
+</head>
+<body>${imagesHtml}</body>
+</html>`;
+
+await pdfPage.setContent(pdfHtml, { waitUntil: 'load' });
+await pdfPage.pdf({
+  path: OUTPUT_PDF,
+  width: `${VP_WIDTH}px`,
+  height: `${VP_HEIGHT}px`,
+  printBackground: true,
+  margin: { top: 0, right: 0, bottom: 0, left: 0 },
+});
+
+await browser2.close();
+
+// Clean up screenshots
+screenshotPaths.forEach(p => unlinkSync(p));
+
+console.log(`  ✓ PDF saved to: ${OUTPUT_PDF}`);
+EXPORT_SCRIPT
+
+# ─── Step 3: Install Playwright in temp directory ──────────
+# We install Playwright locally in the temp dir so the Node script can import it.
+# This avoids polluting global packages and ensures the script is self-contained.
+
+info "Setting up Playwright (headless browser for screenshots)..."
+info "This may take a moment on first run..."
+echo ""
+
+cd "$TEMP_DIR"
+
+# Create a minimal package.json so npm install works
+cat > "$TEMP_DIR/package.json" << 'PKG'
+{ "name": "slide-export", "private": true, "type": "module" }
+PKG
+
+# Install Playwright into the temp directory
+npm install playwright &>/dev/null || {
+    err "Failed to install Playwright."
+    err "Try running: npm install playwright"
+    rm -rf "$TEMP_DIR"
+    exit 1
+}
+
+# Ensure Chromium browser binary is downloaded
+npx playwright install chromium 2>/dev/null || {
+    err "Failed to install Chromium browser for Playwright."
+    err "Try running manually: npx playwright install chromium"
+    rm -rf "$TEMP_DIR"
+    exit 1
+}
+ok "Playwright ready"
+echo ""
+
+# ─── Step 4: Run the export ───────────────────────────────
+
+SCREENSHOT_DIR="$TEMP_DIR/screenshots"
+
+info "Exporting slides to PDF..."
+echo ""
+
+# Run from the temp dir so Node can find the locally-installed playwright
+if [[ "$COMPACT" == "true" ]]; then
+    info "Using compact mode (1280×720) for smaller file size"
+fi
+
+node "$TEMP_SCRIPT" "$SERVE_DIR" "$HTML_FILENAME" "$OUTPUT_PDF" "$SCREENSHOT_DIR" "$VIEWPORT_W" "$VIEWPORT_H" || {
+    err "PDF export failed."
+    rm -rf "$TEMP_DIR"
+    exit 1
+}
+
+# ─── Step 5: Cleanup and success ──────────────────────────
+
+rm -rf "$TEMP_DIR"
+
+echo ""
+echo -e "${BOLD}════════════════════════════════════════${NC}"
+ok "PDF exported successfully!"
+echo ""
+echo -e "  ${BOLD}File:${NC}  $OUTPUT_PDF"
+echo ""
+FILE_SIZE=$(du -h "$OUTPUT_PDF" | cut -f1 | xargs)
+echo "  Size: $FILE_SIZE"
+echo ""
+echo "  This PDF works everywhere — email, Slack, Notion, print."
+echo "  Note: Animations are not preserved (it's a static export)."
+echo -e "${BOLD}════════════════════════════════════════${NC}"
+echo ""
+
+# Open the PDF automatically
+if command -v open &>/dev/null; then
+    open "$OUTPUT_PDF"
+elif command -v xdg-open &>/dev/null; then
+    xdg-open "$OUTPUT_PDF"
+fi