Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

619 changed files with 73450 additions and 15750 deletions

172
.cursor/commands/review.md Normal file
View File

@ -0,0 +1,172 @@
# Code Review Checklist
Review every point below carefully to ensure files follow consistent code style and best practices.
---
## Function Signatures & Parameters
- [ ] Every function accepts a single object parameter with destructuring in the signature (for readability and future extensibility)
- Exception: tiny one-liner callbacks (e.g. `array.find(x => ...)`, `map`, `filter`, `sort`) do not need destructuring if it hurts readability
```tsx
// ❌ wrong
function formatTime(seconds: number, fps: number) { ... }
// ✅ correct
function formatTime({ seconds, fps }: { seconds: number; fps: number }) { ... }
```
## TypeScript & Type Safety
- [ ] No `any` references
- [ ] General interfaces are in the `types` folder, not scattered in components
- Example: `TimelineTrack` interface belongs in `src/types/timeline.ts`, not `src/components/timeline/index.tsx`
## JSX & Components
- [ ] JSX is clean — no comments explaining what each part does
- [ ] Complex/reusable JSX is extracted into sub-components (placed below the main component)
- [ ] Components shared across multiple files are in separate files
- [ ] File order: constants specific to file (top) -> utils specific to file -> main component → sub-components (bottom)
- [ ] Components render UI only — domain logic lives in hooks, utilities, or managers
- Simple interaction logic (gestures, modifier keys) can stay if not complex
## Code Organization & File Structure
- [ ] 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
- [ ] No AI comments — only human comments that explain _why_, not _what_
- Bad: changelog-style comments, explaining readable code, using more words than necessary
- [ ] All comments are lowercase
## Naming Conventions
- [ ] Readability over brevity — use `element` not `el`, `event` not `e`
- [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something`
- [ ] No title case for multi-word text/UI — use `Hello world` not `Hello World`
## Tailwind & Styling
- [ ] Always use `cn()` for `className` — never string interpolation with `${}` or ternaries inline
```tsx
// ❌ wrong
className={`base-class ${isActive && "active"} ${someHelper()}`}
// ✅ correct
className={cn("base-class", isActive && "active", someHelper())}
```
- [ ] Use `gap-*` instead of `mb-*` or `mt-*` for consistent spacing
- [ ] Use `size-*` instead of `h-* w-*` when width and height are the same
- [ ] When using `size-*` on icons inside `<Button>`, use `!` modifier to override default `size-4`
```tsx
<Button>
<PlusIcon className="!size-6" /> {/* ✅ correct */}
<PlusIcon className="size-6" /> {/* ❌ wrong */}
<PlusIcon className="!size-4" /> {/* ❌ unnecessary, size-4 is default */}
<PlusIcon className="size-4" />{" "}
{/* ❌ completely wrong, 1) doesn't override and 2) size-4 is default */}
</Button>
```
## State Management (Zustand)
- [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead
- [ ] High-frequency stores (timeline, playback, selections) use selectors — `useStore((s) => s.value)` not `const { value } = useStore()`
- [ ] Store/manager methods are not passed as props — sub-components access them directly
```tsx
// ❌ wrong
function Parent() {
const { selectedElements } = useTimelineStore();
return <Child selectedElements={selectedElements} />;
}
// ✅ correct
function Parent() {
return <Child />;
}
function Child() {
const { selectedElements } = useTimelineStore();
}
```
- [ ] Components and hooks should use the `useEditor` hook. Only use `EditorCore.getInstance()` if you are outside of a react component/hook. Eg: in a utility function, event handler.
## Code Quality
- [ ] Code is scannable — use variables and helper functions to make intent clear at a glance
- [ ] Complex logic is extracted into well-named variables or helpers
- [ ] No magic numbers or magic values — extract inline literals into named constants
- Applies to colors, durations, thresholds, sizes, config values, etc.
- If it's domain-specific to one file, a `const` at the top of that file is fine
- If it's generic enough, it belongs in `constants/`
- [ ] No redundant single/plural function variants — if a function can operate on multiple items, it should accept an array and handle both cases. Don't create `doThing()` + `doThings()`.
```tsx
// ❌ wrong — redundant variants
function updateElement({ element }: { element: Element }) { ... }
function updateElements({ elements }: { elements: Element[] }) { ... }
// ✅ correct — one function, accepts array
function updateElements({ elements }: { elements: Element[] }) { ... }
```
---
## Function Keywords
| Context | Keyword |
| --------------------------------- | ------------------------- |
| Next.js page components | `export default function` |
| Main react component | `export function` |
| Sub-components | `function` |
| Utility functions | `export function` |
| Functions inside react components | `const` |
---
## Review Methodology
Do NOT review by reading the file top-to-bottom and noting what jumps out. Instead:
1. Go through each checklist section **one at a time**
2. For each section, scan the **entire file** for violations of that specific rule
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 review, list each checklist section and confirm you checked it:
`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | File Names ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓`
---
## IMPORTANT: Review Rules
- **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 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.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
---
alwaysApply: false
---
# Comment Guidelines
## Good Comments (Human-style)
- Explain WHY, not WHAT
- Document non-obvious behavior or edge cases
- Warn about performance implications or side effects
- Explain business logic that isn't clear from code
Examples:
```javascript
// transfer, not copy; sender buffer detaches
// satisfies: check shape; keep literals
// keep multibyte across chunks
// timingSafeEqual throws on length mismatch
```
## Bad Comments (AI-style)
- Don't explain what the code literally does
- Don't add changelog-style comments in code
- Don't comment every line or obvious operations
Avoid:
```javascript
// Prevent duplicate initialization
// Check if project is already loaded
// Mark as initializing to prevent race conditions
// (changed from blah to blah)
```
## Rule
Only add comments when there's genuinely non-obvious behavior, performance considerations, or business logic that needs context. Code should be self-documenting through naming and structure.

View File

@ -0,0 +1,20 @@
---
alwaysApply: false
---
# Handling Uncertainty
## Principle
If you can't confidently respond due to missing context, data access, or ambiguity (multiple interpretations), report it instead of guessing. Seek clarification to avoid errors.
Apply when: query lacks details, no access to info/tools, or unclear intent.
## How to Report
1. **Description**: Why uncertain and what you need.
2. **Questions**: 1-3 targeted ones.
3. **Assumptions** (opt.): State if proceeding; omit otherwise.
Direct and concise.
**Assumptions**: None.
Builds transparency.

View File

@ -0,0 +1,8 @@
---
alwaysApply: false
---
# Readability First
Optimize code for AI agents to understand and modify.
Never abbreviate. `event` not `e`, `element` not `el`. If it's easy to read, it's correct. In this case, "config" is better than "configuration" because it's shorter and is **still very readable**. "El" is not very readable.

View File

@ -0,0 +1,51 @@
---
alwaysApply: false
---
# Separation of Concerns
## Core Principle
Each file should have one single purpose/responsibility. Related functionality should be grouped together, unrelated functionality should be separated.
## Good Separation
- One file per major concern (auth, validation, data transformation)
- Group related utilities together
- Extract shared logic into dedicated files
- Keep API routes focused on their specific endpoint logic
Examples:
```javascript
// ✅ Good: Each file has clear responsibility
/lib/rate-limit.ts // Rate limiting utilities
/lib/validation.ts // Input validation schemas
/lib/freesound-api.ts // External API integration
/api/sounds/search/route.ts // Route handler only
```
## Bad Mixing of Concerns
Avoid cramming multiple responsibilities into one file:
```javascript
// ❌ Bad: Route file doing everything
/api/sounds/search/route.ts
- Rate limiting logic
- Validation schemas
- API transformation
- External API calls
- Response formatting
- Error handling utilities
```
## When to Separate
- File is getting long (>500 lines)
- Multiple distinct responsibilities in one file
- Logic could be reused elsewhere
- Complex utilities that distract from main purpose
## Rule
One file, one responsibility. Extract shared concerns into focused utility files

101
.cursor/rules/ultracite.mdc Normal file
View File

@ -0,0 +1,101 @@
---
alwaysApply: false
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's formatter.
## Key Principles
- Zero configuration required
- Subsecond performance
- Maximum type safety
- AI-friendly code generation
## Before Writing Code
1. Analyze existing patterns in the codebase
2. Consider edge cases and error scenarios
3. Follow the rules below strictly
4. Validate accessibility requirements
5. Avoid code duplication
## Rules
### Accessibility (a11y)
- Always include a `title` element for icons unless there's text beside the icon.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
### Code Complexity and Quality
- Don't use primitive type aliases or misleading types.
- Don't use the comma operator.
- Use for...of statements instead of Array.forEach.
- Don't initialize variables to undefined.
- Use .flatMap() instead of map().flat() when possible.
### React and JSX Best Practices
- Don't import `React` itself.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't insert comments as text nodes.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
### Function Parameters and Props
- Always use destructured props objects instead of individual parameters in functions.
- Example: `function helloWorld({ prop }: { prop: string })` instead of `function helloWorld(param: string)`.
- This applies to all functions, not just React components.
### Correctness and Safety
- Don't assign a value to itself.
- Avoid unused imports and variables.
- Don't use await inside loops.
- Don't hardcode sensitive data like API keys and tokens.
- Don't use the TypeScript directive @ts-ignore.
- Make sure the `preconnect` attribute is used when using Google Fonts.
- Don't use the `delete` operator.
- Don't use `require()` in TypeScript/ES modules - use proper `import` statements.
### TypeScript Best Practices
- Don't use TypeScript enums.
- Use either `T[]` or `Array<T>` consistently.
- Don't use the `any` type.
### Style and Consistency
- Don't use global `eval()`.
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
- Don't use `else` blocks when the `if` block breaks early.
- Put default function parameters and optional function parameters last.
- Use `new` when throwing an error.
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
### Next.js Specific Rules
- Don't use `<img>` elements in Next.js projects.
- Don't use `<head>` elements in Next.js projects.
## Example: Error Handling
```typescript
// ✅ Good: Comprehensive error handling
try {
const result = await fetchData();
return { success: true, data: result };
} catch (error) {
console.error("API call failed:", error);
return { success: false, error: error.message };
}
// ❌ Bad: Swallowing errors
try {
return await fetchData();
} catch (e) {
console.log(e);
}
```

View File

@ -0,0 +1,52 @@
---
alwaysApply: false
---
# Scannable Code Guidelines/Separating Concerns.
## Core Principle
Code should be scannable through proper abstraction, not comments. Use variables and helper functions to make intent clear at a glance.
## Good Scannable Code
- Extract complex logic into well-named variables
- Create helper functions for multi-step operations
- Use descriptive names that explain intent
Examples:
```javascript
// ✅ Scannable: Intent is clear from variable names
const isValidUser = user.isActive && user.hasPermissions;
const shouldProcessPayment = amount > 0 && !order.isPaid;
// ✅ Scannable: Complex logic extracted to helper
const searchParams = buildFreesoundSearchParams({ query, filters, pagination });
const transformedResults = transformFreesoundResults({ rawResults });
```
## Bad Unscannable Code
Avoid:
```javascript
// ❌ Hard to scan: What does this condition mean?
if (type === "effects" || !type) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
if (commercial_only) {
params.append("filter", 'license:("Attribution" OR "Creative Commons 0")');
}
}
// ❌ Hard to scan: Complex ternary
const sortParam = query
? sort === "score"
? "score"
: `${sort}_desc`
: `${sort}_desc`;
```
## Rule
Make code scannable by extracting intent into variables and helper functions. If you need to think about what code does, extract it. The reader should understand the flow without diving into implementation details.

7
.cursor/settings.json Normal file
View File

@ -0,0 +1,7 @@
{
"plugins": {
"figma": {
"enabled": true
}
}
}

View File

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

15
.dockerignore Normal file
View File

@ -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

View File

@ -1 +0,0 @@
R2_BUCKET=opencut-assets

188
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,188 @@
# Contributing to OpenCut
⚠️ We are currently NOT accepting feature PRs while we build out the core editor.
If you want to contribute:
1. Open an issue first to discuss
2. Wait for maintainer approval
3. Only then start coding
Critical bug fixes may be accepted on a case-by-case basis.
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
## Getting Started
### Prerequisites
- [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 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!
### 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`:
```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
```
5. Install dependencies: `bun install`
6. Start the development server: `bun run dev`
> **Note:** If you see an error like `Unsupported URL Type "workspace:*"` when running `npm install`, you have two options:
>
> 1. Upgrade to a recent npm version (v9 or later), which has full workspace protocol support.
> 2. Use an alternative package manager such as **bun** or **pnpm**.
## What to Focus On
**🎯 Good Areas to Contribute:**
- Timeline functionality and UI improvements
- Project management features
- Performance optimizations
- Bug fixes in existing functionality
- UI/UX improvements
- Documentation and testing
**⚠️ Areas to Avoid:**
- Preview panel enhancements (text fonts, stickers, effects)
- Export functionality improvements
- Preview rendering optimizations
**Why?** We're currently planning a major refactor of the preview system. The current preview renders DOM elements (HTML), but we're moving to a binary rendering approach similar to CapCut. This new system will ensure consistency between preview and export, and provide much better performance and quality.
The current HTML-based preview is essentially a prototype - the binary approach will be the "real deal." To avoid wasted effort, please focus on other areas of the application until this refactor is complete.
If you're unsure whether your idea falls into the preview category, feel free to ask us [directly in discord](https://discord.gg/zmR9N35cjK) or create a GitHub issue!
## Development Setup
### Local Development
1. Start the database and Redis services:
```bash
# From project root
docker-compose up -d
```
2. Navigate to the web app directory:
```bash
cd apps/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"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
# Redis (matches docker-compose.yaml)
UPSTASH_REDIS_REST_URL="http://localhost:8079"
UPSTASH_REDIS_REST_TOKEN="example_token"
# 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`
6. Start the development server: `bun run dev`
## How to Contribute
### Reporting Bugs
- Use the bug report template
- Include steps to reproduce
- Provide screenshots if applicable
### Suggesting Features
- Use the feature request template
- Explain the use case
- Consider implementation details
### Code Contributions
1. Create a new branch: `git checkout -b feature/your-feature-name`
2. Make your changes
3. Navigate to the web app directory: `cd apps/web`
4. Run the linter: `bun run lint`
5. Format your code: `bunx biome format --write .`
6. Commit your changes with a descriptive message
7. Push to your fork and create a pull request
## Code Style
- We use Biome for code formatting and linting
- Run `bunx biome format --write .` from the `apps/web` directory to format code
- Run `bun run lint` from the `apps/web` directory to check for linting issues
- Follow the existing code patterns
## Pull Request Process
1. Fill out the pull request template completely
2. Link any related issues
3. Ensure CI passes
4. Request review from maintainers
5. Address any feedback
## Community
- Be respectful and inclusive
- Follow our Code of Conduct
- Help others in discussions and issues
Thank you for contributing!

