Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
{
"extends": "next/core-web-vitals"
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": { "jsx": true }
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"env": {
"browser": true,
"es2022": true,
"node": true,
"jest": true
},
"ignorePatterns": ["dist", "node_modules", "public/themes"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"no-constant-condition": ["error", { "checkLoops": false }]
}
}
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: scastiel
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
name: Lint, format, test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- run: npm ci

- name: ESLint
run: npm run lint

- name: Prettier
run: npm run format:check

- name: Jest
run: npm test
33 changes: 0 additions & 33 deletions .github/workflows/update-site.yml

This file was deleted.

11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
# testing
/coverage

# next.js
/.next/
/out/
# vite
/dist/

# production
/build
Expand All @@ -34,7 +33,9 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts

# local postgres data
postgres-data
postgres-data

# Claude Code
.claude/
86 changes: 86 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CLAUDE.md

Guidance for Claude Code when working in this repo.

## Commands

- `npm run dev` — Vite dev server on port 3000
- `npm run build` — `tsc --noEmit && vite build` → `dist/`
- `npm start` — `vite preview` on port 3000 (serves the built `dist/`)
- `npm run lint` — ESLint over `src/**/*.{ts,tsx}`
- `npm test` — Jest (ts-jest, jsdom). Run one test with `npx jest path/to/file.test.ts` or `-t 'name'`
- `npm run remotion-studio` — Remotion Studio pointed at `src/remotion/index.ts` for previewing the composition in isolation

TypeScript path alias: `@/*` → `src/*`.

## Architecture

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

### The snippet → video pipeline

The markdown snippet format is the central data model. Everything else is a transform over it:

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`.
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`.
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.
4. **Preview vs render.** The same Remotion composition is used two ways, both client-side:
- **Preview** via `@remotion/player` in `src/components/snippet-player.tsx`.
- **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.

### Web-renderer caveats

`@remotion/web-renderer` (experimental) has strict CSS limits. Don't introduce:

- `radial-gradient` (use `<canvas>` — see `gradient-canvas.tsx` + `gradients.ts:drawGradientOnCanvas`)
- `z-index` (use DOM order instead — the window chrome in `code-composition.tsx` relies on this)
- `clip-path`, `backdrop-filter`, `mix-blend-mode`
- Safari `filter` is broken (pre-accepted)

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'`.

### Persistence

All snippet state lives in `localStorage` under key `codevideo:snippets:v1`. API in `src/lib/snippet-storage.ts`:

```ts
listSnippets(): StoredSnippet[]
getSnippet(slug): StoredSnippet | null
createSnippet(): StoredSnippet // random 6-char hex slug, seeded with initialMarkdown
createTutorialSnippet(): StoredSnippet
updateSnippet(slug, content): void
deleteSnippet(slug): void
```

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.

### App routes

Routes are declared in `src/routes.tsx` and mounted from `src/main.tsx`:

- `/` — `src/pages/landing-page.tsx` (plays `landing-page-snippet.ts` via `src/components/landing-player.tsx`)
- `/my` — redirects to `/my/snippets` via `beforeLoad`
- `/my/snippets` — `src/pages/snippets-page.tsx` (reads from localStorage)
- `/my/snippets/$snippetSlug` — `src/pages/snippet-editor-page.tsx` (param via `useParams({ from: '/my/snippets/$snippetSlug' })`)
- `/help` — `src/pages/help-page.tsx` (plain TSX with Tailwind `prose`)

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

### Theme

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.

### UI conventions

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

### Third-party services

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.

### Env vars

Parsed by Zod in `src/lib/env.ts` from `import.meta.env`:

- `VITE_BASE_URL` — absolute URL used by the watermark link (default `http://localhost:3000`)
- `VITE_PLAUSIBLE_DOMAIN` — optional; enables Plausible injection if set
69 changes: 49 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,65 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# CodeBit

## Getting Started
**Tell a story with your code.** CodeBit turns markdown-defined code snippets into animated videos you can share with your community.

