Conventions contract for Claude Code working in deck-forge. Read this first every session, then mirror the real files it points at — never invent APIs.
An open-source, agent-powered presentation template. Vite + React + TypeScript + Tailwind + shadcn/ui single-page app. Each slide is a React component authored on a fixed 1920×1080 canvas, then auto-scaled to fit any screen (laptop, projector, phone). A deck is a folder of slide components. You drive it from Claude Code (or Cursor): run /setup once to brand it, then /new-deck and /new-slide to build presentations.
The template ships brand-neutral on purpose. Keep all examples generic. Do not reintroduce any specific company name, logo, or color into the template itself — /setup is the only path that applies a real brand, and it writes to tokens/config, never to slide code.
src/
brand.config.ts Identity (name/tagline/url/logo/landingTheme) + decks[] registry + DeckMeta type
App.tsx Registry-driven routing; deckPages map (deck id -> page component)
index.css THE theme token surface (HSL channels, type scale, dark-slide remap)
types/slide.ts DeckSlide + TemplateType
pages/
ExampleDeck.tsx A deck page = ~6 lines: <DeckShell title idPrefix slides/>
Landing.tsx, NotFound.tsx
components/
slides/DeckShell.tsx Editor/present/presenter/mobile shell (props: slides, title, idPrefix)
slides/ScaledSlide.tsx SLIDE_WIDTH/SLIDE_HEIGHT (1920×1080) + scale context
mobile/MobileDeckReader.tsx Orientation-aware swipeable phone reader
slides/
_shared/SlideChrome.tsx SlideFrame, SectionLabel, SlideNumber, SafeImg, getInitials
_shared/data.ts Example copy kept out of layout
example-deck/ index.ts (the deck array) + Slide01Cover.tsx … Slide06Close.tsx
tailwind.config.ts Token aliases + fontFamily.sans / .mono
scripts/export-pdf.mjs Env-driven headless PDF export
package.json Scripts: dev, build, lint, test, format, format:check, export:pdf
- A slide lives at
src/slides/<deck-id>/SlideNN<Name>.tsxand has a default export React component. - It returns
<SlideFrame theme="light" | "dark">as the root. Design inside the 1920×1080 space using explicit pixel sizes and/or the type utilities:type-display,type-h1,type-h2,type-h3,type-body-lg,type-body,type-caption,type-label,type-metric,type-mono. - All color comes from CSS tokens via
hsl(var(--token)). Never hardcode hex or brand colors in a slide. - Use
SectionLabelfor the kicker,SlideNumberfor the page number,SafeImgfor logos/portraits (graceful fallback —fallback="hide" | "initials" | "placeholder"). - SVG is the recommended way to draw flows, diagrams, and charts at fixed size (see
Slide04Flow.tsx). SVG fills/strokes also usehsl(var(--token)). - Keep copy out of layout where it helps (see
_shared/data.ts) so slides retheme and edit cleanly. - Optional
templatelayout tags (for reference, not enforced):title,section-header,two-column,three-up,data-viz,chart-focus,comparison,timeline,quote,blank.
- Slides: create
src/slides/my-deck/with oneSlideNN<Name>.tsxper slide (default export), plusindex.tsthat does:Mirrorimport type { DeckSlide } from '@/types/slide' import Slide01Cover from './Slide01Cover' export const myDeck: DeckSlide[] = [{ component: Slide01Cover, name: 'Cover', template: 'title' } /* … */]
src/slides/example-deck/index.ts. The array order is the deck order. - Page: create
src/pages/MyDeck.tsxmirroringExampleDeck.tsxexactly — default-export a component returning<DeckShell title="My Deck" idPrefix="my-deck" slides={myDeck} />, importingmyDeckfrom@/slides/my-deck. - Register: in
src/brand.config.ts, add todecks:{ id: 'my-deck', title: 'My Deck', description: '…', path: '/d/my-deck' }. For private decks use an unguessable slug like/d/x7k2m9qp4wn3and setunlisted: true. Keep every deck under the/d/prefix so one SPA-fallback rule serves deep links on hard refresh. - Route: in
src/App.tsx, addimport MyDeck from './pages/MyDeck'and an entry indeckPages:'my-deck': MyDeck. App.tsx generates the<Route>from the registry + this map.
Create the new SlideNN<Name>.tsx in the deck folder, then add its import + an array entry in that deck's index.ts at the right position. Keep SlideNumber n/total consistent across the deck (or omit total by editing all slides).
- The brand surface is
src/index.css. Colors are stored as raw HSL channels, format"H S% L%"(e.g.221 83% 53%), so they compose with opacity:hsl(var(--brand-primary) / 0.4). - Edit first:
--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+*-dark), borders, status. Keep--primaryand--ringin sync with--brand-primaryfor the editor UI. - Dark slides: the
.slide-darkblock remaps surfaces/text to their*-darkcounterparts. Set the token VALUES; the remap handles dark mode automatically when a slide uses<SlideFrame theme="dark">. - HEX → HSL channels is required when a user gives a hex color. Convert
#RRGGBBto"H S% L%"(H 0–360 deg, S/L integer percents), sanity-check the result, and never store hex in a token. - Fonts: change the Google Fonts
@importat the top ofsrc/index.cssANDfontFamily.sans/fontFamily.monointailwind.config.tsAND thebodyfont-familyinindex.css. Default is Inter + JetBrains Mono. - Non-color identity (name, tagline, url, logo path, landingTheme) lives in
src/brand.config.ts. The logo is a path under/public; an empty string renders the name as a wordmark.
Golden rule: never hardcode brand colors. Always reference a token via
hsl(var(--token)). This applies in JSX styles, Tailwind aliases (bg-brand-primary,text-text-muted, …), and SVG fills/strokes.
/setup— first-run brand interview. Asks: name, tagline, primary color (hex), optional accent (hex), landing background (dark/light), logo path or skip, fonts, canonical URL. Then applies across the repo:index.csstokens (hex→HSL, light +*-darktints) and font@import/body;tailwind.config.tsfonts;brand.config.ts;index.html<title>+ meta description;package.jsonname(kebab-case). Finally offers to delete the example deck (folder, page, registry entry, App.tsx import +deckPagesentry) while keeping the app building./new-deck— scaffolds a deck: the 4 add-a-deck steps above (folder + slides +index.ts, page, registry entry, route)./new-slide— adds or rewrites one slide: createsSlideNN<Name>.tsxand wires its import + array entry in the deckindex.ts./theme— re-skins via tokens inindex.css(and fonts); does the hex→HSL conversion./export— runs the PDF export (scripts/export-pdf.mjs). Configure with env:DECK_PATH,SLIDE_COUNT,DECK_NAME, optionalDECK_URL,CHROME_PATH. Output is../<DECK_NAME>-YYYY-MM-DD.pdfunless you pass an explicit path./review-deck— reviews a deck for the conventions in this file (tokens-only color, slide contract, numbering, build/lint/format).
After generating or editing code, run (and tell the user to run):
npm run build && npm run lint && npx prettier --check .
Formatting is Prettier with semi: false, singleQuote: true, printWidth: 120, trailingComma: es5. Use npm run format to fix. CI runs prettier check, lint, build, and test. First time, run npm install then npm run dev.
Arrows navigate · ⇧G overview grid · ⇧N presenter notes · ⇧S sidebar · ⇧P present (fullscreen) · ⇧V presenter view. Notes persist to localStorage (no backend).
On phones, DeckShell renders MobileDeckReader instead of the editor. A 1920×1080 slide scaled to a ~390px portrait phone is unreadable, so in portrait the reader rotates the slide 90° and scales against the long axis (roughly doubling text size); rotating to landscape renders it upright and larger. It measures its own container via ResizeObserver (not window.innerWidth), so it stays correct across rotation, browser-chrome show/hide, and split-screen. Swipe (or arrow keys on a tablet) to navigate; dots jump to a slide.
AGENTS.md for the tool-agnostic version of these conventions, and .cursor/rules/ for Cursor-specific guidance.