Skip to content

Latest commit

 

History

History
102 lines (75 loc) · 8.87 KB

File metadata and controls

102 lines (75 loc) · 8.87 KB

CLAUDE.md

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.

What deck-forge is

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.

Directory map

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

Slide contract

  • A slide lives at src/slides/<deck-id>/SlideNN<Name>.tsx and 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 SectionLabel for the kicker, SlideNumber for the page number, SafeImg for 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 use hsl(var(--token)).
  • Keep copy out of layout where it helps (see _shared/data.ts) so slides retheme and edit cleanly.
  • Optional template layout tags (for reference, not enforced): title, section-header, two-column, three-up, data-viz, chart-focus, comparison, timeline, quote, blank.

Add a deck (exact, 4 steps — for id my-deck, title "My Deck")

  1. Slides: create src/slides/my-deck/ with one SlideNN<Name>.tsx per slide (default export), plus index.ts that does:
    import type { DeckSlide } from '@/types/slide'
    import Slide01Cover from './Slide01Cover'
    export const myDeck: DeckSlide[] = [{ component: Slide01Cover, name: 'Cover', template: 'title' } /* … */]
    Mirror src/slides/example-deck/index.ts. The array order is the deck order.
  2. Page: create src/pages/MyDeck.tsx mirroring ExampleDeck.tsx exactly — default-export a component returning <DeckShell title="My Deck" idPrefix="my-deck" slides={myDeck} />, importing myDeck from @/slides/my-deck.
  3. Register: in src/brand.config.ts, add to decks: { id: 'my-deck', title: 'My Deck', description: '…', path: '/d/my-deck' }. For private decks use an unguessable slug like /d/x7k2m9qp4wn3 and set unlisted: true. Keep every deck under the /d/ prefix so one SPA-fallback rule serves deep links on hard refresh.
  4. Route: in src/App.tsx, add import MyDeck from './pages/MyDeck' and an entry in deckPages: 'my-deck': MyDeck. App.tsx generates the <Route> from the registry + this map.

Add a slide

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

Theming contract

  • 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 --primary and --ring in sync with --brand-primary for the editor UI.
  • Dark slides: the .slide-dark block remaps surfaces/text to their *-dark counterparts. 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 #RRGGBB to "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 @import at the top of src/index.css AND fontFamily.sans / fontFamily.mono in tailwind.config.ts AND the body font-family in index.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.

Slash commands

  • /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.css tokens (hex→HSL, light + *-dark tints) and font @import/body; tailwind.config.ts fonts; brand.config.ts; index.html <title> + meta description; package.json name (kebab-case). Finally offers to delete the example deck (folder, page, registry entry, App.tsx import + deckPages entry) 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: creates SlideNN<Name>.tsx and wires its import + array entry in the deck index.ts.
  • /theme — re-skins via tokens in index.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, optional DECK_URL, CHROME_PATH. Output is ../<DECK_NAME>-YYYY-MM-DD.pdf unless 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).

Verify gate

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.

Keyboard (deck view)

Arrows navigate · ⇧G overview grid · ⇧N presenter notes · ⇧S sidebar · ⇧P present (fullscreen) · ⇧V presenter view. Notes persist to localStorage (no backend).

Mobile rendering

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.

See also

AGENTS.md for the tool-agnostic version of these conventions, and .cursor/rules/ for Cursor-specific guidance.