First, run the development server:
Try it live at [codebit.xyz](https://codebit.xyz).

![CodeBit banner](public/banner.png)

## Features

- Write code sequences in plain Markdown — no new syntax to learn
- Live preview with typing animations between steps
- Customizable colors, fonts, backgrounds, and highlight themes
- Export as MP4 video, rendered entirely in your browser
- 100% free, no account, no sign-up — everything runs client-side

## Stack

- **Build / dev**: [Vite](https://vitejs.dev/) + [React 18](https://react.dev/)
- **Routing**: [TanStack Router](https://tanstack.com/router) (code-based)
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [shadcn/ui](https://ui.shadcn.com/)
- **Editor**: [Monaco](https://microsoft.github.io/monaco-editor/)
- **Video pipeline**: [Remotion](https://www.remotion.dev/) — `@remotion/player` for preview, `@remotion/web-renderer` (WebCodecs) for in-browser MP4 export
- **Persistence**: `localStorage` — no backend
- **Tests**: Jest + ts-jest + jsdom
- **Language**: TypeScript

## Run it locally

```bash
git clone https://github.com/scastiel/codebit.git
cd codebit
npm install
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
The app will be available at [http://localhost:3000](http://localhost:3000).

### Scripts

- `npm run dev` — start the Vite dev server
- `npm run build` — type-check and build to `dist/`
- `npm start` — preview the production build
- `npm run lint` — run ESLint
- `npm test` — run the Jest test suite
- `npm run remotion-studio` — open Remotion Studio to preview the composition in isolation

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
### Environment variables

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
Both are optional:

## Learn More
- `VITE_BASE_URL` — absolute URL used by the video watermark (defaults to `http://localhost:3000`)
- `VITE_PLAUSIBLE_DOMAIN` — enables [Plausible Analytics](https://plausible.io/) injection if set

To learn more about Next.js, take a look at the following resources:
## Sponsor

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
CodeBit is free and open source. If you enjoy using it, you can [sponsor me on GitHub](https://github.com/sponsors/scastiel) to support its development. ♥

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## License

## Deploy on Vercel
MIT — see [LICENSE](LICENSE) if present, otherwise consider the code MIT-licensed.

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
## Credits

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Made with ♥ in Montreal by [@scastiel](https://scastiel.dev) and [@maxday](https://maxday.dev).
64 changes: 64 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="en" class="dark scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<title>Tell a story with your code – CodeBit</title>
<meta
name="description"
content="Create animations from code snippets, and export them as videos to share with your community."
/>
<meta property="og:type" content="website" />
<meta property="og:title" content="Tell a story with your code – CodeBit" />
<meta
property="og:description"
content="Create animations from code snippets, and export them as videos to share with your community."
/>
<meta property="og:image" content="/banner.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@codebitxyz" />
<meta name="twitter:site" content="@codebitxyz" />
<meta name="twitter:title" content="Tell a story with your code – CodeBit" />
<meta
name="twitter:description"
content="Create animations from code snippets, and export them as videos to share with your community."
/>
<meta name="twitter:image" content="/banner.png" />
<script>
(function () {
function patch(proto) {
if (!proto) return
var d = Object.getOwnPropertyDescriptor(proto, 'fontStretch')
if (!d || !d.set) return
var orig = d.set
Object.defineProperty(
proto,
'fontStretch',
Object.assign({}, d, {
set: function (v) {
if (typeof v === 'string' && v.charAt(v.length - 1) === '%') {
orig.call(this, 'normal')
return
}
try {
orig.call(this, v)
} catch (e) {
orig.call(this, 'normal')
}
},
}),
)
}
if (typeof CanvasRenderingContext2D !== 'undefined')
patch(CanvasRenderingContext2D.prototype)
if (typeof OffscreenCanvasRenderingContext2D !== 'undefined')
patch(OffscreenCanvasRenderingContext2D.prototype)
})()
</script>
</head>
<body class="bg-gradient-to-br from-slate-950 to-slate-800">
<div id="root" class="min-h-[100dvh] flex flex-col"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
}
15 changes: 0 additions & 15 deletions jest.config.js

This file was deleted.

1 change: 1 addition & 0 deletions jest.setup.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@testing-library/jest-dom')
2 changes: 0 additions & 2 deletions jest.setup.js

This file was deleted.

7 changes: 0 additions & 7 deletions mdx-components.tsx

This file was deleted.

Loading
Loading