Spaces:
deepak191z
/
Runtime error

deepak191z commited on
Commit
229b3b8
·
verified ·
1 Parent(s): 48b5ca6

Upload 84 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eslintrc.cjs +18 -0
  2. .gitignore +26 -0
  3. CHANGELOG.md +94 -0
  4. README.md +46 -12
  5. components.json +17 -0
  6. images/preview.png +0 -0
  7. index.html +29 -0
  8. package.json +60 -0
  9. pnpm-lock.yaml +0 -0
  10. postcss.config.js +6 -0
  11. public/vite.svg +1 -0
  12. src/App.css +42 -0
  13. src/App.tsx +38 -0
  14. src/assets/logo-dark-full.png +0 -0
  15. src/assets/logo-dark.png +0 -0
  16. src/assets/react.svg +1 -0
  17. src/components/editor/control-item/animations.tsx +76 -0
  18. src/components/editor/control-item/basic-audio.tsx +11 -0
  19. src/components/editor/control-item/basic-image.tsx +11 -0
  20. src/components/editor/control-item/basic-text.tsx +439 -0
  21. src/components/editor/control-item/basic-video.tsx +11 -0
  22. src/components/editor/control-item/common/opacity.tsx +48 -0
  23. src/components/editor/control-item/common/transform.tsx +102 -0
  24. src/components/editor/control-item/control-item.tsx +104 -0
  25. src/components/editor/control-item/index.tsx +1 -0
  26. src/components/editor/control-item/presets.tsx +11 -0
  27. src/components/editor/control-item/smart.tsx +11 -0
  28. src/components/editor/control-list.tsx +237 -0
  29. src/components/editor/editor.tsx +63 -0
  30. src/components/editor/index.ts +1 -0
  31. src/components/editor/menu-item/audios.tsx +68 -0
  32. src/components/editor/menu-item/elements.tsx +9 -0
  33. src/components/editor/menu-item/images.tsx +61 -0
  34. src/components/editor/menu-item/index.tsx +1 -0
  35. src/components/editor/menu-item/menu-item.tsx +72 -0
  36. src/components/editor/menu-item/texts.tsx +44 -0
  37. src/components/editor/menu-item/transitions.tsx +90 -0
  38. src/components/editor/menu-item/uploads.tsx +132 -0
  39. src/components/editor/menu-item/videos.tsx +50 -0
  40. src/components/editor/menu-list.tsx +132 -0
  41. src/components/editor/navbar.tsx +336 -0
  42. src/components/editor/resize-video.tsx +3 -0
  43. src/components/editor/use-hotkeys.ts +105 -0
  44. src/components/shared/draggable.tsx +81 -0
  45. src/components/shared/icons.tsx +253 -0
  46. src/components/theme-provider.tsx +73 -0
  47. src/components/ui/avatar.tsx +48 -0
  48. src/components/ui/button.tsx +57 -0
  49. src/components/ui/command.tsx +153 -0
  50. 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
- title: RAI
3
- emoji: 📉
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: unlicense
9
- app_port: 6001
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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://ik.imagekit.io/snapmotion/preview-editor-2.png)](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
+ }