|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for Claude Code when working in this repo. |
| 4 | + |
| 5 | +## Commands |
| 6 | + |
| 7 | +- `npm run dev` — Vite dev server on port 3000 |
| 8 | +- `npm run build` — `tsc --noEmit && vite build` → `dist/` |
| 9 | +- `npm start` — `vite preview` on port 3000 (serves the built `dist/`) |
| 10 | +- `npm run lint` — ESLint over `src/**/*.{ts,tsx}` |
| 11 | +- `npm test` — Jest (ts-jest, jsdom). Run one test with `npx jest path/to/file.test.ts` or `-t 'name'` |
| 12 | +- `npm run remotion-studio` — Remotion Studio pointed at `src/remotion/index.ts` for previewing the composition in isolation |
| 13 | + |
| 14 | +TypeScript path alias: `@/*` → `src/*`. |
| 15 | + |
| 16 | +## Architecture |
| 17 | + |
| 18 | +**CodeBit** is a Vite + React SPA that turns markdown-defined code snippets into animated videos. It is **fully client-side**: no backend, no database, no accounts, no billing, no SSR. Snippets live in `localStorage`; videos are rendered in the browser via `@remotion/web-renderer` (WebCodecs) and downloaded as MP4. Routing is handled by `@tanstack/react-router` (code-based tree in `src/routes.tsx`). |
| 19 | + |
| 20 | +### The snippet → video pipeline |
| 21 | + |
| 22 | +The markdown snippet format is the central data model. Everything else is a transform over it: |
| 23 | + |
| 24 | +1. **Authoring.** A snippet is markdown with YAML front matter (background, font, watermark, zooming, highlightTheme…) plus fenced code blocks separated by `---`. Each fenced block is one "step" — the diff between consecutive steps drives the typing/replace animation. Templates: the `initialMarkdown()` inside `src/lib/snippet-storage.ts` and `src/lib/tutorial-snippet.ts` / `src/lib/landing-page-snippet.ts`. |
| 25 | +2. **Parsing.** `src/lib/code-steps-utils.tsx` (`parseSnippetMardown`) parses front matter with `front-matter`, validates it against `metadataSchema` (Zod, in `src/lib/types.ts`), falls back to defaults for invalid theme/font, and lexes the body with `marked` to produce `Steps` + `Warning[]`. Parse warnings surface in the editor UI via `warning-list.tsx`. |
| 26 | +3. **Composition data.** `src/components/remotion/composition-data.ts` turns steps into per-frame data consumed by the Remotion composition (`code-composition.tsx`), including timings. `compositionDurationInFrames` is the source of truth for video length. |
| 27 | +4. **Preview vs render.** The same Remotion composition is used two ways, both client-side: |
| 28 | + - **Preview** via `@remotion/player` in `src/components/snippet-player.tsx`. |
| 29 | + - **MP4 export** via `renderMediaOnWeb()` in `src/components/generate-button.tsx` — produces a Blob via WebCodecs, triggers a download, no network call. `licenseKey: 'free-license'` is passed. |
| 30 | + |
| 31 | +### Web-renderer caveats |
| 32 | + |
| 33 | +`@remotion/web-renderer` (experimental) has strict CSS limits. Don't introduce: |
| 34 | + |
| 35 | +- `radial-gradient` (use `<canvas>` — see `gradient-canvas.tsx` + `gradients.ts:drawGradientOnCanvas`) |
| 36 | +- `z-index` (use DOM order instead — the window chrome in `code-composition.tsx` relies on this) |
| 37 | +- `clip-path`, `backdrop-filter`, `mix-blend-mode` |
| 38 | +- Safari `filter` is broken (pre-accepted) |
| 39 | + |
| 40 | +Canvas2D `fontStretch` setter rejects the `"100%"` value modern browsers return for `font-stretch: normal`. The shim in `src/lib/canvas-shim.ts` (and an inline `<script>` in `index.html` that runs at page parse time) monkey-patches both `CanvasRenderingContext2D.prototype` and `OffscreenCanvasRenderingContext2D.prototype` to silently map any percentage to `'normal'`. |
| 41 | + |
| 42 | +### Persistence |
| 43 | + |
| 44 | +All snippet state lives in `localStorage` under key `codevideo:snippets:v1`. API in `src/lib/snippet-storage.ts`: |
| 45 | + |
| 46 | +```ts |
| 47 | +listSnippets(): StoredSnippet[] |
| 48 | +getSnippet(slug): StoredSnippet | null |
| 49 | +createSnippet(): StoredSnippet // random 6-char hex slug, seeded with initialMarkdown |
| 50 | +createTutorialSnippet(): StoredSnippet |
| 51 | +updateSnippet(slug, content): void |
| 52 | +deleteSnippet(slug): void |
| 53 | +``` |
| 54 | + |
| 55 | +Preview thumbnails in the snippet list are derived on the fly from the last fenced code block (`derivePreview()` in `snippet-list-item.tsx`) — there is no cached preview column. |
| 56 | + |
| 57 | +### App routes |
| 58 | + |
| 59 | +Routes are declared in `src/routes.tsx` and mounted from `src/main.tsx`: |
| 60 | + |
| 61 | +- `/` — `src/pages/landing-page.tsx` (plays `landing-page-snippet.ts` via `src/components/landing-player.tsx`) |
| 62 | +- `/my` — redirects to `/my/snippets` via `beforeLoad` |
| 63 | +- `/my/snippets` — `src/pages/snippets-page.tsx` (reads from localStorage) |
| 64 | +- `/my/snippets/$snippetSlug` — `src/pages/snippet-editor-page.tsx` (param via `useParams({ from: '/my/snippets/$snippetSlug' })`) |
| 65 | +- `/help` — `src/pages/help-page.tsx` (plain TSX with Tailwind `prose`) |
| 66 | + |
| 67 | +`TopBar` (`src/components/top-bar.tsx`) is rendered inside `MyShell` (`src/components/my-shell.tsx`) by the `/my/*` pages. The help page renders TopBar itself. The landing page uses its own `LandingPageMenu`. |
| 68 | + |
| 69 | +### Theme |
| 70 | + |
| 71 | +The app is **dark-only**. `index.html` sets `<html class="dark">` directly — there is no theme provider. The body has an unconditional `bg-gradient-to-br from-slate-950 to-slate-800` (also applied in `index.html`). Hardcode dark values where needed: `code-editor.tsx` pins Monaco's `github-dark`; `snippet-list.tsx` loads the `github-dark.css` highlight stylesheet. |
| 72 | + |
| 73 | +### UI conventions |
| 74 | + |
| 75 | +shadcn/ui primitives in `src/components/ui/` (config in `components.json`). Tailwind + `tailwind-merge`/`clsx` via `cn()` in `src/lib/utils.ts`. Monaco is the markdown editor (`src/components/code-editor.tsx` wrapping `src/components/markdown-editor.tsx`, themes in `src/lib/monaco-themes.ts`). `ts-pattern` is used for exhaustive pattern matching on tagged unions — prefer it over chained `if`/`switch`. |
| 76 | + |
| 77 | +### Third-party services |
| 78 | + |
| 79 | +Only **Plausible Analytics** — opt-in via `VITE_PLAUSIBLE_DOMAIN` env var. If unset, `src/main.tsx` skips injecting the `<script>` entirely. Events are tracked via `trackEvent()` in `src/lib/plausible.ts` (thin wrapper around `window.plausible`). Everything else is bundled client code. |
| 80 | + |
| 81 | +### Env vars |
| 82 | + |
| 83 | +Parsed by Zod in `src/lib/env.ts` from `import.meta.env`: |
| 84 | + |
| 85 | +- `VITE_BASE_URL` — absolute URL used by the watermark link (default `http://localhost:3000`) |
| 86 | +- `VITE_PLAUSIBLE_DOMAIN` — optional; enables Plausible injection if set |
0 commit comments