UW Slides Plugin
Create branded presentations with any LLM using a fragment-based architecture. Ships with University of Washington and CloudBank themes; extensible to additional brands.
Features
- LLM-friendly: Slides, briefs and the design docs are plain files any LLM can read and write. The skills install as an Agent Plugin in Claude Code, Codex and GitHub Copilot CLI, and as plain skills in OpenCode and other tools that read a skills directory
- Fragment-based: Each slide is self-contained HTML with inline scoped styles
- Two-pass workflow: Pass 1 builds content and layout; pass 2 adds visuals
- Brief-driven:
deck.ymlholds the slide order; one brief per slide (slides/<id>.md) holds the message, layout, notes, owner and status. The HTML is generated from the briefs and can be regenerated safely (locked slides are never overwritten) - UW brand compliant: Design system documented in
design-systems/uw-brand/DESIGN.md - WCAG 2.1 AA accessible: Built-in accessibility requirements
- Simple build: Bash scripts;
python3(standard library only, no pip) powerstools/deckparse.py(validation, slide status,OUTLINE.md) andpublish.sh. Builds still work without it, with the slide list read unvalidated - Schema:
deck.ymland the briefs are defined inreferences/slide-schema.md.SLIDES.md(legacy) still builds, with a deprecation warning - Layout library: 12 brand-neutral slide layouts (
templates/layouts/, documented inreferences/layouts.md) that render in both the UW and CloudBank brands
Installation
The repository is a single Agent Plugins v1.0.0 plugin named uw-slides. Its root holds the portable plugin.json and a .claude-plugin/plugin.json with the same metadata, because Claude Code doesn't read the portable manifest. It also holds a marketplace called uw-slide-deck for each client that supports one. There are no other dependencies.
Claude Code
claude plugin marketplace add aaarendt/uw-slide-deck-plugin
claude plugin install uw-slides@uw-slide-deck
Inside Claude Code the same commands are /plugin marketplace add aaarendt/uw-slide-deck-plugin and /plugin install uw-slides@uw-slide-deck. Skills are namespaced by plugin, for example /uw-slides:new-deck.
Codex
codex plugin marketplace add aaarendt/uw-slide-deck-plugin
codex plugin add uw-slides@uw-slide-deck
Codex reads the portable plugin.json and the marketplace in .agents/plugins/marketplace.json. Ask for a skill by name, for example "use the uw-slides new-deck skill to scaffold my-talk".
GitHub Copilot CLI
copilot plugin marketplace add aaarendt/uw-slide-deck-plugin
copilot plugin install uw-slides@uw-slide-deck
Copilot reads the portable plugin.json and the marketplace in .claude-plugin/marketplace.json. This follows GitHub's plugin reference but hasn't been tested yet.
OpenCode and other skills directories
OpenCode discovers skills, not plugins. Clone the repository, link each skill into a skills directory, and point UW_SLIDES_HOME at the checkout:
git clone https://github.com/aaarendt/uw-slide-deck-plugin ~/src/uw-slide-deck-plugin
mkdir -p ~/.config/opencode/skills
for s in ~/src/uw-slide-deck-plugin/skills/*/; do ln -s "${s%/}" ~/.config/opencode/skills/; done
export UW_SLIDES_HOME=~/src/uw-slide-deck-plugin # add to your shell profile
The same loop works for ~/.agents/skills or any other skills directory. Skills keep their plain names there (new-deck, not uw-slides:new-deck).
Local checkout (development)
The older symlink install still works: ln -s /path/to/uw-slide-deck-plugin ~/.claude/plugins/local/uw-slides. To work on the plugin itself, set UW_SLIDES_HOME to your checkout. It takes precedence over an installed copy.
How skills find the plugin
An installed plugin lives in a cache that differs by client, for example ~/.claude/plugins/cache/... or ~/.codex/plugins/cache/.... Each skill finds the plugin's templates, design systems and tools by checking, in order:
$UW_SLIDES_HOME- the plugin the skill was loaded from (two directories above its
SKILL.md) ~/.claude/plugins/local/uw-slides
New decks vendor tools/deckparse.py, tools/lint.py and tools/resolve-catalog.py, so building a deck never needs the plugin.
Quick Start
# Scaffold a new presentation
/uw-slides:new-deck my-presentation
# Plan your deck from a topic (writes deck.yml and slides/*.md, stops for your approval):
cd my-presentation
/uw-slides:plan-deck "Cloud computing for researchers" --duration=45
# After approving the plan, generate the slides:
/uw-slides:generate-slides
# Download the reveal.js runtime (once per deck):
./setup-reveal.sh
# Build and preview
./build.sh
open build/index.html
How It Works
Directory Structure
presentation-name/
├── shared/
│ ├── header.html # Design tokens, fonts, base styles
│ └── footer.html # reveal.js initialisation, closing tags
├── content/ # Pass-1 slide fragments (text, layout, color)
├── content-with-visuals/ # Pass-2 slide fragments (adds photos, diagrams)
├── assets/
│ ├── images/
│ ├── diagrams/
│ └── reveal/ # reveal.js runtime (downloaded by setup-reveal.sh)
├── tools/
│ └── deckparse.py # Vendored parser/validator (stdlib only)
├── .uw-slides.json # Plugin version stamp and brand
├── deck.yml # Deck metadata, objectives and slide order
├── slides/ # One brief per slide (slides/<id>.md)
├── OUTLINE.md # Generated read-only overview (./outline.sh)
├── VISUALS.md # Pass-2 additions spec — photos, diagrams, icons
├── build.sh # Pass-1 build: content/ → build/index.html
├── build-visuals.sh # Pass-2 build: content-with-visuals/ → build/index-with-visuals.html
├── outline.sh # Regenerates OUTLINE.md
├── setup-reveal.sh # Downloads reveal.js assets (run once after scaffold)
├── export.sh # PDF export via Playwright (optional)
└── publish.sh # Pass-3: inline images into one portable file
deck.yml and the Briefs Are the Source of Truth
deck.yml lists the slides in order; the build script concatenates content/<id>.html in that order, so moving a line reorders the built presentation. Each slide has a brief, slides/<id>.md: a short front matter block (layout, owner, status, duration, locked) and sections for the key message, talking points, source and speaker notes. Format and rules: references/slide-schema.md.
A slide ID is lowercase letters, digits and single hyphens (03-approach). IDs must be unique, and each needs a brief and, once generated, a fragment content/<id>.html.
content/*.html is generated from the briefs and records which version of the brief it came from (data-brief-hash). /uw-slides:generate-slides skips slides with locked: true (hand-edited HTML) and asks before replacing a slide whose brief changed. To check the state of every slide: python3 tools/deckparse.py status <deck-dir>. ./outline.sh writes OUTLINE.md, a generated overview that is never edited by hand.
Legacy decks with only a SLIDES.md still build (headings ## <kebab-case-id> give the order) and print a deprecation warning; the fallback is planned for removal in v0.3.0. See Migrating from SLIDES.md below.
Fragment Pattern
Each slide is a complete HTML <section> saved as content/<slide-id>.html:
<section data-slide="03-approach"
aria-label="Slide 3: Our Approach"
class="slide">
<div class="content">
<h1>Our Approach</h1>
<p>Content goes here...</p>
</div>
<style>
section[data-slide="03-approach"] {
background: var(--uw-spirit-purple);
padding: var(--space-20);
}
section[data-slide="03-approach"] h1 {
font-family: var(--font-display);
color: var(--uw-spirit-gold);
}
</style>
</section>
The section[data-slide="..."] selector scopes all styles to that slide — no class name collisions, no cascade issues.
Build Process
./build.sh # Pass 1 → build/index.html
./build-visuals.sh # Pass 2 → build/index-with-visuals.html
Both scripts accept an optional path argument: ./build.sh /path/to/deck
By default a slide listed in deck.yml with no fragment yet is skipped with a warning, so you can build while the deck is in progress. Use --strict (./build.sh --strict) to fail instead, for example in CI. An invalid deck.yml (such as a duplicate ID) always fails the build, and fragments in content/ that deck.yml doesn't list are reported as warnings.
The scripts read deck.yml with tools/deckparse.py (needs python3). New decks ship a vendored copy in <deck>/tools/deckparse.py (put there by /uw-slides:new-deck); the scripts also check $UW_SLIDES_HOME/tools/ and ~/.claude/plugins/local/uw-slides/tools/ as fallbacks. If none is found they still read the plain slides: list, without validation.
Two-Pass Workflow
Pass 1 — content and structure:
- Run
/uw-slides:plan-deck(or editdeck.ymland the briefs inslides/by hand). The planner drafts learning objectives, picks a layout per slide, fills the slots and stops for your approval before any HTML is written. Run it again later to add, remove, merge or split slides or to change the length; it leaves locked, owned anddoneslides alone - Run
/uw-slides:generate-slides→content/*.htmlfragments - Run
./build.sh→build/index.html - The deck should look complete at this stage — no image placeholders
Pass 2 — visual additions:
- Specify photographs, diagrams, and icons in
VISUALS.md - Run
/uw-slides:apply-visuals→ writes tocontent-with-visuals/ - Run
./build-visuals.sh→build/index-with-visuals.html
Pass 2 is additive. Slides not listed in VISUALS.md are served unchanged from content/. Pass 2 never modifies pass-1 files.
Runtime (reveal.js)
New decks use reveal.js 6.0.2 as the presentation runtime. Run setup-reveal.sh once after scaffolding to download the assets:
./setup-reveal.sh # downloads assets/reveal/ (reveal.min.js, reveal.min.css, plugin/notes/notes.js)
git add assets/reveal/ # track so the deck is offline-capable
./build.sh
open build/index.html
Keyboard shortcuts:
| Key | Action |
|---|---|
→ / Space | Next slide |
← | Previous slide |
S | Presenter view (current slide, next slide, speaker notes, timer) |
Esc | Slide overview (bird's-eye grid) |
F | Fullscreen |
B | Blackout screen |
? | All keyboard shortcuts |
Speaker notes — add a ## Notes section to a slide's brief; it renders as <aside class="notes"> and appears only in presenter view, never on the main slide.
PDF export — requires Playwright (optional):
./export.sh # writes build/deck.pdf, one page per slide
Design System
design-systems/uw-brand/DESIGN.md is the authoritative UW brand reference (~5,500 words) covering color tokens, typography, layout patterns, component examples, accessibility requirements, and a pre-commit checklist.
Key tokens available in all UW slides (injected via shared/header.html):
--uw-spirit-purple: #4b2e83 /* primary brand */
--uw-spirit-gold: #ffc700 /* accent */
--uw-husky-purple: #32006e /* darker variant */
--font-display: Encode Sans
--font-body: Open Sans
--space-4: 32px /* 8px base scale */
--space-8: 64px
--space-16: 128px
--space-20: 160px
For the complete token set (semantic color aliases, weight/leading/tracking tokens, full type scale), see design-systems/uw-brand/colors_and_type.css.
Themes
The plugin ships with two brands under design-systems/:
uw-brand/— University of Washington (Husky Purple, Spirit Gold, Encode Sans)cloudbank-brand/— NSF CloudBank (Deep Navy, Signal Blue, Nunito + Open Sans)
Select a brand when scaffolding or reviewing:
/uw-slides:new-deck my-presentation --brand=cloudbank
/uw-slides:design-review --brand=cloudbank
/uw-slides:accessibility-check --brand=cloudbank
Omitting --brand defaults to uw.
Adding a New Brand
- Create
design-systems/<name>-brand/with:DESIGN.md— brand guidelinescolors_and_type.css— CSS tokensshared/header.html— design tokens, font loading, and base styles for this brand (copy from an existing brand and modify)
- Use
--brand=<name>with any skill command.
Skills
| Skill | Purpose |
|---|---|
new-deck | Scaffold a new presentation directory |
plan-deck | Plan a deck from a topic: objectives, layouts, slot content, deck.yml + briefs, approval gate before any HTML; also re-plans an existing deck |
generate-slides | Generate content/*.html from the briefs using the layout library; skips locked slides, asks before overwriting stale ones |
apply-visuals | Pass 2 — add photos, diagrams, and icons per VISUALS.md |
accessibility-check | WCAG 2.1 AA validation |
design-review | Brand compliance review |
extract-to-markdown | Convert an existing HTML deck to deck.yml + slides/*.md briefs; --migrate migrates a legacy SLIDES.md deck in place |
update-deck | Diff and update managed files (build scripts, shared templates, tools/deckparse.py) against the current plugin; never touches slides, content, or assets |
Migrating from SLIDES.md
Decks built before v0.2.0 use SLIDES.md as the slide-ordering source. The build scripts still accept SLIDES.md (with a deprecation warning) but support is scheduled for removal in v0.3.0.
One-shot migration (from inside the deck directory):
/uw-slides:extract-to-markdown --migrate
This reads SLIDES.md, writes deck.yml + one slides/<id>.md brief per slide,
stamps .uw-slides.json, validates the output with deckparse.py, and removes
SLIDES.md (it stays in git history). Existing HTML fragments in content/ are not
touched.
For step-by-step instructions, verification, and the manual path: see
references/migration-guide.md in the plugin directory.
Customization
Modifying Design Tokens
Edit shared/header.html in your presentation to change colors, spacing, or typography for that deck only. These changes don't affect other presentations or the plugin defaults.
Global Defaults
To change defaults for all future presentations, edit the plugin's files. They are copied when running /uw-slides:new-deck:
design-systems/uw-brand/shared/header.html # UW header (tokens, fonts, base styles)
design-systems/cloudbank-brand/shared/header.html # CloudBank header
templates/shared/footer.html # reveal.js initialisation (shared by all brands)
templates/setup-reveal.sh # reveal.js asset downloader
Build scripts (templates/*.sh), deck.yml, slides/, VISUALS.md and AGENTS.md templates are copied the same way. Existing decks are not updated automatically — run /uw-slides:update-deck inside a deck to pull in changes from the current plugin version.
Troubleshooting
Slides don't render
- Check that HTML files exist in
content/with filenames matching the IDs in theslides:list ofdeck.yml(without.html);python3 tools/deckparse.py status .shows which slides are missing - Each
<section>must haveclass="slide" - Confirm
./build.shran without errors
Wrong slide order
- Order is determined by the
slides:list indeck.yml(top to bottom), not by filename. Editdeck.ymlto reorder.
Images not showing
- Image paths are relative to
build/index.html. From there, go up one level to reach the deck root:<img src="../assets/images/photo.jpg" alt="...">
Fonts not loading
- Font paths in
shared/header.htmlpoint to../assets/fonts/(relative tobuild/). For UW brand decks, Encode Sans fonts are copied toassets/fonts/by thenew-deckskill. If fonts are missing, re-copy fromdesign-systems/uw-brand/fonts/. CloudBank decks load fonts via Google Fonts CDN and don't require local font files.
Build script fails
- Confirm
shared/header.htmlandshared/footer.htmlexist - Confirm
deck.ymlhas aslides:list (the error message gives the file and line) - Check permissions:
chmod +x build.sh build-visuals.sh outline.sh
Presenter view not working (press S)
- Run
./setup-reveal.shif you haven't already — it downloadsplugin/notes/notes.jswhich is required for presenter view - If the deck was scaffolded before v0.2.0, run
/uw-slides:update-deckto pull in the updatedfooter.htmlthat registers the notes plugin, then re-run./setup-reveal.shand rebuild
reveal.js assets not found
- Run
./setup-reveal.shfrom inside the deck directory; it createsassets/reveal/ - If offline, download
reveal.min.js,reveal.min.cssandplugin/notes/notes.jsmanually fromhttps://cdn.jsdelivr.net/npm/reveal.js@6.0.2/and place them inassets/reveal/
License
MIT