- Runtime: Bun (not Node.js, not npm scripts)
- Language: TypeScript strict mode everywhere, no
any, useunknown - CLI: Commander.js (not yargs, not meow)
- Templating: Nunjucks (not Handlebars, not EJS, not JSX for templates)
- Rendering: Puppeteer (not node-canvas, not sharp, not playwright)
- Validation: Zod for all content, deck, and theme schemas
- ZIP: archiver (not adm-zip, not jszip, not fflate)
- Web UI: Vite + React 18 (not Next.js, not Remix, not Astro)
- Web UI state: Zustand, cardStore.ts (single card) + deckStore.ts (deck)
- Web UI DnD: dnd-kit (not react-beautiful-dnd)
- Web UI styling: Tailwind CSS utility classes only (no CSS modules, no styled-components)
- Web UI icons: lucide-react, import individually (no barrel:
import { X } from 'lucide-react') - CLI prompts: @clack/prompts (not inquirer)
- CLI logger: chalk (not picocolors)
- NEVER install a package without asking the user first
- NEVER use a UI component library: no shadcn, Radix, MUI, Ant Design, PrimeNG, etc. All Web UI components are built from scratch with Tailwind
- NEVER hardcode colors in template CSS: every color is a CSS custom property injected from the theme JSON at :root level
- NEVER write to a file unless the user explicitly triggers it (--output flag, Save button, or Ctrl+S)
- NEVER put business logic inside CLI command files. Commands are thin: parse args → validate with Zod → call renderer → log result
- outputs/ is gitignored: never treat it as a source of truth
- React components MUST NOT make direct filesystem calls: all FS goes through Bun server routes (/export, /export-deck, /themes, etc.)
- Facebook carousel decks should use facebook-square (1080×1080). Warn (don't block) if the user picks a non-square size for a deck.
- NEVER write an em dash, en dash or ellipsis character anywhere: code, comments,
docs, commit messages, CLI strings, UI strings. A plain ASCII hyphen is fine.
content/, decks/ and site/samples/ are exempt, since that text is rendered
into the PNGs.
A single line can opt out with an
emdash-okmarker comment. Enforced by scripts/pre-commit; install it withln -sf ../../scripts/pre-commit .git/hooks/pre-commit
"type": "deck"at root → deck file (use DeckContent schema, deck mode in studio)"type": "card"or no type field → single card (use CardContent schema, card mode)- validate, preview, studio, and slides commands all auto-detect from this field
- TypeScript: camelCase.ts
- React: PascalCase.tsx
- Templates: template.njk (in named folder under templates/)
- Themes: kebab-case.json (in themes/)
- Card content: kebab-case.json (in content/)
- Deck content: kebab-case.json (in decks/)
- Block partials: .njk (in templates/_blocks/)
- Schema authority:
src/cli/utils/validator.tsholds SIZES and the Block/Card/Deck/Theme Zod schemas. Read it before writing any docs, sample JSON, or type definition. - SIZES is duplicated in
studio/src/types/index.ts(browser) and counted insrc/__tests__/validator.test.ts: update all 3 when adding a size. - Block schemas are NOT uniform:
headline/blockquoteuseparts: Part[];textusescontent: string;bullet-list/calloutuseitems: LabeledItem[].
src/: CLI + renderer (Bun + Nunjucks + Puppeteer)studio/: bundled WYSIWYG editor (wasweb/; PRD.md comments may still say web/)site/: separate landing + MDX docs SPA (React Router v7, deployed via its own nginx Dockerfile)templates/_base.css: shared responsive base, injected into every template renderthemes/_schema.json: reference; actual validator is Zod in src/
bun run release:patch | release:minor | release:major: runs typecheck + tests (preversion hook), bumpspackage.json, commitsvX.Y.Z, creates an annotated tag, then pushes commit + tag via--follow-tags(postversion hook).- The tag push triggers
.github/workflows/release.yml, which builds binaries, publishes the GitHub Release, and bumps the Homebrew formula. Do NOT hand-craft tags or push bumps without these scripts unless the workflow is broken.
bun quoteforge generate <file> --output <path>expects a FILE path (not directory); errors withEISDIRotherwise.lucide-react@1.8.0is current latest but dropped brand icons (Github, etc.): use inline Simple Icons SVGs for brand marks.- Puppeteer + Google Fonts
opszaxis syntax (e.g.9..144) is unreliable; prefer plainwght@400;700URLs in theme JSON. - Block-level
blockquoteis capped at 28px in_base.css; use aheadlineblock for hero-size quote cards. - In React, do not call
lazy(loader)insideuseMemo: it caches stale trees across param changes. Keeplazy()at module level.