View File

@ -15,23 +15,52 @@ on:
- "*.md" - "*.md"
jobs: jobs:
ci: build:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
env:
DATABASE_URL: "postgresql://opencut:opencut@localhost:5432/opencut"
BETTER_AUTH_SECRET: "supersecret"
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
NEXT_PUBLIC_MARBLE_API_URL: "https://placeholder.example.com"
MARBLE_WORKSPACE_KEY: "placeholder"
FREESOUND_CLIENT_ID: "placeholder"
FREESOUND_API_KEY: "placeholder"
CLOUDFLARE_ACCOUNT_ID: "placeholder"
R2_ACCESS_KEY_ID: "placeholder"
R2_SECRET_ACCESS_KEY: "placeholder"
R2_BUCKET_NAME: "placeholder"
MODAL_TRANSCRIPTION_URL: "https://placeholder.example.com"
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup toolchain - name: Install Bun
uses: moonrepo/setup-toolchain@v0 uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
with: with:
auto-install: true bun-version: 1.2.18
auto-setup: true
- name: Run CI - name: Cache Bun modules
run: moon ci uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }}
- name: Install dependencies
working-directory: apps/web
run: bun install
- name: Build
working-directory: apps/web
run: bun run build
- name: Run tests
working-directory: apps/web
run: echo "No tests implemented yet"
continue-on-error: true

28
.gitignore vendored
View File

@ -1,10 +1,20 @@
node_modules/ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
dist/
target/
.env
.env.*
!.env.example
docs/
# moon runtime cache # asdf version management
.moon/cache/ .tool-versions
node_modules
.cursorignore
.turbo
.env*
!*.env.example
# cursor
bun.lockb
# content-collections
.content-collections
# Twiggy
.cursor/rules/file-structure.mdc

View File

@ -1,9 +0,0 @@
$schema: 'https://moonrepo.dev/schemas/toolchains.json'
javascript:
packageManager: 'bun'
bun:
version: '1.3.11'
rust: {}

View File

@ -1,8 +0,0 @@
$schema: 'https://moonrepo.dev/schemas/workspace.json'
projects:
globs:
- 'apps/*'
- 'crates/*'
sources:
root: '.'

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
install-strategy="nested"
node-linker=isolated

View File

@ -1,5 +0,0 @@
# proto pins tool versions workspace-wide.
# Every developer and CI machine gets the exact same versions automatically.
moon = "2.3.3"
bun = "1.3.11"
rust = "1.97.0"

View File

@ -1,7 +0,0 @@
*.json
*.svg
*.xml
*.lock
**/dist/**
**/build/**
**/node_modules/**

121
AGENTS.md Normal file
View File

@ -0,0 +1,121 @@
# AGENTS.md
## Overview
Privacy-first video editor, with a focus on simplicity and ease of use.
## Lib vs Utils
- `lib/` - domain logic (specific to this app)
- `utils/` - small helper utils (generic, could be copy-pasted into any other app)
## Core Editor System
The editor uses a **singleton EditorCore** that manages all editor state through specialized managers.
### Architecture
```
EditorCore (singleton)
├── playback: PlaybackManager
├── timeline: TimelineManager
├── scene: SceneManager
├── project: ProjectManager
├── media: MediaManager
└── renderer: RendererManager
```
### When to Use What
#### In React Components
**Always use the `useEditor()` hook:**
```typescript
import { useEditor } from '@/hooks/use-editor';
function MyComponent() {
const editor = useEditor();
const tracks = editor.timeline.getTracks();
// Call methods
editor.timeline.addTrack({ type: 'media' });
// Display data (auto re-renders on changes)
return <div>{tracks.length} tracks</div>;
}
```
The hook:
- Returns the singleton instance
- Subscribes to all manager changes
- Automatically re-renders when state changes
#### Outside React Components
**Use `EditorCore.getInstance()` directly:**
```typescript
// In utilities, event handlers, or non-React code
import { EditorCore } from "@/core";
const editor = EditorCore.getInstance();
await editor.export({ format: "mp4", quality: "high" });
```
## Actions System
Actions are the trigger layer for user-initiated operations. The single source of truth is `@/lib/actions/definitions.ts`.
**To add a new action:**
1. Add it to `ACTIONS` in `@/lib/actions/definitions.ts`:
```typescript
export const ACTIONS = {
"my-action": {
description: "What the action does",
category: "editing",
defaultShortcuts: ["ctrl+m"],
},
// ...
};
```
2. Add handler in `@/hooks/use-editor-actions.ts`:
```typescript
useActionHandler(
"my-action",
() => {
// implementation
},
undefined,
);
```
**In components, use `invokeAction()` for user-triggered operations:**
```typescript
import { invokeAction } from '@/lib/actions';
// Good - uses action system
const handleSplit = () => invokeAction("split-selected");
// Avoid - bypasses UX layer (toasts, validation feedback)
const handleSplit = () => editor.timeline.splitElements({ ... });
```
Direct `editor.xxx()` calls are for internal use (commands, tests, complex multi-step operations).
## Commands System
Commands handle undo/redo. They live in `@/lib/commands/` organized by domain (timeline, media, scene).
Each command extends `Command` from `@/lib/commands/base-command` and implements:
- `execute()` - saves current state, then does the mutation
- `undo()` - restores the saved state
Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".

7171
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
[workspace]
resolver = "3"
members = [
'apps/desktop',
# 'crates/*',
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
[workspace.dependencies]
gpui = "0.2.2"

View File

@ -1,4 +1,4 @@
Copyright 2026 OpenCut Copyright 2025 OpenCut
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

141
README.md
View File

@ -1,78 +1,121 @@
<table width="100%"> <table width="100%">
<tr> <tr>
<td align="left" width="120"> <td align="left" width="120">
<img src="https://assets.opencut.app/branding/symbol.svg" alt="OpenCut Logo" width="100" /> <img src="apps/web/public/logos/opencut/1k/logo-white-black.png" alt="OpenCut Logo" width="100" />
</td> </td>
<td align="right"> <td align="right">
<h1>OpenCut</h1> <h1>OpenCut</span></h1>
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3> <h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
</td> </td>
</tr> </tr>
</table> </table>
[![Discord](https://img.shields.io/discord/1386309140057690133?label=Discord&logo=discord&logoColor=fff&color=5865F2&style=flat)](https://discord.gg/zmR9N35cjK) ## Sponsors
[![X](https://img.shields.io/badge/follow-%40opencutapp-000?logo=x&logoColor=fff&style=flat)](https://x.com/opencutapp)
[![License: MIT](https://img.shields.io/badge/license-MIT-green?style=flat)](LICENSE)
## Status 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.
**OpenCut is being rewritten from the ground up.** What's coming: <a href="https://vercel.com/oss">
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>
- An Editor API <a href="https://fal.ai">
- First-class third party plugins (made possible by a plugin-first architecture) <img alt="Powered by fal.ai" src="https://img.shields.io/badge/Powered%20by-fal.ai-000000?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCAxMEwxMy4wOSAxNS43NEwxMiAyMkwxMC45MSAxNS43NEw0IDEwTDEwLjkxIDguMjZMMTIgMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" />
- Desktop, mobile, and browser from one codebase (Rust core) </a>
- MCP server (for AI agents)
- Headless mode (automation, batch rendering)
- A scripting tab directly in the editor
You can still find the previous version at [opencut-app/opencut-classic](https://github.com/opencut-app/opencut-classic), which is the one to reach for today. [opencut.app](https://opencut.app) still runs the classic version. The rewrite will live at [new.opencut.app](https://new.opencut.app) until it's ready to take over. ## Why?
## Development - **Privacy**: Your videos stay on your device
- **Free features**: Most basic CapCut features are now paywalled
- **Simple**: People want editors that are easy to use - CapCut proved that
Install [proto](https://moonrepo.dev/proto) if you haven't already: ## Features
**Linux, macOS, WSL:** - Timeline-based editing
- Multi-track support
- Real-time preview
- No watermarks or subscriptions
- Analytics provided by [Databuddy](https://www.databuddy.cc?utm_source=opencut), 100% Anonymized & Non-invasive.
- Blog powered by [Marble](https://marblecms.com?utm_source=opencut), Headless CMS.
```sh ## Project Structure
bash <(curl -fsSL https://moonrepo.dev/install/proto.sh)
- `apps/web/` Main Next.js web application
- `src/components/` UI and editor components
- `src/hooks/` Custom React hooks
- `src/lib/` Utility and API logic
- `src/stores/` State management (Zustand, etc.)
- `src/types/` TypeScript types
## Getting Started
### Prerequisites
- [Bun](https://bun.sh/docs/installation)
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
> **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 and clone the repository
2. Copy the environment file:
```bash
# Unix/Linux/Mac
cp apps/web/.env.example apps/web/.env.local
# Windows PowerShell
Copy-Item apps/web/.env.example apps/web/.env.local
```
3. Start the database and Redis:
```bash
docker compose up -d db redis serverless-redis-http
```
4. Install dependencies and start the dev server:
```bash
bun install
bun dev: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
``` ```
**Windows (PowerShell):** The app will be available at [http://localhost:3100](http://localhost:3100).
```powershell
irm https://moonrepo.dev/install/proto.ps1 | iex
```
If shims fail to run, allow local scripts for your user:
```powershell
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
```
From the repo root:
```sh
proto use # installs the tools pinned in .prototools
```
```sh
moon run web:dev # localhost:5173
moon run api:dev # localhost:8787
moon run desktop:dev # see apps/desktop/README.md
```
## Contributing ## Contributing
We're not set up to take outside contributions yet while the architecture is being designed. If you want to follow along, ask questions, or just hang out, [join the Discord](https://discord.gg/zmR9N35cjK) or [open an issue](https://github.com/opencut-app/opencut/issues). We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
## Sponsors **🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
OpenCut is supported by companies that believe in open source creator tools. **⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place. See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
Want your logo here? Reach out at [sponsor@opencut.app](mailto:sponsor@opencut.app). **Quick start for contributors:**
- Fork the repo and clone locally
- Follow the setup instructions in CONTRIBUTING.md
- Create a feature branch and submit a PR
## License ## License
[MIT](LICENSE) [MIT LICENSE](LICENSE)
---
![Star History Chart](https://api.star-history.com/svg?repos=opencut-app/opencut&type=Date)

View File

@ -1,24 +0,0 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'typescript'
layer: 'application'
tasks:
dev:
command: 'bun run dev'
options:
runInCI: false # dev server: skipped in CI, never cached
build:
command: 'bun run build'
inputs:
- 'src/**/*'
- 'wrangler.jsonc'
- 'package.json'
outputs:
- 'dist'
deploy:
command: 'bun run deploy'
deps:
- '~:build'

