Compare commits
242 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
5e0696bc9b | |
|
|
bab8af831b | |
|
|
b59255cca8 | |
|
|
a2b520fbb6 | |
|
|
a53cd53f55 | |
|
|
7ae51862b6 | |
|
|
6c81db47c0 | |
|
|
8768f6dd8f | |
|
|
107d7ab714 | |
|
|
b315020416 | |
|
|
a5888e2087 | |
|
|
b601ab4b24 | |
|
|
6482461a63 | |
|
|
f402fb8337 | |
|
|
446e68e65e | |
|
|
238750c025 | |
|
|
6fdb1559aa | |
|
|
3f2d5cfcaa | |
|
|
b3d35fbc2a | |
|
|
fbe3db74d7 | |
|
|
d1f4cb615b | |
|
|
19f1086492 | |
|
|
8ab3aeba7c | |
|
|
6423b352d6 | |
|
|
7b942b363d | |
|
|
8b8d65b509 | |
|
|
23187b0b6a | |
|
|
f66cdbfeb2 | |
|
|
99137fe493 | |
|
|
79dcec8a2e | |
|
|
97f8d1968f | |
|
|
ae1292a14b | |
|
|
6ea5a156b6 | |
|
|
401cc35743 | |
|
|
b8d08df244 | |
|
|
2ee0a44735 | |
|
|
a9b9cf6bf5 | |
|
|
d6622dc6b3 | |
|
|
6ec818fc11 | |
|
|
6daa69635b | |
|
|
6b1f38a9a3 | |
|
|
56ca09692a | |
|
|
0b7597b31f | |
|
|
e6c6193638 | |
|
|
685d902740 | |
|
|
eea6d43c82 | |
|
|
6a22a3ecbd | |
|
|
0dffefdd59 | |
|
|
cceb835a84 | |
|
|
f1b45b4328 | |
|
|
7428dc75cf | |
|
|
1301d027e9 | |
|
|
f5f31b43df | |
|
|
a709af0059 | |
|
|
75eebf52bc | |
|
|
1582423dbf | |
|
|
148d38d7c9 | |
|
|
3e89d29985 | |
|
|
729d10592f | |
|
|
9da365bc01 | |
|
|
8bdc894690 | |
|
|
0387bd4c80 | |
|
|
b1d0b10354 | |
|
|
94eed96cad | |
|
|
794d4ae149 | |
|
|
011f51051c | |
|
|
c46d28891f | |
|
|
66e4367bc4 | |
|
|
8161de00e6 | |
|
|
eb28942dab | |
|
|
68f175d3be | |
|
|
d5c1b7bf0e | |
|
|
0868d9cf13 | |
|
|
8a3577d2e7 | |
|
|
ac43a5a73f | |
|
|
6312fc5b07 | |
|
|
2da93aa98f | |
|
|
f4bd689f51 | |
|
|
4f7dc5ee3a | |
|
|
4e8b7d3745 | |
|
|
ee372266ca | |
|
|
e10b0a3355 | |
|
|
3914ea9b71 | |
|
|
370944f443 | |
|
|
c839db9234 | |
|
|
769a7e708a | |
|
|
1fa5442f21 | |
|
|
dc0a8e7c96 | |
|
|
3db30c72e7 | |
|
|
eca13e3e71 | |
|
|
1943a4d18f | |
|
|
3eccc2f9e3 | |
|
|
368a20ddc2 | |
|
|
64588ceeea | |
|
|
8fe66090e7 | |
|
|
91e1232112 | |
|
|
f726d95295 | |
|
|
78a0a27a72 | |
|
|
b68bf00454 | |
|
|
cd19a4409f | |
|
|
f1ed84006a | |
|
|
8d82ea9c0b | |
|
|
6aa4ffe462 | |
|
|
8a54e5d884 | |
|
|
5e6c26f3c1 | |
|
|
520c0b3264 | |
|
|
607eaeacc7 | |
|
|
6de6c7fd95 | |
|
|
34c67ef8e8 | |
|
|
5f2f00dde0 | |
|
|
6db7ad0696 | |
|
|
24a0d10a92 | |
|
|
e3d4f3fa92 | |
|
|
d85566ab8d | |
|
|
3191e038d8 | |
|
|
ba994d8bf9 | |
|
|
270e2951b1 | |
|
|
496f7d690d | |
|
|
11ee0dbf27 | |
|
|
2c344fa01d | |
|
|
3d9d669cb9 | |
|
|
7feb3b238c | |
|
|
7d4e166678 | |
|
|
cad88ee4d1 | |
|
|
36efdf66e7 | |
|
|
2272598d42 | |
|
|
5a2aaf3601 | |
|
|
108d3f4eca | |
|
|
8230faf082 | |
|
|
dbd68ff139 | |
|
|
0d0d737563 | |
|
|
f06a930fc1 | |
|
|
532f050a64 | |
|
|
d422b602c9 | |
|
|
cbd1b82123 | |
|
|
4f7d401a97 | |
|
|
30709d7134 | |
|
|
93a0afdf64 | |
|
|
b8979c65d4 | |
|
|
f47e85c3bb | |
|
|
e43b859439 | |
|
|
e4b67094e7 | |
|
|
79df736431 | |
|
|
c411edb7c1 | |
|
|
2776adaae1 | |
|
|
964ac82d5e | |
|
|
05243616ea | |
|
|
ccd552a1c0 | |
|
|
fcc09e80e2 | |
|
|
dedb13546a | |
|
|
c56d6f8188 | |
|
|
e62227914a | |
|
|
44d8c4c876 | |
|
|
20235d0724 | |
|
|
0997466331 | |
|
|
1bd9fac203 | |
|
|
69da5bee0c | |
|
|
cb99925d75 | |
|
|
78c6c497f1 | |
|
|
22725f71fa | |
|
|
1a8a1e889a | |
|
|
3516bd69f4 | |
|
|
57fc0fd323 | |
|
|
bd9ec023ee | |
|
|
f4b9f40ab1 | |
|
|
93cc5d50f3 | |
|
|
8a1d37cf3e | |
|
|
e579ae1202 | |
|
|
b048b739bd | |
|
|
dc82826951 | |
|
|
608dc1a62f | |
|
|
f1d5932c54 | |
|
|
cc150c7cfb | |
|
|
f8fb238b22 | |
|
|
0488a6be45 | |
|
|
0f5dca4f12 | |
|
|
154b78c7d1 | |
|
|
d23be079cc | |
|
|
70a6d58b84 | |
|
|
dcf54c1250 | |
|
|
5cc6abc3a4 | |
|
|
77a14b1023 | |
|
|
1c3b22edec | |
|
|
41562baeb5 | |
|
|
df207e0f83 | |
|
|
dc5956900e | |
|
|
b095862cc0 | |
|
|
0a6ede73ae | |
|
|
7ce0018c9b | |
|
|
9528d0053b | |
|
|
689853312d | |
|
|
5a1d9ef3f4 | |
|
|
eb66e03f0e | |
|
|
a2418751e6 | |
|
|
5777a555f6 | |
|
|
220ec757d4 | |
|
|
585e28d49b | |
|
|
1261e663e1 | |
|
|
c34e01c63e | |
|
|
3e75852ffe | |
|
|
e95992b406 | |
|
|
6df9d70b9b | |
|
|
f8a0390827 | |
|
|
d862e56e34 | |
|
|
d08fbd73e1 | |
|
|
4d127a038b | |
|
|
82817cb50c | |
|
|
a54b3ef076 | |
|
|
6dd8191f4b | |
|
|
722dcdd88f | |
|
|
8db3bead13 | |
|
|
39ea298a9c | |
|
|
2c0fd4f86b | |
|
|
5e2b829f8a | |
|
|
b09635064a | |
|
|
6018efc5ac | |
|
|
7e2a913e13 | |
|
|
63496d05a2 | |
|
|
f2eed05d36 | |
|
|
26d523ebad | |
|
|
0be9275a2d | |
|
|
a2d08719f9 | |
|
|
ca3e5c98e6 | |
|
|
5ca2de25c6 | |
|
|
ed8815c755 | |
|
|
24dac1232c | |
|
|
46e55674de | |
|
|
8e8f1c3596 | |
|
|
4e0483e425 | |
|
|
31dc78637f | |
|
|
705faea56f | |
|
|
21f12ff763 | |
|
|
719e0d4398 | |
|
|
df518cbf4e | |
|
|
d9a214ef07 | |
|
|
dc58c10e64 | |
|
|
8fd9b96f80 | |
|
|
5216f4398f | |
|
|
3a2ec3c291 | |
|
|
a87b344ba5 | |
|
|
a635ae9e14 | |
|
|
88bf59d6b4 |
|
|
@ -1,172 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
---
|
|
||||||
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
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
---
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"figma": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
node_modules
|
|
||||||
.next
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
*.md
|
|
||||||
.env*
|
|
||||||
!.env.example
|
|
||||||
.cursor
|
|
||||||
.vscode
|
|
||||||
diffs
|
|
||||||
docs
|
|
||||||
agent-transcripts
|
|
||||||
mcps
|
|
||||||
terminals
|
|
||||||
apps/web/.font-cache
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
# 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!
|
|
||||||
|
|
@ -15,52 +15,23 @@ on:
|
||||||
- "*.md"
|
- "*.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
ci:
|
||||||
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
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
|
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.18
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Cache Bun modules
|
- name: Setup toolchain
|
||||||
uses: actions/cache@v4
|
uses: moonrepo/setup-toolchain@v0
|
||||||
with:
|
with:
|
||||||
path: ~/.bun/install/cache
|
auto-install: true
|
||||||
key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }}
|
auto-setup: true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Run CI
|
||||||
working-directory: apps/web
|
run: moon ci
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
node_modules/
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
docs/
|
||||||
|
|
||||||
# asdf version management
|
# moon runtime cache
|
||||||
.tool-versions
|
.moon/cache/
|
||||||
|
|
||||||
node_modules
|
|
||||||
.cursorignore
|
|
||||||
.turbo
|
|
||||||
|
|
||||||
.env*
|
|
||||||
!*.env.example
|
|
||||||
|
|
||||||
# cursor
|
|
||||||
bun.lockb
|
|
||||||
|
|
||||||
# content-collections
|
|
||||||
.content-collections
|
|
||||||
|
|
||||||
# Twiggy
|
|
||||||
.cursor/rules/file-structure.mdc
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
$schema: 'https://moonrepo.dev/schemas/toolchains.json'
|
||||||
|
|
||||||
|
javascript:
|
||||||
|
packageManager: 'bun'
|
||||||
|
|
||||||
|
bun:
|
||||||
|
version: '1.3.11'
|
||||||
|
|
||||||
|
rust: {}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
$schema: 'https://moonrepo.dev/schemas/workspace.json'
|
||||||
|
|
||||||
|
# Discover all projects under apps/ automatically.
|
||||||
|
# When shared crates land under crates/, add a 'crates/*' glob here.
|
||||||
|
projects:
|
||||||
|
- 'apps/*'
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 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"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
*.json
|
||||||
|
*.svg
|
||||||
|
*.xml
|
||||||
|
*.lock
|
||||||
|
**/dist/**
|
||||||
|
**/build/**
|
||||||
|
**/node_modules/**
|
||||||
121
AGENTS.md
|
|
@ -1,121 +0,0 @@
|
||||||
# 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)".
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = [
|
||||||
|
'apps/desktop',
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
gpui = "0.2.2"
|
||||||
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2025 OpenCut
|
Copyright 2026 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:
|
||||||
|
|
||||||
|
|
|
||||||
129
README.md
|
|
@ -1,121 +1,64 @@
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" width="120">
|
<td align="left" width="120">
|
||||||
<img src="apps/web/public/logos/opencut/1k/logo-white-black.png" alt="OpenCut Logo" width="100" />
|
<img src="https://assets.opencut.app/branding/symbol.svg" alt="OpenCut Logo" width="100" />
|
||||||
</td>
|
</td>
|
||||||
<td align="right">
|
<td align="right">
|
||||||
<h1>OpenCut</span></h1>
|
<h1>OpenCut</h1>
|
||||||
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
|
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Sponsors
|
[](https://discord.gg/zmR9N35cjK)
|
||||||
|
[](https://x.com/opencutapp)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
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.
|
## Status
|
||||||
|
|
||||||
<a href="https://vercel.com/oss">
|
**OpenCut is being rewritten from the ground up.** What's coming:
|
||||||
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://fal.ai">
|
- An Editor API
|
||||||
<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=" />
|
- First-class third party plugins (made possible by a plugin-first architecture)
|
||||||
</a>
|
- Desktop, mobile, and browser from one codebase (Rust core)
|
||||||
|
- MCP server (for AI agents)
|
||||||
|
- Headless mode (automation, batch rendering)
|
||||||
|
- A scripting tab directly in the editor
|
||||||
|
|
||||||
## Why?
|
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.
|
||||||
|
|
||||||
- **Privacy**: Your videos stay on your device
|
## Development
|
||||||
- **Free features**: Most basic CapCut features are now paywalled
|
|
||||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
|
||||||
|
|
||||||
## Features
|
Install [proto](https://moonrepo.dev/proto) if you haven't already:
|
||||||
|
|
||||||
- Timeline-based editing
|
```sh
|
||||||
- Multi-track support
|
bash <(curl -fsSL https://moonrepo.dev/install/proto.sh)
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The app will be available at [http://localhost:3100](http://localhost:3100).
|
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 welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
|
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).
|
||||||
|
|
||||||
**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
|
## Sponsors
|
||||||
|
|
||||||
**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
|
OpenCut is supported by companies that believe in open source creator tools.
|
||||||
|
|
||||||
See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
|
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place.
|
||||||
|
|
||||||
**Quick start for contributors:**
|
Want your logo here? Reach out at [sponsor@opencut.app](mailto:sponsor@opencut.app).
|
||||||
|
|
||||||
- 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](LICENSE)
|
[MIT](LICENSE)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
$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'
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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();
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
[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 }
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# 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`.
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
$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'
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
use gpui::{
|
||||||
|
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString,
|
||||||
|
TitlebarOptions, Window, WindowBounds, WindowOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Root {
|
||||||
|
status: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Root {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.bg(rgb(0x111111))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(div().text_xl().child("OpenCut"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(rgb(0x888888))
|
||||||
|
.child(self.status.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
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::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|_, cx| {
|
||||||
|
cx.new(|_| Root {
|
||||||
|
status: "desktop shell scaffold".into(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to open the main window");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
# Turborepo
|
node_modules
|
||||||
.turbo
|
.DS_Store
|
||||||
|
dist
|
||||||
# Env vars
|
dist-ssr
|
||||||
.env*
|
*.local
|
||||||
!.env.example
|
.env
|
||||||
|
.nitro
|
||||||
.next/
|
.tanstack
|
||||||
|
.wrangler
|
||||||
.font-cache/
|
.output
|
||||||
|
.vinxi
|
||||||
|
__unconfig*
|
||||||
|
todos.json
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "base-mira",
|
||||||
"rsc": true,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/styles.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"iconLibrary": "hugeicons",
|
||||||
"components": "@/components",
|
"rtl": false,
|
||||||
"utils": "@/lib/utils",
|
"aliases": {
|
||||||
"ui": "@/components/ui",
|
"components": "#/components",
|
||||||
"lib": "@/lib",
|
"utils": "#/lib/utils",
|
||||||
"hooks": "@/hooks"
|
"ui": "#/components/ui",
|
||||||
},
|
"lib": "#/lib",
|
||||||
"iconLibrary": "lucide"
|
"hooks": "#/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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],
|
|
||||||
});
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
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."
|
|
||||||
---
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
---
|
|
||||||
version: "0.2.0"
|
|
||||||
date: "2026-02-30"
|
|
||||||
title: "Motion & effects"
|
|
||||||
description: "This release adds the foundation for motion and effects, alongside many improvements & fixes."
|
|
||||||
changes:
|
|
||||||
- type: new
|
|
||||||
text: "Keyframe animation system for animating element properties over time."
|
|
||||||
- type: new
|
|
||||||
text: "Entirely new efects system. Effects can be applied per-clip or added as standalone elements on the timeline (with our first effect: Blur!)"
|
|
||||||
- type: new
|
|
||||||
text: "Added a changelog page to the site."
|
|
||||||
- type: new
|
|
||||||
text: "Ripple editing mode."
|
|
||||||
- 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."
|
|
||||||
- type: fixed
|
|
||||||
text: "Dragging a clip onto the timeline and pressing Ctrl+Z now removes both the clip and the track it created in one step, instead of requiring two undos."
|
|
||||||
- type: fixed
|
|
||||||
text: "A few values in the properties panel were previously showing incorrect values. This has been fixed."
|
|
||||||
- type: improved
|
|
||||||
text: "New text elements no longer have a black background by default."
|
|
||||||
- type: improved
|
|
||||||
text: "Selection outline in the preview is now dashed."
|
|
||||||
- type: improved
|
|
||||||
text: "Adjacent clips on the timeline now have a small gap between them so selected clips are always clearly visible."
|
|
||||||
- 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."
|
|
||||||
- type: fixed
|
|
||||||
text: "Clips being dragged on the timeline now render on top of other clips instead of underneath them."
|
|
||||||
- type: fixed
|
|
||||||
text: "Undoing a trim-from-start operation now correctly restores the clip to its original position and length, instead of stretching it to the right."
|
|
||||||
- type: fixed
|
|
||||||
text: "Resizing a clip on the timeline would deselect it when the mouse was released. The clip now stays selected after a resize."
|
|
||||||
- type: improved
|
|
||||||
text: "Press Escape to deselect the current element."
|
|
||||||
- type: fixed
|
|
||||||
text: "Resizing a clip could extend it past another clip on the same track, causing an overlap. The resize now stops at the next clip's edge."
|
|
||||||
- type: fixed
|
|
||||||
text: "Dragging a clip to a different track would deselect it on drop. The clip stays selected after moving."
|
|
||||||
- type: fixed
|
|
||||||
text: "Finishing a resize or drag with the mouse over empty track space would deselect the clip."
|
|
||||||
---
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1750753385927,
|
|
||||||
"tag": "0000_brainy_saracen",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
$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'
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/// <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.
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
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));
|
|
||||||
|
|
@ -1,110 +1,73 @@
|
||||||
{
|
{
|
||||||
"name": "@opencut/web",
|
"name": "@opencut/web",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.2.18",
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#/*": "./src/*"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "vite dev --port 5173",
|
||||||
"build": "next build",
|
"build": "vite build",
|
||||||
"start": "next start",
|
"preview": "vite preview",
|
||||||
"lint": "biome check src/",
|
"test": "vitest run",
|
||||||
"lint:fix": "biome check src/ --write",
|
"deploy": "bun run build && wrangler deploy"
|
||||||
"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": {
|
||||||
"@ffmpeg/core": "^0.12.10",
|
"@base-ui/react": "^1.4.1",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@cloudflare/vite-plugin": "^1.26.0",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hugeicons/core-free-icons": "^4.1.2",
|
||||||
"@hugeicons/core-free-icons": "^3.1.1",
|
"@hugeicons/react": "^1.1.6",
|
||||||
"@hugeicons/react": "^1.1.4",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@huggingface/transformers": "^3.8.1",
|
"@tanstack/react-devtools": "latest",
|
||||||
"@opencut/env": "workspace:*",
|
"@tanstack/react-router": "latest",
|
||||||
"@opencut/ui": "workspace:*",
|
"@tanstack/react-router-devtools": "latest",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@tanstack/react-router-ssr-query": "latest",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@tanstack/react-start": "latest",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@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.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"culori": "^4.0.2",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"drizzle-orm": "^0.44.2",
|
"input-otp": "^1.4.2",
|
||||||
"embla-carousel-react": "^8.5.1",
|
"lucide-react": "^1.14.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"next-themes": "^0.4.6",
|
||||||
"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.0.0",
|
"react": "^19.2.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^10.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.75.0",
|
||||||
"react-icons": "^5.4.0",
|
"react-resizable-panels": "^4.11.0",
|
||||||
"react-markdown": "^10.1.0",
|
"recharts": "3.8.0",
|
||||||
"react-phone-number-input": "^3.4.11",
|
"shadcn": "^4.7.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"sonner": "^2.0.7",
|
||||||
"react-window": "^2.2.7",
|
"tailwind-merge": "^3.5.0",
|
||||||
"recharts": "^2.14.1",
|
"tailwindcss": "^4.1.18",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"tw-animate-css": "^1.4.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",
|
||||||
"wavesurfer.js": "^7.9.8",
|
"zod": "^4.4.3"
|
||||||
"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",
|
||||||
"@types/bun": "latest",
|
"@tanstack/devtools-vite": "latest",
|
||||||
"@types/node": "^24.2.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/pg": "^8.15.4",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/react": "^19",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react-dom": "^19",
|
"@types/react": "^19.2.0",
|
||||||
"cross-env": "^7.0.3",
|
"@types/react-dom": "^19.2.0",
|
||||||
"dotenv": "^16.5.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"drizzle-kit": "^0.31.4",
|
"jsdom": "^28.1.0",
|
||||||
"postcss": "^8",
|
"typescript": "^6.0.2",
|
||||||
"raw-loader": "^4.0.2",
|
"vite": "^8.0.0",
|
||||||
"sharp": "^0.34.5",
|
"vitest": "^4.1.5",
|
||||||
"tailwindcss": "^4.1.11",
|
"wrangler": "^4.70.0"
|
||||||
"tsx": "^4.7.1",
|
},
|
||||||
"typescript": "^5.8.3"
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild",
|
||||||
|
"lightningcss"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 768 B |
|
Before Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 843 B |
|
Before Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 820 B |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 814 B |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |