Spaces:
Runtime error
Runtime error
Upload 84 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .eslintrc.cjs +18 -0
- .gitignore +26 -0
- CHANGELOG.md +94 -0
- README.md +46 -12
- components.json +17 -0
- images/preview.png +0 -0
- index.html +29 -0
- package.json +60 -0
- pnpm-lock.yaml +0 -0
- postcss.config.js +6 -0
- public/vite.svg +1 -0
- src/App.css +42 -0
- src/App.tsx +38 -0
- src/assets/logo-dark-full.png +0 -0
- src/assets/logo-dark.png +0 -0
- src/assets/react.svg +1 -0
- src/components/editor/control-item/animations.tsx +76 -0
- src/components/editor/control-item/basic-audio.tsx +11 -0
- src/components/editor/control-item/basic-image.tsx +11 -0
- src/components/editor/control-item/basic-text.tsx +439 -0
- src/components/editor/control-item/basic-video.tsx +11 -0
- src/components/editor/control-item/common/opacity.tsx +48 -0
- src/components/editor/control-item/common/transform.tsx +102 -0
- src/components/editor/control-item/control-item.tsx +104 -0
- src/components/editor/control-item/index.tsx +1 -0
- src/components/editor/control-item/presets.tsx +11 -0
- src/components/editor/control-item/smart.tsx +11 -0
- src/components/editor/control-list.tsx +237 -0
- src/components/editor/editor.tsx +63 -0
- src/components/editor/index.ts +1 -0
- src/components/editor/menu-item/audios.tsx +68 -0
- src/components/editor/menu-item/elements.tsx +9 -0
- src/components/editor/menu-item/images.tsx +61 -0
- src/components/editor/menu-item/index.tsx +1 -0
- src/components/editor/menu-item/menu-item.tsx +72 -0
- src/components/editor/menu-item/texts.tsx +44 -0
- src/components/editor/menu-item/transitions.tsx +90 -0
- src/components/editor/menu-item/uploads.tsx +132 -0
- src/components/editor/menu-item/videos.tsx +50 -0
- src/components/editor/menu-list.tsx +132 -0
- src/components/editor/navbar.tsx +336 -0
- src/components/editor/resize-video.tsx +3 -0
- src/components/editor/use-hotkeys.ts +105 -0
- src/components/shared/draggable.tsx +81 -0
- src/components/shared/icons.tsx +253 -0
- src/components/theme-provider.tsx +73 -0
- src/components/ui/avatar.tsx +48 -0
- src/components/ui/button.tsx +57 -0
- src/components/ui/command.tsx +153 -0
- src/components/ui/dialog.tsx +120 -0
.eslintrc.cjs
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
root: true,
|
3 |
+
env: { browser: true, es2020: true },
|
4 |
+
extends: [
|
5 |
+
'eslint:recommended',
|
6 |
+
'plugin:@typescript-eslint/recommended',
|
7 |
+
'plugin:react-hooks/recommended',
|
8 |
+
],
|
9 |
+
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
10 |
+
parser: '@typescript-eslint/parser',
|
11 |
+
plugins: ['react-refresh'],
|
12 |
+
rules: {
|
13 |
+
'react-refresh/only-export-components': [
|
14 |
+
'warn',
|
15 |
+
{ allowConstantExport: true },
|
16 |
+
],
|
17 |
+
},
|
18 |
+
}
|
.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
25 |
+
.vercel
|
26 |
+
.config
|
CHANGELOG.md
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# video-editor
|
2 |
+
|
3 |
+
## 0.0.13
|
4 |
+
|
5 |
+
### Patch Changes
|
6 |
+
|
7 |
+
- Updated dependencies
|
8 |
+
- @designcombo/[email protected]
|
9 |
+
|
10 |
+
## 0.0.12
|
11 |
+
|
12 |
+
### Patch Changes
|
13 |
+
|
14 |
+
- Updated dependencies
|
15 |
+
- @designcombo/[email protected]
|
16 |
+
|
17 |
+
## 0.0.11
|
18 |
+
|
19 |
+
### Patch Changes
|
20 |
+
|
21 |
+
- Updated dependencies
|
22 |
+
- @designcombo/[email protected]
|
23 |
+
|
24 |
+
## 0.0.10
|
25 |
+
|
26 |
+
### Patch Changes
|
27 |
+
|
28 |
+
- Updated dependencies
|
29 |
+
- @designcombo/[email protected]
|
30 |
+
|
31 |
+
## 0.0.9
|
32 |
+
|
33 |
+
### Patch Changes
|
34 |
+
|
35 |
+
- Updated dependencies
|
36 |
+
- @designcombo/[email protected]
|
37 |
+
|
38 |
+
## 0.0.8
|
39 |
+
|
40 |
+
### Patch Changes
|
41 |
+
|
42 |
+
- Updated dependencies
|
43 |
+
- @designcombo/[email protected]
|
44 |
+
|
45 |
+
## 0.0.7
|
46 |
+
|
47 |
+
### Patch Changes
|
48 |
+
|
49 |
+
- Updated dependencies
|
50 |
+
- @designcombo/[email protected]
|
51 |
+
|
52 |
+
## 0.0.6
|
53 |
+
|
54 |
+
### Patch Changes
|
55 |
+
|
56 |
+
- Updated dependencies
|
57 |
+
- @designcombo/[email protected]
|
58 |
+
|
59 |
+
## 0.0.5
|
60 |
+
|
61 |
+
### Patch Changes
|
62 |
+
|
63 |
+
- Updated dependencies
|
64 |
+
- @designcombo/[email protected]
|
65 |
+
|
66 |
+
## 0.0.4
|
67 |
+
|
68 |
+
### Patch Changes
|
69 |
+
|
70 |
+
- Updated dependencies
|
71 |
+
- @designcombo/[email protected]
|
72 |
+
|
73 |
+
## 0.0.3
|
74 |
+
|
75 |
+
### Patch Changes
|
76 |
+
|
77 |
+
- Updated dependencies
|
78 |
+
- @designcombo/[email protected]
|
79 |
+
|
80 |
+
## 0.0.2
|
81 |
+
|
82 |
+
### Patch Changes
|
83 |
+
|
84 |
+
- Updated dependencies
|
85 |
+
- @designcombo/[email protected]
|
86 |
+
|
87 |
+
## 0.0.1
|
88 |
+
|
89 |
+
### Patch Changes
|
90 |
+
|
91 |
+
- Updated dependencies [9439fb3]
|
92 |
+
- Updated dependencies [9439fb3]
|
93 |
+
- @designcombo/[email protected]
|
94 |
+
- @designcombo/[email protected]
|
README.md
CHANGED
@@ -1,12 +1,46 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<p align="center">
|
2 |
+
<a href="https://github.com/lh-sn/react-video-editor">
|
3 |
+
<img width="150px" height="150px" src="https://ik.imagekit.io/snapmotion/logo-preview-public.svg"/>
|
4 |
+
</a>
|
5 |
+
</p>
|
6 |
+
<h1 align="center">React Video Editor</h1>
|
7 |
+
|
8 |
+
<div align="center">
|
9 |
+
|
10 |
+
Video Editor application using React and TypeScript.
|
11 |
+
|
12 |
+
<p align="center">
|
13 |
+
<a href="https://github.com/lh-sn/react-video-editor">Wombo</a>
|
14 |
+
·
|
15 |
+
<a href="https://discord.gg/jrZs3wZyM5">Discord</a>
|
16 |
+
·
|
17 |
+
<a href="https://github.com/lh-sn/react-video-editor">X</a>
|
18 |
+
</p>
|
19 |
+
</div>
|
20 |
+
|
21 |
+
[](https://github.com/lh-sn/react-video-editor)
|
22 |
+
|
23 |
+
## ✨ Features
|
24 |
+
|
25 |
+
- 🎬 Timeline Editing: Arrange and trim media on a visual timeline.
|
26 |
+
- 🌟 Effects and Transitions: Apply visual effects, filters, and transitions.
|
27 |
+
- 🔀 Multi-track Support: Edit multiple video and audio tracks simultaneously.
|
28 |
+
- 📤 Export Options: Save videos in various resolutions and formats.
|
29 |
+
- 👀 Real-time Preview: See immediate previews of edits.
|
30 |
+
|
31 |
+
## ⌨️ Development
|
32 |
+
|
33 |
+
Clone locally:
|
34 |
+
|
35 |
+
```bash
|
36 |
+
$ git clone [email protected]:designcombo/react-video-editor.git
|
37 |
+
$ cd react-video-editor
|
38 |
+
$ pnpm install
|
39 |
+
$ pnpm dev
|
40 |
+
```
|
41 |
+
|
42 |
+
Open your browser and visit http://127.0.0.1:8001 , see more at [Development](https://github.com/lh-sn/react-video-editor/react-video-editor).
|
43 |
+
|
44 |
+
## 📝 License
|
45 |
+
|
46 |
+
Copyright © 2023 [DesignCombo](https://github.com/lh-sn/react-video-editor).
|
components.json
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
3 |
+
"style": "default",
|
4 |
+
"rsc": false,
|
5 |
+
"tsx": true,
|
6 |
+
"tailwind": {
|
7 |
+
"config": "tailwind.config.js",
|
8 |
+
"css": "src/globals.css",
|
9 |
+
"baseColor": "slate",
|
10 |
+
"cssVariables": true,
|
11 |
+
"prefix": ""
|
12 |
+
},
|
13 |
+
"aliases": {
|
14 |
+
"components": "@/components",
|
15 |
+
"utils": "@/lib/utils"
|
16 |
+
}
|
17 |
+
}
|
images/preview.png
ADDED
![]() |
index.html
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Vite + React + TS</title>
|
8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
10 |
+
<link
|
11 |
+
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
|
12 |
+
rel="stylesheet"
|
13 |
+
/>
|
14 |
+
|
15 |
+
<link
|
16 |
+
href="https://fonts.googleapis.com/css2?family=Outfit:[email protected]&display=swap"
|
17 |
+
rel="stylesheet"
|
18 |
+
/>
|
19 |
+
|
20 |
+
<link
|
21 |
+
href="https://fonts.googleapis.com/css2?family=Reddit+Mono:[email protected]&display=swap"
|
22 |
+
rel="stylesheet"
|
23 |
+
/>
|
24 |
+
</head>
|
25 |
+
<body>
|
26 |
+
<div id="root"></div>
|
27 |
+
<script type="module" src="/src/main.tsx"></script>
|
28 |
+
</body>
|
29 |
+
</html>
|
package.json
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "video-editor",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.13",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "tsc && vite build",
|
9 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
10 |
+
"preview": "vite preview"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"@designcombo/core": "0.7.0",
|
14 |
+
"@hookform/resolvers": "^3.6.0",
|
15 |
+
"@radix-ui/react-avatar": "^1.0.4",
|
16 |
+
"@radix-ui/react-dialog": "^1.1.1",
|
17 |
+
"@radix-ui/react-hover-card": "^1.1.1",
|
18 |
+
"@radix-ui/react-label": "^2.0.2",
|
19 |
+
"@radix-ui/react-popover": "^1.0.7",
|
20 |
+
"@radix-ui/react-progress": "^1.1.0",
|
21 |
+
"@radix-ui/react-scroll-area": "^1.0.5",
|
22 |
+
"@radix-ui/react-slider": "^1.1.2",
|
23 |
+
"@radix-ui/react-slot": "^1.0.2",
|
24 |
+
"@radix-ui/react-tabs": "^1.1.0",
|
25 |
+
"@radix-ui/react-toggle": "^1.1.0",
|
26 |
+
"@radix-ui/react-toggle-group": "^1.1.0",
|
27 |
+
"class-variance-authority": "^0.7.0",
|
28 |
+
"clsx": "^2.1.1",
|
29 |
+
"cmdk": "^1.0.0",
|
30 |
+
"hotkeys-js": "^3.13.7",
|
31 |
+
"lodash": "^4.17.21",
|
32 |
+
"lucide-react": "^0.378.0",
|
33 |
+
"nanoid": "^5.0.7",
|
34 |
+
"react": "^18.2.0",
|
35 |
+
"react-colorful": "^5.6.1",
|
36 |
+
"react-dom": "^18.2.0",
|
37 |
+
"react-hook-form": "^7.51.5",
|
38 |
+
"tailwind-merge": "^2.3.0",
|
39 |
+
"tailwindcss-animate": "^1.0.7",
|
40 |
+
"zod": "^3.23.8",
|
41 |
+
"zustand": "^4.5.2"
|
42 |
+
},
|
43 |
+
"devDependencies": {
|
44 |
+
"@types/lodash": "^4.17.5",
|
45 |
+
"@types/node": "^20.12.12",
|
46 |
+
"@types/react": "^18.2.66",
|
47 |
+
"@types/react-dom": "^18.2.22",
|
48 |
+
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
49 |
+
"@typescript-eslint/parser": "^7.2.0",
|
50 |
+
"@vitejs/plugin-react": "^4.2.1",
|
51 |
+
"autoprefixer": "^10.4.19",
|
52 |
+
"eslint": "^8.57.0",
|
53 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
54 |
+
"eslint-plugin-react-refresh": "^0.4.6",
|
55 |
+
"postcss": "^8.4.38",
|
56 |
+
"tailwindcss": "^3.4.3",
|
57 |
+
"typescript": "^5.2.2",
|
58 |
+
"vite": "^5.2.0"
|
59 |
+
}
|
60 |
+
}
|
pnpm-lock.yaml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
}
|
public/vite.svg
ADDED
|
src/App.css
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#root {
|
2 |
+
max-width: 1280px;
|
3 |
+
margin: 0 auto;
|
4 |
+
padding: 2rem;
|
5 |
+
text-align: center;
|
6 |
+
}
|
7 |
+
|
8 |
+
.logo {
|
9 |
+
height: 6em;
|
10 |
+
padding: 1.5em;
|
11 |
+
will-change: filter;
|
12 |
+
transition: filter 300ms;
|
13 |
+
}
|
14 |
+
.logo:hover {
|
15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
16 |
+
}
|
17 |
+
.logo.react:hover {
|
18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
19 |
+
}
|
20 |
+
|
21 |
+
@keyframes logo-spin {
|
22 |
+
from {
|
23 |
+
transform: rotate(0deg);
|
24 |
+
}
|
25 |
+
to {
|
26 |
+
transform: rotate(360deg);
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
@media (prefers-reduced-motion: no-preference) {
|
31 |
+
a:nth-of-type(2) .logo {
|
32 |
+
animation: logo-spin infinite 20s linear;
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
.card {
|
37 |
+
padding: 2em;
|
38 |
+
}
|
39 |
+
|
40 |
+
.read-the-docs {
|
41 |
+
color: #888;
|
42 |
+
}
|
src/App.tsx
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect } from 'react';
|
2 |
+
import useDataState from './store/use-data-state';
|
3 |
+
import { getCompactFontData } from './utils/fonts';
|
4 |
+
import { FONTS } from './data/fonts';
|
5 |
+
import { Editor } from './components/editor';
|
6 |
+
|
7 |
+
export const theme = {
|
8 |
+
colors: {
|
9 |
+
gray: {
|
10 |
+
50: '#fafafa',
|
11 |
+
100: '#f4f4f5',
|
12 |
+
200: '#e4e4e7',
|
13 |
+
300: '#d4d4d8',
|
14 |
+
400: '#a1a1aa',
|
15 |
+
500: '#71717a',
|
16 |
+
600: '#52525b',
|
17 |
+
700: '#3f3f46',
|
18 |
+
800: '#27272a',
|
19 |
+
900: '#18181b',
|
20 |
+
950: '#09090b',
|
21 |
+
1000: '#040405',
|
22 |
+
1100: '#010101',
|
23 |
+
},
|
24 |
+
},
|
25 |
+
};
|
26 |
+
|
27 |
+
function App() {
|
28 |
+
const { setCompactFonts, setFonts } = useDataState();
|
29 |
+
|
30 |
+
useEffect(() => {
|
31 |
+
setCompactFonts(getCompactFontData(FONTS));
|
32 |
+
setFonts(FONTS);
|
33 |
+
}, []);
|
34 |
+
|
35 |
+
return <Editor />;
|
36 |
+
}
|
37 |
+
|
38 |
+
export default App;
|
src/assets/logo-dark-full.png
ADDED
![]() |
src/assets/logo-dark.png
ADDED
![]() |
src/assets/react.svg
ADDED
|
src/components/editor/control-item/animations.tsx
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
2 |
+
import {
|
3 |
+
ANIMATIONS,
|
4 |
+
EDIT_OBJECT,
|
5 |
+
IAnimate,
|
6 |
+
dispatcher,
|
7 |
+
useEditorState,
|
8 |
+
} from '@designcombo/core';
|
9 |
+
import React from 'react';
|
10 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
11 |
+
|
12 |
+
const Animations = () => {
|
13 |
+
const handleOnClick = (animation: any) => {
|
14 |
+
dispatcher.dispatch(EDIT_OBJECT, {
|
15 |
+
payload: {
|
16 |
+
animation: {
|
17 |
+
[animation.type]: {
|
18 |
+
name: animation.name,
|
19 |
+
},
|
20 |
+
},
|
21 |
+
},
|
22 |
+
});
|
23 |
+
};
|
24 |
+
const { trackItemsMap, activeIds } = useEditorState();
|
25 |
+
const targetType = trackItemsMap[activeIds[0]]?.type;
|
26 |
+
const filteredAnimations =
|
27 |
+
targetType === 'text'
|
28 |
+
? ANIMATIONS
|
29 |
+
: ANIMATIONS.filter((animation) => animation.category === 'element');
|
30 |
+
return (
|
31 |
+
<div className="flex-1 flex flex-col">
|
32 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
33 |
+
Animations
|
34 |
+
</div>
|
35 |
+
<div className="px-4">
|
36 |
+
<Tabs defaultValue="in" className="w-full">
|
37 |
+
<TabsList className="grid w-full grid-cols-2">
|
38 |
+
<TabsTrigger value="in">In</TabsTrigger>
|
39 |
+
<TabsTrigger value="out">Out</TabsTrigger>
|
40 |
+
</TabsList>
|
41 |
+
<TabsContent value="in">
|
42 |
+
{filteredAnimations
|
43 |
+
.filter((i) => i.type === 'in')
|
44 |
+
.map((animation, index) => (
|
45 |
+
<div
|
46 |
+
onClick={() => handleOnClick(animation)}
|
47 |
+
key={index}
|
48 |
+
className="flex items-center gap-2 cursor-pointer"
|
49 |
+
>
|
50 |
+
<div className="w-8 h-8 bg-zinc-600"></div>
|
51 |
+
<div> {animation.name || animation.type}</div>
|
52 |
+
</div>
|
53 |
+
))}
|
54 |
+
</TabsContent>
|
55 |
+
<TabsContent value="out">
|
56 |
+
{' '}
|
57 |
+
{filteredAnimations
|
58 |
+
.filter((i) => i.type === 'out')
|
59 |
+
.map((animation, index) => (
|
60 |
+
<div
|
61 |
+
onClick={() => handleOnClick(animation)}
|
62 |
+
key={index}
|
63 |
+
className="flex items-center gap-2 cursor-pointer"
|
64 |
+
>
|
65 |
+
<div className="w-8 h-8 bg-zinc-600"></div>
|
66 |
+
<div> {animation.name || animation.type}</div>
|
67 |
+
</div>
|
68 |
+
))}
|
69 |
+
</TabsContent>
|
70 |
+
</Tabs>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
);
|
74 |
+
};
|
75 |
+
|
76 |
+
export default Animations;
|
src/components/editor/control-item/basic-audio.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BasicAudio = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1 flex flex-col">
|
4 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
BasicAudio
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
10 |
+
|
11 |
+
export default BasicAudio;
|
src/components/editor/control-item/basic-image.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BasicImage = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1 flex flex-col">
|
4 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
BasicImage
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
10 |
+
|
11 |
+
export default BasicImage;
|
src/components/editor/control-item/basic-text.tsx
ADDED
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import {
|
3 |
+
Popover,
|
4 |
+
PopoverContent,
|
5 |
+
PopoverTrigger,
|
6 |
+
} from '@/components/ui/popover';
|
7 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
8 |
+
import { DEFAULT_FONT } from '@/data/fonts';
|
9 |
+
import { ICompactFont, IFont } from '@/interfaces/editor';
|
10 |
+
import useDataState from '@/store/use-data-state';
|
11 |
+
import { loadFonts } from '@/utils/fonts';
|
12 |
+
import { EDIT_OBJECT, ITrackItem, dispatcher } from '@designcombo/core';
|
13 |
+
import { ChevronDown, Ellipsis, Strikethrough, Underline } from 'lucide-react';
|
14 |
+
import { useEffect, useState } from 'react';
|
15 |
+
import Opacity from './common/opacity';
|
16 |
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
17 |
+
import { AlignCenter, AlignLeft, AlignRight } from 'lucide-react';
|
18 |
+
import { Input } from '@/components/ui/input';
|
19 |
+
import Transform from './common/transform';
|
20 |
+
|
21 |
+
interface ITextControlProps {
|
22 |
+
color: string;
|
23 |
+
colorDisplay: string;
|
24 |
+
fontSize: number;
|
25 |
+
fontSizeDisplay: string;
|
26 |
+
fontFamily: string;
|
27 |
+
fontFamilyDisplay: string;
|
28 |
+
opacity: number;
|
29 |
+
opacityDisplay: string;
|
30 |
+
textAlign: string;
|
31 |
+
textDecoration: string;
|
32 |
+
}
|
33 |
+
|
34 |
+
const getStyleNameFromFontName = (fontName: string) => {
|
35 |
+
const fontFamilyEnd = fontName.lastIndexOf('-');
|
36 |
+
const styleName = fontName
|
37 |
+
.substring(fontFamilyEnd + 1)
|
38 |
+
.replace('Italic', ' Italic');
|
39 |
+
return styleName;
|
40 |
+
};
|
41 |
+
|
42 |
+
const BasicText = ({ trackItem }: { trackItem: ITrackItem }) => {
|
43 |
+
const [properties, setProperties] = useState<ITextControlProps>({
|
44 |
+
color: '#000000',
|
45 |
+
colorDisplay: '#000000',
|
46 |
+
fontSize: 12,
|
47 |
+
fontSizeDisplay: '12px',
|
48 |
+
fontFamily: 'Open Sans',
|
49 |
+
fontFamilyDisplay: 'Open Sans',
|
50 |
+
opacity: 1,
|
51 |
+
opacityDisplay: '100%',
|
52 |
+
textAlign: 'left',
|
53 |
+
textDecoration: 'none',
|
54 |
+
});
|
55 |
+
|
56 |
+
const [selectedFont, setSelectedFont] = useState<ICompactFont>({
|
57 |
+
family: 'Open Sans',
|
58 |
+
styles: [],
|
59 |
+
default: DEFAULT_FONT,
|
60 |
+
name: 'Regular',
|
61 |
+
});
|
62 |
+
const { compactFonts, fonts } = useDataState();
|
63 |
+
|
64 |
+
useEffect(() => {
|
65 |
+
const fontFamily =
|
66 |
+
trackItem.details.fontFamily || DEFAULT_FONT.postScriptName;
|
67 |
+
const currentFont = fonts.find(
|
68 |
+
(font) => font.postScriptName === fontFamily,
|
69 |
+
);
|
70 |
+
const selectedFont = compactFonts.find(
|
71 |
+
(font) => font.family === currentFont?.family,
|
72 |
+
);
|
73 |
+
|
74 |
+
setSelectedFont({
|
75 |
+
...selectedFont,
|
76 |
+
name: getStyleNameFromFontName(currentFont.postScriptName),
|
77 |
+
});
|
78 |
+
|
79 |
+
if (trackItem.details.opacityDisplay == undefined) {
|
80 |
+
trackItem.details.opacityDisplay = '100';
|
81 |
+
}
|
82 |
+
|
83 |
+
if (trackItem.details.fontSizeDisplay == undefined) {
|
84 |
+
trackItem.details.fontSizeDisplay = '62';
|
85 |
+
}
|
86 |
+
setProperties({
|
87 |
+
color: trackItem.details.color || '#ffffff',
|
88 |
+
colorDisplay: trackItem.details.color || '#ffffff',
|
89 |
+
fontSize: trackItem.details.fontSize || 62,
|
90 |
+
fontSizeDisplay: (trackItem.details.fontSize || 62) + 'px',
|
91 |
+
fontFamily: selectedFont?.family || 'Open Sans',
|
92 |
+
fontFamilyDisplay: selectedFont?.family || 'Open Sans',
|
93 |
+
opacity: trackItem.details.opacity || 1,
|
94 |
+
opacityDisplay: (trackItem.details.opacityDisplay || '100') + '%',
|
95 |
+
textAlign: trackItem.details.textAlign || 'left',
|
96 |
+
textDecoration: trackItem.details.textDecoration || 'none',
|
97 |
+
});
|
98 |
+
}, [trackItem.id]);
|
99 |
+
|
100 |
+
const handleChangeFont = async (font: ICompactFont) => {
|
101 |
+
const fontName = font.default.postScriptName;
|
102 |
+
const fontUrl = font.default.url;
|
103 |
+
|
104 |
+
await loadFonts([
|
105 |
+
{
|
106 |
+
name: fontName,
|
107 |
+
url: fontUrl,
|
108 |
+
},
|
109 |
+
]);
|
110 |
+
setSelectedFont({ ...font, name: getStyleNameFromFontName(fontName) });
|
111 |
+
setProperties({
|
112 |
+
...properties,
|
113 |
+
fontFamily: font.default.family,
|
114 |
+
fontFamilyDisplay: font.default.family,
|
115 |
+
});
|
116 |
+
|
117 |
+
dispatcher.dispatch(EDIT_OBJECT, {
|
118 |
+
payload: {
|
119 |
+
details: {
|
120 |
+
fontFamily: fontName,
|
121 |
+
fontUrl: fontUrl,
|
122 |
+
},
|
123 |
+
},
|
124 |
+
});
|
125 |
+
};
|
126 |
+
|
127 |
+
const handleChangeFontStyle = async (font: IFont) => {
|
128 |
+
const fontName = font.postScriptName;
|
129 |
+
const fontUrl = font.url;
|
130 |
+
const styleName = getStyleNameFromFontName(fontName);
|
131 |
+
await loadFonts([
|
132 |
+
{
|
133 |
+
name: fontName,
|
134 |
+
url: fontUrl,
|
135 |
+
},
|
136 |
+
]);
|
137 |
+
setSelectedFont({ ...selectedFont, name: styleName });
|
138 |
+
dispatcher.dispatch(EDIT_OBJECT, {
|
139 |
+
payload: {
|
140 |
+
details: {
|
141 |
+
fontFamily: fontName,
|
142 |
+
fontUrl: fontUrl,
|
143 |
+
},
|
144 |
+
},
|
145 |
+
});
|
146 |
+
};
|
147 |
+
|
148 |
+
return (
|
149 |
+
<div className="flex-1 flex flex-col">
|
150 |
+
<div className="text-md text-text-primary font-medium h-12 flex items-center px-4 flex-none">
|
151 |
+
Text
|
152 |
+
</div>
|
153 |
+
<ScrollArea className="h-full">
|
154 |
+
<div className="px-4 flex flex-col gap-2">
|
155 |
+
<div className="flex flex-col gap-2">
|
156 |
+
<div className="flex gap-2">
|
157 |
+
<FontFamily
|
158 |
+
handleChangeFont={handleChangeFont}
|
159 |
+
fontFamilyDisplay={properties.fontFamilyDisplay}
|
160 |
+
/>
|
161 |
+
</div>
|
162 |
+
<div className="grid grid-cols-2 gap-2">
|
163 |
+
<FontStyle
|
164 |
+
selectedFont={selectedFont}
|
165 |
+
handleChangeFontStyle={handleChangeFontStyle}
|
166 |
+
/>
|
167 |
+
<div className="relative">
|
168 |
+
<Input className="h-9" defaultValue={88} />
|
169 |
+
<div className="absolute top-1/2 transform -translate-y-1/2 right-2.5 text-sm text-zinc-200">
|
170 |
+
px
|
171 |
+
</div>
|
172 |
+
</div>
|
173 |
+
</div>
|
174 |
+
</div>
|
175 |
+
<div className="grid grid-cols-2 gap-2">
|
176 |
+
<Alignment />
|
177 |
+
<TextDecoration />
|
178 |
+
</div>
|
179 |
+
</div>
|
180 |
+
<div className="p-4 flex flex-col gap-2">
|
181 |
+
<div className="text-sm">Style</div>
|
182 |
+
<Fill />
|
183 |
+
<Stroke />
|
184 |
+
<Shadow />
|
185 |
+
<Background />
|
186 |
+
</div>
|
187 |
+
|
188 |
+
<div className="p-4">
|
189 |
+
<Opacity />
|
190 |
+
</div>
|
191 |
+
<div className="p-4">
|
192 |
+
<Transform />
|
193 |
+
</div>
|
194 |
+
</ScrollArea>
|
195 |
+
</div>
|
196 |
+
);
|
197 |
+
};
|
198 |
+
|
199 |
+
const Fill = () => {
|
200 |
+
return (
|
201 |
+
<div
|
202 |
+
style={{
|
203 |
+
display: 'grid',
|
204 |
+
gridTemplateColumns: '1fr 24px 24px',
|
205 |
+
gap: '4px',
|
206 |
+
}}
|
207 |
+
>
|
208 |
+
<div className="text-sm text-zinc-500 flex items-center">Fill</div>
|
209 |
+
<div>
|
210 |
+
<div className="w-6 h-6 rounded-sm border-2 border-zinc-800 bg-green-700"></div>
|
211 |
+
</div>
|
212 |
+
<div>
|
213 |
+
<Button size="icon" variant="ghost" className="h-6 w-6">
|
214 |
+
<Ellipsis size={14} />
|
215 |
+
</Button>
|
216 |
+
</div>
|
217 |
+
</div>
|
218 |
+
);
|
219 |
+
};
|
220 |
+
|
221 |
+
const Stroke = () => {
|
222 |
+
return (
|
223 |
+
<div
|
224 |
+
style={{
|
225 |
+
display: 'grid',
|
226 |
+
gridTemplateColumns: '1fr 24px 24px',
|
227 |
+
gap: '4px',
|
228 |
+
}}
|
229 |
+
>
|
230 |
+
<div className="text-sm text-zinc-500 flex items-center">Stroke</div>
|
231 |
+
<div>
|
232 |
+
<div className="w-6 h-6 rounded-sm border-2 border-zinc-800 bg-green-700"></div>
|
233 |
+
</div>
|
234 |
+
<div>
|
235 |
+
<Button size="icon" variant="ghost" className="h-6 w-6">
|
236 |
+
<Ellipsis size={14} />
|
237 |
+
</Button>
|
238 |
+
</div>
|
239 |
+
</div>
|
240 |
+
);
|
241 |
+
};
|
242 |
+
const Shadow = () => {
|
243 |
+
return (
|
244 |
+
<div
|
245 |
+
style={{
|
246 |
+
display: 'grid',
|
247 |
+
gridTemplateColumns: '1fr 24px 24px',
|
248 |
+
gap: '4px',
|
249 |
+
}}
|
250 |
+
>
|
251 |
+
<div className="text-sm text-zinc-500 flex items-center">Shadow</div>
|
252 |
+
<div>
|
253 |
+
<div className="w-6 h-6 rounded-sm border-2 border-zinc-800 bg-green-700"></div>
|
254 |
+
</div>
|
255 |
+
<div>
|
256 |
+
<Button size="icon" variant="ghost" className="h-6 w-6">
|
257 |
+
<Ellipsis size={14} />
|
258 |
+
</Button>
|
259 |
+
</div>
|
260 |
+
</div>
|
261 |
+
);
|
262 |
+
};
|
263 |
+
const Background = () => {
|
264 |
+
return (
|
265 |
+
<div
|
266 |
+
style={{
|
267 |
+
display: 'grid',
|
268 |
+
gridTemplateColumns: '1fr 24px 24px',
|
269 |
+
gap: '4px',
|
270 |
+
}}
|
271 |
+
>
|
272 |
+
<div className="text-sm text-zinc-500 flex items-center">Background</div>
|
273 |
+
<div>
|
274 |
+
<div className="w-6 h-6 rounded-sm border-2 border-zinc-800 bg-green-700"></div>
|
275 |
+
</div>
|
276 |
+
<div>
|
277 |
+
<Button size="icon" variant="ghost" className="h-6 w-6">
|
278 |
+
<Ellipsis size={14} />
|
279 |
+
</Button>
|
280 |
+
</div>
|
281 |
+
</div>
|
282 |
+
);
|
283 |
+
};
|
284 |
+
|
285 |
+
const FontFamily = ({
|
286 |
+
handleChangeFont,
|
287 |
+
fontFamilyDisplay,
|
288 |
+
}: {
|
289 |
+
handleChangeFont: (font: ICompactFont) => void;
|
290 |
+
fontFamilyDisplay: string;
|
291 |
+
}) => {
|
292 |
+
const { compactFonts } = useDataState();
|
293 |
+
|
294 |
+
return (
|
295 |
+
<Popover>
|
296 |
+
<PopoverTrigger asChild>
|
297 |
+
<Button
|
298 |
+
size="sm"
|
299 |
+
className="flex items-center justify-between text-sm w-full"
|
300 |
+
variant="outline"
|
301 |
+
>
|
302 |
+
<div className="w-full text-left ">
|
303 |
+
<p className="truncate">{fontFamilyDisplay}</p>
|
304 |
+
</div>
|
305 |
+
<ChevronDown size={14} />
|
306 |
+
</Button>
|
307 |
+
</PopoverTrigger>
|
308 |
+
<PopoverContent className="p-0 w-56 z-[300]">
|
309 |
+
<ScrollArea className="h-[400px] w-full py-2">
|
310 |
+
{compactFonts.map((font, index) => (
|
311 |
+
<div
|
312 |
+
onClick={() => handleChangeFont(font)}
|
313 |
+
className="hover:bg-zinc-800/50 cursor-pointer px-2 py-1"
|
314 |
+
key={index}
|
315 |
+
>
|
316 |
+
<img
|
317 |
+
style={{
|
318 |
+
filter: 'invert(100%)',
|
319 |
+
}}
|
320 |
+
src={font.default.preview}
|
321 |
+
alt={font.family}
|
322 |
+
/>
|
323 |
+
</div>
|
324 |
+
))}
|
325 |
+
</ScrollArea>
|
326 |
+
</PopoverContent>
|
327 |
+
</Popover>
|
328 |
+
);
|
329 |
+
};
|
330 |
+
|
331 |
+
const FontStyle = ({
|
332 |
+
selectedFont,
|
333 |
+
handleChangeFontStyle,
|
334 |
+
}: {
|
335 |
+
selectedFont: ICompactFont;
|
336 |
+
handleChangeFontStyle: (font: IFont) => void;
|
337 |
+
}) => {
|
338 |
+
return (
|
339 |
+
<Popover>
|
340 |
+
<PopoverTrigger asChild>
|
341 |
+
<Button
|
342 |
+
size="sm"
|
343 |
+
className="w-full flex items-center justify-between text-sm"
|
344 |
+
variant="outline"
|
345 |
+
>
|
346 |
+
<div className="w-full text-left overflow-hidden">
|
347 |
+
<p className="truncate"> {selectedFont.name}</p>
|
348 |
+
</div>
|
349 |
+
<ChevronDown size={14} />
|
350 |
+
</Button>
|
351 |
+
</PopoverTrigger>
|
352 |
+
|
353 |
+
<PopoverContent className="p-0 w-28 z-[300]">
|
354 |
+
{selectedFont.styles.map((style, index) => {
|
355 |
+
const fontFamilyEnd = style.postScriptName.lastIndexOf('-');
|
356 |
+
const styleName = style.postScriptName
|
357 |
+
.substring(fontFamilyEnd + 1)
|
358 |
+
.replace('Italic', ' Italic');
|
359 |
+
return (
|
360 |
+
<div
|
361 |
+
className="text-sm h-6 hover:bg-zinc-800 flex items-center px-2 py-3.5 cursor-pointer text-zinc-300 hover:text-zinc-100"
|
362 |
+
key={index}
|
363 |
+
onClick={() => handleChangeFontStyle(style)}
|
364 |
+
>
|
365 |
+
{styleName}
|
366 |
+
</div>
|
367 |
+
);
|
368 |
+
})}
|
369 |
+
</PopoverContent>
|
370 |
+
</Popover>
|
371 |
+
);
|
372 |
+
};
|
373 |
+
const TextDecoration = () => {
|
374 |
+
const [value, setValue] = useState(['left']);
|
375 |
+
const onChangeAligment = (value: string[]) => {
|
376 |
+
setValue(value);
|
377 |
+
};
|
378 |
+
return (
|
379 |
+
<ToggleGroup
|
380 |
+
value={value}
|
381 |
+
size="sm"
|
382 |
+
className="grid grid-cols-3"
|
383 |
+
type="multiple"
|
384 |
+
onValueChange={onChangeAligment}
|
385 |
+
>
|
386 |
+
<ToggleGroupItem size="sm" value="left" aria-label="Toggle left">
|
387 |
+
<Underline size={18} />
|
388 |
+
</ToggleGroupItem>
|
389 |
+
<ToggleGroupItem value="strikethrough" aria-label="Toggle italic">
|
390 |
+
<Strikethrough size={18} />
|
391 |
+
</ToggleGroupItem>
|
392 |
+
<ToggleGroupItem value="overline" aria-label="Toggle strikethrough">
|
393 |
+
<div>
|
394 |
+
<svg
|
395 |
+
width={18}
|
396 |
+
viewBox="0 0 24 24"
|
397 |
+
fill="none"
|
398 |
+
xmlns="http://www.w3.org/2000/svg"
|
399 |
+
>
|
400 |
+
<path
|
401 |
+
fillRule="evenodd"
|
402 |
+
clipRule="evenodd"
|
403 |
+
d="M5.59996 1.75977C5.43022 1.75977 5.26744 1.82719 5.14741 1.94722C5.02739 2.06724 4.95996 2.23003 4.95996 2.39977C4.95996 2.5695 5.02739 2.73229 5.14741 2.85231C5.26744 2.97234 5.43022 3.03977 5.59996 3.03977H18.4C18.5697 3.03977 18.7325 2.97234 18.8525 2.85231C18.9725 2.73229 19.04 2.5695 19.04 2.39977C19.04 2.23003 18.9725 2.06724 18.8525 1.94722C18.7325 1.82719 18.5697 1.75977 18.4 1.75977H5.59996ZM7.99996 6.79977C7.99996 6.58759 7.91568 6.38411 7.76565 6.23408C7.61562 6.08405 7.41213 5.99977 7.19996 5.99977C6.98779 5.99977 6.7843 6.08405 6.63428 6.23408C6.48425 6.38411 6.39996 6.58759 6.39996 6.79977V15.2798C6.39996 16.765 6.98996 18.1894 8.04016 19.2396C9.09037 20.2898 10.5147 20.8798 12 20.8798C13.4852 20.8798 14.9096 20.2898 15.9598 19.2396C17.01 18.1894 17.6 16.765 17.6 15.2798V6.79977C17.6 6.58759 17.5157 6.38411 17.3656 6.23408C17.2156 6.08405 17.0121 5.99977 16.8 5.99977C16.5878 5.99977 16.3843 6.08405 16.2343 6.23408C16.0842 6.38411 16 6.58759 16 6.79977V15.2798C16 16.3406 15.5785 17.358 14.8284 18.1082C14.0782 18.8583 13.0608 19.2798 12 19.2798C10.9391 19.2798 9.92168 18.8583 9.17153 18.1082C8.42139 17.358 7.99996 16.3406 7.99996 15.2798V6.79977Z"
|
404 |
+
fill="currentColor"
|
405 |
+
/>
|
406 |
+
</svg>
|
407 |
+
</div>
|
408 |
+
</ToggleGroupItem>
|
409 |
+
</ToggleGroup>
|
410 |
+
);
|
411 |
+
};
|
412 |
+
|
413 |
+
const Alignment = () => {
|
414 |
+
const [value, setValue] = useState('left');
|
415 |
+
const onChangeAligment = (value: string) => {
|
416 |
+
setValue(value);
|
417 |
+
};
|
418 |
+
return (
|
419 |
+
<ToggleGroup
|
420 |
+
value={value}
|
421 |
+
size="sm"
|
422 |
+
className="grid grid-cols-3"
|
423 |
+
type="single"
|
424 |
+
onValueChange={onChangeAligment}
|
425 |
+
>
|
426 |
+
<ToggleGroupItem size="sm" value="left" aria-label="Toggle left">
|
427 |
+
<AlignLeft size={18} />
|
428 |
+
</ToggleGroupItem>
|
429 |
+
<ToggleGroupItem value="italic" aria-label="Toggle italic">
|
430 |
+
<AlignCenter size={18} />
|
431 |
+
</ToggleGroupItem>
|
432 |
+
<ToggleGroupItem value="strikethrough" aria-label="Toggle strikethrough">
|
433 |
+
<AlignRight size={18} />
|
434 |
+
</ToggleGroupItem>
|
435 |
+
</ToggleGroup>
|
436 |
+
);
|
437 |
+
};
|
438 |
+
|
439 |
+
export default BasicText;
|
src/components/editor/control-item/basic-video.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const BasicVideo = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1 flex flex-col">
|
4 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
BasicVideo
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
10 |
+
|
11 |
+
export default BasicVideo;
|
src/components/editor/control-item/common/opacity.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import { Input } from '@/components/ui/input';
|
3 |
+
import { Label } from '@/components/ui/label';
|
4 |
+
|
5 |
+
import { Slider } from '@/components/ui/slider';
|
6 |
+
import { Plus, RefreshCcw, RotateCw } from 'lucide-react';
|
7 |
+
import { useState } from 'react';
|
8 |
+
|
9 |
+
const Opacity = () => {
|
10 |
+
const [value, setValue] = useState([10]);
|
11 |
+
|
12 |
+
return (
|
13 |
+
<div>
|
14 |
+
<div className="grid gap-1.5">
|
15 |
+
<div className="flex items-center justify-between">
|
16 |
+
<Label htmlFor="temperature">Opacity</Label>
|
17 |
+
</div>
|
18 |
+
<div
|
19 |
+
style={{
|
20 |
+
display: 'grid',
|
21 |
+
gridTemplateColumns: '1fr 40px 24px',
|
22 |
+
gap: '4px',
|
23 |
+
}}
|
24 |
+
>
|
25 |
+
<Slider
|
26 |
+
id="opacity"
|
27 |
+
max={1}
|
28 |
+
step={0.1}
|
29 |
+
onValueChange={setValue}
|
30 |
+
aria-label="Temperature"
|
31 |
+
/>
|
32 |
+
<Input className="w-11 px-2 text-sm text-center" defaultValue={100} />
|
33 |
+
<div className="flex items-center">
|
34 |
+
<Button
|
35 |
+
size="icon"
|
36 |
+
variant="ghost"
|
37 |
+
className="h-6 w-6 text-zinc-400"
|
38 |
+
>
|
39 |
+
<RotateCw size={14} />
|
40 |
+
</Button>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
);
|
46 |
+
};
|
47 |
+
|
48 |
+
export default Opacity;
|
src/components/editor/control-item/common/transform.tsx
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import { Input } from '@/components/ui/input';
|
3 |
+
import { Slider } from '@/components/ui/slider';
|
4 |
+
import { RotateCw } from 'lucide-react';
|
5 |
+
import { useState } from 'react';
|
6 |
+
|
7 |
+
const Transform = () => {
|
8 |
+
const [value, setValue] = useState([10]);
|
9 |
+
|
10 |
+
return (
|
11 |
+
<div className="flex flex-col gap-2">
|
12 |
+
<div>Transform</div>
|
13 |
+
<div className="flex flex-col gap-2">
|
14 |
+
<div className="text-sm text-zinc-400">Scale</div>
|
15 |
+
<div
|
16 |
+
style={{
|
17 |
+
display: 'grid',
|
18 |
+
gridTemplateColumns: '1fr 40px 24px',
|
19 |
+
gap: '4px',
|
20 |
+
}}
|
21 |
+
>
|
22 |
+
<Slider
|
23 |
+
id="opacity"
|
24 |
+
max={1}
|
25 |
+
step={0.1}
|
26 |
+
onValueChange={setValue}
|
27 |
+
aria-label="Temperature"
|
28 |
+
/>
|
29 |
+
<Input className="w-11 px-2 text-sm text-center" defaultValue={100} />
|
30 |
+
<div className="flex items-center">
|
31 |
+
<Button
|
32 |
+
size="icon"
|
33 |
+
variant="ghost"
|
34 |
+
className="h-6 w-6 text-zinc-400"
|
35 |
+
>
|
36 |
+
<RotateCw size={14} />
|
37 |
+
</Button>
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<div className="flex flex-col gap-2">
|
43 |
+
<div className="text-sm text-zinc-400">Position</div>
|
44 |
+
<div
|
45 |
+
style={{
|
46 |
+
display: 'grid',
|
47 |
+
gridTemplateColumns: '1fr 1fr 24px',
|
48 |
+
gap: '4px',
|
49 |
+
}}
|
50 |
+
>
|
51 |
+
<div className="relative">
|
52 |
+
<Input className=" px-2 text-sm" defaultValue={100} />
|
53 |
+
<div className="absolute top-1/2 transform -translate-y-1/2 right-2.5 text-zinc-200">
|
54 |
+
x
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
<div className="relative">
|
58 |
+
<Input className=" px-2 text-sm" defaultValue={100} />
|
59 |
+
<div className="absolute top-1/2 transform -translate-y-1/2 right-2.5 text-zinc-200">
|
60 |
+
y
|
61 |
+
</div>
|
62 |
+
</div>
|
63 |
+
|
64 |
+
<div className="flex items-center">
|
65 |
+
<Button
|
66 |
+
size="icon"
|
67 |
+
variant="ghost"
|
68 |
+
className="h-6 w-6 text-zinc-400"
|
69 |
+
>
|
70 |
+
<RotateCw size={14} />
|
71 |
+
</Button>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
|
76 |
+
<div className="flex flex-col gap-2">
|
77 |
+
<div className="text-sm text-zinc-400">Rotate</div>
|
78 |
+
<div
|
79 |
+
style={{
|
80 |
+
display: 'grid',
|
81 |
+
gridTemplateColumns: '1fr 1fr 24px',
|
82 |
+
gap: '4px',
|
83 |
+
}}
|
84 |
+
>
|
85 |
+
<Input className="px-2 text-sm" defaultValue={100} />
|
86 |
+
<div></div>
|
87 |
+
<div className="flex items-center">
|
88 |
+
<Button
|
89 |
+
size="icon"
|
90 |
+
variant="ghost"
|
91 |
+
className="h-6 w-6 text-zinc-400"
|
92 |
+
>
|
93 |
+
<RotateCw size={14} />
|
94 |
+
</Button>
|
95 |
+
</div>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
);
|
100 |
+
};
|
101 |
+
|
102 |
+
export default Transform;
|
src/components/editor/control-item/control-item.tsx
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import useLayoutStore from '@/store/use-layout-store';
|
3 |
+
import { ITrackItem, useEditorState } from '@designcombo/core';
|
4 |
+
import { useEffect, useState } from 'react';
|
5 |
+
import { Button } from '@/components/ui/button';
|
6 |
+
import { X } from 'lucide-react';
|
7 |
+
import Presets from './presets';
|
8 |
+
import Animations from './animations';
|
9 |
+
import Smart from './smart';
|
10 |
+
import BasicText from './basic-text';
|
11 |
+
import BasicImage from './basic-image';
|
12 |
+
import BasicVideo from './basic-video';
|
13 |
+
import BasicAudio from './basic-audio';
|
14 |
+
|
15 |
+
const Container = ({ children }: { children: React.ReactNode }) => {
|
16 |
+
const { activeToolboxItem } = useLayoutStore();
|
17 |
+
const { activeIds, trackItemsMap } = useEditorState();
|
18 |
+
const [trackItem, setTrackItem] = useState<ITrackItem | null>(null);
|
19 |
+
const [displayToolbox, setDisplayToolbox] = useState<boolean>(false);
|
20 |
+
|
21 |
+
useEffect(() => {
|
22 |
+
if (activeIds.length === 1) {
|
23 |
+
const [id] = activeIds;
|
24 |
+
const trackItem = trackItemsMap[id];
|
25 |
+
setTrackItem(trackItem);
|
26 |
+
} else {
|
27 |
+
setTrackItem(null);
|
28 |
+
setDisplayToolbox(false);
|
29 |
+
}
|
30 |
+
}, [activeIds]);
|
31 |
+
|
32 |
+
useEffect(() => {
|
33 |
+
if (activeToolboxItem) {
|
34 |
+
setDisplayToolbox(true);
|
35 |
+
} else {
|
36 |
+
setDisplayToolbox(false);
|
37 |
+
}
|
38 |
+
}, [activeToolboxItem]);
|
39 |
+
|
40 |
+
if (!trackItem) {
|
41 |
+
return null;
|
42 |
+
}
|
43 |
+
|
44 |
+
return (
|
45 |
+
<div
|
46 |
+
style={{
|
47 |
+
right: activeToolboxItem && displayToolbox ? '0' : '-100%',
|
48 |
+
transition: 'right 0.25s ease-in-out',
|
49 |
+
zIndex: 200,
|
50 |
+
}}
|
51 |
+
className="w-[340px] h-[calc(100%-32px-64px)] mt-6 absolute top-1/2 -translate-y-1/2 rounded-lg shadow-lg flex"
|
52 |
+
>
|
53 |
+
<div className="w-[266px] h-full relative bg-zinc-950 flex">
|
54 |
+
<Button
|
55 |
+
variant="ghost"
|
56 |
+
className="absolute top-2 right-2 w-8 h-8 text-muted-foreground"
|
57 |
+
size="icon"
|
58 |
+
>
|
59 |
+
<X width={16} onClick={() => setDisplayToolbox(false)} />
|
60 |
+
</Button>
|
61 |
+
{React.cloneElement(children as React.ReactElement<any>, {
|
62 |
+
trackItem,
|
63 |
+
activeToolboxItem,
|
64 |
+
})}
|
65 |
+
</div>
|
66 |
+
<div className="w-[74px]"></div>
|
67 |
+
</div>
|
68 |
+
);
|
69 |
+
};
|
70 |
+
|
71 |
+
const ActiveControlItem = ({
|
72 |
+
trackItem,
|
73 |
+
activeToolboxItem,
|
74 |
+
}: {
|
75 |
+
trackItem?: ITrackItem;
|
76 |
+
activeToolboxItem?: string;
|
77 |
+
}) => {
|
78 |
+
if (!trackItem || !activeToolboxItem) {
|
79 |
+
return null;
|
80 |
+
}
|
81 |
+
return (
|
82 |
+
<>
|
83 |
+
{
|
84 |
+
{
|
85 |
+
'basic-text': <BasicText trackItem={trackItem} />,
|
86 |
+
'basic-image': <BasicImage />,
|
87 |
+
'basic-video': <BasicVideo />,
|
88 |
+
'basic-audio': <BasicAudio />,
|
89 |
+
'preset-text': <Presets />,
|
90 |
+
animation: <Animations />,
|
91 |
+
smart: <Smart />,
|
92 |
+
}[activeToolboxItem]
|
93 |
+
}
|
94 |
+
</>
|
95 |
+
);
|
96 |
+
};
|
97 |
+
|
98 |
+
export const ControlItem = () => {
|
99 |
+
return (
|
100 |
+
<Container>
|
101 |
+
<ActiveControlItem />
|
102 |
+
</Container>
|
103 |
+
);
|
104 |
+
};
|
src/components/editor/control-item/index.tsx
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { ControlItem } from './control-item';
|
src/components/editor/control-item/presets.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const Presets = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1 flex flex-col">
|
4 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
Presets
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
10 |
+
|
11 |
+
export default Presets;
|
src/components/editor/control-item/smart.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const Smart = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1 flex flex-col">
|
4 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
Ai things
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
10 |
+
|
11 |
+
export default Smart;
|
src/components/editor/control-list.tsx
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ItemType, useEditorState } from '@designcombo/core';
|
2 |
+
import { useCallback, useEffect, useState } from 'react';
|
3 |
+
import { Icons } from '@/components/shared/icons';
|
4 |
+
import { Button } from '@/components/ui/button';
|
5 |
+
import useLayoutStore from '@/store/use-layout-store';
|
6 |
+
|
7 |
+
export default function ControlList() {
|
8 |
+
const { activeIds, trackItemsMap } = useEditorState();
|
9 |
+
const [controlType, setControlType] = useState<ItemType | null>(null);
|
10 |
+
|
11 |
+
useEffect(() => {
|
12 |
+
if (activeIds.length === 1) {
|
13 |
+
const [id] = activeIds;
|
14 |
+
const trackItem = trackItemsMap[id];
|
15 |
+
setControlType(trackItem.type);
|
16 |
+
} else {
|
17 |
+
setControlType(null);
|
18 |
+
}
|
19 |
+
}, [activeIds, trackItemsMap]);
|
20 |
+
|
21 |
+
return <>{controlType && <ControlMenu controlType={controlType} />}</>;
|
22 |
+
}
|
23 |
+
|
24 |
+
function ControlMenu({ controlType }: { controlType: ItemType }) {
|
25 |
+
const { setShowToolboxItem, setActiveToolboxItem, activeToolboxItem } =
|
26 |
+
useLayoutStore();
|
27 |
+
|
28 |
+
const openToolboxItem = useCallback(
|
29 |
+
(type: string) => {
|
30 |
+
if (type === activeToolboxItem) {
|
31 |
+
setShowToolboxItem(false);
|
32 |
+
setActiveToolboxItem(null);
|
33 |
+
} else {
|
34 |
+
setShowToolboxItem(true);
|
35 |
+
setActiveToolboxItem(type);
|
36 |
+
}
|
37 |
+
},
|
38 |
+
[activeToolboxItem],
|
39 |
+
);
|
40 |
+
|
41 |
+
return (
|
42 |
+
<div
|
43 |
+
style={{ zIndex: 201 }}
|
44 |
+
className="w-14 py-2 absolute top-1/2 -translate-y-1/2 right-2.5 bg-zinc-950 rounded-lg shadow-lg flex flex-col items-center"
|
45 |
+
>
|
46 |
+
{
|
47 |
+
{
|
48 |
+
image: (
|
49 |
+
<ImageMenuList
|
50 |
+
type={controlType}
|
51 |
+
openToolboxItem={openToolboxItem}
|
52 |
+
/>
|
53 |
+
),
|
54 |
+
video: (
|
55 |
+
<VideoMenuList
|
56 |
+
type={controlType}
|
57 |
+
openToolboxItem={openToolboxItem}
|
58 |
+
/>
|
59 |
+
),
|
60 |
+
audio: (
|
61 |
+
<AudioMenuList
|
62 |
+
type={controlType}
|
63 |
+
openToolboxItem={openToolboxItem}
|
64 |
+
/>
|
65 |
+
),
|
66 |
+
text: (
|
67 |
+
<TextMenuList
|
68 |
+
type={controlType}
|
69 |
+
openToolboxItem={openToolboxItem}
|
70 |
+
/>
|
71 |
+
),
|
72 |
+
}[controlType]
|
73 |
+
}
|
74 |
+
</div>
|
75 |
+
);
|
76 |
+
}
|
77 |
+
|
78 |
+
const ImageMenuList = ({
|
79 |
+
openToolboxItem,
|
80 |
+
type,
|
81 |
+
}: {
|
82 |
+
openToolboxItem: (type: string) => void;
|
83 |
+
type: ItemType;
|
84 |
+
}) => {
|
85 |
+
return (
|
86 |
+
<div className="flex flex-col items-center">
|
87 |
+
<BasicMenuListItem openToolboxItem={openToolboxItem} type={type} />
|
88 |
+
<AnimationMenuListItem openToolboxItem={openToolboxItem} />
|
89 |
+
<SmartMenuListItem openToolboxItem={openToolboxItem} />
|
90 |
+
</div>
|
91 |
+
);
|
92 |
+
};
|
93 |
+
|
94 |
+
const TextMenuList = ({
|
95 |
+
openToolboxItem,
|
96 |
+
type,
|
97 |
+
}: {
|
98 |
+
openToolboxItem: (type: string) => void;
|
99 |
+
type: ItemType;
|
100 |
+
}) => {
|
101 |
+
return (
|
102 |
+
<div className="flex flex-col items-center">
|
103 |
+
<PresetsMenuListItem type={type} openToolboxItem={openToolboxItem} />
|
104 |
+
|
105 |
+
<BasicMenuListItem openToolboxItem={openToolboxItem} type={type} />
|
106 |
+
<AnimationMenuListItem openToolboxItem={openToolboxItem} />
|
107 |
+
<SmartMenuListItem openToolboxItem={openToolboxItem} />
|
108 |
+
</div>
|
109 |
+
);
|
110 |
+
};
|
111 |
+
|
112 |
+
const VideoMenuList = ({
|
113 |
+
openToolboxItem,
|
114 |
+
type,
|
115 |
+
}: {
|
116 |
+
openToolboxItem: (type: string) => void;
|
117 |
+
type: ItemType;
|
118 |
+
}) => {
|
119 |
+
return (
|
120 |
+
<div className="flex flex-col items-center">
|
121 |
+
<BasicMenuListItem openToolboxItem={openToolboxItem} type={type} />
|
122 |
+
<AnimationMenuListItem openToolboxItem={openToolboxItem} />
|
123 |
+
</div>
|
124 |
+
);
|
125 |
+
};
|
126 |
+
|
127 |
+
const AudioMenuList = ({
|
128 |
+
openToolboxItem,
|
129 |
+
type,
|
130 |
+
}: {
|
131 |
+
openToolboxItem: (type: string) => void;
|
132 |
+
type: ItemType;
|
133 |
+
}) => {
|
134 |
+
return (
|
135 |
+
<div className="flex flex-col items-center">
|
136 |
+
<BasicMenuListItem openToolboxItem={openToolboxItem} type={type} />
|
137 |
+
<SmartMenuListItem openToolboxItem={openToolboxItem} />
|
138 |
+
</div>
|
139 |
+
);
|
140 |
+
};
|
141 |
+
|
142 |
+
const PresetsMenuListItem = ({
|
143 |
+
openToolboxItem,
|
144 |
+
type,
|
145 |
+
}: {
|
146 |
+
openToolboxItem: (type: string) => void;
|
147 |
+
type: ItemType;
|
148 |
+
}) => {
|
149 |
+
return (
|
150 |
+
<Button
|
151 |
+
size="icon"
|
152 |
+
onClick={() => openToolboxItem(`preset-${type}`)}
|
153 |
+
variant="ghost"
|
154 |
+
>
|
155 |
+
<Icons.preset size={20} className="text-white" />
|
156 |
+
</Button>
|
157 |
+
);
|
158 |
+
};
|
159 |
+
|
160 |
+
const BasicMenuListItem = ({
|
161 |
+
openToolboxItem,
|
162 |
+
type,
|
163 |
+
}: {
|
164 |
+
openToolboxItem: (type: string) => void;
|
165 |
+
type: string;
|
166 |
+
}) => {
|
167 |
+
const Icon = Icons[type];
|
168 |
+
return (
|
169 |
+
<Button
|
170 |
+
size="icon"
|
171 |
+
onClick={() => openToolboxItem(`basic-${type}`)}
|
172 |
+
variant="ghost"
|
173 |
+
>
|
174 |
+
<Icon size={20} className="text-white" />
|
175 |
+
</Button>
|
176 |
+
);
|
177 |
+
};
|
178 |
+
|
179 |
+
const SmartMenuListItem = ({
|
180 |
+
openToolboxItem,
|
181 |
+
}: {
|
182 |
+
openToolboxItem: (type: string) => void;
|
183 |
+
}) => {
|
184 |
+
return (
|
185 |
+
<Button
|
186 |
+
size="icon"
|
187 |
+
onClick={() => openToolboxItem('smart')}
|
188 |
+
variant="ghost"
|
189 |
+
>
|
190 |
+
<Icons.smart size={20} className="text-white" />
|
191 |
+
</Button>
|
192 |
+
);
|
193 |
+
};
|
194 |
+
|
195 |
+
const AnimationMenuListItem = ({
|
196 |
+
openToolboxItem,
|
197 |
+
}: {
|
198 |
+
openToolboxItem: (type: string) => void;
|
199 |
+
}) => {
|
200 |
+
return (
|
201 |
+
<Button
|
202 |
+
size="icon"
|
203 |
+
onClick={() => openToolboxItem('animation')}
|
204 |
+
variant="ghost"
|
205 |
+
>
|
206 |
+
<svg
|
207 |
+
width={20}
|
208 |
+
viewBox="0 0 24 24"
|
209 |
+
fill="none"
|
210 |
+
xmlns="http://www.w3.org/2000/svg"
|
211 |
+
>
|
212 |
+
<path
|
213 |
+
d="M6.77329 21.1395C6.2479 21.3357 5.67727 21.3772 5.12902 21.2591C4.58077 21.1409 4.07788 20.8681 3.67995 20.4729C3.40573 20.202 3.18839 19.879 3.0407 19.523C2.89302 19.1669 2.81797 18.785 2.81995 18.3995C2.82282 18.1286 2.8632 17.8594 2.93995 17.5995C2.65089 17.0591 2.42709 16.4861 2.27329 15.8929C1.66062 16.7379 1.37897 17.7782 1.48164 18.8169C1.5843 19.8557 2.06417 20.8207 2.83041 21.5295C3.59666 22.2382 4.59613 22.6415 5.63969 22.663C6.68325 22.6845 7.69849 22.3228 8.49329 21.6462C7.90225 21.5446 7.32504 21.3745 6.77329 21.1395ZM12.2733 18.4529C11.3101 18.9897 10.1982 19.1982 9.10595 19.0466C8.0137 18.895 7.00057 18.3917 6.21995 17.6129C5.44017 16.8357 4.93549 15.8252 4.78266 14.735C4.62984 13.6447 4.83722 12.5344 5.37329 11.5729C5.22747 10.9494 5.14483 10.3129 5.12662 9.67285C3.99221 10.8831 3.3732 12.4873 3.40057 14.1459C3.42794 15.8045 4.09956 17.3873 5.27329 18.5595C6.4502 19.7295 8.03445 20.3983 9.69374 20.4257C11.353 20.453 12.9585 19.8368 14.1733 18.7062C13.5331 18.6849 12.8967 18.6 12.2733 18.4529Z"
|
214 |
+
fill="currentColor"
|
215 |
+
fillOpacity="0.5"
|
216 |
+
/>
|
217 |
+
<path
|
218 |
+
d="M2.31998 18.3942H2.31998L2.31996 18.3969C2.31763 18.849 2.40565 19.297 2.57886 19.7145C2.75198 20.1319 3.00671 20.5105 3.3281 20.8281C3.79378 21.2904 4.3822 21.6096 5.02368 21.7478C5.59915 21.8719 6.19568 21.8456 6.7562 21.6732C6.91495 21.7365 7.07554 21.7947 7.23776 21.848C6.74151 22.0651 6.20021 22.1745 5.64999 22.1631C4.72873 22.1441 3.84638 21.7881 3.16993 21.1624C2.49348 20.5367 2.06985 19.6847 1.97921 18.7678C1.92477 18.217 1.99273 17.6657 2.17288 17.1516C2.2453 17.3233 2.32356 17.4926 2.40755 17.6592C2.352 17.9001 2.3226 18.1466 2.31998 18.3942ZM9.03722 19.5418C10.1726 19.6994 11.3273 19.5029 12.3446 18.9816C12.5134 19.0181 12.683 19.0503 12.8533 19.0782C11.9099 19.6462 10.8192 19.9442 9.70198 19.9257C8.1721 19.9005 6.7114 19.284 5.62619 18.2053C4.54461 17.1249 3.92573 15.6662 3.9005 14.1376C3.8821 13.0223 4.18082 11.9336 4.74979 10.9932C4.7771 11.1632 4.80878 11.3325 4.84483 11.5009C4.32414 12.517 4.12857 13.6706 4.2875 14.8044C4.45544 16.0024 5.00998 17.1128 5.8668 17.9668C6.72435 18.8224 7.83733 19.3753 9.03722 19.5418Z"
|
219 |
+
stroke="currentColor"
|
220 |
+
strokeOpacity="0.5"
|
221 |
+
/>
|
222 |
+
<mask id="path-3-inside-1_4741_350" fill="white">
|
223 |
+
<path d="M14.4394 17.4732C12.5735 17.4704 10.7663 16.8208 9.32553 15.6352C7.88477 14.4495 6.89961 12.801 6.53784 10.9705C6.17608 9.13998 6.46008 7.24067 7.34149 5.59605C8.22289 3.95144 9.64718 2.66324 11.3718 1.95087C13.0963 1.2385 15.0145 1.14602 16.7996 1.68919C18.5847 2.23235 20.1263 3.37756 21.1619 4.92976C22.1974 6.48196 22.6628 8.34514 22.4788 10.202C22.2948 12.0588 21.4728 13.7944 20.1528 15.1132C19.4024 15.8629 18.5115 16.4572 17.5311 16.8622C16.5508 17.2671 15.5002 17.4747 14.4394 17.4732ZM14.4394 2.6665C13.5538 2.66563 12.6766 2.83932 11.8581 3.17764C11.0396 3.51597 10.2958 4.01229 9.66923 4.63825C9.04266 5.2642 8.5456 6.00751 8.20646 6.82568C7.86733 7.64385 7.69277 8.52083 7.69278 9.4065C7.6929 10.293 7.86985 11.1707 8.21326 11.988C8.55668 12.8053 9.05966 13.5459 9.69278 14.1665C10.958 15.4314 12.6737 16.1419 14.4628 16.1419C16.2518 16.1419 17.9676 15.4314 19.2328 14.1665C20.175 13.2219 20.8157 12.019 21.0738 10.71C21.3318 9.401 21.1955 8.04488 20.6822 6.81339C20.1689 5.58191 19.3017 4.53047 18.1904 3.79224C17.079 3.05402 15.7736 2.66223 14.4394 2.6665Z" />
|
224 |
+
</mask>
|
225 |
+
<path
|
226 |
+
d="M14.4394 17.4732C12.5735 17.4704 10.7663 16.8208 9.32553 15.6352C7.88477 14.4495 6.89961 12.801 6.53784 10.9705C6.17608 9.13998 6.46008 7.24067 7.34149 5.59605C8.22289 3.95144 9.64718 2.66324 11.3718 1.95087C13.0963 1.2385 15.0145 1.14602 16.7996 1.68919C18.5847 2.23235 20.1263 3.37756 21.1619 4.92976C22.1974 6.48196 22.6628 8.34514 22.4788 10.202C22.2948 12.0588 21.4728 13.7944 20.1528 15.1132C19.4024 15.8629 18.5115 16.4572 17.5311 16.8622C16.5508 17.2671 15.5002 17.4747 14.4394 17.4732ZM14.4394 2.6665C13.5538 2.66563 12.6766 2.83932 11.8581 3.17764C11.0396 3.51597 10.2958 4.01229 9.66923 4.63825C9.04266 5.2642 8.5456 6.00751 8.20646 6.82568C7.86733 7.64385 7.69277 8.52083 7.69278 9.4065C7.6929 10.293 7.86985 11.1707 8.21326 11.988C8.55668 12.8053 9.05966 13.5459 9.69278 14.1665C10.958 15.4314 12.6737 16.1419 14.4628 16.1419C16.2518 16.1419 17.9676 15.4314 19.2328 14.1665C20.175 13.2219 20.8157 12.019 21.0738 10.71C21.3318 9.401 21.1955 8.04488 20.6822 6.81339C20.1689 5.58191 19.3017 4.53047 18.1904 3.79224C17.079 3.05402 15.7736 2.66223 14.4394 2.6665Z"
|
227 |
+
fill="currentColor"
|
228 |
+
/>
|
229 |
+
<path
|
230 |
+
d="M14.4394 17.4732L14.435 20.4732L14.4394 17.4732ZM22.4788 10.202L19.4934 9.90613L22.4788 10.202ZM20.1528 15.1132L22.2731 17.2355L20.1528 15.1132ZM14.4394 2.6665L14.4365 5.66653L14.449 5.66649L14.4394 2.6665ZM7.69278 9.4065L4.69278 9.4065L4.69278 9.40693L7.69278 9.4065ZM9.69278 14.1665L11.8138 12.0449L11.8034 12.0344L11.7928 12.0241L9.69278 14.1665ZM14.4628 16.1419L14.4628 19.1419L14.4628 16.1419ZM19.2328 14.1665L21.3538 16.2881L21.3567 16.2852L19.2328 14.1665ZM14.4439 14.4732C13.2719 14.4714 12.1368 14.0635 11.2319 13.3187L7.4192 17.9516C9.39577 19.5782 11.8751 20.4694 14.435 20.4732L14.4439 14.4732ZM11.2319 13.3187C10.3269 12.574 9.70814 11.5386 9.48092 10.3888L3.59477 11.5521C4.09107 14.0634 5.44262 16.325 7.4192 17.9516L11.2319 13.3187ZM9.48092 10.3888C9.25369 9.2391 9.43208 8.04615 9.98569 7.01317L4.69729 4.17894C3.48809 6.43519 3.09846 9.04086 3.59477 11.5521L9.48092 10.3888ZM9.98569 7.01317C10.5393 5.98018 11.4339 5.17107 12.5171 4.72363L10.2264 -0.821889C7.86046 0.15541 5.90649 1.92269 4.69729 4.17894L9.98569 7.01317ZM12.5171 4.72363C13.6003 4.27619 14.8051 4.21811 15.9263 4.55927L17.6729 -1.18089C15.2239 -1.92606 12.5924 -1.79919 10.2264 -0.821889L12.5171 4.72363ZM15.9263 4.55927C17.0476 4.90043 18.0158 5.61973 18.6663 6.59467L23.6575 3.26485C22.2368 1.13539 20.1219 -0.435727 17.6729 -1.18089L15.9263 4.55927ZM18.6663 6.59467C19.3167 7.5696 19.609 8.73986 19.4934 9.90613L25.4642 10.4978C25.7166 7.95041 25.0781 5.39431 23.6575 3.26485L18.6663 6.59467ZM19.4934 9.90613C19.3779 11.0724 18.8615 12.1625 18.0324 12.9909L22.2731 17.2355C24.084 15.4262 25.2118 13.0452 25.4642 10.4978L19.4934 9.90613ZM18.0324 12.9909C17.5611 13.4617 17.0016 13.835 16.3858 14.0894L18.6765 19.6349C20.0214 19.0793 21.2436 18.264 22.2731 17.2355L18.0324 12.9909ZM16.3858 14.0894C15.77 14.3438 15.1101 14.4742 14.4439 14.4732L14.435 20.4732C15.8902 20.4753 17.3315 20.1905 18.6765 19.6349L16.3858 14.0894ZM14.4424 -0.333495C13.1625 -0.334761 11.8949 -0.0837622 10.7121 0.405152L13.0041 5.95013C13.4583 5.7624 13.945 5.66602 14.4365 5.6665L14.4424 -0.333495ZM10.7121 0.405152C9.52929 0.894066 8.45442 1.61131 7.54896 2.51588L11.7895 6.76061C12.1372 6.41327 12.5499 6.13787 13.0041 5.95013L10.7121 0.405152ZM7.54896 2.51588C6.6435 3.42044 5.92519 4.4946 5.43511 5.67694L10.9778 7.97441C11.166 7.52041 11.4418 7.10795 11.7895 6.76061L7.54896 2.51588ZM5.43511 5.67694C4.94503 6.85928 4.69277 8.12662 4.69278 9.4065L10.6928 9.4065C10.6928 8.91505 10.7896 8.42841 10.9778 7.97441L5.43511 5.67694ZM4.69278 9.40693C4.69296 10.6924 4.94953 11.965 5.44748 13.1501L10.979 10.8259C10.7902 10.3764 10.6928 9.89367 10.6928 9.40607L4.69278 9.40693ZM5.44748 13.1501C5.94543 14.3352 6.67476 15.4091 7.59278 16.3089L11.7928 12.0241C11.4446 11.6828 11.1679 11.2754 10.979 10.8259L5.44748 13.1501ZM7.57173 16.2881C9.39951 18.1154 11.8782 19.1419 14.4628 19.1419L14.4628 13.1419C13.4693 13.1419 12.5164 12.7473 11.8138 12.0449L7.57173 16.2881ZM14.4628 19.1419C17.0473 19.1419 19.526 18.1154 21.3538 16.2881L17.1117 12.0449C16.4091 12.7473 15.4563 13.1419 14.4628 13.1419L14.4628 19.1419ZM21.3567 16.2852C22.7183 14.9202 23.6442 13.1818 24.0171 11.2902L18.1304 10.1298C17.9872 10.8561 17.6317 11.5237 17.1088 12.0478L21.3567 16.2852ZM24.0171 11.2902C24.39 9.39856 24.1931 7.43883 23.4513 5.6592L17.9131 7.96758C18.198 8.65093 18.2736 9.40343 18.1304 10.1298L24.0171 11.2902ZM23.4513 5.6592C22.7096 3.87958 21.4563 2.36014 19.8503 1.29333L16.5304 6.29116C17.1471 6.70079 17.6283 7.28423 17.9131 7.96758L23.4513 5.6592ZM19.8503 1.29333C18.2443 0.226522 16.3579 -0.339653 14.4298 -0.333481L14.449 5.66649C15.1894 5.66412 15.9137 5.88152 16.5304 6.29116L19.8503 1.29333Z"
|
231 |
+
fill="currentColor"
|
232 |
+
mask="url(#path-3-inside-1_4741_350)"
|
233 |
+
/>
|
234 |
+
</svg>
|
235 |
+
</Button>
|
236 |
+
);
|
237 |
+
};
|
src/components/editor/editor.tsx
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Timeline, Provider, Scene } from '@designcombo/core';
|
2 |
+
import Navbar from './navbar';
|
3 |
+
import MenuList from './menu-list';
|
4 |
+
import { MenuItem } from './menu-item';
|
5 |
+
import ControlList from './control-list';
|
6 |
+
import { ControlItem } from './control-item';
|
7 |
+
|
8 |
+
import useHotkeys from './use-hotkeys';
|
9 |
+
import { useEffect } from 'react';
|
10 |
+
import { getCompactFontData } from '@/utils/fonts';
|
11 |
+
import { FONTS } from '@/data/fonts';
|
12 |
+
import useDataState from '@/store/use-data-state';
|
13 |
+
|
14 |
+
export const theme = {
|
15 |
+
colors: {
|
16 |
+
gray: {
|
17 |
+
50: '#fafafa',
|
18 |
+
100: '#f4f4f5',
|
19 |
+
200: '#e4e4e7',
|
20 |
+
300: '#d4d4d8',
|
21 |
+
400: '#a1a1aa',
|
22 |
+
500: '#71717a',
|
23 |
+
600: '#52525b',
|
24 |
+
700: '#3f3f46',
|
25 |
+
800: '#27272a',
|
26 |
+
900: '#18181b',
|
27 |
+
950: '#09090b',
|
28 |
+
1000: '#040405',
|
29 |
+
1100: '#010101',
|
30 |
+
},
|
31 |
+
},
|
32 |
+
};
|
33 |
+
|
34 |
+
const Editor = () => {
|
35 |
+
const { setCompactFonts, setFonts } = useDataState();
|
36 |
+
|
37 |
+
useHotkeys();
|
38 |
+
|
39 |
+
useEffect(() => {
|
40 |
+
setCompactFonts(getCompactFontData(FONTS));
|
41 |
+
setFonts(FONTS);
|
42 |
+
}, []);
|
43 |
+
|
44 |
+
return (
|
45 |
+
<Provider theme={theme}>
|
46 |
+
<div className="h-screen w-screen flex flex-col">
|
47 |
+
<Navbar />
|
48 |
+
<div className="flex-1 relative overflow-hidden">
|
49 |
+
<MenuList />
|
50 |
+
<MenuItem />
|
51 |
+
<ControlList />
|
52 |
+
<ControlItem />
|
53 |
+
<Scene />
|
54 |
+
</div>
|
55 |
+
<div className="h-80 flex" style={{ zIndex: 201 }}>
|
56 |
+
<Timeline />
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
</Provider>
|
60 |
+
);
|
61 |
+
};
|
62 |
+
|
63 |
+
export default Editor;
|
src/components/editor/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { default as Editor } from './editor';
|
src/components/editor/menu-item/audios.tsx
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
3 |
+
import { AUDIOS } from '@/data/audio';
|
4 |
+
import { ADD_AUDIO, dispatcher } from '@designcombo/core';
|
5 |
+
import { Music } from 'lucide-react';
|
6 |
+
import { nanoid } from 'nanoid';
|
7 |
+
|
8 |
+
export const Audios = () => {
|
9 |
+
const handleAddAudio = (src: string) => {
|
10 |
+
dispatcher?.dispatch(ADD_AUDIO, {
|
11 |
+
payload: {
|
12 |
+
id: nanoid(),
|
13 |
+
details: {
|
14 |
+
src: 'https://ik.imagekit.io/snapmotion/timer-voice.mp3',
|
15 |
+
},
|
16 |
+
},
|
17 |
+
options: {},
|
18 |
+
});
|
19 |
+
};
|
20 |
+
|
21 |
+
return (
|
22 |
+
<div className="flex-1 flex flex-col">
|
23 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
24 |
+
Audios
|
25 |
+
</div>
|
26 |
+
<ScrollArea>
|
27 |
+
<div className="px-2 flex flex-col">
|
28 |
+
{AUDIOS.map((audio, index) => {
|
29 |
+
return (
|
30 |
+
<AudioItem
|
31 |
+
handleAddAudio={handleAddAudio}
|
32 |
+
audio={audio}
|
33 |
+
key={index}
|
34 |
+
/>
|
35 |
+
);
|
36 |
+
})}
|
37 |
+
</div>
|
38 |
+
</ScrollArea>
|
39 |
+
</div>
|
40 |
+
);
|
41 |
+
};
|
42 |
+
|
43 |
+
const AudioItem = ({
|
44 |
+
audio,
|
45 |
+
handleAddAudio,
|
46 |
+
}: {
|
47 |
+
audio: any;
|
48 |
+
handleAddAudio: (src: string) => void;
|
49 |
+
}) => {
|
50 |
+
return (
|
51 |
+
<div
|
52 |
+
onClick={() => handleAddAudio(audio.src)}
|
53 |
+
style={{
|
54 |
+
display: 'grid',
|
55 |
+
gridTemplateColumns: '48px 1fr',
|
56 |
+
}}
|
57 |
+
className="flex px-2 py-1 gap-4 text-sm hover:bg-zinc-800/70 cursor-pointer"
|
58 |
+
>
|
59 |
+
<div className="bg-zinc-800 flex items-center justify-center h-12">
|
60 |
+
<Music width={16} />
|
61 |
+
</div>
|
62 |
+
<div className="flex flex-col justify-center">
|
63 |
+
<div>{audio.name}</div>
|
64 |
+
<div className="text-zinc-400">{audio.author}</div>
|
65 |
+
</div>
|
66 |
+
</div>
|
67 |
+
);
|
68 |
+
};
|
src/components/editor/menu-item/elements.tsx
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const Elements = () => {
|
2 |
+
return (
|
3 |
+
<div className="flex-1">
|
4 |
+
<div className="text-md text-text-primary font-medium h-12 flex items-center px-4">
|
5 |
+
Shapes
|
6 |
+
</div>
|
7 |
+
</div>
|
8 |
+
);
|
9 |
+
};
|
src/components/editor/menu-item/images.tsx
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
3 |
+
import { IMAGES } from '@/data/images';
|
4 |
+
import { ADD_IMAGE, dispatcher } from '@designcombo/core';
|
5 |
+
import { nanoid } from 'nanoid';
|
6 |
+
|
7 |
+
export const Images = () => {
|
8 |
+
const handleAddImage = (src: string) => {
|
9 |
+
dispatcher?.dispatch(ADD_IMAGE, {
|
10 |
+
payload: {
|
11 |
+
id: nanoid(),
|
12 |
+
details: {
|
13 |
+
src: src,
|
14 |
+
},
|
15 |
+
},
|
16 |
+
options: {
|
17 |
+
trackId: 'main',
|
18 |
+
},
|
19 |
+
});
|
20 |
+
};
|
21 |
+
|
22 |
+
return (
|
23 |
+
<div className="flex-1 flex flex-col">
|
24 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
25 |
+
Photos
|
26 |
+
</div>
|
27 |
+
<ScrollArea>
|
28 |
+
<div className="px-4 masonry-sm">
|
29 |
+
{IMAGES.map((image, index) => {
|
30 |
+
return (
|
31 |
+
<div
|
32 |
+
onClick={() => handleAddImage(image.src)}
|
33 |
+
key={index}
|
34 |
+
className="flex items-center justify-center w-full bg-zinc-950 pb-2 overflow-hidden cursor-pointer"
|
35 |
+
>
|
36 |
+
<img
|
37 |
+
src={image.src}
|
38 |
+
className="w-full h-full object-cover rounded-md"
|
39 |
+
alt="image"
|
40 |
+
/>
|
41 |
+
</div>
|
42 |
+
);
|
43 |
+
})}
|
44 |
+
</div>
|
45 |
+
</ScrollArea>
|
46 |
+
</div>
|
47 |
+
);
|
48 |
+
};
|
49 |
+
|
50 |
+
function modifyImageUrl(url: string): string {
|
51 |
+
const uploadIndex = url.indexOf('/upload');
|
52 |
+
if (uploadIndex === -1) {
|
53 |
+
throw new Error('Invalid URL: /upload not found');
|
54 |
+
}
|
55 |
+
|
56 |
+
const modifiedUrl =
|
57 |
+
url.slice(0, uploadIndex + 7) +
|
58 |
+
'/w_0.05,c_scale' +
|
59 |
+
url.slice(uploadIndex + 7);
|
60 |
+
return modifiedUrl;
|
61 |
+
}
|
src/components/editor/menu-item/index.tsx
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { MenuItem } from './menu-item';
|
src/components/editor/menu-item/menu-item.tsx
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import useLayoutStore from '@/store/use-layout-store';
|
2 |
+
import { Transitions } from './transitions';
|
3 |
+
import { Texts } from './texts';
|
4 |
+
import { Uploads } from './uploads';
|
5 |
+
import { Audios } from './audios';
|
6 |
+
import { Elements } from './elements';
|
7 |
+
import { Images } from './images';
|
8 |
+
import { Videos } from './videos';
|
9 |
+
import { X } from 'lucide-react';
|
10 |
+
import { Button } from '@/components/ui/button';
|
11 |
+
|
12 |
+
const Container = ({ children }: { children: React.ReactNode }) => {
|
13 |
+
const { showMenuItem, setShowMenuItem } = useLayoutStore();
|
14 |
+
return (
|
15 |
+
<div
|
16 |
+
style={{
|
17 |
+
left: showMenuItem ? '0' : '-100%',
|
18 |
+
transition: 'left 0.25s ease-in-out',
|
19 |
+
zIndex: 200,
|
20 |
+
}}
|
21 |
+
className="w-[340px] h-[calc(100%-32px-64px)] mt-6 absolute top-1/2 -translate-y-1/2 rounded-lg shadow-lg flex"
|
22 |
+
>
|
23 |
+
<div className="w-[74px]"></div>
|
24 |
+
<div className="flex-1 relative bg-zinc-950 flex">
|
25 |
+
<Button
|
26 |
+
variant="ghost"
|
27 |
+
className="absolute top-2 right-2 w-8 h-8 text-muted-foreground"
|
28 |
+
size="icon"
|
29 |
+
>
|
30 |
+
<X width={16} onClick={() => setShowMenuItem(false)} />
|
31 |
+
</Button>
|
32 |
+
{children}
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
);
|
36 |
+
};
|
37 |
+
|
38 |
+
const ActiveMenuItem = () => {
|
39 |
+
const { activeMenuItem } = useLayoutStore();
|
40 |
+
if (activeMenuItem === 'transitions') {
|
41 |
+
return <Transitions />;
|
42 |
+
}
|
43 |
+
if (activeMenuItem === 'texts') {
|
44 |
+
return <Texts />;
|
45 |
+
}
|
46 |
+
if (activeMenuItem === 'shapes') {
|
47 |
+
return <Elements />;
|
48 |
+
}
|
49 |
+
if (activeMenuItem === 'videos') {
|
50 |
+
return <Videos />;
|
51 |
+
}
|
52 |
+
|
53 |
+
if (activeMenuItem === 'audios') {
|
54 |
+
return <Audios />;
|
55 |
+
}
|
56 |
+
|
57 |
+
if (activeMenuItem === 'images') {
|
58 |
+
return <Images />;
|
59 |
+
}
|
60 |
+
if (activeMenuItem === 'uploads') {
|
61 |
+
return <Uploads />;
|
62 |
+
}
|
63 |
+
return null;
|
64 |
+
};
|
65 |
+
|
66 |
+
export const MenuItem = () => {
|
67 |
+
return (
|
68 |
+
<Container>
|
69 |
+
<ActiveMenuItem />
|
70 |
+
</Container>
|
71 |
+
);
|
72 |
+
};
|
src/components/editor/menu-item/texts.tsx
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import { DEFAULT_FONT } from '@/data/fonts';
|
3 |
+
import { ADD_TEXT, dispatcher } from '@designcombo/core';
|
4 |
+
import { nanoid } from 'nanoid';
|
5 |
+
|
6 |
+
export const Texts = () => {
|
7 |
+
const handleAddText = () => {
|
8 |
+
dispatcher?.dispatch(ADD_TEXT, {
|
9 |
+
payload: {
|
10 |
+
id: nanoid(),
|
11 |
+
details: {
|
12 |
+
text: 'Heading',
|
13 |
+
fontSize: 120,
|
14 |
+
width: 600,
|
15 |
+
fontUrl: DEFAULT_FONT.url,
|
16 |
+
fontFamily: DEFAULT_FONT.postScriptName,
|
17 |
+
color: '#ffffff',
|
18 |
+
wordWrap: 'break-word',
|
19 |
+
wordBreak: 'break-all',
|
20 |
+
textAlign: 'center',
|
21 |
+
},
|
22 |
+
},
|
23 |
+
options: {},
|
24 |
+
});
|
25 |
+
};
|
26 |
+
|
27 |
+
return (
|
28 |
+
<div className="flex-1">
|
29 |
+
<div className="text-md text-text-primary font-medium h-12 flex items-center px-4">
|
30 |
+
Text
|
31 |
+
</div>
|
32 |
+
<div className="px-4">
|
33 |
+
<Button
|
34 |
+
onClick={handleAddText}
|
35 |
+
size="sm"
|
36 |
+
variant="secondary"
|
37 |
+
className="w-full"
|
38 |
+
>
|
39 |
+
Add text
|
40 |
+
</Button>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
);
|
44 |
+
};
|
src/components/editor/menu-item/transitions.tsx
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
DRAG_END,
|
3 |
+
DRAG_PREFIX,
|
4 |
+
DRAG_START,
|
5 |
+
ITransition,
|
6 |
+
TRANSITIONS,
|
7 |
+
dispatcher,
|
8 |
+
filter,
|
9 |
+
} from '@designcombo/core';
|
10 |
+
import React, { useEffect, useState } from 'react';
|
11 |
+
import Draggable from '@/components/shared/draggable';
|
12 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
13 |
+
|
14 |
+
export const Transitions = () => {
|
15 |
+
const [shouldDisplayPreview, setShouldDisplayPreview] = useState(true);
|
16 |
+
// handle track and track item events - updates
|
17 |
+
useEffect(() => {
|
18 |
+
const dragEvents = dispatcher.bus.pipe(
|
19 |
+
filter(({ key }) => key.startsWith(DRAG_PREFIX)),
|
20 |
+
);
|
21 |
+
|
22 |
+
const dragEventsSubscription = dragEvents.subscribe((obj) => {
|
23 |
+
if (obj.key === DRAG_START) {
|
24 |
+
setShouldDisplayPreview(false);
|
25 |
+
} else if (obj.key === DRAG_END) {
|
26 |
+
setShouldDisplayPreview(true);
|
27 |
+
}
|
28 |
+
});
|
29 |
+
return () => dragEventsSubscription.unsubscribe();
|
30 |
+
}, []);
|
31 |
+
return (
|
32 |
+
<div className="flex-1 overflow-auto flex flex-col">
|
33 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
34 |
+
Transitions
|
35 |
+
</div>
|
36 |
+
<ScrollArea>
|
37 |
+
<div className="grid grid-cols-3 gap-2 px-4">
|
38 |
+
{TRANSITIONS.map((transition, index) => (
|
39 |
+
<TransitionsMenuItem
|
40 |
+
key={index}
|
41 |
+
transition={transition}
|
42 |
+
shouldDisplayPreview={shouldDisplayPreview}
|
43 |
+
/>
|
44 |
+
))}
|
45 |
+
</div>
|
46 |
+
</ScrollArea>
|
47 |
+
</div>
|
48 |
+
);
|
49 |
+
};
|
50 |
+
|
51 |
+
const TransitionsMenuItem = ({
|
52 |
+
transition,
|
53 |
+
shouldDisplayPreview,
|
54 |
+
}: {
|
55 |
+
transition: Partial<ITransition>;
|
56 |
+
shouldDisplayPreview: boolean;
|
57 |
+
}) => {
|
58 |
+
const style = React.useMemo(
|
59 |
+
() => ({
|
60 |
+
backgroundImage: `url(${transition.preview})`,
|
61 |
+
backgroundSize: 'cover',
|
62 |
+
width: '70px',
|
63 |
+
height: '70px',
|
64 |
+
}),
|
65 |
+
[transition.preview],
|
66 |
+
);
|
67 |
+
|
68 |
+
return (
|
69 |
+
<Draggable
|
70 |
+
data={transition}
|
71 |
+
renderCustomPreview={<div style={style} className="draggable" />}
|
72 |
+
shouldDisplayPreview={shouldDisplayPreview}
|
73 |
+
>
|
74 |
+
<div>
|
75 |
+
<div>
|
76 |
+
<div
|
77 |
+
// ref={divRef}
|
78 |
+
style={style}
|
79 |
+
className="draggable"
|
80 |
+
/>
|
81 |
+
</div>
|
82 |
+
<div className="text-muted-foreground text-[12px] text-nowrap overflow-ellipsis h-6 flex items-center capitalize">
|
83 |
+
{transition.name || transition.type}
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
</Draggable>
|
87 |
+
);
|
88 |
+
};
|
89 |
+
|
90 |
+
export default TransitionsMenuItem;
|
src/components/editor/menu-item/uploads.tsx
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from '@/components/ui/button';
|
2 |
+
import {
|
3 |
+
ADD_AUDIO,
|
4 |
+
ADD_IMAGE,
|
5 |
+
ADD_TEXT,
|
6 |
+
ADD_VIDEO,
|
7 |
+
dispatcher,
|
8 |
+
} from '@designcombo/core';
|
9 |
+
import { nanoid } from 'nanoid';
|
10 |
+
import { IMAGES } from '@/data/images';
|
11 |
+
import { DEFAULT_FONT } from '@/data/fonts';
|
12 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
13 |
+
import { UploadIcon } from 'lucide-react';
|
14 |
+
|
15 |
+
export const Uploads = () => {
|
16 |
+
const handleAddImage = () => {
|
17 |
+
dispatcher?.dispatch(ADD_IMAGE, {
|
18 |
+
payload: {
|
19 |
+
id: nanoid(),
|
20 |
+
details: {
|
21 |
+
src: IMAGES[4].src,
|
22 |
+
},
|
23 |
+
},
|
24 |
+
options: {
|
25 |
+
trackId: 'main',
|
26 |
+
},
|
27 |
+
});
|
28 |
+
};
|
29 |
+
|
30 |
+
const handleAddText = () => {
|
31 |
+
dispatcher?.dispatch(ADD_TEXT, {
|
32 |
+
payload: {
|
33 |
+
id: nanoid(),
|
34 |
+
details: {
|
35 |
+
text: 'Heading',
|
36 |
+
fontSize: 200,
|
37 |
+
width: 900,
|
38 |
+
fontUrl: DEFAULT_FONT.url,
|
39 |
+
fontFamily: DEFAULT_FONT.postScriptName,
|
40 |
+
color: '#ffffff',
|
41 |
+
WebkitTextStrokeColor: 'green',
|
42 |
+
WebkitTextStrokeWidth: '20px',
|
43 |
+
textShadow: '30px 30px 100px rgba(255, 255, 0, 1)',
|
44 |
+
wordWrap: 'break-word',
|
45 |
+
wordBreak: 'break-all',
|
46 |
+
},
|
47 |
+
},
|
48 |
+
options: {},
|
49 |
+
});
|
50 |
+
};
|
51 |
+
|
52 |
+
const handleAddAudio = () => {
|
53 |
+
dispatcher?.dispatch(ADD_AUDIO, {
|
54 |
+
payload: {
|
55 |
+
id: nanoid(),
|
56 |
+
details: {
|
57 |
+
src: 'https://ik.imagekit.io/snapmotion/timer-voice.mp3',
|
58 |
+
},
|
59 |
+
},
|
60 |
+
options: {},
|
61 |
+
});
|
62 |
+
};
|
63 |
+
|
64 |
+
const handleAddVideo = () => {
|
65 |
+
dispatcher?.dispatch(ADD_VIDEO, {
|
66 |
+
payload: {
|
67 |
+
id: nanoid(),
|
68 |
+
details: {
|
69 |
+
src: 'https://ik.imagekit.io/snapmotion/75475-556034323_medium.mp4',
|
70 |
+
},
|
71 |
+
metadata: {
|
72 |
+
resourceId: '7415538a-5d61-4a81-ad79-c00689b6cc10',
|
73 |
+
},
|
74 |
+
},
|
75 |
+
options: {
|
76 |
+
trackId: 'main',
|
77 |
+
},
|
78 |
+
});
|
79 |
+
};
|
80 |
+
|
81 |
+
const handleAddVideo2 = () => {
|
82 |
+
dispatcher?.dispatch(ADD_VIDEO, {
|
83 |
+
payload: {
|
84 |
+
id: nanoid(),
|
85 |
+
details: {
|
86 |
+
src: 'https://ik.imagekit.io/snapmotion/flat.mp4',
|
87 |
+
},
|
88 |
+
metadata: {
|
89 |
+
resourceId: '7415538a-5do1-4m81-a279-c00689b6cc10',
|
90 |
+
},
|
91 |
+
},
|
92 |
+
});
|
93 |
+
};
|
94 |
+
return (
|
95 |
+
<div className="flex-1">
|
96 |
+
<div className="text-md text-text-primary font-medium h-12 flex items-center px-4">
|
97 |
+
Your media
|
98 |
+
</div>
|
99 |
+
<div className="px-4">
|
100 |
+
<div>
|
101 |
+
<Tabs defaultValue="projects" className="w-full">
|
102 |
+
<TabsList className="grid w-full grid-cols-2">
|
103 |
+
<TabsTrigger value="projects">Project</TabsTrigger>
|
104 |
+
<TabsTrigger value="workspace">Workspace</TabsTrigger>
|
105 |
+
</TabsList>
|
106 |
+
<TabsContent value="projects">
|
107 |
+
<Button
|
108 |
+
onClick={handleAddAudio}
|
109 |
+
className="flex gap-2 w-full"
|
110 |
+
size="sm"
|
111 |
+
variant="secondary"
|
112 |
+
>
|
113 |
+
<UploadIcon size={16} /> Upload
|
114 |
+
</Button>
|
115 |
+
<div></div>
|
116 |
+
</TabsContent>
|
117 |
+
<TabsContent value="workspace">
|
118 |
+
<Button
|
119 |
+
className="flex gap-2 w-full"
|
120 |
+
size="sm"
|
121 |
+
variant="secondary"
|
122 |
+
>
|
123 |
+
<UploadIcon size={16} /> Upload
|
124 |
+
</Button>
|
125 |
+
<div>Some assets</div>
|
126 |
+
</TabsContent>
|
127 |
+
</Tabs>
|
128 |
+
</div>
|
129 |
+
</div>
|
130 |
+
</div>
|
131 |
+
);
|
132 |
+
};
|
src/components/editor/menu-item/videos.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
2 |
+
import { VIDEOS } from '@/data/video';
|
3 |
+
import { ADD_VIDEO, dispatcher } from '@designcombo/core';
|
4 |
+
import { nanoid } from 'nanoid';
|
5 |
+
|
6 |
+
export const Videos = () => {
|
7 |
+
const handleAddVideo = (src: string) => {
|
8 |
+
dispatcher?.dispatch(ADD_VIDEO, {
|
9 |
+
payload: {
|
10 |
+
id: nanoid(),
|
11 |
+
details: {
|
12 |
+
src: src,
|
13 |
+
},
|
14 |
+
metadata: {
|
15 |
+
resourceId: src,
|
16 |
+
},
|
17 |
+
},
|
18 |
+
options: {
|
19 |
+
trackId: 'main',
|
20 |
+
},
|
21 |
+
});
|
22 |
+
};
|
23 |
+
|
24 |
+
return (
|
25 |
+
<div className="flex-1 flex flex-col">
|
26 |
+
<div className="text-md flex-none text-text-primary font-medium h-12 flex items-center px-4">
|
27 |
+
Videos
|
28 |
+
</div>
|
29 |
+
<ScrollArea>
|
30 |
+
<div className="px-4 masonry-sm">
|
31 |
+
{VIDEOS.map((image, index) => {
|
32 |
+
return (
|
33 |
+
<div
|
34 |
+
onClick={() => handleAddVideo(image.src)}
|
35 |
+
key={index}
|
36 |
+
className="flex items-center justify-center w-full bg-zinc-950 pb-2 overflow-hidden cursor-pointer"
|
37 |
+
>
|
38 |
+
<img
|
39 |
+
src={image.preview}
|
40 |
+
className="w-full h-full object-cover rounded-md"
|
41 |
+
alt="image"
|
42 |
+
/>
|
43 |
+
</div>
|
44 |
+
);
|
45 |
+
})}
|
46 |
+
</div>
|
47 |
+
</ScrollArea>
|
48 |
+
</div>
|
49 |
+
);
|
50 |
+
};
|
src/components/editor/menu-list.tsx
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import useLayoutStore from '@/store/use-layout-store';
|
2 |
+
import { Icons } from '@/components/shared/icons';
|
3 |
+
import { Button } from '@/components/ui/button';
|
4 |
+
import { cn } from '@/lib/utils';
|
5 |
+
|
6 |
+
export default function MenuList() {
|
7 |
+
const { setActiveMenuItem, setShowMenuItem, activeMenuItem, showMenuItem } =
|
8 |
+
useLayoutStore();
|
9 |
+
return (
|
10 |
+
<div
|
11 |
+
style={{ zIndex: 201 }}
|
12 |
+
className="w-14 py-2 absolute top-1/2 -translate-y-1/2 mt-6 left-2.5 bg-zinc-950 rounded-lg shadow-lg flex flex-col items-center"
|
13 |
+
>
|
14 |
+
<Button
|
15 |
+
onClick={() => {
|
16 |
+
setActiveMenuItem('uploads');
|
17 |
+
setShowMenuItem(true);
|
18 |
+
}}
|
19 |
+
className={cn(
|
20 |
+
showMenuItem && activeMenuItem === 'uploads'
|
21 |
+
? 'bg-grey-900'
|
22 |
+
: 'text-muted-foreground',
|
23 |
+
)}
|
24 |
+
variant={'ghost'}
|
25 |
+
size={'icon'}
|
26 |
+
>
|
27 |
+
<Icons.upload width={20} />
|
28 |
+
</Button>
|
29 |
+
<Button
|
30 |
+
onClick={() => {
|
31 |
+
setActiveMenuItem('texts');
|
32 |
+
setShowMenuItem(true);
|
33 |
+
}}
|
34 |
+
className={cn(
|
35 |
+
showMenuItem && activeMenuItem === 'texts'
|
36 |
+
? 'bg-grey-900'
|
37 |
+
: 'text-muted-foreground',
|
38 |
+
)}
|
39 |
+
variant={'ghost'}
|
40 |
+
size={'icon'}
|
41 |
+
>
|
42 |
+
<Icons.type width={20} />
|
43 |
+
</Button>
|
44 |
+
<Button
|
45 |
+
onClick={() => {
|
46 |
+
setActiveMenuItem('videos');
|
47 |
+
setShowMenuItem(true);
|
48 |
+
}}
|
49 |
+
className={cn(
|
50 |
+
showMenuItem && activeMenuItem === 'videos'
|
51 |
+
? 'bg-grey-900'
|
52 |
+
: 'text-muted-foreground',
|
53 |
+
)}
|
54 |
+
variant={'ghost'}
|
55 |
+
size={'icon'}
|
56 |
+
>
|
57 |
+
<Icons.video width={20} />
|
58 |
+
</Button>
|
59 |
+
<Button
|
60 |
+
onClick={() => {
|
61 |
+
setActiveMenuItem('images');
|
62 |
+
setShowMenuItem(true);
|
63 |
+
}}
|
64 |
+
className={cn(
|
65 |
+
showMenuItem && activeMenuItem === 'images'
|
66 |
+
? 'bg-grey-900'
|
67 |
+
: 'text-muted-foreground',
|
68 |
+
)}
|
69 |
+
variant={'ghost'}
|
70 |
+
size={'icon'}
|
71 |
+
>
|
72 |
+
<Icons.image width={20} />
|
73 |
+
</Button>
|
74 |
+
<Button
|
75 |
+
onClick={() => {
|
76 |
+
setActiveMenuItem('shapes');
|
77 |
+
setShowMenuItem(true);
|
78 |
+
}}
|
79 |
+
className={cn(
|
80 |
+
showMenuItem && activeMenuItem === 'shapes'
|
81 |
+
? 'bg-grey-900'
|
82 |
+
: 'text-muted-foreground',
|
83 |
+
)}
|
84 |
+
variant={'ghost'}
|
85 |
+
size={'icon'}
|
86 |
+
>
|
87 |
+
<Icons.shapes width={20} />
|
88 |
+
</Button>
|
89 |
+
<Button
|
90 |
+
onClick={() => {
|
91 |
+
setActiveMenuItem('audios');
|
92 |
+
setShowMenuItem(true);
|
93 |
+
}}
|
94 |
+
className={cn(
|
95 |
+
showMenuItem && activeMenuItem === 'audios'
|
96 |
+
? 'bg-grey-900'
|
97 |
+
: 'text-muted-foreground',
|
98 |
+
)}
|
99 |
+
variant={'ghost'}
|
100 |
+
size={'icon'}
|
101 |
+
>
|
102 |
+
<Icons.audio width={20} />
|
103 |
+
</Button>
|
104 |
+
|
105 |
+
<Button
|
106 |
+
onClick={() => {
|
107 |
+
setActiveMenuItem('transitions');
|
108 |
+
setShowMenuItem(true);
|
109 |
+
}}
|
110 |
+
className={cn(
|
111 |
+
showMenuItem && activeMenuItem === 'transitions'
|
112 |
+
? 'bg-grey-900'
|
113 |
+
: 'text-muted-foreground',
|
114 |
+
)}
|
115 |
+
variant={'ghost'}
|
116 |
+
size={'icon'}
|
117 |
+
>
|
118 |
+
<svg
|
119 |
+
width={20}
|
120 |
+
viewBox="0 0 24 24"
|
121 |
+
fill="none"
|
122 |
+
xmlns="http://www.w3.org/2000/svg"
|
123 |
+
>
|
124 |
+
<path
|
125 |
+
d="M3 5.30359C3 3.93159 4.659 3.24359 5.629 4.21359L11.997 10.5826L10.583 11.9966L5 6.41359V17.5856L10.586 11.9996L10.583 11.9966L11.997 10.5826L12 10.5856L18.371 4.21459C19.341 3.24459 21 3.93159 21 5.30359V18.6956C21 20.0676 19.341 20.7556 18.371 19.7856L12 13.5L13.414 11.9996L19 17.5866V6.41359L13.414 11.9996L13.421 12.0056L12.006 13.4206L12 13.4136L5.629 19.7846C4.659 20.7546 3 20.0676 3 18.6956V5.30359Z"
|
126 |
+
fill="currentColor"
|
127 |
+
/>
|
128 |
+
</svg>
|
129 |
+
</Button>
|
130 |
+
</div>
|
131 |
+
);
|
132 |
+
}
|
src/components/editor/navbar.tsx
ADDED
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useRef, useState } from 'react';
|
2 |
+
import { Button } from '@/components/ui/button';
|
3 |
+
import {
|
4 |
+
DESIGN_RESIZE,
|
5 |
+
HISTORY_UNDO,
|
6 |
+
HISTORY_REDO,
|
7 |
+
dispatcher,
|
8 |
+
useEditorState,
|
9 |
+
} from '@designcombo/core';
|
10 |
+
import logoDark from '@/assets/logo-dark.png';
|
11 |
+
import { Icons } from '../shared/icons';
|
12 |
+
import {
|
13 |
+
Popover,
|
14 |
+
PopoverContent,
|
15 |
+
PopoverTrigger,
|
16 |
+
} from '@/components/ui/popover';
|
17 |
+
import { ChevronDown, Download } from 'lucide-react';
|
18 |
+
import { Label } from '../ui/label';
|
19 |
+
import { Progress } from '../ui/progress';
|
20 |
+
import { download } from '@/utils/download';
|
21 |
+
|
22 |
+
const baseUrl = 'https://renderer.designcombo.dev';
|
23 |
+
// https://renderer.designcombo.dev/status/{id}
|
24 |
+
export default function Navbar() {
|
25 |
+
const handleUndo = () => {
|
26 |
+
dispatcher.dispatch(HISTORY_UNDO);
|
27 |
+
};
|
28 |
+
|
29 |
+
const handleRedo = () => {
|
30 |
+
dispatcher.dispatch(HISTORY_REDO);
|
31 |
+
};
|
32 |
+
|
33 |
+
const openLink = (url: string) => {
|
34 |
+
window.open(url, '_blank'); // '_blank' will open the link in a new tab
|
35 |
+
};
|
36 |
+
|
37 |
+
return (
|
38 |
+
<div
|
39 |
+
style={{
|
40 |
+
display: 'grid',
|
41 |
+
gridTemplateColumns: '320px 1fr 320px',
|
42 |
+
}}
|
43 |
+
className="h-[72px] absolute top-0 left-0 right-0 px-2 z-[205] pointer-events-none flex items-center"
|
44 |
+
>
|
45 |
+
<div className="flex items-center gap-2 pointer-events-auto h-14">
|
46 |
+
<div className="bg-zinc-950 h-12 w-12 flex items-center justify-center rounded-md">
|
47 |
+
<img src={logoDark} alt="logo" className="h-5 w-5" />
|
48 |
+
</div>
|
49 |
+
<div className="bg-zinc-950 px-1.5 h-12 flex items-center">
|
50 |
+
<Button
|
51 |
+
onClick={handleUndo}
|
52 |
+
className="text-muted-foreground"
|
53 |
+
variant="ghost"
|
54 |
+
size="icon"
|
55 |
+
>
|
56 |
+
<Icons.undo width={20} />
|
57 |
+
</Button>
|
58 |
+
<Button
|
59 |
+
onClick={handleRedo}
|
60 |
+
className="text-muted-foreground"
|
61 |
+
variant="ghost"
|
62 |
+
size="icon"
|
63 |
+
>
|
64 |
+
<Icons.redo width={20} />
|
65 |
+
</Button>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
|
69 |
+
<div className="pointer-events-auto h-14 flex items-center gap-2 justify-center">
|
70 |
+
<div className="bg-zinc-950 px-2.5 rounded-md h-12 gap-4 flex items-center">
|
71 |
+
<div className="font-medium text-sm px-1">Untitled video</div>
|
72 |
+
<ResizeVideo />
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
|
76 |
+
<div className="flex items-center gap-2 pointer-events-auto h-14 justify-end">
|
77 |
+
<div className="flex items-center gap-2 bg-zinc-950 px-2.5 rounded-md h-12">
|
78 |
+
<Button
|
79 |
+
className="border border-white/10 flex gap-2"
|
80 |
+
onClick={() => openLink('https://discord.gg/jrZs3wZyM5')}
|
81 |
+
size="xs"
|
82 |
+
variant="secondary"
|
83 |
+
>
|
84 |
+
<svg
|
85 |
+
stroke="currentColor"
|
86 |
+
fill="currentColor"
|
87 |
+
strokeWidth="0"
|
88 |
+
viewBox="0 0 640 512"
|
89 |
+
height={16}
|
90 |
+
xmlns="http://www.w3.org/2000/svg"
|
91 |
+
>
|
92 |
+
<path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"></path>
|
93 |
+
</svg>
|
94 |
+
Discord
|
95 |
+
</Button>
|
96 |
+
<DownloadPopover />
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
</div>
|
100 |
+
);
|
101 |
+
}
|
102 |
+
|
103 |
+
interface IDownloadState {
|
104 |
+
renderId: string;
|
105 |
+
progress: number;
|
106 |
+
isDownloading: boolean;
|
107 |
+
}
|
108 |
+
const DownloadPopover = () => {
|
109 |
+
const [open, setOpen] = useState(false);
|
110 |
+
const popoverRef = useRef<HTMLDivElement>(null);
|
111 |
+
const [downloadState, setDownloadState] = useState<IDownloadState>({
|
112 |
+
progress: 0,
|
113 |
+
isDownloading: false,
|
114 |
+
renderId: '',
|
115 |
+
});
|
116 |
+
const {
|
117 |
+
trackItemIds,
|
118 |
+
trackItemsMap,
|
119 |
+
transitionIds,
|
120 |
+
transitionsMap,
|
121 |
+
tracks,
|
122 |
+
duration,
|
123 |
+
size,
|
124 |
+
} = useEditorState();
|
125 |
+
|
126 |
+
const handleExport = () => {
|
127 |
+
const payload = {
|
128 |
+
trackItemIds,
|
129 |
+
trackItemsMap,
|
130 |
+
transitionIds,
|
131 |
+
transitionsMap,
|
132 |
+
tracks,
|
133 |
+
size: size,
|
134 |
+
duration: duration - 750,
|
135 |
+
fps: 30,
|
136 |
+
projectId: 'main',
|
137 |
+
};
|
138 |
+
|
139 |
+
setDownloadState({
|
140 |
+
...downloadState,
|
141 |
+
isDownloading: true,
|
142 |
+
});
|
143 |
+
fetch(`${baseUrl}`, {
|
144 |
+
method: 'POST',
|
145 |
+
headers: {
|
146 |
+
'Content-Type': 'application/json',
|
147 |
+
},
|
148 |
+
body: JSON.stringify(payload),
|
149 |
+
})
|
150 |
+
.then((res) => res.json())
|
151 |
+
.then(({ render }) => {
|
152 |
+
setDownloadState({
|
153 |
+
...downloadState,
|
154 |
+
renderId: render.id,
|
155 |
+
isDownloading: true,
|
156 |
+
});
|
157 |
+
});
|
158 |
+
};
|
159 |
+
|
160 |
+
useEffect(() => {
|
161 |
+
console.log('renderId', downloadState.renderId);
|
162 |
+
|
163 |
+
let interval: NodeJS.Timeout;
|
164 |
+
if (downloadState.renderId) {
|
165 |
+
interval = setInterval(() => {
|
166 |
+
fetch(`${baseUrl}/status/${downloadState.renderId}`)
|
167 |
+
.then((res) => res.json())
|
168 |
+
.then(({ render: { progress, output } }) => {
|
169 |
+
if (progress === 100) {
|
170 |
+
clearInterval(interval);
|
171 |
+
setDownloadState({
|
172 |
+
...downloadState,
|
173 |
+
renderId: '',
|
174 |
+
progress: 0,
|
175 |
+
isDownloading: false,
|
176 |
+
});
|
177 |
+
download(output, `${downloadState.renderId}`);
|
178 |
+
setOpen(false);
|
179 |
+
} else {
|
180 |
+
setDownloadState({
|
181 |
+
...downloadState,
|
182 |
+
progress,
|
183 |
+
isDownloading: true,
|
184 |
+
});
|
185 |
+
}
|
186 |
+
});
|
187 |
+
}, 1000);
|
188 |
+
}
|
189 |
+
return () => {
|
190 |
+
if (interval) clearInterval(interval);
|
191 |
+
};
|
192 |
+
}, [downloadState.renderId]);
|
193 |
+
|
194 |
+
return (
|
195 |
+
<Popover open={open} onOpenChange={setOpen}>
|
196 |
+
<PopoverTrigger asChild>
|
197 |
+
<Button className="flex gap-1 h-8 w-8" size="icon" variant="default">
|
198 |
+
<Download width={18} />
|
199 |
+
</Button>
|
200 |
+
</PopoverTrigger>
|
201 |
+
<PopoverContent className="w-60 z-[250] flex flex-col gap-4">
|
202 |
+
{downloadState.isDownloading ? (
|
203 |
+
<>
|
204 |
+
<Label>Downloading</Label>
|
205 |
+
<div className="flex items-center gap-2">
|
206 |
+
<Progress
|
207 |
+
className="h-2 rounded-sm"
|
208 |
+
value={downloadState.progress}
|
209 |
+
/>
|
210 |
+
<div className="text-zinc-400 text-sm border border-border p-1 rounded-sm">
|
211 |
+
{parseInt(downloadState.progress.toString())}%
|
212 |
+
</div>
|
213 |
+
</div>
|
214 |
+
<div>
|
215 |
+
<Button className="w-full" size="xs">
|
216 |
+
Copy link
|
217 |
+
</Button>
|
218 |
+
</div>
|
219 |
+
</>
|
220 |
+
) : (
|
221 |
+
<>
|
222 |
+
<Label>Export settings</Label>
|
223 |
+
<Button className="w-full justify-between" variant="outline">
|
224 |
+
<div>MP4</div>
|
225 |
+
<ChevronDown width={16} />
|
226 |
+
</Button>
|
227 |
+
<div>
|
228 |
+
<Button onClick={handleExport} className="w-full" size="xs">
|
229 |
+
Export
|
230 |
+
</Button>
|
231 |
+
</div>
|
232 |
+
</>
|
233 |
+
)}
|
234 |
+
</PopoverContent>
|
235 |
+
</Popover>
|
236 |
+
);
|
237 |
+
};
|
238 |
+
|
239 |
+
interface ResizeOptionProps {
|
240 |
+
label: string;
|
241 |
+
icon: string;
|
242 |
+
value: ResizeValue;
|
243 |
+
}
|
244 |
+
|
245 |
+
interface ResizeValue {
|
246 |
+
width: number;
|
247 |
+
height: number;
|
248 |
+
name: string;
|
249 |
+
}
|
250 |
+
|
251 |
+
const RESIZE_OPTIONS: ResizeOptionProps[] = [
|
252 |
+
{
|
253 |
+
label: '16:9',
|
254 |
+
icon: 'landscape',
|
255 |
+
value: {
|
256 |
+
width: 1920,
|
257 |
+
height: 1080,
|
258 |
+
name: '16:9',
|
259 |
+
},
|
260 |
+
},
|
261 |
+
{
|
262 |
+
label: '9:16',
|
263 |
+
icon: 'portrait',
|
264 |
+
value: {
|
265 |
+
width: 1080,
|
266 |
+
height: 1920,
|
267 |
+
name: '9:16',
|
268 |
+
},
|
269 |
+
},
|
270 |
+
{
|
271 |
+
label: '1:1',
|
272 |
+
icon: 'square',
|
273 |
+
value: {
|
274 |
+
width: 1080,
|
275 |
+
height: 1080,
|
276 |
+
name: '1:1',
|
277 |
+
},
|
278 |
+
},
|
279 |
+
];
|
280 |
+
|
281 |
+
const ResizeVideo = () => {
|
282 |
+
const handleResize = (payload: ResizeValue) => {
|
283 |
+
dispatcher.dispatch(DESIGN_RESIZE, {
|
284 |
+
payload,
|
285 |
+
});
|
286 |
+
};
|
287 |
+
return (
|
288 |
+
<Popover>
|
289 |
+
<PopoverTrigger asChild>
|
290 |
+
<Button
|
291 |
+
className="border border-white/10"
|
292 |
+
size="xs"
|
293 |
+
variant="secondary"
|
294 |
+
>
|
295 |
+
Resize
|
296 |
+
</Button>
|
297 |
+
</PopoverTrigger>
|
298 |
+
<PopoverContent className="w-60 z-[250]">
|
299 |
+
<div className="grid gap-4 text-sm">
|
300 |
+
{RESIZE_OPTIONS.map((option, index) => (
|
301 |
+
<ResizeOption
|
302 |
+
key={index}
|
303 |
+
label={option.label}
|
304 |
+
icon={option.icon}
|
305 |
+
value={option.value}
|
306 |
+
handleResize={handleResize}
|
307 |
+
/>
|
308 |
+
))}
|
309 |
+
</div>
|
310 |
+
</PopoverContent>
|
311 |
+
</Popover>
|
312 |
+
);
|
313 |
+
};
|
314 |
+
|
315 |
+
const ResizeOption = ({
|
316 |
+
label,
|
317 |
+
icon,
|
318 |
+
value,
|
319 |
+
handleResize,
|
320 |
+
}: ResizeOptionProps & { handleResize: (payload: ResizeValue) => void }) => {
|
321 |
+
const Icon = Icons[icon];
|
322 |
+
return (
|
323 |
+
<div
|
324 |
+
onClick={() => handleResize(value)}
|
325 |
+
className="flex items-center gap-4 hover:bg-zinc-50/10 cursor-pointer"
|
326 |
+
>
|
327 |
+
<div className="text-muted-foreground">
|
328 |
+
<Icon />
|
329 |
+
</div>
|
330 |
+
<div>
|
331 |
+
<div>{label}</div>
|
332 |
+
<div className="text-muted-foreground">Tiktok, Instagram</div>
|
333 |
+
</div>
|
334 |
+
</div>
|
335 |
+
);
|
336 |
+
};
|
src/components/editor/resize-video.tsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
const ResizeVideo = () => {
|
2 |
+
return <div>ResizeVideo</div>;
|
3 |
+
};
|
src/components/editor/use-hotkeys.ts
ADDED
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { DEFAULT_FONT } from '@/data/fonts';
|
2 |
+
import {
|
3 |
+
ACTIVE_CLONE,
|
4 |
+
ACTIVE_DELETE,
|
5 |
+
ADD_TEXT,
|
6 |
+
HISTORY_REDO,
|
7 |
+
HISTORY_UNDO,
|
8 |
+
LAYER_SELECT,
|
9 |
+
PLAYER_SEEK_BY,
|
10 |
+
PLAYER_TOGGLE_PLAY,
|
11 |
+
dispatcher,
|
12 |
+
} from '@designcombo/core';
|
13 |
+
import hotkeys from 'hotkeys-js';
|
14 |
+
import { nanoid } from 'nanoid';
|
15 |
+
import { useEffect } from 'react';
|
16 |
+
|
17 |
+
const useHotkeys = () => {
|
18 |
+
useEffect(() => {
|
19 |
+
const dispatch = dispatcher.dispatch;
|
20 |
+
// handle undo
|
21 |
+
hotkeys('ctrl+z,command+z', (event) => {
|
22 |
+
event.preventDefault(); // Prevent the default action
|
23 |
+
dispatch(HISTORY_UNDO);
|
24 |
+
// dispatch(UNDO);
|
25 |
+
});
|
26 |
+
|
27 |
+
// handle redo: ctrl+shift+z
|
28 |
+
hotkeys('ctrl+shift+z,command+shift+z', (event) => {
|
29 |
+
event.preventDefault(); // Prevent the default action
|
30 |
+
// redo();
|
31 |
+
dispatch(HISTORY_REDO);
|
32 |
+
});
|
33 |
+
|
34 |
+
// Define the shortcut and corresponding action
|
35 |
+
hotkeys('ctrl+s,command+s', (event) => {
|
36 |
+
event.preventDefault(); // Prevent the default action
|
37 |
+
console.log('split action');
|
38 |
+
// dispatch(ACTIVE_SPLIT);
|
39 |
+
});
|
40 |
+
|
41 |
+
// duplicate item
|
42 |
+
hotkeys('ctrl+d,command+d', (event) => {
|
43 |
+
event.preventDefault(); // Prevent the default action
|
44 |
+
dispatch(ACTIVE_CLONE);
|
45 |
+
});
|
46 |
+
|
47 |
+
hotkeys('backspace,delete', (event) => {
|
48 |
+
event.preventDefault(); // Prevent the default action
|
49 |
+
dispatch(ACTIVE_DELETE);
|
50 |
+
});
|
51 |
+
|
52 |
+
hotkeys('esc', (event) => {
|
53 |
+
event.preventDefault(); // Prevent the default action
|
54 |
+
dispatcher.dispatch(LAYER_SELECT, { payload: { activeIds: [] } });
|
55 |
+
});
|
56 |
+
|
57 |
+
hotkeys('space', (event) => {
|
58 |
+
event.preventDefault();
|
59 |
+
dispatch(PLAYER_TOGGLE_PLAY);
|
60 |
+
});
|
61 |
+
|
62 |
+
hotkeys('down', (event) => {
|
63 |
+
event.preventDefault();
|
64 |
+
dispatch(PLAYER_SEEK_BY, { payload: { frames: 1 } });
|
65 |
+
});
|
66 |
+
|
67 |
+
hotkeys('up', (event) => {
|
68 |
+
event.preventDefault();
|
69 |
+
dispatch(PLAYER_SEEK_BY, { payload: { frames: -1 } });
|
70 |
+
});
|
71 |
+
|
72 |
+
// New shortcut for the 'T' key
|
73 |
+
hotkeys('t', (event) => {
|
74 |
+
dispatcher.dispatch(ADD_TEXT, {
|
75 |
+
payload: {
|
76 |
+
id: nanoid(),
|
77 |
+
details: {
|
78 |
+
text: 'Add text',
|
79 |
+
fontSize: 62,
|
80 |
+
fontFamily: DEFAULT_FONT.postScriptName,
|
81 |
+
fontUrl: DEFAULT_FONT.url,
|
82 |
+
width: 400,
|
83 |
+
textAlign: 'left',
|
84 |
+
color: '#ffffff',
|
85 |
+
},
|
86 |
+
},
|
87 |
+
});
|
88 |
+
});
|
89 |
+
|
90 |
+
return () => {
|
91 |
+
hotkeys.unbind('ctrl+shift+z,command+shift+z');
|
92 |
+
hotkeys.unbind('ctrl+z,command+z');
|
93 |
+
hotkeys.unbind('ctrl+s,command+s');
|
94 |
+
hotkeys.unbind('ctrl+d,command+d');
|
95 |
+
hotkeys.unbind('backspace,delete');
|
96 |
+
hotkeys.unbind('escape');
|
97 |
+
hotkeys.unbind('down');
|
98 |
+
hotkeys.unbind('up');
|
99 |
+
hotkeys.unbind('space');
|
100 |
+
hotkeys.unbind('t');
|
101 |
+
};
|
102 |
+
}, []);
|
103 |
+
};
|
104 |
+
|
105 |
+
export default useHotkeys;
|
src/components/shared/draggable.tsx
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEditorState } from '@designcombo/core';
|
2 |
+
import React, { useState, cloneElement, ReactElement, useRef } from 'react';
|
3 |
+
import { createPortal } from 'react-dom';
|
4 |
+
|
5 |
+
interface DraggableProps {
|
6 |
+
children: ReactElement;
|
7 |
+
shouldDisplayPreview?: boolean;
|
8 |
+
renderCustomPreview?: ReactElement;
|
9 |
+
data?: Record<string, any>;
|
10 |
+
}
|
11 |
+
|
12 |
+
const Draggable: React.FC<DraggableProps> = ({
|
13 |
+
children,
|
14 |
+
renderCustomPreview,
|
15 |
+
data = {},
|
16 |
+
shouldDisplayPreview = true,
|
17 |
+
}) => {
|
18 |
+
const [isDragging, setIsDragging] = useState(false);
|
19 |
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
20 |
+
const previewRef = useRef<HTMLDivElement>(null);
|
21 |
+
const handleDragStart = (e: React.DragEvent<HTMLElement>) => {
|
22 |
+
setIsDragging(true);
|
23 |
+
e.dataTransfer.setDragImage(new Image(), 0, 0); // Hides default preview
|
24 |
+
// set drag data
|
25 |
+
e.dataTransfer.setData('transition', JSON.stringify(data));
|
26 |
+
setPosition({
|
27 |
+
x: e.clientX,
|
28 |
+
y: e.clientY,
|
29 |
+
});
|
30 |
+
};
|
31 |
+
|
32 |
+
const handleDragEnd = () => {
|
33 |
+
setIsDragging(false);
|
34 |
+
};
|
35 |
+
|
36 |
+
const handleDrag = (e: React.DragEvent<HTMLElement>) => {
|
37 |
+
if (isDragging) {
|
38 |
+
setPosition({
|
39 |
+
x: e.clientX,
|
40 |
+
y: e.clientY,
|
41 |
+
});
|
42 |
+
}
|
43 |
+
};
|
44 |
+
|
45 |
+
const childWithProps = cloneElement(children, {
|
46 |
+
draggable: true,
|
47 |
+
onDragStart: handleDragStart,
|
48 |
+
onDragEnd: handleDragEnd,
|
49 |
+
onDrag: handleDrag,
|
50 |
+
style: {
|
51 |
+
...children.props.style,
|
52 |
+
cursor: 'grab',
|
53 |
+
},
|
54 |
+
});
|
55 |
+
|
56 |
+
return (
|
57 |
+
<>
|
58 |
+
{childWithProps}
|
59 |
+
{isDragging && shouldDisplayPreview && renderCustomPreview
|
60 |
+
? createPortal(
|
61 |
+
<div
|
62 |
+
ref={previewRef}
|
63 |
+
style={{
|
64 |
+
position: 'fixed',
|
65 |
+
left: position.x,
|
66 |
+
top: position.y,
|
67 |
+
pointerEvents: 'none',
|
68 |
+
zIndex: 9999,
|
69 |
+
transform: 'translate(-50%, -50%)', // Center the preview
|
70 |
+
}}
|
71 |
+
>
|
72 |
+
{renderCustomPreview}
|
73 |
+
</div>,
|
74 |
+
document.body,
|
75 |
+
)
|
76 |
+
: null}
|
77 |
+
</>
|
78 |
+
);
|
79 |
+
};
|
80 |
+
|
81 |
+
export default Draggable;
|
src/components/shared/icons.tsx
ADDED
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
AlertTriangle,
|
3 |
+
ArrowRight,
|
4 |
+
ArrowUpRight,
|
5 |
+
BookOpen,
|
6 |
+
Check,
|
7 |
+
ChevronLeft,
|
8 |
+
ChevronRight,
|
9 |
+
Copy,
|
10 |
+
CreditCard,
|
11 |
+
File,
|
12 |
+
FileText,
|
13 |
+
FolderClosed,
|
14 |
+
HelpCircle,
|
15 |
+
Home,
|
16 |
+
Image,
|
17 |
+
Laptop,
|
18 |
+
LayoutPanelLeft,
|
19 |
+
LineChart,
|
20 |
+
Loader2,
|
21 |
+
LucideIcon,
|
22 |
+
LucideProps,
|
23 |
+
MessagesSquare,
|
24 |
+
Moon,
|
25 |
+
MoreVertical,
|
26 |
+
Package,
|
27 |
+
Plus,
|
28 |
+
Puzzle,
|
29 |
+
Search,
|
30 |
+
Settings,
|
31 |
+
SunMedium,
|
32 |
+
Trash,
|
33 |
+
Text,
|
34 |
+
Type,
|
35 |
+
User,
|
36 |
+
X,
|
37 |
+
Square,
|
38 |
+
RectangleVertical,
|
39 |
+
RectangleHorizontal,
|
40 |
+
WandSparkles,
|
41 |
+
Zap,
|
42 |
+
Music,
|
43 |
+
VideoIcon,
|
44 |
+
} from 'lucide-react';
|
45 |
+
|
46 |
+
export type Icon = LucideIcon;
|
47 |
+
|
48 |
+
export const Icons = {
|
49 |
+
add: Plus,
|
50 |
+
audio: Music,
|
51 |
+
arrowRight: ArrowRight,
|
52 |
+
arrowUpRight: ArrowUpRight,
|
53 |
+
billing: CreditCard,
|
54 |
+
bookOpen: BookOpen,
|
55 |
+
chevronLeft: ChevronLeft,
|
56 |
+
chevronRight: ChevronRight,
|
57 |
+
check: Check,
|
58 |
+
close: X,
|
59 |
+
copy: Copy,
|
60 |
+
dashboard: LayoutPanelLeft,
|
61 |
+
ellipsis: MoreVertical,
|
62 |
+
folder: FolderClosed,
|
63 |
+
gitHub: ({ ...props }: LucideProps) => (
|
64 |
+
<svg
|
65 |
+
aria-hidden="true"
|
66 |
+
focusable="false"
|
67 |
+
data-prefix="fab"
|
68 |
+
data-icon="github"
|
69 |
+
role="img"
|
70 |
+
xmlns="http://www.w3.org/2000/svg"
|
71 |
+
viewBox="0 0 496 512"
|
72 |
+
{...props}
|
73 |
+
>
|
74 |
+
<path
|
75 |
+
fill="currentColor"
|
76 |
+
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
|
77 |
+
></path>
|
78 |
+
</svg>
|
79 |
+
),
|
80 |
+
google: ({ ...props }: LucideProps) => (
|
81 |
+
<svg
|
82 |
+
aria-hidden="true"
|
83 |
+
focusable="false"
|
84 |
+
data-prefix="fab"
|
85 |
+
data-icon="google"
|
86 |
+
role="img"
|
87 |
+
xmlns="http://www.w3.org/2000/svg"
|
88 |
+
viewBox="0 0 488 512"
|
89 |
+
{...props}
|
90 |
+
>
|
91 |
+
<path
|
92 |
+
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
93 |
+
fill="currentColor"
|
94 |
+
/>
|
95 |
+
</svg>
|
96 |
+
),
|
97 |
+
nextjs: ({ ...props }: LucideProps) => (
|
98 |
+
<svg
|
99 |
+
aria-hidden="true"
|
100 |
+
focusable="false"
|
101 |
+
data-prefix="fab"
|
102 |
+
data-icon="nextjs"
|
103 |
+
role="img"
|
104 |
+
xmlns="http://www.w3.org/2000/svg"
|
105 |
+
viewBox="0 0 15 15"
|
106 |
+
{...props}
|
107 |
+
>
|
108 |
+
<path
|
109 |
+
fill="currentColor"
|
110 |
+
d="m4.5 4.5l.405-.293A.5.5 0 0 0 4 4.5zm3 9.5A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1zM5 12V4.5H4V12zm-.905-7.207l6.5 9l.81-.586l-6.5-9zM10 4v6h1V4z"
|
111 |
+
></path>
|
112 |
+
</svg>
|
113 |
+
),
|
114 |
+
help: HelpCircle,
|
115 |
+
home: Home,
|
116 |
+
image: Image,
|
117 |
+
landscape: RectangleHorizontal,
|
118 |
+
laptop: Laptop,
|
119 |
+
lineChart: LineChart,
|
120 |
+
logo: Puzzle,
|
121 |
+
media: Image,
|
122 |
+
messages: MessagesSquare,
|
123 |
+
moon: Moon,
|
124 |
+
package: Package,
|
125 |
+
page: File,
|
126 |
+
portrait: RectangleVertical,
|
127 |
+
post: FileText,
|
128 |
+
preset: Zap,
|
129 |
+
search: Search,
|
130 |
+
square: Square,
|
131 |
+
redo: ({ ...props }: LucideProps) => (
|
132 |
+
<svg
|
133 |
+
viewBox="0 0 24 24"
|
134 |
+
fill="none"
|
135 |
+
xmlns="http://www.w3.org/2000/svg"
|
136 |
+
{...props}
|
137 |
+
>
|
138 |
+
<path
|
139 |
+
fillRule="evenodd"
|
140 |
+
clipRule="evenodd"
|
141 |
+
d="M14.2957 4.15721C13.9052 3.76669 13.9052 3.13352 14.2957 2.743C14.6862 2.35247 15.3194 2.35247 15.7099 2.743L20.953 7.98603C21.1387 8.16758 21.2539 8.42087 21.2539 8.70108C21.2539 8.70111 21.2539 8.70114 21.2539 8.70117C21.2539 8.70431 21.2539 8.70744 21.2539 8.71057C21.2518 8.93273 21.1773 9.13757 21.0528 9.30262C21.0249 9.33973 20.9942 9.37505 20.961 9.40828L15.7099 14.6593C15.3194 15.0499 14.6862 15.0499 14.2957 14.6593C13.9052 14.2688 13.9052 13.6357 14.2957 13.2451L17.8398 9.70108H9.22665C8.59943 9.70108 7.97836 9.82462 7.39888 10.0646C6.81941 10.3047 6.29289 10.6565 5.84938 11.1C5.40587 11.5435 5.05406 12.07 4.81403 12.6495C4.57401 13.229 4.45047 13.85 4.45047 14.4773C4.45047 15.1045 4.57401 15.7256 4.81403 16.305C5.05406 16.8845 5.40587 17.411 5.84938 17.8545C6.29289 18.298 6.81941 18.6498 7.39888 18.8899C7.97836 19.1299 8.59943 19.2534 9.22665 19.2534H12.9024C13.4547 19.2534 13.9024 19.7012 13.9024 20.2534C13.9024 20.8057 13.4547 21.2534 12.9024 21.2534H9.22665C8.33679 21.2534 7.45564 21.0782 6.63352 20.7376C5.81139 20.3971 5.06439 19.898 4.43517 19.2687C3.80594 18.6395 3.30681 17.8925 2.96627 17.0704C2.62574 16.2483 2.45047 15.3671 2.45047 14.4773C2.45047 13.5874 2.62574 12.7063 2.96627 11.8841C3.30681 11.062 3.80594 10.315 4.43517 9.68578C5.06439 9.05655 5.81139 8.55742 6.63352 8.21689C7.45564 7.87635 8.33679 7.70108 9.22665 7.70108H17.8396L14.2957 4.15721Z"
|
142 |
+
fill="currentColor"
|
143 |
+
/>
|
144 |
+
</svg>
|
145 |
+
),
|
146 |
+
shapes: ({ ...props }: LucideProps) => (
|
147 |
+
<svg
|
148 |
+
viewBox="0 0 24 24"
|
149 |
+
fill="none"
|
150 |
+
xmlns="http://www.w3.org/2000/svg"
|
151 |
+
{...props}
|
152 |
+
>
|
153 |
+
<g clipPath="url(#clip0_2077_2705)">
|
154 |
+
<path
|
155 |
+
d="M3.75 12H2.25V20.25C2.25 20.6478 2.40804 21.0294 2.68934 21.3107C2.97064 21.592 3.35218 21.75 3.75 21.75H9.75V20.25H3.75V12ZM21 21.75H12.75C12.6197 21.75 12.4916 21.716 12.3784 21.6514C12.2652 21.5868 12.1708 21.4938 12.1045 21.3816C12.0382 21.2694 12.0022 21.1419 12.0002 21.0116C11.9982 20.8813 12.0302 20.7527 12.093 20.6385L16.218 13.1385C16.2821 13.0203 16.377 12.9218 16.4928 12.8534C16.6085 12.7849 16.7406 12.7492 16.875 12.75C17.1322 12.75 17.3895 12.879 17.532 13.1385L21.657 20.6385C21.7198 20.7527 21.7518 20.8813 21.7498 21.0116C21.7478 21.1419 21.7118 21.2694 21.6455 21.3816C21.5792 21.4938 21.4848 21.5868 21.3716 21.6514C21.2584 21.716 21.1303 21.75 21 21.75ZM14.0182 20.25H19.7318L16.875 15.0563L14.0182 20.25ZM20.25 2.25H12V3.75H20.25V14.244H21.75V3.75C21.75 3.35218 21.592 2.97064 21.3107 2.68934C21.0294 2.40804 20.6478 2.25 20.25 2.25ZM8.25 2.25H3.75C3.35218 2.25 2.97064 2.40804 2.68934 2.68934C2.40804 2.97064 2.25 3.35218 2.25 3.75V8.25C2.25 8.64782 2.40804 9.02936 2.68934 9.31066C2.97064 9.59196 3.35218 9.75 3.75 9.75H8.25C8.64782 9.75 9.02936 9.59196 9.31066 9.31066C9.59196 9.02936 9.75 8.64782 9.75 8.25V3.75C9.75 3.35218 9.59196 2.97064 9.31066 2.68934C9.02936 2.40804 8.64782 2.25 8.25 2.25ZM8.25 8.25H3.75V3.75H8.25V8.25Z"
|
156 |
+
fill="currentColor"
|
157 |
+
/>
|
158 |
+
</g>
|
159 |
+
<defs>
|
160 |
+
<clipPath id="clip0_2077_2705">
|
161 |
+
<rect width="24" height="24" fill="white" />
|
162 |
+
</clipPath>
|
163 |
+
</defs>
|
164 |
+
</svg>
|
165 |
+
),
|
166 |
+
settings: Settings,
|
167 |
+
smart: WandSparkles,
|
168 |
+
spinner: Loader2,
|
169 |
+
sun: SunMedium,
|
170 |
+
templates: ({ ...props }: LucideProps) => (
|
171 |
+
<svg
|
172 |
+
viewBox="0 0 24 24"
|
173 |
+
fill="none"
|
174 |
+
xmlns="http://www.w3.org/2000/svg"
|
175 |
+
{...props}
|
176 |
+
>
|
177 |
+
<g clipPath="url(#clip0_2077_2714)">
|
178 |
+
<path
|
179 |
+
d="M19.5 4.5V7.5H4.5V4.5H19.5ZM19.5 3H4.5C4.10218 3 3.72064 3.15804 3.43934 3.43934C3.15804 3.72064 3 4.10218 3 4.5V7.5C3 7.89782 3.15804 8.27936 3.43934 8.56066C3.72064 8.84196 4.10218 9 4.5 9H19.5C19.8978 9 20.2794 8.84196 20.5607 8.56066C20.842 8.27936 21 7.89782 21 7.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 12V19.5H4.5V12H7.5ZM7.5 10.5H4.5C4.10218 10.5 3.72064 10.658 3.43934 10.9393C3.15804 11.2206 3 11.6022 3 12V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H7.5C7.89782 21 8.27936 20.842 8.56066 20.5607C8.84196 20.2794 9 19.8978 9 19.5V12C9 11.6022 8.84196 11.2206 8.56066 10.9393C8.27936 10.658 7.89782 10.5 7.5 10.5ZM19.5 12V19.5H12V12H19.5ZM19.5 10.5H12C11.6022 10.5 11.2206 10.658 10.9393 10.9393C10.658 11.2206 10.5 11.6022 10.5 12V19.5C10.5 19.8978 10.658 20.2794 10.9393 20.5607C11.2206 20.842 11.6022 21 12 21H19.5C19.8978 21 20.2794 20.842 20.5607 20.5607C20.842 20.2794 21 19.8978 21 19.5V12C21 11.6022 20.842 11.2206 20.5607 10.9393C20.2794 10.658 19.8978 10.5 19.5 10.5Z"
|
180 |
+
fill="currentColor"
|
181 |
+
/>
|
182 |
+
</g>
|
183 |
+
<defs>
|
184 |
+
<clipPath id="clip0_2077_2714">
|
185 |
+
<rect width="24" height="24" fill="white" />
|
186 |
+
</clipPath>
|
187 |
+
</defs>
|
188 |
+
</svg>
|
189 |
+
),
|
190 |
+
text: Type,
|
191 |
+
|
192 |
+
trash: Trash,
|
193 |
+
twitter: ({ ...props }: LucideProps) => (
|
194 |
+
<svg
|
195 |
+
xmlns="http://www.w3.org/2000/svg"
|
196 |
+
viewBox="0 0 24 24"
|
197 |
+
aria-hidden="true"
|
198 |
+
focusable="false"
|
199 |
+
data-prefix="fab"
|
200 |
+
data-icon="twitter"
|
201 |
+
role="img"
|
202 |
+
{...props}
|
203 |
+
>
|
204 |
+
<path
|
205 |
+
d="M14.258 10.152L23.176 0h-2.113l-7.747 8.813L7.133 0H0l9.352 13.328L0 23.973h2.113l8.176-9.309 6.531 9.309h7.133zm-2.895 3.293l-.949-1.328L2.875 1.56h3.246l6.086 8.523.945 1.328 7.91 11.078h-3.246zm0 0"
|
206 |
+
fill="currentColor"
|
207 |
+
/>
|
208 |
+
</svg>
|
209 |
+
),
|
210 |
+
type: Type,
|
211 |
+
undo: ({ ...props }: LucideProps) => (
|
212 |
+
<svg
|
213 |
+
viewBox="0 0 24 24"
|
214 |
+
fill="none"
|
215 |
+
xmlns="http://www.w3.org/2000/svg"
|
216 |
+
{...props}
|
217 |
+
>
|
218 |
+
<path
|
219 |
+
fillRule="evenodd"
|
220 |
+
clipRule="evenodd"
|
221 |
+
d="M9.60387 4.30711C9.99439 3.91659 9.99439 3.28342 9.60387 2.8929C9.21334 2.50238 8.58018 2.50237 8.18965 2.8929L2.8925 8.19004C2.70497 8.37758 2.59961 8.63193 2.59961 8.89715C2.59961 9.16237 2.70497 9.41672 2.8925 9.60426L8.18965 14.9014C8.58018 15.2919 9.21334 15.2919 9.60387 14.9014C9.99439 14.5109 9.99439 13.8777 9.60387 13.4872L5.86478 9.7481H15.1044C15.7383 9.7481 16.366 9.87295 16.9516 10.1155C17.5372 10.3581 18.0693 10.7136 18.5176 11.1619C18.9658 11.6101 19.3213 12.1422 19.5639 12.7278C19.8065 13.3134 19.9313 13.9411 19.9313 14.575C19.9313 15.2088 19.8065 15.8365 19.5639 16.4221C19.3213 17.0077 18.9658 17.5398 18.5176 17.9881C18.0693 18.4363 17.5372 18.7918 16.9516 19.0344C16.366 19.277 15.7383 19.4018 15.1044 19.4018H11.3964C10.8442 19.4018 10.3964 19.8495 10.3964 20.4018C10.3964 20.9541 10.8442 21.4018 11.3964 21.4018H15.1044C16.001 21.4018 16.8887 21.2252 17.717 20.8822C18.5452 20.5391 19.2978 20.0362 19.9318 19.4023C20.5657 18.7683 21.0686 18.0158 21.4116 17.1875C21.7547 16.3592 21.9313 15.4715 21.9313 14.575C21.9313 13.6784 21.7547 12.7907 21.4116 11.9624C21.0686 11.1342 20.5657 10.3816 19.9318 9.74764C19.2978 9.11371 18.5452 8.61085 17.717 8.26777C16.8887 7.92469 16.001 7.7481 15.1044 7.7481H6.16287L9.60387 4.30711Z"
|
222 |
+
fill="currentColor"
|
223 |
+
/>
|
224 |
+
</svg>
|
225 |
+
),
|
226 |
+
upload: ({ ...props }: LucideProps) => (
|
227 |
+
<svg
|
228 |
+
viewBox="0 0 24 24"
|
229 |
+
fill="none"
|
230 |
+
xmlns="http://www.w3.org/2000/svg"
|
231 |
+
{...props}
|
232 |
+
>
|
233 |
+
<g clipPath="url(#clip0_2077_2699)">
|
234 |
+
<path
|
235 |
+
d="M8.25 13.5L9.3075 14.5575L11.25 12.6225V21.75H12.75V12.6225L14.6925 14.5575L15.75 13.5L12 9.75L8.25 13.5Z"
|
236 |
+
fill="currentColor"
|
237 |
+
/>
|
238 |
+
<path
|
239 |
+
d="M17.6249 16.5004H17.2499V15.0004H17.6249C18.52 15.0362 19.3927 14.715 20.0509 14.1074C20.7092 13.4997 21.0991 12.6555 21.1349 11.7604C21.1707 10.8653 20.8495 9.99264 20.2418 9.33438C19.6342 8.67613 18.79 8.28621 17.8949 8.25041H17.2499L17.1749 7.63541C17.0085 6.37275 16.3888 5.21362 15.4312 4.37395C14.4736 3.53428 13.2435 3.07132 11.9699 3.07132C10.6963 3.07132 9.46616 3.53428 8.50857 4.37395C7.55099 5.21362 6.93129 6.37275 6.76489 7.63541L6.74989 8.25041H6.10489C5.20979 8.28621 4.36557 8.67613 3.75795 9.33438C3.15033 9.99264 2.82909 10.8653 2.86489 11.7604C2.9007 12.6555 3.29062 13.4997 3.94887 14.1074C4.60712 14.715 5.47979 15.0362 6.37489 15.0004H6.74989V16.5004H6.37489C5.1722 16.4928 4.01477 16.0409 3.12513 15.2315C2.2355 14.4221 1.67646 13.3124 1.55549 12.1158C1.43453 10.9192 1.76018 9.72009 2.46983 8.74905C3.17949 7.77801 4.22305 7.10357 5.39989 6.85541C5.72367 5.3453 6.55552 3.99189 7.75663 3.02101C8.95774 2.05013 10.4555 1.52051 11.9999 1.52051C13.5443 1.52051 15.042 2.05013 16.2432 3.02101C17.4443 3.99189 18.2761 5.3453 18.5999 6.85541C19.7767 7.10357 20.8203 7.77801 21.53 8.74905C22.2396 9.72009 22.5653 10.9192 22.4443 12.1158C22.3233 13.3124 21.7643 14.4221 20.8747 15.2315C19.985 16.0409 18.8276 16.4928 17.6249 16.5004Z"
|
240 |
+
fill="currentColor"
|
241 |
+
/>
|
242 |
+
</g>
|
243 |
+
<defs>
|
244 |
+
<clipPath id="clip0_2077_2699">
|
245 |
+
<rect width="24" height="24" fill="white" />
|
246 |
+
</clipPath>
|
247 |
+
</defs>
|
248 |
+
</svg>
|
249 |
+
),
|
250 |
+
user: User,
|
251 |
+
video: VideoIcon,
|
252 |
+
warning: AlertTriangle,
|
253 |
+
};
|
src/components/theme-provider.tsx
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createContext, useContext, useEffect, useState } from 'react';
|
2 |
+
|
3 |
+
type Theme = 'dark' | 'light' | 'system';
|
4 |
+
|
5 |
+
type ThemeProviderProps = {
|
6 |
+
children: React.ReactNode;
|
7 |
+
defaultTheme?: Theme;
|
8 |
+
storageKey?: string;
|
9 |
+
};
|
10 |
+
|
11 |
+
type ThemeProviderState = {
|
12 |
+
theme: Theme;
|
13 |
+
setTheme: (theme: Theme) => void;
|
14 |
+
};
|
15 |
+
|
16 |
+
const initialState: ThemeProviderState = {
|
17 |
+
theme: 'system',
|
18 |
+
setTheme: () => null,
|
19 |
+
};
|
20 |
+
|
21 |
+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
22 |
+
|
23 |
+
export function ThemeProvider({
|
24 |
+
children,
|
25 |
+
defaultTheme = 'system',
|
26 |
+
storageKey = 'vite-ui-theme',
|
27 |
+
...props
|
28 |
+
}: ThemeProviderProps) {
|
29 |
+
const [theme, setTheme] = useState<Theme>(
|
30 |
+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
31 |
+
);
|
32 |
+
|
33 |
+
useEffect(() => {
|
34 |
+
const root = window.document.documentElement;
|
35 |
+
|
36 |
+
root.classList.remove('light', 'dark');
|
37 |
+
|
38 |
+
if (theme === 'system') {
|
39 |
+
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
40 |
+
.matches
|
41 |
+
? 'dark'
|
42 |
+
: 'light';
|
43 |
+
|
44 |
+
root.classList.add(systemTheme);
|
45 |
+
return;
|
46 |
+
}
|
47 |
+
|
48 |
+
root.classList.add(theme);
|
49 |
+
}, [theme]);
|
50 |
+
|
51 |
+
const value = {
|
52 |
+
theme,
|
53 |
+
setTheme: (theme: Theme) => {
|
54 |
+
localStorage.setItem(storageKey, theme);
|
55 |
+
setTheme(theme);
|
56 |
+
},
|
57 |
+
};
|
58 |
+
|
59 |
+
return (
|
60 |
+
<ThemeProviderContext.Provider {...props} value={value}>
|
61 |
+
{children}
|
62 |
+
</ThemeProviderContext.Provider>
|
63 |
+
);
|
64 |
+
}
|
65 |
+
|
66 |
+
export const useTheme = () => {
|
67 |
+
const context = useContext(ThemeProviderContext);
|
68 |
+
|
69 |
+
if (context === undefined)
|
70 |
+
throw new Error('useTheme must be used within a ThemeProvider');
|
71 |
+
|
72 |
+
return context;
|
73 |
+
};
|
src/components/ui/avatar.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
const Avatar = React.forwardRef<
|
7 |
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
8 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
9 |
+
>(({ className, ...props }, ref) => (
|
10 |
+
<AvatarPrimitive.Root
|
11 |
+
ref={ref}
|
12 |
+
className={cn(
|
13 |
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
14 |
+
className
|
15 |
+
)}
|
16 |
+
{...props}
|
17 |
+
/>
|
18 |
+
))
|
19 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
20 |
+
|
21 |
+
const AvatarImage = React.forwardRef<
|
22 |
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
23 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
24 |
+
>(({ className, ...props }, ref) => (
|
25 |
+
<AvatarPrimitive.Image
|
26 |
+
ref={ref}
|
27 |
+
className={cn("aspect-square h-full w-full", className)}
|
28 |
+
{...props}
|
29 |
+
/>
|
30 |
+
))
|
31 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
32 |
+
|
33 |
+
const AvatarFallback = React.forwardRef<
|
34 |
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
35 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
36 |
+
>(({ className, ...props }, ref) => (
|
37 |
+
<AvatarPrimitive.Fallback
|
38 |
+
ref={ref}
|
39 |
+
className={cn(
|
40 |
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
41 |
+
className
|
42 |
+
)}
|
43 |
+
{...props}
|
44 |
+
/>
|
45 |
+
))
|
46 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
47 |
+
|
48 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
src/components/ui/button.tsx
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from 'react';
|
2 |
+
import { Slot } from '@radix-ui/react-slot';
|
3 |
+
import { cva, type VariantProps } from 'class-variance-authority';
|
4 |
+
|
5 |
+
import { cn } from '@/lib/utils';
|
6 |
+
|
7 |
+
const buttonVariants = cva(
|
8 |
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
9 |
+
{
|
10 |
+
variants: {
|
11 |
+
variant: {
|
12 |
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
13 |
+
destructive:
|
14 |
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
15 |
+
outline:
|
16 |
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
17 |
+
secondary:
|
18 |
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
19 |
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
20 |
+
link: 'text-primary underline-offset-4 hover:underline',
|
21 |
+
},
|
22 |
+
size: {
|
23 |
+
default: 'h-10 px-4 py-2',
|
24 |
+
xs: 'h-8 rounded-md px-3',
|
25 |
+
sm: 'h-9 rounded-md px-3',
|
26 |
+
lg: 'h-11 rounded-md px-8',
|
27 |
+
icon: 'h-10 w-10',
|
28 |
+
},
|
29 |
+
},
|
30 |
+
defaultVariants: {
|
31 |
+
variant: 'default',
|
32 |
+
size: 'default',
|
33 |
+
},
|
34 |
+
},
|
35 |
+
);
|
36 |
+
|
37 |
+
export interface ButtonProps
|
38 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
39 |
+
VariantProps<typeof buttonVariants> {
|
40 |
+
asChild?: boolean;
|
41 |
+
}
|
42 |
+
|
43 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
44 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
45 |
+
const Comp = asChild ? Slot : 'button';
|
46 |
+
return (
|
47 |
+
<Comp
|
48 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
49 |
+
ref={ref}
|
50 |
+
{...props}
|
51 |
+
/>
|
52 |
+
);
|
53 |
+
},
|
54 |
+
);
|
55 |
+
Button.displayName = 'Button';
|
56 |
+
|
57 |
+
export { Button, buttonVariants };
|
src/components/ui/command.tsx
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { type DialogProps } from "@radix-ui/react-dialog"
|
3 |
+
import { Command as CommandPrimitive } from "cmdk"
|
4 |
+
import { Search } from "lucide-react"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
8 |
+
|
9 |
+
const Command = React.forwardRef<
|
10 |
+
React.ElementRef<typeof CommandPrimitive>,
|
11 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
12 |
+
>(({ className, ...props }, ref) => (
|
13 |
+
<CommandPrimitive
|
14 |
+
ref={ref}
|
15 |
+
className={cn(
|
16 |
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
17 |
+
className
|
18 |
+
)}
|
19 |
+
{...props}
|
20 |
+
/>
|
21 |
+
))
|
22 |
+
Command.displayName = CommandPrimitive.displayName
|
23 |
+
|
24 |
+
interface CommandDialogProps extends DialogProps {}
|
25 |
+
|
26 |
+
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
27 |
+
return (
|
28 |
+
<Dialog {...props}>
|
29 |
+
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
30 |
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
31 |
+
{children}
|
32 |
+
</Command>
|
33 |
+
</DialogContent>
|
34 |
+
</Dialog>
|
35 |
+
)
|
36 |
+
}
|
37 |
+
|
38 |
+
const CommandInput = React.forwardRef<
|
39 |
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
40 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
41 |
+
>(({ className, ...props }, ref) => (
|
42 |
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
43 |
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
44 |
+
<CommandPrimitive.Input
|
45 |
+
ref={ref}
|
46 |
+
className={cn(
|
47 |
+
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
48 |
+
className
|
49 |
+
)}
|
50 |
+
{...props}
|
51 |
+
/>
|
52 |
+
</div>
|
53 |
+
))
|
54 |
+
|
55 |
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
56 |
+
|
57 |
+
const CommandList = React.forwardRef<
|
58 |
+
React.ElementRef<typeof CommandPrimitive.List>,
|
59 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
60 |
+
>(({ className, ...props }, ref) => (
|
61 |
+
<CommandPrimitive.List
|
62 |
+
ref={ref}
|
63 |
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
64 |
+
{...props}
|
65 |
+
/>
|
66 |
+
))
|
67 |
+
|
68 |
+
CommandList.displayName = CommandPrimitive.List.displayName
|
69 |
+
|
70 |
+
const CommandEmpty = React.forwardRef<
|
71 |
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
72 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
73 |
+
>((props, ref) => (
|
74 |
+
<CommandPrimitive.Empty
|
75 |
+
ref={ref}
|
76 |
+
className="py-6 text-center text-sm"
|
77 |
+
{...props}
|
78 |
+
/>
|
79 |
+
))
|
80 |
+
|
81 |
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
82 |
+
|
83 |
+
const CommandGroup = React.forwardRef<
|
84 |
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
85 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
86 |
+
>(({ className, ...props }, ref) => (
|
87 |
+
<CommandPrimitive.Group
|
88 |
+
ref={ref}
|
89 |
+
className={cn(
|
90 |
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
91 |
+
className
|
92 |
+
)}
|
93 |
+
{...props}
|
94 |
+
/>
|
95 |
+
))
|
96 |
+
|
97 |
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
98 |
+
|
99 |
+
const CommandSeparator = React.forwardRef<
|
100 |
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<CommandPrimitive.Separator
|
104 |
+
ref={ref}
|
105 |
+
className={cn("-mx-1 h-px bg-border", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
110 |
+
|
111 |
+
const CommandItem = React.forwardRef<
|
112 |
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
113 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
114 |
+
>(({ className, ...props }, ref) => (
|
115 |
+
<CommandPrimitive.Item
|
116 |
+
ref={ref}
|
117 |
+
className={cn(
|
118 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
119 |
+
className
|
120 |
+
)}
|
121 |
+
{...props}
|
122 |
+
/>
|
123 |
+
))
|
124 |
+
|
125 |
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
126 |
+
|
127 |
+
const CommandShortcut = ({
|
128 |
+
className,
|
129 |
+
...props
|
130 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
131 |
+
return (
|
132 |
+
<span
|
133 |
+
className={cn(
|
134 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
135 |
+
className
|
136 |
+
)}
|
137 |
+
{...props}
|
138 |
+
/>
|
139 |
+
)
|
140 |
+
}
|
141 |
+
CommandShortcut.displayName = "CommandShortcut"
|
142 |
+
|
143 |
+
export {
|
144 |
+
Command,
|
145 |
+
CommandDialog,
|
146 |
+
CommandInput,
|
147 |
+
CommandList,
|
148 |
+
CommandEmpty,
|
149 |
+
CommandGroup,
|
150 |
+
CommandItem,
|
151 |
+
CommandShortcut,
|
152 |
+
CommandSeparator,
|
153 |
+
}
|
src/components/ui/dialog.tsx
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
3 |
+
import { X } from "lucide-react"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
|
7 |
+
const Dialog = DialogPrimitive.Root
|
8 |
+
|
9 |
+
const DialogTrigger = DialogPrimitive.Trigger
|
10 |
+
|
11 |
+
const DialogPortal = DialogPrimitive.Portal
|
12 |
+
|
13 |
+
const DialogClose = DialogPrimitive.Close
|
14 |
+
|
15 |
+
const DialogOverlay = React.forwardRef<
|
16 |
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
18 |
+
>(({ className, ...props }, ref) => (
|
19 |
+
<DialogPrimitive.Overlay
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
))
|
28 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
29 |
+
|
30 |
+
const DialogContent = React.forwardRef<
|
31 |
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
32 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
33 |
+
>(({ className, children, ...props }, ref) => (
|
34 |
+
<DialogPortal>
|
35 |
+
<DialogOverlay />
|
36 |
+
<DialogPrimitive.Content
|
37 |
+
ref={ref}
|
38 |
+
className={cn(
|
39 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
40 |
+
className
|
41 |
+
)}
|
42 |
+
{...props}
|
43 |
+
>
|
44 |
+
{children}
|
45 |
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
46 |
+
<X className="h-4 w-4" />
|
47 |
+
<span className="sr-only">Close</span>
|
48 |
+
</DialogPrimitive.Close>
|
49 |
+
</DialogPrimitive.Content>
|
50 |
+
</DialogPortal>
|
51 |
+
))
|
52 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
53 |
+
|
54 |
+
const DialogHeader = ({
|
55 |
+
className,
|
56 |
+
...props
|
57 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
58 |
+
<div
|
59 |
+
className={cn(
|
60 |
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
61 |
+
className
|
62 |
+
)}
|
63 |
+
{...props}
|
64 |
+
/>
|
65 |
+
)
|
66 |
+
DialogHeader.displayName = "DialogHeader"
|
67 |
+
|
68 |
+
const DialogFooter = ({
|
69 |
+
className,
|
70 |
+
...props
|
71 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
72 |
+
<div
|
73 |
+
className={cn(
|
74 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
75 |
+
className
|
76 |
+
)}
|
77 |
+
{...props}
|
78 |
+
/>
|
79 |
+
)
|
80 |
+
DialogFooter.displayName = "DialogFooter"
|
81 |
+
|
82 |
+
const DialogTitle = React.forwardRef<
|
83 |
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
84 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
85 |
+
>(({ className, ...props }, ref) => (
|
86 |
+
<DialogPrimitive.Title
|
87 |
+
ref={ref}
|
88 |
+
className={cn(
|
89 |
+
"text-lg font-semibold leading-none tracking-tight",
|
90 |
+
className
|
91 |
+
)}
|
92 |
+
{...props}
|
93 |
+
/>
|
94 |
+
))
|
95 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
96 |
+
|
97 |
+
const DialogDescription = React.forwardRef<
|
98 |
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
99 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
100 |
+
>(({ className, ...props }, ref) => (
|
101 |
+
<DialogPrimitive.Description
|
102 |
+
ref={ref}
|
103 |
+
className={cn("text-sm text-muted-foreground", className)}
|
104 |
+
{...props}
|
105 |
+
/>
|
106 |
+
))
|
107 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
108 |
+
|
109 |
+
export {
|
110 |
+
Dialog,
|
111 |
+
DialogPortal,
|
112 |
+
DialogOverlay,
|
113 |
+
DialogClose,
|
114 |
+
DialogTrigger,
|
115 |
+
DialogContent,
|
116 |
+
DialogHeader,
|
117 |
+
DialogFooter,
|
118 |
+
DialogTitle,
|
119 |
+
DialogDescription,
|
120 |
+
}
|