View File

@ -1,17 +0,0 @@
{
"name": "@opencut/api",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"build": "wrangler deploy --dry-run"
},
"dependencies": {
"elysia": "latest"
},
"devDependencies": {
"@cloudflare/workers-types": "latest",
"wrangler": "latest"
}
}

View File

@ -1,15 +0,0 @@
import { Elysia, t } from "elysia";
import { CloudflareAdapter } from "elysia/adapter/cloudflare-worker";
export default new Elysia({ adapter: CloudflareAdapter })
.get("/", () => ({ status: "ok" }))
.get("/health", () => ({ healthy: true, timestamp: new Date().toISOString() }))
.post(
"/echo",
({ body }) => body,
{
body: t.Object({ message: t.String() }),
}
)
// .compile() is required, it triggers AoT compilation at startup
.compile();

View File

@ -1,7 +0,0 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "opencut-api",
"main": "src/index.ts",
// Minimum date required for CloudflareAdapter AoT support
"compatibility_date": "2025-06-01"
}

View File

@ -1,13 +0,0 @@
[package]
name = "opencut-desktop"
description = "OpenCut desktop app, built with GPUI"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "opencut-desktop"
path = "src/main.rs"
[dependencies]
gpui = { workspace = true }

View File

@ -1,25 +0,0 @@
# OpenCut Desktop
Built with [GPUI](https://www.gpui.rs).
> [!WARNING]
> Very early. Right now this is just a window that opens.
## Running
Rust is pinned in `.prototools` at the repo root (`proto use` installs it).
```sh
moon run desktop:dev # cargo run
moon run desktop:check # cargo check
moon run desktop:build # cargo build --release
```
The first build compiles GPUI from source and takes a while. The root `Cargo.lock` is committed.
## Platform requirements
- **macOS**: Xcode command line tools (Metal renderer).
- **Windows**: no extra dependencies (Win32 + DirectWrite).
- **Linux**: renders via Vulkan (Blade), windows via Wayland or X11 (both enabled by default). System packages (Debian/Ubuntu names): `libvulkan1` + working Vulkan drivers, `libwayland-dev`, `libx11-xcb-dev`, `libxkbcommon-x11-dev`, `libfontconfig-dev`, plus a C toolchain and `cmake`.
- **WSL2/WSLg**: uses XWayland automatically when available. GPUI 0.2.2 requires `xdg_wm_base` v25, while WSLg advertises v1.

View File

@ -1,28 +0,0 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'rust'
layer: 'application'
tasks:
dev:
command: 'cargo run'
options:
runInCI: false
check:
command: 'cargo check'
inputs:
- 'src/**/*'
- 'Cargo.toml'
- '/Cargo.toml'
- '/Cargo.lock'
build:
command: 'cargo build --release'
inputs:
- 'src/**/*'
- 'Cargo.toml'
- '/Cargo.toml'
- '/Cargo.lock'
outputs:
- '/target/release/opencut-desktop'

View File

@ -1,57 +0,0 @@
use gpui::{
px, size, App, AppContext, Application, Bounds, SharedString, TitlebarOptions, WindowBounds,
WindowOptions,
};
mod panels;
mod shell;
mod theme;
use shell::Shell;
fn main() {
#[cfg(target_os = "linux")]
{
let is_wsl = std::fs::read_to_string("/proc/sys/kernel/osrelease")
.is_ok_and(|release| release.to_ascii_lowercase().contains("microsoft"));
let x11_configured =
std::env::var_os("DISPLAY").is_some_and(|display| !display.is_empty());
let wayland_configured =
std::env::var_os("WAYLAND_DISPLAY").is_some_and(|display| !display.is_empty());
if is_wsl && x11_configured && wayland_configured {
// GPUI 0.2.2 requires xdg_wm_base v2+, while current WSLg advertises v1 and panics.
// Safety: this is the first statement in `main`, before GPUI or anything
// else has spawned a thread. No other thread exists yet, so keep this
// block first if anything is added above it.
unsafe {
std::env::remove_var("WAYLAND_DISPLAY");
}
}
}
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(960.), px(600.)), cx);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("OpenCut")),
..Default::default()
}),
window_bounds: Some(WindowBounds::Maximized(bounds)),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
cx.observe_window_appearance(window, |_, window, _| {
window.refresh();
})
.detach();
Shell::new(cx)
})
},
)
.expect("failed to open the main window");
});
}

