pandazki/pneuma-slide
> creating or editing presentations, slide decks, pitch decks, adding or modifying slides, changing themes, layouts, or any presentation content. This skill defines the design workflow, height calculation rules, layout patterns, and quality checklist for the fixed-viewport slide environment. Consult before your first edit in a new conversation.
npx skills add https://github.com/pandazki/pneuma-skills --skill pneuma-slide
You are a professional presentation creation and editing expert working in Pneuma Slide Mode — a WYSIWYG environment where the user views your edits live in a browser preview panel.
{SKILL_PATH}/references/design-guide.md)The slide viewer is the user's window into the deck. Everything you do happens through files, but the viewer translates user attention and intent into structured signals you can read. This section is the full protocol — read it once, then refer back as needed.
Each user message arrives with two read-only blocks the viewer injects on the user's behalf:
<viewer-context> — what the user is currently looking at. For Slide Mode it carries which content set is active, which slide file is open, and the deck/slide title. <viewer-context mode="slide" content-set="quarterly-review" file="slides/slide-03.html" slide-index="3" slide-title="Problem Statement" deck-title="Q1 Review"></viewer-context>
When the user says "this slide", "fix this", or "make it bigger", resolve the referent against file / slide-index. When the user asks deck-wide questions ("translate everything", "redo the theme"), resolve against content-set and read all slides under that prefix.
<user-actions> — recent UI events the user performed since their last message, ordered oldest-first. For Slide Mode you'll see things like: <user-actions>
<action type="select-element" file="slides/slide-03.html" element="h1" text="Our Solution" />
<action type="reorder-slides" content-set="quarterly-review" from="slides/slide-05.html" to-index="2" />
<action type="switch-content-set" from="en-dark" to="quarterly-review" />
<action type="toggle-presenter-mode" enabled="true" />
</user-actions>
Use these to anchor edits ("the heading you clicked"), to confirm reorders the user already performed (manifest.json was already updated by drag), and to keep your mental model of which deck is in front of them.
Slide Mode has one vocabulary for "which object in the viewer". The same
shape — a ViewerAddress — is what a <viewer-locator> card points at, what
the capture action screenshots, what navigate-to jumps to, and what a
<viewer-context> selection reports back to you. Learn it once; it works
across all of them.
| Key | Half | Meaning |
|---|---|---|
| contentSet | coarse | Top-level directory acting as a switchable deck (en-dark, quarterly-review). |
| slide | coarse | 1-indexed slide number within the active deck. |
| file | coarse | Slide HTML filename (slides/slide-03.html) — names the slide by path. |
| selector | fine | A CSS selector resolved inside the rendered slide (.title, section.intro). |
Slide navigation is per-slide: slide and file are two ways to name the
same coarse target — prefer slide for natural references ("slide 3"), file
when you've just touched that exact path. A within-slide element selection
adds selector as the fine half. Use only the keys you need:
{"slide":3} names a whole slide; {"file":"slides/slide-03.html","selector":".title"}
names one element of it. When the user clicks an element, the Address: line
in <viewer-context> hands you a ready-made ViewerAddress — copy that JSON
straight back. (The navigate-to action's params is a coarse slide address.)
After creating or editing slides, embed <viewer-locator> cards inline so the user can jump to them in one click. The card's address attribute is a ViewerAddress — locators navigate the user to a slide, so use the coarse keys (contentSet, slide, file). Always emit fully-formed cards with real values — never placeholders.
<viewer-locator label="Open slide 3" address='{"file":"slides/slide-03.html"}' />
<viewer-locator label="Slide 3" address='{"slide":3}' />
<viewer-locator label="Open the dark deck" address='{"contentSet":"en-dark"}' />
<viewer-locator label="Dark deck, slide 1" address='{"contentSet":"en-dark","slide":1}' />
<viewer-locator label="Quarterly review cover" address='{"contentSet":"quarterly-review","file":"slides/slide-01.html"}' />
Rule of thumb: prefer slide for natural references, file when you've just touched that exact path, and always include contentSet when crossing decks.
The viewer exposes agent-invocable actions via POST $PNEUMA_API/api/viewer/action. For Slide Mode the action surface is intentionally small — most editing happens through file writes. Currently exposed:
navigate-to — jump the viewer to a specific slide. Useful after a multi-slide edit when you want to land the user on the most relevant slide. curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"navigate-to","params":{"file":"slides/slide-03.html"}}'
checkContentFit — measure slides against the fixed canvas and report overflow. Each slide is overflow: hidden, so overflowing content is silently clipped — a screenshot cannot reveal it, which is why this measurement check exists. Pass slides (a JSON array of 1-indexed numbers) to check specific slides, or omit it to check the whole deck. curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"checkContentFit"}'
Returns {"success":true,"data":{"allFit":<bool>,"results":[{"slide":<n>,"file":"...","fits":<bool>,"issues":["..."]}]}}. The viewer also runs this automatically after edits and notifies you whenever a slide newly overflows.
The scaffold action (below) is a separate viewer capability with its own confirmation flow.
The user is already watching a live preview of every edit you make — you do not need to prove the slide renders.
Hard rule: do NOT open an external browser, the chrome-devtools MCP, headless Chrome, or browser-use tooling to verify your work.
Why: those tools render the raw files *outside* the slide viewer. Each slide is an HTML *fragment* — no <html>, <head>, or <body> tags — and theme.css is injected by the viewer at render time. Open a slide file directly in a browser and you see an unstyled fragment. What an external browser shows is not what the user sees. The Pneuma viewer is the only faithful render.
When you genuinely need to *see* the rendered result for a "quality check → improve" loop, use the framework-level capture viewer action — it returns a PNG screenshot of the live viewer, exactly what the user sees:
# Full viewer
curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"capture"}'
# A specific region — pass a ViewerAddress; `selector` resolves inside the rendered slide
curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"capture","params":{"address":{"selector":".slide"}}}'
# A region on a non-active slide — the coarse `slide`/`file` key navigates first
curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"capture","params":{"address":{"slide":3,"selector":".title"}}}'
params.address is a ViewerAddress — omit it for a full-viewer shot. On success the response is {"success":true,"data":{"path":"<absolute .png path>","width":<n>,"height":<n>}}. Use your Read tool on that path to view the screenshot inline. The screenshot is native-resolution and shows exactly what export and print produce.
capture is for *visual* judgement — appearance, hierarchy, color, spacing. It does NOT reveal overflow: the slide canvas is overflow: hidden, so content past the edge is clipped and simply absent from the picture. For overflow, use the checkContentFit action (see "Viewer actions" above and "Layout Verification" below).
A content set is a top-level directory inside the workspace that holds one self-contained deck (manifest.json, theme.css, slides/, assets/). One workspace can hold many — the user flips between them with the set switcher, and the viewer reports the active one in <viewer-context content-set="…">.
How to read content sets:
file in <viewer-context> (e.g. content-set="quarterly-review" ⇒ files live under quarterly-review/slides/*.html).slides/slide-01.html (no prefix) lands at the workspace root and won't appear in any deck.manifest.json and theme.css from its directory rather than guessing.scaffold is a viewer action that creates a deck skeleton in one shot — placeholder slide files plus manifest.json — based on a structure spec. It's the fastest way to start a new deck or import a structure from source material. The action requires user confirmation in the browser before any file writes happen.
Parameters (from the manifest):
title (string, required) — Presentation title written into manifest.json.slides (string, required) — JSON array of {title, subtitle?} entries describing each slide.contentSet (string, optional) — Target content set name. Always pass this when creating a new deck, otherwise scaffold will overwrite the currently active set (e.g. a seed template).The scaffold clears slides/*.html and manifest.json inside the target content set before writing — that's why the user confirmation step exists.
curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{
"actionId":"scaffold",
"params":{
"title":"Q1 Review",
"contentSet":"quarterly-review",
"slides":"[{\"title\":\"Cover\"},{\"title\":\"Highlights\"},{\"title\":\"Problem Statement\"},{\"title\":\"Next Steps\"}]"
}
}'
After the user confirms, the placeholder files and manifest.json exist; you then fill content slide by slide (see "Phase 4: Fill Content"). If scaffold fails or the user cancels, fall back to writing each slides/slide-XX.html and updating manifest.json manually.
workspace/
manifest.json # Deck metadata + slide ordering (source of truth)
theme.css # Shared CSS theme (custom properties + base styles)
slides/
slide-01.html # Individual slide HTML fragments
slide-02.html
...
assets/ # Images, icons, media files
design_outline.md # (optional) Design specification for the deck
{
"title": "Deck Title",
"slides": [
{ "file": "slides/slide-01.html", "title": "Cover" },
{ "file": "slides/slide-02.html", "title": "Problem Statement" }
]
}
Always update manifest.json when adding, removing, or reordering slides.
Each slide is an HTML fragment (no <html>, <head>, <body> tags). The theme CSS is injected by the viewer automatically.
<div class="slide slide-title">
<h1>Slide Title</h1>
<p>Subtitle text</p>
</div>
Defines CSS custom properties and base layout classes. All slides share this theme. Modify theme.css for global style changes (colors, fonts, spacing).
Key custom properties: --color-bg, --color-fg, --color-primary, --color-secondary, --color-accent, --color-muted, --color-surface, --color-border, --font-sans, --font-mono, --slide-padding.
Base layout classes and when to use each:
| Class | Vertical Alignment | When to Use |
|---|---|---|
| .slide | Center | Default for most slides. Content is vertically centered — best when content doesn't fill the full height. |
| .slide-title | Center + text-center | Cover pages and section dividers with a centered title. |
| .slide-content | Top (flex-start) | Only for content-heavy slides where content fills most of the vertical space (e.g., long lists, dense grids). Do NOT use as a generic "content slide" class. |
| .slide-split | Center, horizontal | Two-column layouts with gap: 48px. |
| .slide-image | Center, no padding | Full-bleed image or media slides. |
Decision rule: If total content height < 70% of available height ({{slideHeight-128}}px), use .slide (centered). Only use .slide-content when content is tall enough that top-alignment looks intentional.
Default: use .slide (centered) and do NOT override justify-content. The entire content group (heading + body) centers vertically as a unit. This looks good for most slides — even with a heading, centered content is visually balanced.
Only for dense slides (content fills 70%+ of vertical space), use the heading-top + body-centered pattern:
<div class="slide" style="justify-content: flex-start;">
<h2>Heading</h2>
<p>Subtitle</p>
<div style="flex:1; display:flex; flex-direction:column; justify-content:center;">
<!-- dense content here -->
</div>
</div>
Do NOT use this pattern for light/medium content. A slide with heading + 3 cards + subtitle looks much better fully centered than with the heading pinned to the top and a giant gap above the cards.
When the user asks you to create a presentation from scratch or from source material:
Always create a new top-level directory (content set) for a new presentation task — never overwrite existing content sets or seed templates. Name the directory descriptively (e.g. quarterly-review/, product-launch/, tech-talk/). The viewer auto-discovers top-level directories as switchable content sets, so the user can flip between decks.
Importing external content also gets a new content set. When the user provides original material (uploaded files, pasted slides, a URL to scrape), create a new content set for it with its own manifest.json and theme.css. Don't dump imported files alongside an existing deck — that breaks set switching, comparison, and export.
All subsequent files (manifest.json, theme.css, slides/, assets/) go inside this new directory.
Before writing any slide HTML, create design_outline.md:
design_outline.md — reference {SKILL_PATH}/references/design-outline.md for the full template structureIf the user's workspace has no theme.css, create one. Read {SKILL_PATH}/references/design-guide.md for typography, color, spacing defaults, and design direction. Key decisions:
--font-sans for multilingual supportUse the scaffold viewer action to create the deck skeleton instantly. This is much faster than writing files one by one, and the user confirms the operation in the browser before it executes.
IMPORTANT: Always pass contentSet matching the directory name from Phase 0. Without it, scaffold will overwrite the currently active content set (e.g. a seed template) instead of creating files in your new directory.
manifest.json are created in the specified content set directory.Now the viewer shows the full deck structure. The user can browse all slides and see the outline taking shape.
> Fallback: If scaffold fails or the user cancels, create files manually (write each slides/slide-XX.html + update manifest.json).
Generate slide content in order, establishing visual identity early:
For each slide:
design_outline.mdslides/slide-XX.html (replacing the placeholder)After all slides are generated:
When the user asks to modify existing slides:
Edit tool for surgical changes, Write for full rewritesvar(--slide-padding)) → available area: {{slideWidth-128}}px × {{slideHeight-128}}pxWhen slides need capabilities beyond theme.css (charts, icons, advanced layouts):
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/umd/lucide.min.js"></script>) or inline SVG — never use emoji for professional icons<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>)var(--font-sans) / var(--font-mono) from theme.css. For custom web fonts, use CSS @import from Google Fonts. CJK requirement: --font-sans must include CJK system fonts ("PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei") before sans-serif — otherwise Chinese/Japanese/Korean text will be invisible in print/PDF exportWhen using external scripts, add them as <script> tags at the end of the slide fragment. The viewer's iframe sandbox allows scripts.
Do not use CSS transition, animation, @keyframes, motion transform, or JavaScript animation libraries. Static transforms like rotate(45deg) for decorative elements are fine.
The viewer's export and print features capture a single-frame snapshot of each slide — animations would never be seen and can cause blank or half-rendered captures.
Overflow is the #1 quality issue because slides are fixed-viewport — there's no scroll, so anything beyond {{slideHeight}}px is simply clipped and invisible. Reference {SKILL_PATH}/references/layout-patterns.md for detailed examples. Key rules:
font-size × line-height × number-of-linescontent + padding-top + padding-bottom + margin-top + margin-bottomvar(--color-primary), etc.)For deeper guidance on typography selection, color theory, and visual hierarchy, read {SKILL_PATH}/references/design-guide.md.
slides/slide-XX.html (zero-padded number, next available)manifest.json slides array at desired positionmanifest.jsonUpdate the slides array order in manifest.json. The viewer's drag-reorder also updates manifest.json automatically.
When the user wants to combine 2+ slides into one:
When a slide has too much content:
For one slide's visual changes: edit the slide HTML directly (colors, layout, spacing).
For deck-wide style changes: edit theme.css. All slides inherit changes immediately through CSS custom properties.
assets/Place image files in assets/ and reference them in HTML:
<img src="assets/product-screenshot.png" alt="Product screenshot" style="max-width: 100%; border-radius: 8px;" />
The viewer resolves assets/ paths relative to the workspace. The export endpoint uses <base href="/content/"> for correct resolution.
{{#imageGenEnabled}}
You have access to an AI image generation script at {SKILL_PATH}/scripts/generate_image.mjs. Use it proactively — don't wait for the user to ask. When the design outline's Visual field calls for a photo, illustration, or mood image, generate it.
When to generate:
gpt-image-2 modelWhen NOT to generate:
Model picking (default is gpt-image-2):
| Model | Pick when | Backends |
|---|---|---|
| gpt-image-2 (default) | Most slides. Especially strong at legible text, labels, logos, UI mockups, signage, diagrams with text, and precise mask-based edits. | fal.ai only |
| gemini-3-pro | Painterly backgrounds, watercolor illustrations, broad artistic reach, or when only OpenRouter is configured. | fal.ai or OpenRouter |
If the user only configured OPENROUTER_API_KEY, pass --model gemini-3-pro — gpt-image-2 is fal.ai-only and will error out otherwise.
Workflow:
gpt-image-2):cd {SKILL_PATH} && node scripts/generate_image.mjs \
"Your detailed prompt here" \
--aspect-ratio 16:9 \
--quality high \
--output-format png \
--output-dir <workspace>/assets \
--filename-prefix slide-03-hero
Slide-specific flag usage:
| Flag | Slide guidance |
|---|---|
| --aspect-ratio | 16:9 for full-width heroes, 1:1 for thumbnails, 4:3 for content images |
| --quality | high for anything the user will look at; drop to medium for draft passes. GPT-Image-2 only. |
| --output-format | png for illustrations (crisp text / transparent edges); jpeg for photos |
| --filename-prefix | Slide number + purpose, e.g. slide-05-hero |
| --output-dir | Always the workspace's assets/ directory |
| --model gemini-3-pro + --resolution 2K | Reach for these only on painterly/artistic backgrounds or full-bleed hero frames |
API reference: The script auto-routes between OpenRouter and fal.ai based on configured API keys. Outputs JSON to stdout with backend, model, files (local paths), and description.
Style consistency: When generating multiple images for a deck, maintain consistent style descriptors across all prompts (color palette, rendering style, mood). Reuse the same descriptor sentences verbatim.
{{/imageGenEnabled}}
When the user asks to improve, polish, refine, or critique a deck, follow the practices in {SKILL_PATH}/references/refinement.md. The available refinement approaches are:
| Request | Practice | What It Does |
|---------|----------|-------------|
| "polish this" / "clean it up" | Polish | Fix alignment, spacing, typography consistency, optical adjustments |
| "review this" / "critique" | Critique | Evaluate design effectiveness — hierarchy, consistency, emotional resonance, AI slop check |
| "simplify" / "too crowded" | Distill | Strip unnecessary complexity, one idea per slide, increase whitespace |
| "make it more impactful" / "too bland" | Bolder | Amplify scale, weight contrast, palette confidence, asymmetry |
| "tone it down" / "too busy" | Quieter | Reduce saturation, font weight, decorations. Refined, not boring. |
| "add more color" / "too gray" | Colorize | Strategic color introduction — tinted neutrals, accent data, section coding |
Process: Read the corresponding section in refinement.md, assess the current state, plan changes, then apply systematically across all affected slides. For deck-wide refinement, read all slides first (via manifest.json) to ensure consistent application.
The viewer includes an Inspiration Pool — a panel of curated style presets the user can browse when they need design direction. This is opt-in; most users will describe their vision directly.
When the user selects a preset, you receive a notification with:
<preset-theme-css> block containing the preset's theme.cssHow to use preset selections:
theme.css{SKILL_PATH}/references/design-guide.md to make informed adaptationsDo NOT mechanically copy-paste the preset CSS. The presets are starting points that should be interpreted through the lens of the user's content and purpose.
Before considering a slide "done", verify:
If you suspect overflow, mentally calculate total height:
<html>, <head>, or <body> tags — slide files are HTML fragments injected into the viewer's iframe.claude/ directory — managed by the runtime, edits get overwritten on next sessiontransition, animation, or motion transform — see Animation Prohibition aboveOverflow is the #1 quality issue — slides are fixed-viewport, so anything beyond {{slideWidth}}×{{slideHeight}}px is clipped and invisible. A screenshot cannot catch this: the canvas is overflow: hidden, so clipped content is simply absent from the picture — the slide looks fine, just with content silently missing. Overflow has to be *measured*, not eyeballed.
checkContentFit. When a slide is dense or you've reworked its layout, call the checkContentFit viewer action — it measures every element's geometry against the {{slideWidth}}×{{slideHeight}}px canvas and reports exactly what overflows and by how much: curl -s -X POST "$PNEUMA_API/api/viewer/action" \
-H 'Content-Type: application/json' \
-d '{"actionId":"checkContentFit"}'
Inspect data.results for any slide with fits: false and read its issues. (The viewer also runs this automatically after edits and notifies you when a slide newly overflows — so you often hear about overflow without asking.)
checkContentFit again. Attempt layout fixes at most once per slide — if issues persist, report to the user.For *visual* QA — does the slide look good, is the hierarchy right, are the colors balanced — use the capture action (see "Verifying your work"). Keep the two distinct: capture judges appearance, checkContentFit catches overflow.
For detailed guidance, read these files from the skill directory on demand:
{SKILL_PATH}/references/design-guide.md — Design direction: typography, color (OKLCH), visual hierarchy, spacing, layout templates, and AI image usage. Read when creating themes or making design decisions.{SKILL_PATH}/references/refinement.md — Refinement practices: critique, polish, distill, bolder, quieter, colorize. Read when the user wants to improve a completed deck.{SKILL_PATH}/references/design-outline.md — Full template for creating design outlines. Read during Phase 1.{SKILL_PATH}/references/layout-patterns.md — Common layout patterns with height calculations and examples. Read when dealing with overflow or complex layouts.Take pandazki/pneuma-slide from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.