diff --git a/.cursor/commands/review.md b/.cursor/commands/review.md
index ab36f7c4..4361b397 100644
--- a/.cursor/commands/review.md
+++ b/.cursor/commands/review.md
@@ -37,6 +37,7 @@ Review every point below carefully to ensure files follow consistent code style
- [ ] Each file has one single purpose/responsibility
- Example: `timeline/index.tsx` should not define `validateElementTrackCompatibility` — that belongs in a lib file
- Example: `lib/timeline-utils.ts` should not declare `TRACK_COLORS` — that belongs in `constants/`
+- [ ] File name accurately reflects what the file contains — a misleading name is a bug waiting to happen
- [ ] Business logic lives in either `src/lib`, `src/core` or `src/services` folder
## Comments
@@ -133,8 +134,8 @@ Do NOT review by reading the file top-to-bottom and noting what jumps out. Inste
3. Only move to the next section after you've exhausted the current one
4. After all sections are checked, do a final pass: re-read every checklist item and confirm you didn't skip it
-Before outputting the table, list each checklist section and confirm you checked it:
-`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓`
+Before outputting the review, list each checklist section and confirm you checked it:
+`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | File Names ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓`
---
@@ -142,7 +143,22 @@ Before outputting the table, list each checklist section and confirm you checked
- **ONLY** flag issues that are explicitly covered by a checklist item above.
- Do **NOT** invent your own rules, suggestions, or "nice to haves" as primary issues.
-- The review output must be a table of issues, each one mapping to a specific checklist item.
+- The review output must be a list of issues, each one mapping to a specific checklist item.
- If something looks off but isn't covered by the checklist, you can mention it as a brief side note at the end — but keep it clearly separate from the actual review. Always default to fixing the issues covered by the checklist above, unless the user says otherwise.
> You WILL miss things if you try to review the whole file in one pass. Iterate rule by rule.
+
+---
+
+## Think Bigger
+
+After the checklist review, step back and ask the hard questions. The biggest architectural problems get solved by the biggest questions.
+
+- Does this abstraction actually need to exist? Could it be deleted entirely?
+- Is this the right layer for this logic? (wrong layer = future pain)
+- Is this solving a real problem, or a problem we invented?
+- Would a simpler data model make this whole file unnecessary?
+- Are we adding complexity to work around a bad decision made earlier?
+- Could this field be derived from other existing fields? Redundant data in a model is a source of bugs.
+
+Don't be shy about flagging these. A "why does this exist?" question is often worth more than 10 style fixes.
diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc
index 977f4363..3dcaabaa 100644
--- a/.cursor/rules/ultracite.mdc
+++ b/.cursor/rules/ultracite.mdc
@@ -28,7 +28,6 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
### Accessibility (a11y)
- Always include a `title` element for icons unless there's text beside the icon.
-- Always include a `type` attribute for button elements.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
diff --git a/.cursor/skills/design/SKILL.md b/.cursor/skills/design/SKILL.md
new file mode 100644
index 00000000..d5a05e27
--- /dev/null
+++ b/.cursor/skills/design/SKILL.md
@@ -0,0 +1,45 @@
+---
+name: frontend-design
+description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
+license: Complete terms in LICENSE.txt
+---
+
+This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
+
+The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
+
+## Design Thinking
+
+Before coding, understand the context and commit to a BOLD aesthetic direction:
+
+- **Purpose**: What problem does this interface solve? Who uses it?
+- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
+- **Constraints**: Technical requirements (framework, performance, accessibility).
+- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
+
+**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
+
+Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
+
+- Production-grade and functional
+- Visually striking and memorable
+- Cohesive with a clear aesthetic point-of-view
+- Meticulously refined in every detail
+
+## Frontend Aesthetics Guidelines
+
+Focus on:
+
+- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
+- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
+- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
+- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
+
+NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
+
+Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
+
+**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
+
+Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..25735115
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+node_modules
+.next
+.git
+.gitignore
+*.md
+.env*
+!.env.example
+.cursor
+.vscode
+diffs
+docs
+agent-transcripts
+mcps
+terminals
+apps/web/.font-cache
diff --git a/.gitignore b/.gitignore
index e2b17bce..fa3b5f68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,8 @@ node_modules
# cursor
bun.lockb
+# content-collections
+.content-collections
+
# Twiggy
.cursor/rules/file-structure.mdc
diff --git a/README.md b/README.md
index 673e38a7..cacc4555 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,18 @@
+## Sponsors
+
+Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
+
+
+
+
+
+
+
+
+
## Why?
- **Privacy**: Your videos stay on your device
@@ -38,110 +50,52 @@
### Prerequisites
-Before you begin, ensure you have the following installed on your system:
-
-- [Node.js](https://nodejs.org/en/) (v18 or later)
- [Bun](https://bun.sh/docs/installation)
- (for `npm` alternative)
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
-> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
+> **Note:** Docker is optional but recommended for running the local database and Redis. If you only want to work on frontend features, you can skip it.
### Setup
-1. Fork the repository
-2. Clone your fork locally
-3. Navigate to the web app directory: `cd apps/web`
-4. Copy `.env.example` to `.env.local`:
+1. Fork and clone the repository
+
+2. Copy the environment file:
```bash
# Unix/Linux/Mac
- cp .env.example .env.local
-
- # Windows Command Prompt
- copy .env.example .env.local
+ cp apps/web/.env.example apps/web/.env.local
# Windows PowerShell
- Copy-Item .env.example .env.local
+ Copy-Item apps/web/.env.example apps/web/.env.local
```
-5. Install dependencies: `bun install`
-6. Start the development server: `bun dev`
-
-## Development Setup
-
-### Local Development
-
-1. Start the database and Redis services:
+3. Start the database and Redis:
```bash
- # From project root
- docker-compose up -d
+ docker compose up -d db redis serverless-redis-http
```
-2. Navigate to the web app directory:
+4. Install dependencies and start the dev server:
```bash
- cd apps/web
+ bun install
+ bun dev:web
```
-3. Copy `.env.example` to `.env.local`:
-
- ```bash
- # Unix/Linux/Mac
- cp .env.example .env.local
-
- # Windows Command Prompt
- copy .env.example .env.local
-
- # Windows PowerShell
- Copy-Item .env.example .env.local
- ```
-
-4. Configure required environment variables in `.env.local`:
-
- **Required Variables:**
-
- ```bash
- # Database (matches docker-compose.yaml)
- DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
-
- # Generate a secure secret for Better Auth
- BETTER_AUTH_SECRET="your-generated-secret-here"
- BETTER_AUTH_URL="http://localhost:3000"
-
- # Redis (matches docker-compose.yaml)
- UPSTASH_REDIS_REST_URL="http://localhost:8079"
- UPSTASH_REDIS_REST_TOKEN="example_token"
-
- # Marble Blog
- MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
- NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
-
- # Development
- NODE_ENV="development"
- ```
-
- **Generate BETTER_AUTH_SECRET:**
-
- ```bash
- # Unix/Linux/Mac
- openssl rand -base64 32
-
- # Windows PowerShell (simple method)
- [System.Web.Security.Membership]::GeneratePassword(32, 0)
-
- # Cross-platform (using Node.js)
- node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
-
- # Or use an online generator: https://generate-secret.vercel.app/32
- ```
-
-5. Run database migrations: `bun run db:migrate` from (inside apps/web)
-6. Start the development server: `bun run dev` from (inside apps/web)
-
The application will be available at [http://localhost:3000](http://localhost:3000).
+The `.env.example` has sensible defaults that match the Docker Compose config — it should work out of the box.
+
+### Self-Hosting with Docker
+
+To run everything (including a production build of the app) in Docker:
+
+```bash
+docker compose up -d
+```
+
+The app will be available at [http://localhost:3100](http://localhost:3100).
+
## Contributing
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
@@ -158,22 +112,6 @@ See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instruc
- Follow the setup instructions in CONTRIBUTING.md
- Create a feature branch and submit a PR
-## Sponsors
-
-Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
-
-
-
-
-
-
-
-
-
----
-
-[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FOpenCut-app%2FOpenCut&project-name=opencut&repository-name=opencut)
-
## License
[MIT LICENSE](LICENSE)
diff --git a/apps/web/.env.example b/apps/web/.env.example
index b321f09a..e1943571 100644
--- a/apps/web/.env.example
+++ b/apps/web/.env.example
@@ -13,7 +13,7 @@ DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
BETTER_AUTH_SECRET=your_better_auth_secret
UPSTASH_REDIS_REST_URL=http://localhost:8079
-UPSTASH_REDIS_REST_TOKEN=example_token_here
+UPSTASH_REDIS_REST_TOKEN=example_token
MARBLE_WORKSPACE_KEY=your_workspace_key_here
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
new file mode 100644
index 00000000..9f2dfa2c
--- /dev/null
+++ b/apps/web/Dockerfile
@@ -0,0 +1,70 @@
+FROM oven/bun:alpine AS base
+
+FROM base AS builder
+
+WORKDIR /app
+
+ARG FREESOUND_CLIENT_ID
+ARG FREESOUND_API_KEY
+
+COPY package.json package.json
+COPY bun.lock bun.lock
+COPY turbo.json turbo.json
+
+COPY apps/web/package.json apps/web/package.json
+COPY packages/env/package.json packages/env/package.json
+COPY packages/ui/package.json packages/ui/package.json
+
+RUN bun install
+
+COPY apps/web/ apps/web/
+COPY packages/env/ packages/env/
+COPY packages/ui/ packages/ui/
+
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+# Build-time env stubs to pass zod validation
+ENV DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
+ENV BETTER_AUTH_SECRET="build-time-secret"
+ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
+ENV UPSTASH_REDIS_REST_TOKEN="example_token"
+ENV NEXT_PUBLIC_SITE_URL="http://localhost:3000"
+ENV NEXT_PUBLIC_MARBLE_API_URL="https://api.marblecms.com"
+ENV MARBLE_WORKSPACE_KEY="build-placeholder"
+ENV CLOUDFLARE_ACCOUNT_ID="build-placeholder"
+ENV R2_ACCESS_KEY_ID="build-placeholder"
+ENV R2_SECRET_ACCESS_KEY="build-placeholder"
+ENV R2_BUCKET_NAME="build-placeholder"
+ENV MODAL_TRANSCRIPTION_URL="http://localhost:0"
+
+ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID
+ENV FREESOUND_API_KEY=$FREESOUND_API_KEY
+
+WORKDIR /app/apps/web
+RUN bun run build
+
+# Production image
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
+COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
+
+RUN chown nextjs:nodejs apps
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+CMD ["bun", "apps/web/server.js"]
diff --git a/apps/web/content-collections.ts b/apps/web/content-collections.ts
new file mode 100644
index 00000000..b3028846
--- /dev/null
+++ b/apps/web/content-collections.ts
@@ -0,0 +1,32 @@
+import { defineCollection, defineConfig } from "@content-collections/core";
+import { z } from "zod";
+
+const changelog = defineCollection({
+ name: "changelog",
+ directory: "content/changelog",
+ include: "*.md",
+ schema: z.object({
+ version: z.string(),
+ date: z.string(),
+ title: z.string(),
+ description: z.string().optional(),
+ changes: z.array(
+ z.object({
+ type: z.string(),
+ text: z.string(),
+ }),
+ ),
+ }),
+ transform: async (doc, { collection }) => {
+ const allDocs = await collection.documents();
+ const sorted = [...allDocs].sort((a, b) =>
+ b.version.localeCompare(a.version, undefined, { numeric: true }),
+ );
+ const isLatest = sorted[0]?.version === doc.version;
+ return { ...doc, isLatest };
+ },
+});
+
+export default defineConfig({
+ content: [changelog],
+});
diff --git a/apps/web/content/changelog/0.1.0.md b/apps/web/content/changelog/0.1.0.md
new file mode 100644
index 00000000..08a0a9ed
--- /dev/null
+++ b/apps/web/content/changelog/0.1.0.md
@@ -0,0 +1,35 @@
+---
+version: "0.1.0"
+date: "2026-02-23"
+title: "Editor foundation"
+description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use."
+changes:
+ - type: new
+ text: "Rebuilt the properties panel from scratch. Number inputs now support math expressions and click-drag scrubbing instead of just typing values in."
+ - type: new
+ text: "The properties panel went from a flat list of basic text styles to organized sections: transform, blending, typography, spacing, and background controls. Text elements finally have position, scale, and rotation."
+ - type: new
+ text: "New color picker with eyedropper, opacity control, and multiple color formats."
+ - type: new
+ text: "Bookmarks now support notes, color labels, and optional duration to plan scenes more clearly."
+ - type: new
+ text: "Move, scale, and rotate elements directly in the preview panel."
+ - type: new
+ text: "Blend modes (Multiply, Screen, Overlay, etc). Only available on text elements for now."
+ - type: new
+ text: "Paste images, videos, or audio from your clipboard straight into the editor."
+ - type: new
+ text: "Hold Shift while moving or resizing to temporarily turn off snapping."
+ - type: improved
+ text: "Expanded font selection from 7 to over 1,000."
+ - type: improved
+ text: "Fonts load much faster."
+ - type: improved
+ text: "All asset panel tabs now share a consistent layout."
+ - type: improved
+ text: "Text rendering properly supports multiple lines, line height, and letter spacing."
+ - type: improved
+ text: "Better contrast in the dark theme."
+ - type: fixed
+ text: "Fixed undo/redo for drag-and-drop so dropped items are reliably undoable."
+---
diff --git a/apps/web/content/changelog/0.2.0.md b/apps/web/content/changelog/0.2.0.md
new file mode 100644
index 00000000..ea69f78b
--- /dev/null
+++ b/apps/web/content/changelog/0.2.0.md
@@ -0,0 +1,15 @@
+---
+version: "0.2.0"
+date: "2026-02-29"
+title: "Motion & effects"
+description: "This release adds the foundation for motion and effects."
+changes:
+ - type: new
+ text: "Keyframe animation system, starting with transform properties (position, scale, rotation)."
+ - type: new
+ text: "Effects system (with our first effect: Blur!)"
+ - type: fixed
+ text: "Fixed an issue where click-and-drag selection on one timeline track could accidentally select items from the track below."
+ - type: fixed
+ text: "Audio could flicker or cut out at the start of playback when certain elements were selected. The scheduler now recovers from timing slips without losing audio."
+---
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index fa1ac7f5..2f509d2a 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -1,7 +1,16 @@
import type { NextConfig } from "next";
import { withBotId } from "botid/next/config";
+import { withContentCollections } from "@content-collections/next";
const nextConfig: NextConfig = {
+ turbopack: {
+ rules: {
+ "*.glsl": {
+ loaders: [require.resolve("raw-loader")],
+ as: "*.js",
+ },
+ },
+ },
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
@@ -46,4 +55,4 @@ const nextConfig: NextConfig = {
},
};
-export default withBotId(nextConfig);
+export default withContentCollections(withBotId(nextConfig));
diff --git a/apps/web/package.json b/apps/web/package.json
index 9a57a379..8094c548 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,107 +1,110 @@
{
- "name": "@opencut/web",
- "version": "0.1.0",
- "private": true,
- "packageManager": "bun@1.2.18",
- "scripts": {
- "dev": "next dev --turbopack",
- "build": "next build",
- "start": "next start",
- "lint": "biome check src/",
- "lint:fix": "biome check src/ --write",
- "format": "biome format src/ --write",
- "db:generate": "drizzle-kit generate",
- "db:migrate": "drizzle-kit migrate",
- "db:push:local": "cross-env NODE_ENV=development drizzle-kit push",
- "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push"
- },
- "dependencies": {
- "@ffmpeg/core": "^0.12.10",
- "@ffmpeg/ffmpeg": "^0.12.15",
- "@ffmpeg/util": "^0.12.2",
- "@hello-pangea/dnd": "^18.0.1",
- "@hookform/resolvers": "^3.9.1",
- "@hugeicons/core-free-icons": "^3.1.1",
- "@hugeicons/react": "^1.1.4",
- "@huggingface/transformers": "^3.8.1",
- "@opencut/env": "workspace:*",
- "@opencut/ui": "workspace:*",
- "@radix-ui/react-accordion": "^1.2.12",
- "@radix-ui/react-checkbox": "^1.3.3",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-primitive": "^2.1.4",
- "@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
- "@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-tooltip": "^1.2.8",
- "@types/culori": "^4.0.1",
- "@upstash/ratelimit": "^2.0.6",
- "@upstash/redis": "^1.35.4",
- "@vercel/analytics": "^1.4.1",
- "aws4fetch": "^1.0.20",
- "better-auth": "^1.2.7",
- "botid": "^1.4.2",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cmdk": "^1.0.0",
- "culori": "^4.0.2",
- "dayjs": "^1.11.13",
- "drizzle-orm": "^0.44.2",
- "embla-carousel-react": "^8.5.1",
- "eventemitter3": "^5.0.1",
- "feed": "^5.1.0",
- "input-otp": "^1.4.1",
- "lucide-react": "^0.562.0",
- "mediabunny": "^1.29.1",
- "motion": "^12.18.1",
- "nanoid": "^5.1.5",
- "next": "16.1.3",
- "next-themes": "^0.4.4",
- "pg": "^8.16.2",
- "postgres": "^3.4.5",
- "radix-ui": "^1.4.3",
- "react": "^19.0.0",
- "react-day-picker": "^8.10.1",
- "react-dom": "^19.0.0",
- "react-hook-form": "^7.54.0",
- "react-icons": "^5.4.0",
- "react-markdown": "^10.1.0",
- "react-phone-number-input": "^3.4.11",
- "react-resizable-panels": "^2.1.7",
- "react-window": "^2.2.7",
- "recharts": "^2.14.1",
- "rehype-autolink-headings": "^7.1.0",
- "rehype-parse": "^9.0.1",
- "rehype-sanitize": "^6.0.0",
- "rehype-slug": "^6.0.0",
- "rehype-stringify": "^10.0.1",
- "sonner": "^1.7.1",
- "tailwind-merge": "^2.5.5",
- "tailwindcss-animate": "^1.0.7",
- "unified": "^11.0.5",
- "use-deep-compare-effect": "^1.8.1",
- "vaul": "^1.1.2",
- "wavesurfer.js": "^7.9.8",
- "zod": "^3.25.67",
- "zustand": "^5.0.2"
- },
- "devDependencies": {
- "@napi-rs/canvas": "^0.1.92",
- "@tailwindcss/postcss": "^4.1.11",
- "@tailwindcss/typography": "^0.5.16",
- "@types/bun": "latest",
- "@types/node": "^24.2.1",
- "@types/pg": "^8.15.4",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "cross-env": "^7.0.3",
- "dotenv": "^16.5.0",
- "drizzle-kit": "^0.31.4",
- "postcss": "^8",
- "sharp": "^0.34.5",
- "tailwindcss": "^4.1.11",
- "tsx": "^4.7.1",
- "typescript": "^5.8.3"
- }
+ "name": "@opencut/web",
+ "version": "0.1.0",
+ "private": true,
+ "packageManager": "bun@1.2.18",
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build",
+ "start": "next start",
+ "lint": "biome check src/",
+ "lint:fix": "biome check src/ --write",
+ "format": "biome format src/ --write",
+ "db:generate": "drizzle-kit generate",
+ "db:migrate": "drizzle-kit migrate",
+ "db:push:local": "cross-env NODE_ENV=development drizzle-kit push",
+ "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push"
+ },
+ "dependencies": {
+ "@ffmpeg/core": "^0.12.10",
+ "@ffmpeg/ffmpeg": "^0.12.15",
+ "@ffmpeg/util": "^0.12.2",
+ "@hello-pangea/dnd": "^18.0.1",
+ "@hookform/resolvers": "^3.9.1",
+ "@hugeicons/core-free-icons": "^3.1.1",
+ "@hugeicons/react": "^1.1.4",
+ "@huggingface/transformers": "^3.8.1",
+ "@opencut/env": "workspace:*",
+ "@opencut/ui": "workspace:*",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-primitive": "^2.1.4",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@types/culori": "^4.0.1",
+ "@upstash/ratelimit": "^2.0.6",
+ "@upstash/redis": "^1.35.4",
+ "@vercel/analytics": "^1.4.1",
+ "aws4fetch": "^1.0.20",
+ "better-auth": "^1.2.7",
+ "botid": "^1.4.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
+ "culori": "^4.0.2",
+ "dayjs": "^1.11.13",
+ "drizzle-orm": "^0.44.2",
+ "embla-carousel-react": "^8.5.1",
+ "eventemitter3": "^5.0.1",
+ "feed": "^5.1.0",
+ "input-otp": "^1.4.1",
+ "lucide-react": "^0.562.0",
+ "mediabunny": "^1.29.1",
+ "motion": "^12.18.1",
+ "nanoid": "^5.1.5",
+ "next": "16.1.3",
+ "next-themes": "^0.4.4",
+ "pg": "^8.16.2",
+ "postgres": "^3.4.5",
+ "radix-ui": "^1.4.3",
+ "react": "^19.0.0",
+ "react-day-picker": "^8.10.1",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.0",
+ "react-icons": "^5.4.0",
+ "react-markdown": "^10.1.0",
+ "react-phone-number-input": "^3.4.11",
+ "react-resizable-panels": "^2.1.7",
+ "react-window": "^2.2.7",
+ "recharts": "^2.14.1",
+ "rehype-autolink-headings": "^7.1.0",
+ "rehype-parse": "^9.0.1",
+ "rehype-sanitize": "^6.0.0",
+ "rehype-slug": "^6.0.0",
+ "rehype-stringify": "^10.0.1",
+ "sonner": "^1.7.1",
+ "tailwind-merge": "^2.5.5",
+ "tailwindcss-animate": "^1.0.7",
+ "unified": "^11.0.5",
+ "use-deep-compare-effect": "^1.8.1",
+ "vaul": "^1.1.2",
+ "wavesurfer.js": "^7.9.8",
+ "zod": "^3.25.67",
+ "zustand": "^5.0.2"
+ },
+ "devDependencies": {
+ "@content-collections/core": "^0.14.1",
+ "@content-collections/next": "^0.2.11",
+ "@napi-rs/canvas": "^0.1.92",
+ "@tailwindcss/postcss": "^4.1.11",
+ "@tailwindcss/typography": "^0.5.16",
+ "@types/bun": "latest",
+ "@types/node": "^24.2.1",
+ "@types/pg": "^8.15.4",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "cross-env": "^7.0.3",
+ "dotenv": "^16.5.0",
+ "drizzle-kit": "^0.31.4",
+ "postcss": "^8",
+ "raw-loader": "^4.0.2",
+ "sharp": "^0.34.5",
+ "tailwindcss": "^4.1.11",
+ "tsx": "^4.7.1",
+ "typescript": "^5.8.3"
+ }
}
diff --git a/apps/web/public/effects/preview.jpg b/apps/web/public/effects/preview.jpg
new file mode 100644
index 00000000..6519189c
Binary files /dev/null and b/apps/web/public/effects/preview.jpg differ
diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx
new file mode 100644
index 00000000..a9072485
--- /dev/null
+++ b/apps/web/src/app/changelog/page.tsx
@@ -0,0 +1,142 @@
+import type { Metadata } from "next";
+import { BasePage } from "@/app/base-page";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/utils/ui";
+import { allChangelogs } from "content-collections";
+
+export const metadata: Metadata = {
+ title: "Changelog - OpenCut",
+ description: "Every update, improvement, and fix to OpenCut — documented.",
+ openGraph: {
+ title: "Changelog - OpenCut",
+ description: "Every update, improvement, and fix to OpenCut — documented.",
+ type: "website",
+ },
+};
+
+const knownSectionOrder = ["new", "improved", "fixed", "breaking"];
+
+const knownSectionTitles: Record = {
+ new: "Features",
+ improved: "Improvements",
+ fixed: "Fixes",
+ breaking: "Breaking Changes",
+};
+
+function getSectionTitle(type: string): string {
+ return (
+ knownSectionTitles[type] ?? type.charAt(0).toUpperCase() + type.slice(1)
+ );
+}
+
+export default function ChangelogPage() {
+ const releases = [...allChangelogs].sort((a, b) =>
+ b.version.localeCompare(a.version, undefined, { numeric: true }),
+ );
+
+ return (
+
+
+
+
+
+
+ {releases.map((release, releaseIndex) => (
+
+
+ {releaseIndex < releases.length - 1 && (
+ // ml-1.5 aligns with the center of the 11px timeline dot
+
+ )}
+