View File

@ -1,23 +0,0 @@
use gpui::{div, prelude::*, Context, Window};
use crate::theme::ActiveTheme;
pub(crate) struct Browser;
impl Render for Browser {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = window.theme().colors;
div()
.flex()
.w_1_4()
.h_full()
.items_center()
.justify_center()
.border_r_1()
.border_color(colors.sidebar_border)
.bg(colors.sidebar)
.text_color(colors.sidebar_foreground)
.child("Browser")
}
}

View File

@ -1,23 +0,0 @@
use gpui::{div, prelude::*, Context, Window};
use crate::theme::ActiveTheme;
pub(crate) struct Inspector;
impl Render for Inspector {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = window.theme().colors;
div()
.flex()
.w_1_4()
.h_full()
.items_center()
.justify_center()
.border_l_1()
.border_color(colors.border)
.bg(colors.card)
.text_color(colors.card_foreground)
.child("Inspector")
}
}

View File

@ -1,9 +0,0 @@
mod browser;
mod inspector;
mod preview;
mod timeline;
pub(crate) use browser::Browser;
pub(crate) use inspector::Inspector;
pub(crate) use preview::Preview;
pub(crate) use timeline::Timeline;

View File

@ -1,20 +0,0 @@
use gpui::{div, prelude::*, Context, Window};
use crate::theme::ActiveTheme;
pub(crate) struct Preview;
impl Render for Preview {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = window.theme().colors;
div()
.flex()
.w_1_2()
.h_full()
.items_center()
.justify_center()
.bg(colors.background)
.child("Preview")
}
}

View File

@ -1,20 +0,0 @@
use gpui::{div, prelude::*, Context, Window};
use crate::theme::ActiveTheme;
pub(crate) struct Timeline;
impl Render for Timeline {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = window.theme().colors;
div()
.flex()
.h_1_3()
.items_center()
.justify_center()
.bg(colors.card)
.text_color(colors.card_foreground)
.child("Timeline")
}
}

View File

@ -1,49 +0,0 @@
use gpui::{div, prelude::*, App, Context, Entity, Window};
use crate::panels::{Browser, Inspector, Preview, Timeline};
use crate::theme::ActiveTheme;
// Panels are entities created once in `new`, not via `cx.new` inline in
// `render`, so each keeps its own state across renders instead of being
// torn down and rebuilt on every frame.
pub(crate) struct Shell {
browser: Entity<Browser>,
preview: Entity<Preview>,
inspector: Entity<Inspector>,
timeline: Entity<Timeline>,
}
impl Shell {
pub(crate) fn new(cx: &mut App) -> Self {
Self {
browser: cx.new(|_| Browser),
preview: cx.new(|_| Preview),
inspector: cx.new(|_| Inspector),
timeline: cx.new(|_| Timeline),
}
}
}
impl Render for Shell {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let colors = window.theme().colors;
div()
.flex()
.flex_col()
.size_full()
.bg(colors.background)
.text_color(colors.foreground)
.child(
div()
.flex()
.h_2_3()
.border_b_1()
.border_color(colors.border)
.child(self.browser.clone())
.child(self.preview.clone())
.child(self.inspector.clone()),
)
.child(self.timeline.clone())
}
}

