Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

deck-forge

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.

Features

  • 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-deck for Cursor and Claude Code (works fine without them too).

Quick start

# scaffold a fresh copy (degit), or clone the repo
npx degit your-org/deck-forge my-deck
cd my-deck

npm install
npm run dev

Open http://localhost:8080, then visit the example deck at http://localhost:8080/d/example.

To produce a production build:

npm run build

First run: brand it

Open 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 *-dark variants), plus the font @import and body font.
  • tailwind.config.ts — the sans / mono font families.
  • src/brand.config.ts — name, tagline, URL, logo path, landing theme.
  • index.html — <title> and meta description.
  • package.json — the kebab-case name.

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 compiles

No agent? Edit those same files by hand — see Theming below.

Build a deck

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:

  1. Slides — create src/slides/my-deck/ with one file per slide (SlideNN<Name>.tsx, default export) and an index.ts:

    import type { DeckSlide } from '@/types/slide'
    import Slide01Cover from './Slide01Cover'
    // ...
    
    export const myDeck: DeckSlide[] = [
      { component: Slide01Cover, name: 'Cover', template: 'title' },
      // ...
    ]
  2. 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} />
    }
  3. Register — add an entry to the decks array in src/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 optionally unlisted: true. Keep every deck under the same /d/ prefix so one SPA fallback serves deep links.

  4. Route — in src/App.tsx, add import MyDeck from './pages/MyDeck' and one entry to the deckPages map: '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.

Writing a slide

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)).

Directory map

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

Theming

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-brand and *-dark), borders, and status colors.
  • Keep --primary and --ring in sync with --brand-primary (editor UI).
  • Dark slides (theme="dark") are handled automatically — the .slide-dark block remaps surfaces and text to their *-dark counterparts. Just set the token values.
  • Given a hex color, convert it to HSL channels before storing (do not store hex). /setup and /theme do this for you.
  • Fonts: change the Google Fonts @import at the top of src/index.css, the body font-family in the same file, and fontFamily.sans / fontFamily.mono in tailwind.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.

Keyboard shortcuts (deck view)

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.

PDF export

Export any deck to a single 1920×1080 PDF (requires a local Chrome/Chromium):

npm run export:pdf

Configure with environment variables:

DECK_PATH=/d/my-deck SLIDE_COUNT=8 DECK_NAME=my-deck npm run export:pdf
  • DECK_PATH — route to export (default /d/example)
  • SLIDE_COUNT — number of slides (default 6)
  • DECK_NAME — output file basename (default deck)
  • DECK_URL — point at an already-running server (e.g. http://localhost:8080) to skip spawning Vite
  • CHROME_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).

Verify

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.

Deploy

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.

License

MIT — free to use, modify, and distribute.

About

Agent-powered presentation template: author slides as React components on a fixed canvas and drive it from Cursor / Claude Code

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages