Agent-powered presentation template. Author slides as React components on a fixed 1920×1080 canvas that auto-scales to any screen — laptop, projector, or phone. Drive it from Cursor or Claude Code: run /setup once to brand it, then /new-deck and /new-slide to build presentations by describing them. Prefer to work by hand? It is just a Vite + React + TypeScript + Tailwind + shadcn/ui app — write the components yourself, no agent required.
- Slides as code — each slide is a plain React component; full version control, diffs, and reuse.
- Fixed canvas, auto-scale — design once at 1920×1080; the editor and presentation modes scale to fit any display.
- Token-driven theming — all color comes from CSS variables in
src/index.css. Rebrand the whole deck by editing a handful of tokens. - Editor + present modes — sidebar, overview grid, presenter notes (saved to
localStorage), presentation mode, and a separate presenter view. - Mobile reader — on phones, decks open in a swipeable, orientation-aware reader.
- PDF export — headless Chrome captures each slide at 1920×1080 and merges to a single PDF.
- Agent commands —
/setup,/new-deck,/new-slide,/theme,/export,/review-deckfor Cursor and Claude Code (works fine without them too).
# scaffold a fresh copy (degit), or clone the repo
npx degit your-org/deck-forge my-deck
cd my-deck
npm install
npm run devOpen http://localhost:8080, then visit the example deck at http://localhost:8080/d/example.
To produce a production build:
npm run buildOpen the project in Cursor or Claude Code and run:
/setup
(In Cursor without the slash command, just tell the agent: "set up this template for my brand.") It runs a short interview — name, tagline, primary color, optional accent, light/dark landing, logo, fonts, URL — then applies your answers across the repo:
src/index.css— converts your hex colors to HSL channels and sets the brand tokens (light tints and their*-darkvariants), plus the font@importand body font.tailwind.config.ts— thesans/monofont families.src/brand.config.ts— name, tagline, URL, logo path, landing theme.index.html—<title>and meta description.package.json— the kebab-casename.
It then offers to delete the example deck so you start clean. Afterward:
npm install # if you haven't already
npm run dev
npm run build # verify it still compilesNo agent? Edit those same files by hand — see Theming below.
With an agent, describe what you want:
/new-deck "a 5-slide investor update for Q3"
/new-slide "a comparison slide: us vs the status quo"
By hand, adding a deck with id my-deck is four steps:
-
Slides — create
src/slides/my-deck/with one file per slide (SlideNN<Name>.tsx, default export) and anindex.ts:import type { DeckSlide } from '@/types/slide' import Slide01Cover from './Slide01Cover' // ... export const myDeck: DeckSlide[] = [ { component: Slide01Cover, name: 'Cover', template: 'title' }, // ... ]
-
Page — create
src/pages/MyDeck.tsx(a deck page is ~3 lines):import { DeckShell } from '@/components/slides/DeckShell' import { myDeck } from '@/slides/my-deck' export default function MyDeck() { return <DeckShell title="My Deck" idPrefix="my-deck" slides={myDeck} /> }
-
Register — add an entry to the
decksarray insrc/brand.config.ts:{ id: 'my-deck', title: 'My Deck', description: '...', path: '/d/my-deck' }
For a private deck, use an unguessable slug (e.g.
path: '/d/x7k2m9qp4wn3') and optionallyunlisted: true. Keep every deck under the same/d/prefix so one SPA fallback serves deep links. -
Route — in
src/App.tsx, addimport MyDeck from './pages/MyDeck'and one entry to thedeckPagesmap:'my-deck': MyDeck.
Add a slide to an existing deck: create the SlideNN<Name>.tsx file, then add its import and an array entry at the right position in that deck's index.ts. Keep SlideNumber n/total consistent across the deck.
Each slide returns a <SlideFrame> and designs inside the 1920×1080 space using explicit pixel sizes and the type-* utilities (type-display, type-h1…type-body, type-caption, type-label, type-metric, type-mono). All color comes from tokens via hsl(var(--token)) — never hardcode hex or brand colors.
import { SlideFrame, SectionLabel, SlideNumber } from '@/slides/_shared/SlideChrome'
export default function Slide01Cover() {
return (
<SlideFrame theme="light">
<SlideNumber n={1} total={6} />
<div className="absolute left-20 top-1/2 -translate-y-1/2">
<SectionLabel>Overview</SectionLabel>
<h1 className="type-display mt-6" style={{ color: 'hsl(var(--text-primary))' }}>
Your headline
</h1>
</div>
</SlideFrame>
)
}SlideChrome also exports SafeImg (logos/portraits with graceful fallback) and getInitials. For flows, diagrams, and charts, draw SVG at fixed size with fill/stroke set to hsl(var(--token)).
deck-forge/
├─ index.html # <title> + meta (set by /setup)
├─ src/
│ ├─ brand.config.ts # identity + deck registry (DeckMeta)
│ ├─ App.tsx # registry-driven routing + deckPages map
│ ├─ index.css # THE theme token surface (HSL channels)
│ ├─ pages/ # one page per deck (+ Landing, NotFound)
│ ├─ slides/
│ │ ├─ _shared/ # SlideChrome, data, deck chrome
│ │ └─ example-deck/ # SlideNN*.tsx + index.ts (delete after /setup)
│ ├─ components/
│ │ ├─ slides/ # DeckShell, canvas, present/presenter, overview
│ │ ├─ mobile/ # orientation-aware mobile reader
│ │ ├─ layout/ # sidebar + toolbar
│ │ └─ ui/ # shadcn/ui primitives
│ └─ types/slide.ts # DeckSlide + TemplateType
├─ scripts/export-pdf.mjs # env-driven headless PDF export
├─ public/ # logo + static assets
└─ tailwind.config.ts # token aliases + font families
Colors live in src/index.css as raw HSL channels (format H S% L%, e.g. 221 83% 53%) so they compose with opacity: hsl(var(--brand-primary) / 0.4).
- Primary tokens:
--brand-primary,--brand-primary-light,--brand-primary-light-dark,--brand-accent. Then surfaces (--bg-page/-dark,--bg-surface*,--bg-code*), text (--text-primary/secondary/muted/faint/on-brandand*-dark), borders, and status colors. - Keep
--primaryand--ringin sync with--brand-primary(editor UI). - Dark slides (
theme="dark") are handled automatically — the.slide-darkblock remaps surfaces and text to their*-darkcounterparts. Just set the token values. - Given a hex color, convert it to HSL channels before storing (do not store hex).
/setupand/themedo this for you. - Fonts: change the Google Fonts
@importat the top ofsrc/index.css, the bodyfont-familyin the same file, andfontFamily.sans/fontFamily.monointailwind.config.ts. Default is Inter + JetBrains Mono.
Non-color identity (name, tagline, URL, logo path, landing theme) lives in src/brand.config.ts. The logo is a path under /public; an empty string renders the name as a wordmark.
| Key | Action |
|---|---|
← → ↑ ↓ |
Navigate slides |
Shift + G |
Overview grid |
Shift + N |
Presenter notes |
Shift + S |
Toggle sidebar |
Shift + P |
Present (fullscreen) |
Shift + V |
Presenter view |
Presenter notes persist to localStorage (no backend). On phones, the deck opens in a swipeable, orientation-aware reader.
Export any deck to a single 1920×1080 PDF (requires a local Chrome/Chromium):
npm run export:pdfConfigure with environment variables:
DECK_PATH=/d/my-deck SLIDE_COUNT=8 DECK_NAME=my-deck npm run export:pdfDECK_PATH— route to export (default/d/example)SLIDE_COUNT— number of slides (default6)DECK_NAME— output file basename (defaultdeck)DECK_URL— point at an already-running server (e.g.http://localhost:8080) to skip spawning ViteCHROME_PATH— path to your Chrome/Chromium binary
The script spawns Vite (unless DECK_URL is set), enters presentation mode, captures each slide, and writes <DECK_NAME>-YYYY-MM-DD.pdf (or pass an explicit output path as the first argument).
The CI workflow runs the same checks — run them locally before pushing:
npm run build && npm run lint && npx prettier --check .Formatting is Prettier (semi: false, singleQuote: true, printWidth: 120, trailingComma: es5). Run npm run format to fix. Tests: npm test.
npm run build produces a static SPA in dist/ — host it anywhere (any static host or CDN). Because routing is client-side, add a SPA fallback that serves index.html for unknown paths, otherwise /d/* deep links 404 on hard refresh. Keeping every deck under the single /d/ prefix means one fallback rule covers them all.
MIT — free to use, modify, and distribute.