View File

@ -1,222 +0,0 @@
use std::{f32::consts::PI, sync::OnceLock};
use gpui::{rems, Hsla, Rems, Rgba, Window, WindowAppearance};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SemanticColors {
pub background: Hsla,
pub foreground: Hsla,
pub card: Hsla,
pub card_foreground: Hsla,
pub popover: Hsla,
pub popover_foreground: Hsla,
pub primary: Hsla,
pub primary_foreground: Hsla,
pub secondary: Hsla,
pub secondary_foreground: Hsla,
pub muted: Hsla,
pub muted_foreground: Hsla,
pub accent: Hsla,
pub accent_foreground: Hsla,
pub destructive: Hsla,
pub border: Hsla,
pub input: Hsla,
pub ring: Hsla,
pub chart_1: Hsla,
pub chart_2: Hsla,
pub chart_3: Hsla,
pub chart_4: Hsla,
pub chart_5: Hsla,
pub sidebar: Hsla,
pub sidebar_foreground: Hsla,
pub sidebar_primary: Hsla,
pub sidebar_primary_foreground: Hsla,
pub sidebar_accent: Hsla,
pub sidebar_accent_foreground: Hsla,
pub sidebar_border: Hsla,
pub sidebar_ring: Hsla,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Theme {
pub colors: SemanticColors,
pub radius: Rems,
}
static LIGHT_THEME: OnceLock<Theme> = OnceLock::new();
static DARK_THEME: OnceLock<Theme> = OnceLock::new();
impl Theme {
pub fn for_appearance(appearance: WindowAppearance) -> &'static Self {
match appearance {
WindowAppearance::Light | WindowAppearance::VibrantLight => {
LIGHT_THEME.get_or_init(Self::light)
}
WindowAppearance::Dark | WindowAppearance::VibrantDark => {
DARK_THEME.get_or_init(Self::dark)
}
}
}
fn light() -> Self {
Self {
colors: SemanticColors {
background: oklch(1.0, 0.0, 0.0),
foreground: oklch(0.145, 0.0, 0.0),
card: oklch(1.0, 0.0, 0.0),
card_foreground: oklch(0.145, 0.0, 0.0),
popover: oklch(1.0, 0.0, 0.0),
popover_foreground: oklch(0.145, 0.0, 0.0),
primary: oklch(0.205, 0.0, 0.0),
primary_foreground: oklch(0.985, 0.0, 0.0),
secondary: oklch(0.97, 0.0, 0.0),
secondary_foreground: oklch(0.205, 0.0, 0.0),
muted: oklch(0.97, 0.0, 0.0),
muted_foreground: oklch(0.556, 0.0, 0.0),
accent: oklch(0.97, 0.0, 0.0),
accent_foreground: oklch(0.205, 0.0, 0.0),
destructive: oklch(0.577, 0.245, 27.325),
border: oklch(0.922, 0.0, 0.0),
input: oklch(0.922, 0.0, 0.0),
ring: oklch(0.708, 0.0, 0.0),
chart_1: oklch(0.87, 0.0, 0.0),
chart_2: oklch(0.556, 0.0, 0.0),
chart_3: oklch(0.439, 0.0, 0.0),
chart_4: oklch(0.371, 0.0, 0.0),
chart_5: oklch(0.269, 0.0, 0.0),
sidebar: oklch(0.985, 0.0, 0.0),
sidebar_foreground: oklch(0.145, 0.0, 0.0),
sidebar_primary: oklch(0.205, 0.0, 0.0),
sidebar_primary_foreground: oklch(0.985, 0.0, 0.0),
sidebar_accent: oklch(0.97, 0.0, 0.0),
sidebar_accent_foreground: oklch(0.205, 0.0, 0.0),
sidebar_border: oklch(0.922, 0.0, 0.0),
sidebar_ring: oklch(0.708, 0.0, 0.0),
},
radius: rems(0.625),
}
}
fn dark() -> Self {
Self {
colors: SemanticColors {
background: oklch(0.145, 0.0, 0.0),
foreground: oklch(0.985, 0.0, 0.0),
card: oklch(0.205, 0.0, 0.0),
card_foreground: oklch(0.985, 0.0, 0.0),
popover: oklch(0.205, 0.0, 0.0),
popover_foreground: oklch(0.985, 0.0, 0.0),
primary: oklch(0.922, 0.0, 0.0),
primary_foreground: oklch(0.205, 0.0, 0.0),
secondary: oklch(0.269, 0.0, 0.0),
secondary_foreground: oklch(0.985, 0.0, 0.0),
muted: oklch(0.269, 0.0, 0.0),
muted_foreground: oklch(0.708, 0.0, 0.0),
accent: oklch(0.269, 0.0, 0.0),
accent_foreground: oklch(0.985, 0.0, 0.0),
destructive: oklch(0.704, 0.191, 22.216),
border: oklch_with_alpha(1.0, 0.0, 0.0, 0.1),
input: oklch_with_alpha(1.0, 0.0, 0.0, 0.15),
ring: oklch(0.556, 0.0, 0.0),
chart_1: oklch(0.87, 0.0, 0.0),
chart_2: oklch(0.556, 0.0, 0.0),
chart_3: oklch(0.439, 0.0, 0.0),
chart_4: oklch(0.371, 0.0, 0.0),
chart_5: oklch(0.269, 0.0, 0.0),
sidebar: oklch(0.205, 0.0, 0.0),
sidebar_foreground: oklch(0.985, 0.0, 0.0),
sidebar_primary: oklch(0.488, 0.243, 264.376),
sidebar_primary_foreground: oklch(0.985, 0.0, 0.0),
sidebar_accent: oklch(0.269, 0.0, 0.0),
sidebar_accent_foreground: oklch(0.985, 0.0, 0.0),
sidebar_border: oklch_with_alpha(1.0, 0.0, 0.0, 0.1),
sidebar_ring: oklch(0.556, 0.0, 0.0),
},
radius: rems(0.625),
}
}
}
pub trait ActiveTheme {
fn theme(&self) -> &Theme;
}
impl ActiveTheme for Window {
fn theme(&self) -> &Theme {
Theme::for_appearance(self.appearance())
}
}
fn oklch(lightness: f32, chroma: f32, hue_degrees: f32) -> Hsla {
oklch_with_alpha(lightness, chroma, hue_degrees, 1.0)
}
fn oklch_with_alpha(lightness: f32, chroma: f32, hue_degrees: f32, alpha: f32) -> Hsla {
let hue_radians = hue_degrees * PI / 180.0;
let a = chroma * hue_radians.cos();
let b = chroma * hue_radians.sin();
let l = (lightness + 0.396_337_78 * a + 0.215_803_76 * b).powi(3);
let m = (lightness - 0.105_561_346 * a - 0.063_854_17 * b).powi(3);
let s = (lightness - 0.089_484_18 * a - 1.291_485_5 * b).powi(3);
let red = linear_srgb_to_srgb(4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s);
let green = linear_srgb_to_srgb(-1.268_438 * l + 2.609_757_4 * m - 0.341_319_4 * s);
let blue = linear_srgb_to_srgb(-0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s);
Hsla::from(Rgba {
r: red.clamp(0.0, 1.0),
g: green.clamp(0.0, 1.0),
b: blue.clamp(0.0, 1.0),
a: alpha.clamp(0.0, 1.0),
})
}
fn linear_srgb_to_srgb(channel: f32) -> f32 {
if channel >= 0.003_130_8 {
1.055 * channel.powf(1.0 / 2.4) - 0.055
} else {
12.92 * channel
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_neutral_oklch_endpoints() {
let black = Rgba::from(oklch(0.0, 0.0, 0.0));
let white = Rgba::from(oklch(1.0, 0.0, 0.0));
assert_eq!(
black,
Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
);
assert!((white.r - 1.0).abs() < f32::EPSILON);
assert!((white.g - 1.0).abs() < f32::EPSILON);
assert!((white.b - 1.0).abs() < f32::EPSILON);
assert!((white.a - 1.0).abs() < f32::EPSILON);
}
#[test]
fn keeps_translucent_tokens_translucent() {
assert_eq!(Theme::dark().colors.border.a, 0.1);
assert_eq!(Theme::dark().colors.input.a, 0.15);
}
#[test]
fn selects_theme_for_system_appearance() {
let light = Theme::for_appearance(WindowAppearance::Light);
let dark = Theme::for_appearance(WindowAppearance::Dark);
assert!(std::ptr::eq(light, Theme::for_appearance(WindowAppearance::VibrantLight)));
assert!(std::ptr::eq(dark, Theme::for_appearance(WindowAppearance::VibrantDark)));
assert_ne!(light.colors.background, dark.colors.background);
}
}

View File

@ -1,19 +0,0 @@
{
"projectName": "web",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": false,
"install": true,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"cloudflare",
"shadcn"
]
}

28
apps/web/.env.example Normal file
View File

@ -0,0 +1,28 @@
# Environment variables example
# Copy this file to .env.local and update the values as needed
# Node
NODE_ENV=development
# Public
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
# Server
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
MARBLE_WORKSPACE_KEY=your_workspace_key_here
FREESOUND_CLIENT_ID=your_client_id_here
FREESOUND_API_KEY=your_api_key_here
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
R2_ACCESS_KEY_ID=your_access_key_here
R2_SECRET_ACCESS_KEY=your_secret_key_here
R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket
MODAL_TRANSCRIPTION_URL=your_modal_url_here

23
apps/web/.gitignore vendored
View File

@ -1,13 +1,10 @@
node_modules # Turborepo
.DS_Store .turbo
dist
dist-ssr # Env vars
*.local .env*
.env !.env.example
.nitro
.tanstack .next/
.wrangler
.output .font-cache/
.vinxi
__unconfig*
todos.json

View File

@ -1,11 +0,0 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}

70
apps/web/Dockerfile Normal file
View File

@ -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"]

View File

@ -1,25 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "base-mira", "style": "new-york",
"rsc": false, "rsc": true,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "src/styles.css", "css": "src/app/globals.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"iconLibrary": "hugeicons", "aliases": {
"rtl": false, "components": "@/components",
"aliases": { "utils": "@/lib/utils",
"components": "#/components", "ui": "@/components/ui",
"utils": "#/lib/utils", "lib": "@/lib",
"ui": "#/components/ui", "hooks": "@/hooks"
"lib": "#/lib", },
"hooks": "#/hooks" "iconLibrary": "lucide"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
} }

View File

@ -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],
});

View File

@ -1,10 +1,8 @@
--- ---
version: "0.1.0" version: "0.1.0"
date: "2026-02-23" date: "2026-02-23"
published: true
title: "Editor foundation" title: "Editor foundation"
description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use." description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use."
summary: "Properties panel overhaul, 1,000+ fonts, blend modes, a new color picker, and direct manipulation in the preview."
changes: changes:
- type: new - 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." text: "Rebuilt the properties panel from scratch. Number inputs now support math expressions and click-drag scrubbing instead of just typing values in."

View File

@ -1,10 +1,8 @@
--- ---
version: "0.2.0" version: "0.2.0"
date: "2026-03-01" date: "2026-02-30"
published: true
title: "Motion & effects" title: "Motion & effects"
description: "This release adds the foundation for motion and effects, alongside many improvements & fixes." description: "This release adds the foundation for motion and effects, alongside many improvements & fixes."
summary: "Keyframe animation, a new effects system with per-clip blur, and ripple editing mode."
changes: changes:
- type: new - type: new
text: "Keyframe animation system for animating element properties over time." text: "Keyframe animation system for animating element properties over time."
@ -27,7 +25,7 @@ changes:
- type: improved - type: improved
text: "Selection outline in the preview is now dashed." text: "Selection outline in the preview is now dashed."
- type: improved - type: improved
text: "Clips sitting next to each other on the timeline now have a small gap between them so selected clips are always clearly visible." text: "Adjacent clips on the timeline now have a small gap between them so selected clips are always clearly visible."
- type: fixed - type: fixed
text: "Timeline trim handles now sit outside the clip instead of inside them, so the clip's visible area accurately represents where it starts and ends." text: "Timeline trim handles now sit outside the clip instead of inside them, so the clip's visible area accurately represents where it starts and ends."
- type: fixed - type: fixed

View File

@ -0,0 +1,23 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
import { webEnv } from "@opencut/env/web";
// Load the right env file based on environment
if (webEnv.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.local" });
}
export default {
schema: "./src/schema.ts",
dialect: "postgresql",
migrations: {
table: "drizzle_migrations",
},
dbCredentials: {
url: webEnv.DATABASE_URL,
},
out: "./migrations",
strict: webEnv.NODE_ENV === "production",
} satisfies Config;

View File

@ -0,0 +1,62 @@
CREATE TABLE "accounts" (
"id" text PRIMARY KEY NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
"access_token_expires_at" timestamp,
"refresh_token_expires_at" timestamp,
"scope" text,
"password" text,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "sessions" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
CONSTRAINT "sessions_token_unique" UNIQUE("token")
);
--> statement-breakpoint
ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean NOT NULL,
"image" text,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,
"created_at" timestamp,
"updated_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "waitlist" (
"id" text PRIMARY KEY NOT NULL,
"email" text NOT NULL,
"created_at" timestamp NOT NULL,
CONSTRAINT "waitlist_email_unique" UNIQUE("email")
);
--> statement-breakpoint
ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;

View File

@ -0,0 +1,344 @@
{
"id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.accounts": {
"name": "accounts",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"accounts_user_id_users_id_fk": {
"name": "accounts_user_id_users_id_fk",
"tableFrom": "accounts",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sessions_token_unique": {
"name": "sessions_token_unique",
"nullsNotDistinct": false,
"columns": ["token"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.verifications": {
"name": "verifications",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
},
"public.waitlist": {
"name": "waitlist",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"waitlist_email_unique": {
"name": "waitlist_email_unique",
"nullsNotDistinct": false,
"columns": ["email"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": true
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1750753385927,
"tag": "0000_brainy_saracen",
"breakpoints": true
}
]
}

View File

@ -1,32 +0,0 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'typescript'
layer: 'application'
tasks:
dev:
command: 'bun run dev'
options:
runInCI: false # dev server: skipped in CI, never cached
build:
command: 'bun run build'
inputs:
- 'src/**/*'
- 'public/**/*'
- 'vite.config.ts'
- 'tsconfig.json'
- 'package.json'
outputs:
- 'dist'
test:
command: 'bun run test'
inputs:
- 'src/**/*'
- 'tsconfig.json'
deploy:
command: 'bun run deploy'
deps:
- '~:build'

6
apps/web/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

58
apps/web/next.config.ts Normal file
View File

@ -0,0 +1,58 @@
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",
},
reactStrictMode: true,
productionBrowserSourceMaps: true,
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "plus.unsplash.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "images.marblecms.com",
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "api.iconify.design",
},
{
protocol: "https",
hostname: "api.simplesvg.com",
},
{
protocol: "https",
hostname: "api.unisvg.com",
},
],
},
};
export default withContentCollections(withBotId(nextConfig));

View File

@ -1,73 +1,110 @@
{ {
"name": "@opencut/web", "name": "@opencut/web",
"version": "0.1.0",
"private": true, "private": true,
"type": "module", "packageManager": "bun@1.2.18",
"imports": {
"#/*": "./src/*"
},
"scripts": { "scripts": {
"dev": "vite dev --port 5173", "dev": "next dev --turbopack",
"build": "vite build", "build": "next build",
"preview": "vite preview", "start": "next start",
"test": "vitest run", "lint": "biome check src/",
"deploy": "bun run build && wrangler deploy" "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": { "dependencies": {
"@base-ui/react": "^1.4.1", "@ffmpeg/core": "^0.12.10",
"@cloudflare/vite-plugin": "^1.26.0", "@ffmpeg/ffmpeg": "^0.12.15",
"@fontsource-variable/inter": "^5.2.8", "@ffmpeg/util": "^0.12.2",
"@hookform/resolvers": "^5.2.2", "@hello-pangea/dnd": "^18.0.1",
"@hugeicons/core-free-icons": "^4.1.2", "@hookform/resolvers": "^3.9.1",
"@hugeicons/react": "^1.1.6", "@hugeicons/core-free-icons": "^3.1.1",
"@tailwindcss/vite": "^4.1.18", "@hugeicons/react": "^1.1.4",
"@tanstack/react-devtools": "latest", "@huggingface/transformers": "^3.8.1",
"@tanstack/react-router": "latest", "@opencut/env": "workspace:*",
"@tanstack/react-router-devtools": "latest", "@opencut/ui": "workspace:*",
"@tanstack/react-router-ssr-query": "latest", "@radix-ui/react-accordion": "^1.2.12",
"@tanstack/react-start": "latest", "@radix-ui/react-checkbox": "^1.3.3",
"@tanstack/router-plugin": "^1.132.0", "@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", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.0.0",
"date-fns": "^4.1.0", "culori": "^4.0.2",
"embla-carousel-react": "^8.6.0", "dayjs": "^1.11.13",
"input-otp": "^1.4.2", "drizzle-orm": "^0.44.2",
"lucide-react": "^1.14.0", "embla-carousel-react": "^8.5.1",
"next-themes": "^0.4.6", "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", "radix-ui": "^1.4.3",
"react": "^19.2.0", "react": "^19.0.0",
"react-day-picker": "^10.0.0", "react-day-picker": "^8.10.1",
"react-dom": "^19.2.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.75.0", "react-hook-form": "^7.54.0",
"react-resizable-panels": "^4.11.0", "react-icons": "^5.4.0",
"recharts": "3.8.0", "react-markdown": "^10.1.0",
"shadcn": "^4.7.0", "react-phone-number-input": "^3.4.11",
"sonner": "^2.0.7", "react-resizable-panels": "^2.1.7",
"tailwind-merge": "^3.5.0", "react-window": "^2.2.7",
"tailwindcss": "^4.1.18", "recharts": "^2.14.1",
"tw-animate-css": "^1.4.0", "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", "vaul": "^1.1.2",
"zod": "^4.4.3" "wavesurfer.js": "^7.9.8",
"zod": "^3.25.67",
"zustand": "^5.0.2"
}, },
"devDependencies": { "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", "@tailwindcss/typography": "^0.5.16",
"@tanstack/devtools-vite": "latest", "@types/bun": "latest",
"@testing-library/dom": "^10.4.1", "@types/node": "^24.2.1",
"@testing-library/react": "^16.3.0", "@types/pg": "^8.15.4",
"@types/node": "^22.10.2", "@types/react": "^19",
"@types/react": "^19.2.0", "@types/react-dom": "^19",
"@types/react-dom": "^19.2.0", "cross-env": "^7.0.3",
"@vitejs/plugin-react": "^6.0.1", "dotenv": "^16.5.0",
"jsdom": "^28.1.0", "drizzle-kit": "^0.31.4",
"typescript": "^6.0.2", "postcss": "^8",
"vite": "^8.0.0", "raw-loader": "^4.0.2",
"vitest": "^4.1.5", "sharp": "^0.34.5",
"wrangler": "^4.70.0" "tailwindcss": "^4.1.11",
}, "tsx": "^4.7.1",
"pnpm": { "typescript": "^5.8.3"
"onlyBuiltDependencies": [
"esbuild",
"lightningcss"
]
} }
} }

View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/icons/ms-icon-70x70.png"/>
<square150x150logo src="/icons/ms-icon-150x150.png"/>
<square310x310logo src="/icons/ms-icon-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More