├── .eslintrc.cjs
├── .gitignore
├── README.md
├── components.json
├── docs
├── assets
│ ├── index-Cy53lYVy.css
│ ├── index-DiioBMMr.js
│ └── react-CHdo91hT.svg
├── index.html
└── vite.svg
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── vite.svg
├── src
├── App.css
├── App.tsx
├── assets
│ └── react.svg
├── components
│ ├── editor
│ │ ├── TopAttrs.css
│ │ ├── TopAttrs.tsx
│ │ ├── TopLayout.css
│ │ ├── TopLayout.tsx
│ │ ├── TopPlayer.css
│ │ ├── TopPlayer.tsx
│ │ └── index.tsx
│ ├── layout
│ │ ├── Layout.css
│ │ ├── Layout.tsx
│ │ └── index.ts
│ ├── panel
│ │ ├── Header.css
│ │ ├── Header.tsx
│ │ ├── Material.css
│ │ └── Material.tsx
│ ├── shared
│ │ └── icons.tsx
│ ├── theme-provider.tsx
│ └── timelines
│ │ ├── Timeline.css
│ │ ├── Timeline.tsx
│ │ ├── TimelineAxios.tsx
│ │ ├── TimelineContainer.css
│ │ ├── TimelineContainer.tsx
│ │ ├── TimelineDragTrack.tsx
│ │ ├── TimelineHooks.ts
│ │ ├── TimelineSegment.tsx
│ │ ├── TimelineTools.css
│ │ ├── TimelineTools.tsx
│ │ ├── index.tsx
│ │ └── type.ts
├── globals.css
├── main.tsx
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
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 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Video Editor
2 |
3 | this is a video editor like [capcut](https://www.capcut.cn/editor)
4 |
5 | ## How to start
6 |
7 | In order to run the project, make sure Node is installed.
8 |
9 | ```sh
10 | # install dependencies
11 | pnpm i
12 |
13 | # start in development mode
14 | pnpm dev
15 | ```
16 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/docs/assets/index-Cy53lYVy.css:
--------------------------------------------------------------------------------
1 | .arco-layout-header.top-nav{background-color:var(--color-bg-2);height:48px;justify-content:center;margin-bottom:2px;min-width:1256px;position:relative;align-items:center;display:flex}.logo{left:20px;position:absolute}.arco-layout-header.top-nav .draft-input{background-color:transparent;border-radius:2px;color:var(--color-white);display:inline-block;font-size:14px;font-style:normal;font-weight:400;line-height:22px;padding:4px 8px;text-align:center;-webkit-user-select:all;-moz-user-select:all;user-select:all;width:400px}.arco-input{-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--color-fill-6);border:1px solid transparent;border-radius:2px;box-sizing:border-box;color:var(--color-text-0);font-size:14px;line-height:1.5715;outline:none;padding:8px 12px;transition:color .1s linear,border-color .1s linear,background-color .1s linear;width:100%}.sider-material{position:relative}.sider-material__resizebox.arco-resizebox{height:100%;max-width:max(388px,100vw - 500px - 320px - 2px);min-width:388px;width:388px}.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger{background-color:#0a0a0b;width:2px}.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:active,.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:focus,.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:hover{background-color:rgb(var(--primary-6));border-radius:2px}.sider-material .ve-message-wrapper{margin-left:38px;top:72px}.tabs-sider{border-radius:0;height:100%}.tabs-sider>.tab-header{-moz-box-pack:justify;background-color:var(--color-bg-2);border-right:2px solid var(--color-bg-1);color:var(--color-text-2);float:left;height:100%;justify-content:space-between;padding:10px 0 24px;width:76px}.tabs-sider>.tab-header .tab-header__main{gap:4px;width:100%}.tabs-sider>.tab-header,.tabs-sider>.tab-header .tab-header__main{display:flex;flex-direction:column}.tabs-sider>.tab-header .tab-header__main .tab-title-wrapper{cursor:pointer;display:flex;flex-direction:column;height:58px;width:68px}.tabs-sider>.tab-header .tab-header__main,.tabs-sider>.tab-header .tab-header__main .tab-title-wrapper{-moz-box-pack:center;align-items:center;justify-content:center}.tabs-sider>.tab-header .tab-header__main .tab-title-wrapper .tab-title__name{align-items:center;font-size:12px;line-height:20px;text-align:center}.tabs-sider>.tab-header .tab-header__main .tab-title-wrapper--active{color:var(--color-white)}.tabs-sider>.tab-header .tab-header__main .tab-title-wrapper--active .tab-title{font-weight:500}.tabs-sider>.arco-tabs-content{background-color:var(--color-bg-2);padding:0!important}.arco-tabs-content-vertical{height:100%;padding:0;width:auto}.tabs-top{height:100%}.tabs-top>.arco-tabs-header-nav .arco-tabs-header-wrapper{height:40px}.tabs-top>.arco-tabs-content{height:calc(100% - 40px)}.tabs-top>.arco-tabs-content>.arco-tabs-content-inner,.tabs-top>.arco-tabs-content>.arco-tabs-content-inner>.arco-tabs-content-item>.arco-tabs-pane{height:100%}.tabs-top>.arco-tabs-content>.arco-tabs-content-inner>.arco-tabs-content-item>.arco-tabs-pane{display:flex;flex-direction:column}.tabs-top--single>.arco-tabs-header-nav .arco-tabs-header-ink{visibility:hidden}.tabs-top>.arco-tabs-content{padding-top:16px!important}.arco-tabs-header-nav-line .arco-tabs-header-title{line-height:1.5715;margin:0 8px;padding:14px 0}.arco-resizebox-trigger.arco-resizebox-trigger-vertical:before{height:100%;transform:translate(-3px);width:calc(100% + 6px)}.arco-resizebox-direction-bottom,.arco-resizebox-direction-left,.arco-resizebox-direction-right,.arco-resizebox-direction-top{box-sizing:border-box;left:0;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.arco-resizebox-trigger.arco-resizebox-trigger-vertical{width:2px!important}.arco-resizebox-trigger-vertical{cursor:col-resize;height:100%}.arco-resizebox-direction-right{left:unset;right:0}.layout-main .arco-resizebox{overflow:visible}.editor-container .editor-content-top-attrs{min-width:322px;width:322px;position:relative}.sider-attribute{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative}.sider-attribute .sider-attribute-tabs{background-color:var(--color-bg-2);height:100%}.arco-tabs,.arco-tabs-header-nav{position:relative}.arco-tabs{overflow:hidden}.player{background-color:var(--color-bg-2);display:flex;flex-direction:column;height:100%;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none}.editor-container .editor-content-top{display:flex;height:100%}.editor-container .editor-content-top-player{flex:1 1;min-width:500px}.timeline-tools{-moz-box-pack:start;align-items:stretch;border-bottom:2px solid var(--color-border-1);flex-direction:row;height:48px;justify-content:flex-start;padding:12px}.timeline{position:relative;height:calc(100% - 50px);display:flex;top:0;left:0;width:100%;overflow:hidden}.timeline-hd{width:80px}.timeline-bd{position:relative;top:0;left:0;width:calc(100% - 80px);height:100%;overflow-x:scroll;overflow-y:hidden}.content-scroll-body{position:relative;top:0;left:0;min-height:220px;overflow:hidden}.track-operation{-moz-box-flex:0;align-items:stretch;flex-grow:0;flex-shrink:0;min-height:100%;padding:34px 0 26px;width:48px}.track-operation-item{-moz-box-pack:center;align-items:center;display:flex;justify-content:center;width:100%;height:22px}.axios-timelines{position:relative;left:0;top:0;width:-moz-max-content;width:max-content;display:flex;justify-content:flex-start;height:1px;font-size:12px;z-index:10;background:#2d2d30}.axios-timelines .left-gap{width:16px}.axis-second{position:relative;height:8px}.axis-second .second-time{position:relative;width:50px;text-align:center;left:-25px;top:100%;color:#79797b}.axis-second .second-time.littie-size{transform:scale(.85)}.axis-second:first-child .second-time{text-align:left;left:0}.axis-second .second-line{position:absolute;top:0;left:0;width:100%;display:flex;justify-content:space-between;align-items:flex-start}.axis-second .second-line div{width:1px;height:4px;background:#2d2d30}.axis-second .second-line .littie-size:not(.middle-line,.first-line){opacity:0}.axis-second .second-line .middle-line{height:6px;position:relative}.axis-second .second-line .middle-line span{position:absolute;top:6px;left:50%;transform:translate(-50%) scale(.8);color:#79797b}.axis-second .second-line .first-line{height:8px}.axis-second .second-line .last-line{opacity:0}.axis-timelines-mask{position:absolute;left:20px;top:0;z-index:10;height:30px;cursor:pointer}.track-wrapper{align-items:stretch;display:flex;flex-grow:1;flex-shrink:0;left:0;min-height:calc(100% - 18px);position:absolute;top:18px;width:100%}.track-list,.track-operation{-moz-box-pack:center;display:flex;flex-direction:column;justify-content:center}.track-list{padding:16px 0 26px 16px;width:100%}.track.type-filter,.track.type-imageSticker,.track.type-pictureAdjust,.track.type-sticker,.track.type-text,.track.type-textTemplate,.track.type-videoEffect{height:22px}.track.active,.track.selected{background-color:var(--editor-tl-track-active)}.track{background-color:#ffffff03;color:#d4d4d4;pointer-events:none;position:relative;width:100%}.track+.track{margin-top:7px}.track-placeholder-wrapper{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.segment{border-radius:2px;pointer-events:all;position:absolute}.segment-rc-emitter{z-index:1}.cursor-segment-crop,.cursor-segment-front,.cursor-segment-rear{cursor:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize}.segment-edge-front{left:0;transform:translate(2px)}.segment-edge{height:100%;position:absolute}.segment-edge-rear{right:0;transform:translate(-2px)}.segment-active-box.active{display:block}.segment-active-box{border-color:var(--color-white);border-style:solid;border-width:1px;display:none;pointer-events:none;z-index:3}.segment-active-box,.segment-rc-emitter{border-radius:2px;bottom:0;left:0;position:absolute;right:0;top:0}.segment-active-box-edge.front{border-bottom-left-radius:2px;border-top-left-radius:2px;left:-8px}.cursor-segment-crop,.cursor-segment-front,.cursor-segment-rear{cursor:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize}.segment-active-box-edge{-moz-box-pack:center;align-items:center;background-color:var(--color-white);bottom:-1px;content:"";display:flex;justify-content:center;pointer-events:auto;position:absolute;top:-1px;width:8px}.segment-active-box-edge:before{background-color:var(--editor-tl-seg-edge-handle);content:"";display:block;height:10px;width:2px}.cursor-segment-crop,.cursor-segment-front,.cursor-segment-rear{cursor:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize}.segment .segment-hd{-moz-box-pack:start;align-items:stretch;display:flex;flex-direction:row;height:22px;justify-content:flex-start;overflow:hidden;padding:1px 0}.segment-active-box-edge.rear{border-bottom-right-radius:2px;border-top-right-radius:2px;right:-8px}.segment .segment-hd>:last-child{margin-right:8px}.segment .segment-hd>:first-child{margin-left:8px}.segment .segment-hd-group{align-items:center;display:flex;flex-direction:row}.segment .segment-icon-image{align-self:center;display:block;height:16px;width:16px}.segment .segment-icon-image+.segment-title{margin-left:4px}.segment .segment-title{color:var(--color-white);font-family:Nunito,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif,NotoColorEmoji;font-size:12px;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.segment.type-tailLeader,.segment.type-video{background-color:var(--color-fill-video)}.segment.type-audio{background-color:var(--color-fill-audio)}.segment.type-text,.segment.type-textTemplate{background-color:var(--color-fill-text)}.segment.type-videoEffect{background-color:var(--color-fill-effects)}.segment.type-filter,.segment.type-pictureAdjust{background-color:var(--color-fill-filter)}.segment.type-imageSticker,.segment.type-sticker{background-color:var(--color-fill-sticker)}.segment.type-transition{background-color:var(--color-fill-7);border-color:var(--color-text-2)}.segment.type-transition .iconpark-icon{color:var(--color-white)}.track-drag-container{pointer-events:none;position:fixed;z-index:10}.track-drag-container>.segment{pointer-events:none}.track-drag-container>.segment .segment-screenshot-placeholder{display:none}.track-drag-container .segment.type-transition{background-color:var(--color-fill-2);border-color:var(--color-fill-2)}.timeline-container{background-color:var(--color-bg-2);height:100%;position:relative;width:100%}.editor-container{display:flex;height:100%;min-height:592px;min-width:1256px;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.editor-container .editor-content{flex:1 1;min-width:0}.editor-content-resize-box>.second-pane{min-height:220px}.editor-content-resize-box>.first-pane{min-height:320px}.arco-resizebox-split,.arco-resizebox-split-group{display:flex;-webkit-user-select:auto;-moz-user-select:auto;user-select:auto}.arco-layout{-moz-box-flex:1;display:flex;flex:1 1;flex-direction:column;margin:0;padding:0}.arco-resizebox-split-vertical{flex-direction:column}.arco-resizebox-trigger.arco-resizebox-trigger-horizontal{height:2px!important;position:relative}.arco-resizebox-trigger-horizontal{cursor:row-resize;width:100%}.layout-main{height:100vh;min-height:640px;min-width:1256px;overflow:hidden;width:100vw;--editor-cyan-0: #213539;--editor-cyan-1: #004d52;--editor-cyan-2: #00c1cd;--editor-cyan-3: #1e4c51;--editor-black-00: #000;--editor-black-0: #070709;--editor-black-0A: #070709e5;--editor-black-0B: #121212;--editor-black-2: #202023;--editor-black-3: #252528;--editor-black-4: #29292d;--editor-black-5: #343337;--editor-grey-0: #4c4c4c;--editor-grey-1: #6c6c6c;--editor-grey-2: grey;--editor-grey-3: #999;--editor-grey-4: #b2b2b2;--editor-grey-5: #d4d4d4;--editor-grey-6: #e5e5e5;--editor-red-0: #450b0b;--editor-red-1: #ae1c1c;--editor-yellow-2: #cd9541;--editor-yellow-3: #a58744;--editor-white-0: #fafafa;--editor-white-1A: #fafafa1f;--editor-purple-0: #454386;--editor-purple-1: #6e4d7f;--editor-orange-0: #924d3c;--editor-blue-0: #173055;--editor-tl-seg-edge-handle: #0006;--editor-tl-seg-fade: #202023b2;--editor-tl-track-active: #f0f0ff0d;--editor-tl-tool-btn-highlight: #00b6c2 }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 240 5.9% 10%;--primary-foreground: 0 0% 98%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 240 5.9% 10%;--radius: .5rem;--color-white: hsla(0,0%,100%,.9)}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 3.9%;--card-foreground: 0 0% 98%;--popover: 240 10% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 240 5.9% 10%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 240 4.9% 83.9%;--color-black: #000;--color-border: #333335;--color-bg-1: #121212;--color-bg-2: #202023;--color-bg-3: rgba(48,48,51,.98);--color-bg-4: rgba(240,245,255,.06);--color-bg-5: rgba(240,245,255,.03);--color-bg-white: #fff;--color-text-1: hsla(0,0%,100%,.6);--color-text-2: hsla(0,0%,100%,.4);--color-text-3: hsla(0,0%,100%,.2);--color-text-4: #00c1cd;--color-fill-1: hsla(0,0%,100%,.8);--color-fill-2: hsla(0,0%,100%,.4);--color-fill-3: hsla(0,0%,100%,.14);--color-fill-4: rgba(240,240,255,.11);--color-border-1: hsla(0,0%,100%,.06);--color-border-2: hsla(0,0%,100%,.2);--color-border-3: hsla(0,0%,100%,.4);--color-border-4: #00c1cd;--color-primary-light-1: rgba(var(--primary-6),.2);--color-primary-light-2: rgba(var(--primary-6),.35);--color-primary-light-3: rgba(var(--primary-6),.5);--color-primary-light-4: rgba(var(--primary-6),.65);--color-secondary: rgba(var(--gray-9),.08);--color-secondary-hover: rgba(var(--gray-8),.16);--color-secondary-active: rgba(var(--gray-7),.24);--color-secondary-disabled: rgba(var(--gray-9),.08);--color-danger-light-1: rgba(var(--danger-6),.2);--color-danger-light-2: rgba(var(--danger-6),.35);--color-danger-light-3: rgba(var(--danger-6),.5);--color-danger-light-4: rgba(var(--danger-6),.65);--color-success-light-1: rgb(var(--success-6),.2);--color-success-light-2: rgb(var(--success-6),.35);--color-success-light-3: rgb(var(--success-6),.5);--color-success-light-4: rgb(var(--success-6),.65);--color-warning-light-1: rgb(var(--warning-6),.2);--color-warning-light-2: rgb(var(--warning-6),.35);--color-warning-light-3: rgb(var(--warning-6),.5);--color-warning-light-4: rgb(var(--warning-6),.65);--color-link-light-1: rgba(var(--link-6),.2);--color-link-light-2: rgba(var(--link-6),.35);--color-link-light-3: rgba(var(--link-6),.5);--color-link-light-4: rgba(var(--link-6),.65);--color-tooltip-bg: #373739;--color-spin-layer-bg: rgba(51,51,51,.6);--color-menu-dark-bg: #232324;--color-menu-light-bg: #232324;--color-menu-dark-hover: var(--color-fill-2);--color-mask-bg: rgba(23,23,26,.6);--color-text-dark: #000;--color-text-red: #fe2c55;--color-text-3-dark: hsla(0,0%,100%,.2);--color-text-0: #fff;--color-fill-5: #484952;--color-fill-6: rgba(0,0,0,.3);--color-fill-7: rgba(0,0,0,.7);--color-fill-video: #014a51;--color-fill-audio: #0e3058;--color-fill-text: #9c4937;--color-fill-effects: #2e5297;--color-fill-filter: #47418b;--color-fill-textselection: rgba(0,182,194,.3);--color-fill-red: #e95052;--color-fill-orange: #ff6b18;--color-fill-sticker: #f08a34;--color-fill-8: rgba(0,0,0,.8);--color-fill-9: #3e3e42;--color-fill-10: #323236;--color-fill-tooltip: rgba(48,48,51,.99);--color-fill-4-dark: rgba(240,240,255,.1);--color-fill-red-dark: #e95052;--color-fill-3-dark: hsla(0,0%,100%,.14);--color-fill-0: #fff;--color-border-5: #fe2c55;--color-border-1-dark: hsla(0,0%,100%,.06);--color-border-2-dark: hsla(0,0%,100%,.2);--color-bg-black: #000;--color-bg-3-dark: rgba(48,48,51,.98)}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.block{display:block}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}
2 |
--------------------------------------------------------------------------------
/docs/assets/react-CHdo91hT.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS = CapCut
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/docs/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS = CapCut
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-editor",
3 | "private": true,
4 | "version": "0.0.1",
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 | "@radix-ui/react-avatar": "^1.0.4",
14 | "@radix-ui/react-slot": "^1.0.2",
15 | "class-variance-authority": "^0.7.0",
16 | "clsx": "^2.1.1",
17 | "lucide-react": "^0.378.0",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0",
20 | "tailwind-merge": "^2.3.0",
21 | "tailwindcss-animate": "^1.0.7",
22 | "uuid": "^9.0.1"
23 | },
24 | "devDependencies": {
25 | "@types/node": "^20.12.12",
26 | "@types/react": "^18.2.66",
27 | "@types/react-dom": "^18.2.22",
28 | "@types/uuid": "^9.0.8",
29 | "@typescript-eslint/eslint-plugin": "^7.2.0",
30 | "@typescript-eslint/parser": "^7.2.0",
31 | "@vitejs/plugin-react": "^4.2.1",
32 | "autoprefixer": "^10.4.19",
33 | "eslint": "^8.57.0",
34 | "eslint-plugin-react-hooks": "^4.6.0",
35 | "eslint-plugin-react-refresh": "^0.4.6",
36 | "postcss": "^8.4.38",
37 | "tailwindcss": "^3.4.3",
38 | "typescript": "^5.2.2",
39 | "vite": "^5.2.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .layout-main {
2 | height: 100vh;
3 | min-height: 640px;
4 | min-width: 1256px;
5 | overflow: hidden;
6 | width: 100vw;
7 |
8 | --editor-cyan-0: #213539;
9 | --editor-cyan-1: #004d52;
10 | --editor-cyan-2: #00c1cd;
11 | --editor-cyan-3: #1e4c51;
12 | --editor-black-00: #000;
13 | --editor-black-0: #070709;
14 | --editor-black-0A: #070709e5;
15 | --editor-black-0B: #121212;
16 | --editor-black-2: #202023;
17 | --editor-black-3: #252528;
18 | --editor-black-4: #29292d;
19 | --editor-black-5: #343337;
20 | --editor-grey-0: #4c4c4c;
21 | --editor-grey-1: #6c6c6c;
22 | --editor-grey-2: grey;
23 | --editor-grey-3: #999;
24 | --editor-grey-4: #b2b2b2;
25 | --editor-grey-5: #d4d4d4;
26 | --editor-grey-6: #e5e5e5;
27 | --editor-red-0: #450b0b;
28 | --editor-red-1: #ae1c1c;
29 | --editor-yellow-2: #cd9541;
30 | --editor-yellow-3: #a58744;
31 | --editor-white-0: #fafafa;
32 | --editor-white-1A: #fafafa1f;
33 | --editor-purple-0: #454386;
34 | --editor-purple-1: #6e4d7f;
35 | --editor-orange-0: #924d3c;
36 | --editor-blue-0: #173055;
37 | --editor-tl-seg-edge-handle: #0006;
38 | --editor-tl-seg-fade: #202023b2;
39 | --editor-tl-track-active: #f0f0ff0d;
40 | --editor-tl-tool-btn-highlight: #00b6c2
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | // import { Scene } from '@designcombo/scene';
2 | // import { Timeline } from '@designcombo/timeline';
3 | // import HeaderLeft from './components/header-left';
4 | // import HeaderRight from './components/header-right';
5 | import { Layout } from '@/components/layout';
6 | import './App.css';
7 |
8 | function App() {
9 | return (
10 | <>
11 |
12 |
13 |
14 | >
15 | );
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/editor/TopAttrs.css:
--------------------------------------------------------------------------------
1 | .layout-main .arco-resizebox {
2 | overflow: visible;
3 | }
4 |
5 | .editor-container .editor-content-top-attrs {
6 | min-width: 322px;
7 | width: 322px;
8 | position: relative;
9 | }
10 |
11 | .sider-attribute {
12 | height: 100%;
13 | position: relative;
14 | user-select: none;
15 | position: relative;
16 | }
17 |
18 | .sider-attribute .sider-attribute-tabs {
19 | background-color: var(--color-bg-2);
20 | height: 100%;
21 | }
22 |
23 | .arco-tabs, .arco-tabs-header-nav {
24 | position: relative;
25 | }
26 | .arco-tabs {
27 | overflow: hidden;
28 | }
--------------------------------------------------------------------------------
/src/components/editor/TopAttrs.tsx:
--------------------------------------------------------------------------------
1 | import './TopAttrs.css';
2 |
3 | export function TopAttrs() {
4 | return (
5 |
15 | )
16 | }
--------------------------------------------------------------------------------
/src/components/editor/TopLayout.css:
--------------------------------------------------------------------------------
1 | .editor-content-resize-box>.first-pane {
2 | min-height: 320px
3 | }
4 |
5 | .editor-container .editor-content-top {
6 | display: flex;
7 | height: 100%;
8 | }
9 |
10 | .editor-container .editor-content-top-player {
11 | -moz-box-flex: 1;
12 | -webkit-flex: 1 1;
13 | flex: 1 1;
14 | min-width: 500px;
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/editor/TopLayout.tsx:
--------------------------------------------------------------------------------
1 | import { TopAttrs } from "./TopAttrs";
2 | import { TopPlayer } from "./TopPlayer";
3 | import './TopLayout.css'
4 |
5 | export function TopLayout() {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | )
20 | }
--------------------------------------------------------------------------------
/src/components/editor/TopPlayer.css:
--------------------------------------------------------------------------------
1 | .player {
2 | -moz-box-orient: vertical;
3 | -moz-box-direction: normal;
4 | background-color: var(--color-bg-2);
5 | display: -webkit-flex;
6 | display: -moz-box;
7 | display: flex;
8 | flex-direction: column;
9 | height: 100%;
10 | overflow: hidden;
11 | user-select: none
12 | }
13 |
14 | .editor-container .editor-content-top-player {
15 | -moz-box-flex: 1;
16 | -webkit-flex: 1 1;
17 | flex: 1 1;
18 | min-width: 500px;
19 | }
--------------------------------------------------------------------------------
/src/components/editor/TopPlayer.tsx:
--------------------------------------------------------------------------------
1 | import './TopPlayer.css';
2 |
3 | export function TopPlayer(){
4 | return (
5 |
11 | )
12 | }
--------------------------------------------------------------------------------
/src/components/editor/index.tsx:
--------------------------------------------------------------------------------
1 | import { TopLayout } from './TopLayout'
2 |
3 | export function Editor() {
4 | return (
5 |
6 | )
7 | }
--------------------------------------------------------------------------------
/src/components/layout/Layout.css:
--------------------------------------------------------------------------------
1 | .editor-container {
2 | display: -webkit-flex;
3 | display: -moz-box;
4 | display: flex;
5 | height: 100%;
6 | min-height: 592px;
7 | min-width: 1256px;
8 | overflow: hidden;
9 | -webkit-user-select: none;
10 | -moz-user-select: none;
11 | -ms-user-select: none;
12 | user-select: none;
13 | width: 100%
14 | }
15 |
16 | .editor-container .editor-content {
17 | flex: 1 1;
18 | min-width: 0
19 | }
20 | .editor-content-resize-box>.second-pane {
21 | min-height: 220px
22 | }
23 |
24 | .editor-content-resize-box>.first-pane {
25 | min-height: 320px;
26 | }
27 |
28 | .arco-resizebox-split,.arco-resizebox-split-group {
29 | display: -webkit-flex;
30 | display: -moz-box;
31 | display: flex;
32 | -webkit-user-select: auto;
33 | -moz-user-select: auto;
34 | -ms-user-select: auto;
35 | user-select: auto
36 | }
37 |
38 | .arco-layout {
39 | -moz-box-flex: 1;
40 | -moz-box-orient: vertical;
41 | -moz-box-direction: normal;
42 | display: -webkit-flex;
43 | display: -moz-box;
44 | display: flex;
45 | flex: 1 1;
46 | flex-direction: column;
47 | margin: 0;
48 | padding: 0
49 | }
50 |
51 | .arco-resizebox-split-vertical {
52 | flex-direction: column
53 | }
54 |
55 | .arco-resizebox-trigger.arco-resizebox-trigger-horizontal {
56 | height: 2px!important;
57 | position: relative;
58 | }
59 | .arco-resizebox-trigger-horizontal {
60 | cursor: row-resize;
61 | width: 100%;
62 | }
--------------------------------------------------------------------------------
/src/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { Header } from '@/components/panel/Header';
2 | import { Material } from '@/components/panel/Material';
3 | import { Editor } from '@/components/editor/index';
4 | import { TimelinesLayout } from '@/components/timelines';
5 | import './Layout.css';
6 |
7 | export function Layout() {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | >
28 | )
29 | }
--------------------------------------------------------------------------------
/src/components/layout/index.ts:
--------------------------------------------------------------------------------
1 |
2 | export * from './Layout'
--------------------------------------------------------------------------------
/src/components/panel/Header.css:
--------------------------------------------------------------------------------
1 | .arco-layout-header.top-nav {
2 | background-color: var(--color-bg-2);
3 | height: 48px;
4 | justify-content: center;
5 | margin-bottom: 2px;
6 | min-width: 1256px;
7 | position: relative;
8 | align-items: center;
9 | display: flex;
10 | }
11 |
12 | .logo {
13 | left: 20px;
14 | position: absolute;
15 | }
16 |
17 | .arco-layout-header.top-nav .draft-input{
18 | background-color: transparent;
19 | border-radius: 2px;
20 | color: var(--color-white);
21 | display: inline-block;
22 | font-size: 14px;
23 | font-style: normal;
24 | font-weight: 400;
25 | line-height: 22px;
26 | padding: 4px 8px;
27 | text-align: center;
28 | -webkit-user-select: all;
29 | -moz-user-select: all;
30 | user-select: all;
31 | width: 400px;
32 | }
33 |
34 | .arco-input {
35 | -webkit-tap-highlight-color: rgba(0,0,0,0);
36 | -webkit-appearance: none;
37 | -moz-appearance: none;
38 | appearance: none;
39 | background-color: var(--color-fill-6);
40 | border: 1px solid transparent;
41 | border-radius: 2px;
42 | -moz-box-sizing: border-box;
43 | box-sizing: border-box;
44 | color: var(--color-text-0);
45 | font-size: 14px;
46 | line-height: 1.5715;
47 | outline: none;
48 | padding: 8px 12px;
49 | -webkit-transition: color .1s linear,border-color .1s linear,background-color .1s linear;
50 | -o-transition: color .1s linear,border-color .1s linear,background-color .1s linear;
51 | -moz-transition: color .1s linear,border-color .1s linear,background-color .1s linear;
52 | transition: color .1s linear,border-color .1s linear,background-color .1s linear;
53 | width: 100%;
54 | }
--------------------------------------------------------------------------------
/src/components/panel/Header.tsx:
--------------------------------------------------------------------------------
1 | import './Header.css'
2 | import logo from '@/assets/react.svg';
3 |
4 | export function Header() {
5 | return (
6 |
7 |
8 |

9 |
10 |
11 |
12 |
13 | )
14 | }
--------------------------------------------------------------------------------
/src/components/panel/Material.css:
--------------------------------------------------------------------------------
1 | .sider-material {
2 | position: relative
3 | }
4 |
5 |
6 | .sider-material__resizebox.arco-resizebox {
7 | height: 100%;
8 | max-width: max(388px,100vw - 500px - 320px - 2px);
9 | min-width: 388px;
10 | width: 388px
11 | }
12 |
13 | .sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger {
14 | background-color: #0a0a0b;
15 | width: 2px
16 | }
17 |
18 | .sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:active,.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:focus,.sider-material__resizebox.arco-resizebox>.arco-resizebox-trigger:hover {
19 | background-color: rgb(var(--primary-6));
20 | border-radius: 2px
21 | }
22 |
23 | .sider-material .ve-message-wrapper {
24 | margin-left: 38px;
25 | top: 72px
26 | }
27 |
28 | .tabs-sider {
29 | border-radius: 0;
30 | height: 100%;
31 | }
32 |
33 | .arco-tabs, .arco-tabs-header-nav {
34 | position: relative;
35 | }
36 | .arco-tabs {
37 | overflow: hidden;
38 | }
39 |
40 |
41 | .tabs-sider {
42 | border-radius: 0;
43 | height: 100%
44 | }
45 |
46 | .tabs-sider>.tab-header {
47 | -moz-box-pack: justify;
48 | background-color: var(--color-bg-2);
49 | border-right: 2px solid var(--color-bg-1);
50 | color: var(--color-text-2);
51 | float: left;
52 | height: 100%;
53 | -webkit-justify-content: space-between;
54 | justify-content: space-between;
55 | padding: 10px 0 24px;
56 | width: 76px
57 | }
58 |
59 | .tabs-sider>.tab-header,.tabs-sider>.tab-header .tab-header__main {
60 | -moz-box-orient: vertical;
61 | -moz-box-direction: normal;
62 | display: -webkit-flex;
63 | display: -moz-box;
64 | display: flex;
65 | -webkit-flex-direction: column;
66 | flex-direction: column
67 | }
68 |
69 | .tabs-sider>.tab-header .tab-header__main {
70 | gap: 4px;
71 | width: 100%
72 | }
73 |
74 | .tabs-sider>.tab-header {
75 | -moz-box-pack: justify;
76 | background-color: var(--color-bg-2);
77 | border-right: 2px solid var(--color-bg-1);
78 | color: var(--color-text-2);
79 | float: left;
80 | height: 100%;
81 | -webkit-justify-content: space-between;
82 | justify-content: space-between;
83 | padding: 10px 0 24px;
84 | width: 76px
85 | }
86 |
87 | .tabs-sider>.tab-header,.tabs-sider>.tab-header .tab-header__main {
88 | -moz-box-orient: vertical;
89 | -moz-box-direction: normal;
90 | display: -webkit-flex;
91 | display: -moz-box;
92 | display: flex;
93 | -webkit-flex-direction: column;
94 | flex-direction: column
95 | }
96 |
97 | .tabs-sider>.tab-header .tab-header__main, .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper {
98 | -moz-box-pack: center;
99 | -moz-box-align: center;
100 | -webkit-align-items: center;
101 | align-items: center;
102 | -webkit-justify-content: center;
103 | justify-content: center;
104 | }
105 |
106 | .tabs-sider>.tab-header .tab-header__main {
107 | gap: 4px;
108 | width: 100%;
109 | }
110 | .tabs-sider>.tab-header, .tabs-sider>.tab-header .tab-header__main {
111 | -moz-box-orient: vertical;
112 | -moz-box-direction: normal;
113 | display: -webkit-flex;
114 | display: -moz-box;
115 | display: flex;
116 | -webkit-flex-direction: column;
117 | flex-direction: column;
118 | }
119 |
120 | .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper {
121 | -moz-box-orient: vertical;
122 | -moz-box-direction: normal;
123 | cursor: pointer;
124 | display: -webkit-flex;
125 | display: -moz-box;
126 | display: flex;
127 | -webkit-flex-direction: column;
128 | flex-direction: column;
129 | height: 58px;
130 | width: 68px;
131 | }
132 |
133 | .tabs-sider>.tab-header .tab-header__main, .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper {
134 | -moz-box-pack: center;
135 | -moz-box-align: center;
136 | -webkit-align-items: center;
137 | align-items: center;
138 | -webkit-justify-content: center;
139 | justify-content: center;
140 | }
141 |
142 | .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper .tab-title__name {
143 | -moz-box-align: center;
144 | -webkit-align-items: center;
145 | align-items: center;
146 | font-size: 12px;
147 | line-height: 20px;
148 | text-align: center
149 | }
150 |
151 | .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper--active {
152 | color: var(--color-white)
153 | }
154 |
155 | .tabs-sider>.tab-header .tab-header__main .tab-title-wrapper--active .tab-title {
156 | font-weight: 500
157 | }
158 |
159 | .tabs-sider>.arco-tabs-content {
160 | background-color: var(--color-bg-2);
161 | padding: 0!important
162 | }
163 |
164 | .arco-tabs-content-vertical {
165 | height: 100%;
166 | padding: 0;
167 | width: auto
168 | }
169 |
170 |
171 |
172 | .tabs-top {
173 | height: 100%
174 | }
175 |
176 | .tabs-top>.arco-tabs-header-nav .arco-tabs-header-wrapper {
177 | height: 40px
178 | }
179 |
180 | .tabs-top>.arco-tabs-content {
181 | height: -moz-calc(100% - 40px);
182 | height: calc(100% - 40px)
183 | }
184 |
185 | .tabs-top>.arco-tabs-content>.arco-tabs-content-inner,.tabs-top>.arco-tabs-content>.arco-tabs-content-inner>.arco-tabs-content-item>.arco-tabs-pane {
186 | height: 100%
187 | }
188 |
189 | .tabs-top>.arco-tabs-content>.arco-tabs-content-inner>.arco-tabs-content-item>.arco-tabs-pane {
190 | -moz-box-orient: vertical;
191 | -moz-box-direction: normal;
192 | display: -webkit-flex;
193 | display: -moz-box;
194 | display: flex;
195 | -webkit-flex-direction: column;
196 | flex-direction: column
197 | }
198 |
199 | .tabs-top--single>.arco-tabs-header-nav .arco-tabs-header-ink {
200 | visibility: hidden
201 | }
202 |
203 | .tabs-top>.arco-tabs-content {
204 | padding-top: 16px!important
205 | }
206 |
207 |
208 | .arco-tabs-header-nav-line .arco-tabs-header-title {
209 | line-height: 1.5715;
210 | margin: 0 8px;
211 | padding: 14px 0;
212 | }
213 |
214 | .arco-resizebox-trigger.arco-resizebox-trigger-vertical:before {
215 | height: 100%;
216 | transform: translateX(-3px);
217 | width: calc(100% + 6px)
218 | }
219 |
220 | .arco-resizebox-direction-bottom,.arco-resizebox-direction-left,.arco-resizebox-direction-right,.arco-resizebox-direction-top {
221 | box-sizing: border-box;
222 | left: 0;
223 | position: absolute;
224 | top: 0;
225 | user-select: none
226 | }
227 |
228 | .arco-resizebox-trigger.arco-resizebox-trigger-vertical {
229 | width: 2px!important;
230 | }
231 |
232 | .arco-resizebox-trigger-vertical {
233 | cursor: col-resize;
234 | height: 100%;
235 | }
236 | .arco-resizebox-direction-right {
237 | left: unset;
238 | right: 0;
239 | }
--------------------------------------------------------------------------------
/src/components/panel/Material.tsx:
--------------------------------------------------------------------------------
1 | import './Material.css'
2 |
3 | export function Material() {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 |
11 |
12 | 媒体
13 | 音频
14 | 文本
15 | 贴纸
16 | 特效
17 | 转场
18 | 滤镜
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 项目素材
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |

38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | >
54 | )
55 | }
--------------------------------------------------------------------------------
/src/components/shared/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | ArrowRight,
4 | ArrowUpRight,
5 | Check,
6 | ChevronLeft,
7 | ChevronRight,
8 | Copy,
9 | CreditCard,
10 | EllipsisVertical,
11 | File,
12 | FileText,
13 | FolderOpen,
14 | GalleryVerticalEnd,
15 | HelpCircle,
16 | Home,
17 | Image,
18 | Laptop,
19 | LayoutTemplate,
20 | Loader2,
21 | LucideIcon,
22 | LucideProps,
23 | Moon,
24 | MoreVertical,
25 | Plus,
26 | Puzzle,
27 | Search,
28 | Settings,
29 | Shirt,
30 | SunMedium,
31 | Trash,
32 | User,
33 | X,
34 | } from 'lucide-react';
35 |
36 | export type Icon = LucideIcon;
37 |
38 | export const Icons = {
39 | add: Plus,
40 | arrowRight: ArrowRight,
41 | arrowUpRight: ArrowUpRight,
42 | billing: CreditCard,
43 | chevronLeft: ChevronLeft,
44 | chevronRight: ChevronRight,
45 | check: Check,
46 | close: X,
47 | copy: Copy,
48 | ellipsis: MoreVertical,
49 | ellipsisVertical: EllipsisVertical,
50 | folderOpen: FolderOpen,
51 | galleryVerticalEnd: GalleryVerticalEnd,
52 | home: Home,
53 | layoutTemplate: LayoutTemplate,
54 | shirt: Shirt,
55 | gitHub: ({ ...props }: LucideProps) => (
56 |
71 | ),
72 | google: ({ ...props }: LucideProps) => (
73 |
88 | ),
89 | help: HelpCircle,
90 | laptop: Laptop,
91 | logo: Puzzle,
92 | media: Image,
93 | moon: Moon,
94 | page: File,
95 | post: FileText,
96 | redo: ({ ...props }: LucideProps) => (
97 |
110 | ),
111 | search: Search,
112 | settings: Settings,
113 | spinner: Loader2,
114 | sun: SunMedium,
115 | trash: Trash,
116 | twitter: ({ ...props }: LucideProps) => (
117 |
132 | ),
133 | undo: ({ ...props }: LucideProps) => (
134 |
147 | ),
148 |
149 | user: User,
150 | warning: AlertTriangle,
151 | };
152 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
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(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(
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 |
61 | {children}
62 |
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 | };
74 |
--------------------------------------------------------------------------------
/src/components/timelines/Timeline.css:
--------------------------------------------------------------------------------
1 | .timeline {
2 | position: relative;
3 | height: calc(100% - 50px);
4 | width: 100%;
5 | display: flex;
6 | top: 0;
7 | left: 0;
8 | width: 100%;
9 | overflow: hidden;
10 | }
11 |
12 |
13 | .timeline-hd {
14 | width: 80px;
15 | }
16 |
17 | .timeline-bd {
18 | position: relative;
19 | top: 0;
20 | left: 0;
21 | width: calc(100% - 80px);
22 | height: 100%;
23 | overflow-x: scroll;
24 | overflow-y: hidden;
25 | }
26 |
27 | .content-scroll-body {
28 | position: relative;
29 | top: 0;
30 | left: 0;
31 | min-height: 220px;
32 | overflow: hidden;
33 | }
34 |
35 |
36 | .track-operation {
37 | -moz-box-flex: 0;
38 | -moz-box-align: stretch;
39 | -webkit-align-items: stretch;
40 | align-items: stretch;
41 | -webkit-flex-grow: 0;
42 | flex-grow: 0;
43 | -webkit-flex-shrink: 0;
44 | flex-shrink: 0;
45 | min-height: 100%;
46 | padding: 34px 0 26px;
47 | width: 48px;
48 | }
49 | .track-operation-item {
50 | -moz-box-pack: center;
51 | -moz-box-align: center;
52 | -webkit-align-items: center;
53 | align-items: center;
54 | display: -webkit-flex;
55 | display: -moz-box;
56 | display: flex;
57 | -webkit-justify-content: center;
58 | justify-content: center;
59 | width: 100%;
60 | height: 22px;
61 | }
62 |
63 |
64 |
65 | /* 时间轴刻度 */
66 |
67 | .axios-timelines {
68 | position: relative;
69 | left: 0;
70 | top: -1px;
71 | width: max-content;
72 | display: flex;
73 | justify-content: flex-start;
74 | height: 1px;
75 | font-size: 12px;
76 | z-index: 10;
77 | background: #2d2d30;
78 | }
79 |
80 | .axios-timelines .left-gap {
81 | width: 16px
82 | }
83 |
84 | .axis-second {
85 | position: relative;
86 | height: 8px
87 | }
88 |
89 | .axis-second .second-time {
90 | position: relative;
91 | width: 50px;
92 | text-align: center;
93 | left: -25px;
94 | top: 100%;
95 | color: #79797b;
96 | }
97 |
98 | .axis-second .second-time.littie-size {
99 | transform: scale(.85)
100 | }
101 |
102 | .axis-second:first-child .second-time {
103 | text-align: left;
104 | left: 0
105 | }
106 |
107 | .axis-second .second-line {
108 | position: absolute;
109 | top: 0;
110 | left: 0;
111 | width: 100%;
112 | display: flex;
113 | justify-content: space-between;
114 | align-items: flex-start
115 | }
116 |
117 | .axis-second .second-line div {
118 | width: 1px;
119 | height: 4px;
120 | background: #2d2d30;
121 | }
122 |
123 | .axis-second .second-line .littie-size:not(.middle-line,.first-line) {
124 | opacity: 0
125 | }
126 |
127 | .axis-second .second-line .middle-line {
128 | height: 6px;
129 | position: relative;
130 | }
131 | .axis-second .second-line .middle-line span {
132 | position: absolute;
133 | top: 6px;
134 | left: 50%;
135 | transform: translateX(-50%) scale(0.8);
136 | color: #79797b;
137 | }
138 |
139 | .axis-second .second-line .first-line {
140 | height: 8px
141 | }
142 |
143 | .axis-second .second-line .last-line {
144 | opacity: 0
145 | }
146 |
147 | .axis-timelines-mask {
148 | position: absolute;
149 | left: 20px;
150 | top: 0px;
151 | z-index: 10;
152 | height: 30px;
153 | cursor: pointer
154 | }
155 |
156 |
157 | /** 时间轴轨道 **/
158 |
159 | .track-wrapper {
160 | align-items: stretch;
161 | display: flex;
162 | flex-grow: 1;
163 | -webkit-flex-shrink: 0;
164 | flex-shrink: 0;
165 | left: 0;
166 | min-height: -moz-calc(100% - 18px);
167 | min-height: calc(100% - 18px);
168 | position: absolute;
169 | top: 18px;
170 | width: 100%;
171 | }
172 |
173 | .track-list, .track-operation {
174 | -moz-box-orient: vertical;
175 | -moz-box-direction: normal;
176 | -moz-box-pack: center;
177 | display: -webkit-flex;
178 | display: -moz-box;
179 | display: flex;
180 | -webkit-flex-direction: column;
181 | flex-direction: column;
182 | -webkit-justify-content: center;
183 | justify-content: center;
184 | }
185 |
186 | .track-list {
187 | padding: 16px 0 26px;
188 | padding-left: 16px;
189 | width: 100%;
190 | }
191 |
192 | .track.type-filter, .track.type-imageSticker, .track.type-pictureAdjust, .track.type-sticker, .track.type-text, .track.type-textTemplate, .track.type-videoEffect {
193 | height: 22px;
194 | }
195 |
196 | .track.active, .track.selected {
197 | background-color: var(--editor-tl-track-active);
198 | }
199 | .track {
200 | background-color: hsla(0,0%,100%,.01);
201 | color: #d4d4d4;
202 | pointer-events: none;
203 | position: relative;
204 | width: 100%;
205 | }
206 | .track+.track {
207 | margin-top: 7px;
208 | }
209 |
210 | .track-placeholder-wrapper {
211 | bottom: 0;
212 | left: 0;
213 | overflow: hidden;
214 | position: absolute;
215 | right: 0;
216 | top: 0;
217 | }
218 |
219 | .segment.type-imageSticker, .segment.type-sticker {
220 | background-color: var(--color-fill-sticker);
221 | }
222 |
223 | .segment {
224 | border-radius: 2px;
225 | pointer-events: all;
226 | position: absolute;
227 | }
228 |
229 | .segment-active-box, .segment-rc-emitter {
230 | border-radius: 2px;
231 | bottom: 0;
232 | left: 0;
233 | position: absolute;
234 | right: 0;
235 | top: 0;
236 | }
237 |
238 | .segment-rc-emitter {
239 | z-index: 1;
240 | }
241 | .cursor-segment-crop, .cursor-segment-front, .cursor-segment-rear {
242 | cursor: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize;
243 | }
244 |
245 | .segment-edge-front {
246 | left: 0;
247 | -webkit-transform: translateX(2px);
248 | -moz-transform: translateX(2px);
249 | -o-transform: translateX(2px);
250 | transform: translateX(2px);
251 | }
252 | .segment-edge {
253 | height: 100%;
254 | position: absolute;
255 | }
256 |
257 | .segment-edge-rear {
258 | right: 0;
259 | -webkit-transform: translateX(-2px);
260 | -moz-transform: translateX(-2px);
261 | -o-transform: translateX(-2px);
262 | transform: translateX(-2px);
263 | }
264 | .segment-active-box.active {
265 | display: block;
266 | }
267 |
268 | .segment-active-box {
269 | border-color: var(--color-white);
270 | border-style: solid;
271 | border-width: 1px;
272 | display: none;
273 | pointer-events: none;
274 | z-index: 3;
275 | }
276 | .segment-active-box, .segment-rc-emitter {
277 | border-radius: 2px;
278 | bottom: 0;
279 | left: 0;
280 | position: absolute;
281 | right: 0;
282 | top: 0;
283 | }
284 |
285 | .segment-active-box-edge.front {
286 | border-bottom-left-radius: 2px;
287 | border-top-left-radius: 2px;
288 | left: -8px;
289 | }
290 |
291 | .cursor-segment-crop, .cursor-segment-front, .cursor-segment-rear {
292 | cursor: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize;
293 | }
294 | .segment-active-box-edge {
295 | -moz-box-pack: center;
296 | -moz-box-align: center;
297 | -webkit-align-items: center;
298 | align-items: center;
299 | background-color: var(--color-white);
300 | bottom: -1px;
301 | content: "";
302 | display: -webkit-flex;
303 | display: -moz-box;
304 | display: flex;
305 | -webkit-justify-content: center;
306 | justify-content: center;
307 | pointer-events: auto;
308 | position: absolute;
309 | top: -1px;
310 | width: 8px;
311 | }
312 | .segment-active-box-edge:before {
313 | background-color: var(--editor-tl-seg-edge-handle);
314 | content: "";
315 | display: block;
316 | height: 10px;
317 | width: 2px;
318 | }
319 |
320 | .cursor-segment-crop, .cursor-segment-front, .cursor-segment-rear {
321 | cursor: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsdGVyPSJ1cmwoI2EpIj48cGF0aCBkPSJNOS41IDF2Ni4wMUg4LjQ4VjMuNTRMMi41IDkuMDFsNS45OCA1LjQ2di0zLjQ4SDkuNVYxN2gzLjk4di02LjAxaDEuMDZ2My40N2w1Ljk2LTUuNDctNS45Ni01LjQ2VjdoLTEuMDZWMUg5LjVaIiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTE4Ljg0IDguOTktMy4zMi0zLjIydjIuMjVIMTIuNVYyLjA2aC0uMDJ2LS4wMmgtMS45NHY2SDcuNDhWNS43Nkw0LjE2IDkuMDFsMy4zMiAzLjIyVjkuOThoMy4wNnY2aDEuOTZ2LTZoMy4wMnYyLjI2bDMuMzItMy4yNVoiIGZpbGw9IiMwMDAiLz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYSIgeD0iLjciIHk9Ii4yIiB3aWR0aD0iMjEuNiIgaGVpZ2h0PSIxOS42IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+PGZlT2Zmc2V0IGR5PSIxIi8+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iLjkiLz48ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPjxmZUJsZW5kIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180ODAyXzk5NDgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDgwMl85OTQ4IiByZXN1bHQ9InNoYXBlIi8+PC9maWx0ZXI+PC9kZWZzPjwvc3ZnPg==) 12 11,col-resize;
322 | }
323 |
324 | .segment .segment-hd {
325 | -moz-box-orient: horizontal;
326 | -moz-box-direction: normal;
327 | -moz-box-pack: start;
328 | -moz-box-align: stretch;
329 | -webkit-align-items: stretch;
330 | align-items: stretch;
331 | display: -webkit-flex;
332 | display: -moz-box;
333 | display: flex;
334 | -webkit-flex-direction: row;
335 | flex-direction: row;
336 | height: 22px;
337 | -webkit-justify-content: flex-start;
338 | justify-content: flex-start;
339 | overflow: hidden;
340 | padding: 1px 0;
341 | }
342 | .segment-active-box-edge.rear {
343 | border-bottom-right-radius: 2px;
344 | border-top-right-radius: 2px;
345 | right: -8px;
346 | }
347 |
348 | .segment .segment-hd>:last-child {
349 | margin-right: 8px;
350 | }
351 | .segment .segment-hd>:first-child {
352 | margin-left: 8px;
353 | }
354 |
355 | .segment .segment-hd-group {
356 | -moz-box-orient: horizontal;
357 | -moz-box-direction: normal;
358 | -moz-box-align: center;
359 | -webkit-align-items: center;
360 | align-items: center;
361 | display: -webkit-flex;
362 | display: -moz-box;
363 | display: flex;
364 | -webkit-flex-direction: row;
365 | flex-direction: row;
366 | }
367 |
368 | .segment .segment-icon-image {
369 | -webkit-align-self: center;
370 | align-self: center;
371 | display: block;
372 | height: 16px;
373 | width: 16px;
374 | }
375 | .segment .segment-icon-image+.segment-title {
376 | margin-left: 4px;
377 | }
378 |
379 | .segment .segment-title {
380 | color: var(--color-white);
381 | font-family: Nunito,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif,NotoColorEmoji;
382 | font-size: 12px;
383 | -webkit-user-select: none;
384 | -moz-user-select: none;
385 | -ms-user-select: none;
386 | user-select: none;
387 | white-space: nowrap;
388 | }
389 |
390 |
391 | .segment.type-tailLeader,.segment.type-video {
392 | background-color: var(--color-fill-video)
393 | }
394 |
395 | .segment.type-audio {
396 | background-color: var(--color-fill-audio)
397 | }
398 |
399 | .segment.type-text,.segment.type-textTemplate {
400 | background-color: var(--color-fill-text)
401 | }
402 |
403 | .segment.type-videoEffect {
404 | background-color: var(--color-fill-effects)
405 | }
406 |
407 | .segment.type-filter,.segment.type-pictureAdjust {
408 | background-color: var(--color-fill-filter)
409 | }
410 |
411 | .segment.type-imageSticker,.segment.type-sticker {
412 | background-color: var(--color-fill-sticker)
413 | }
414 |
415 | .segment.type-transition {
416 | background-color: var(--color-fill-7);
417 | border-color: var(--color-text-2)
418 | }
419 |
420 | .segment.type-transition .iconpark-icon {
421 | color: var(--color-white)
422 | }
423 |
424 |
425 | /***************************** 拖拽相关 ***********************************/
426 | .track-drag-container {
427 | pointer-events: none;
428 | position: fixed;
429 | z-index: 10;
430 | }
431 |
432 | .track-drag-container>.segment {
433 | pointer-events: none
434 | }
435 |
436 | .track-drag-container>.segment .segment-screenshot-placeholder {
437 | display: none
438 | }
439 | .track-drag-container .segment.type-transition {
440 | background-color: var(--color-fill-2);
441 | border-color: var(--color-fill-2)
442 | }
443 |
444 | .coordinate-line {
445 | display: none;
446 | pointer-events: none;
447 | position: absolute;
448 | z-index: 7
449 | }
450 |
451 | .coordinate-line.active {
452 | display: block
453 | }
454 |
455 | .coordinate-line.style-vertical {
456 | height: 100%;
457 | top: 0;
458 | width: 1px
459 | }
460 |
461 | .coordinate-line.style-horizontal {
462 | height: 1px;
463 | left: 0;
464 | width: 100%
465 | }
466 |
467 | .coordinate-line.style-preview {
468 | background-color: #b2902f
469 | }
470 |
471 | .coordinate-line.style-reference,.coordinate-line.style-track-space {
472 | background-color: rgb(0,193,205)
473 | }
474 |
475 | .coordinate-line.style-reference {
476 | transform: translateX(-175%)
477 | }
478 |
479 | .coordinate-line.style-track-space {
480 | height: 1.5px;
481 | transform: translateY(-50%)
482 | }
483 |
--------------------------------------------------------------------------------
/src/components/timelines/Timeline.tsx:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 | import { SyntheticEvent, useEffect, useRef, useState } from 'react';
3 | import './Timeline.css';
4 | import { TrackType } from './type';
5 | import { TimelineAxios } from './TimelineAxios'
6 | import { useTimelineState, LeftGap, useTrackList, RightHide} from './TimelineHooks';
7 | import { TimelineDragTrackComponent } from './TimelineDragTrack';
8 | import { SegmentComponent } from './TimelineSegment';
9 |
10 |
11 | const Fix3 = (x: number) => Math.round(x * 1000) / 1000;
12 | const Fix1 = (x: number) => Math.round(x * 10) / 10;
13 | /** 盒子交叉比例 */
14 | const OVERLAPRATIO = 20;
15 |
16 | /** 计算元素重叠率 */
17 | function calculateOverlapRatio(rect1: DOMRect, rect2: DOMRect) {
18 | const overlap = !(rect1.right < rect2.left || rect1.left > rect2.right || rect1.bottom < rect2.top || rect1.top > rect2.bottom);
19 | let overlapArea = 0;
20 | // 计算重叠部分的面积
21 | if (overlap) {
22 | const overlapWidth = Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left);
23 | const overlapHeight = Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top);
24 | overlapArea = overlapWidth * overlapHeight;
25 | } else {
26 | overlapArea = 0;
27 | }
28 |
29 | // 计算总面积
30 | const totalArea = rect1.width * rect1.height + rect2.width * rect2.height;
31 |
32 | // 计算重叠占比
33 | const overlapRatio = overlapArea / totalArea;
34 |
35 | return overlapRatio;
36 | }
37 |
38 | export function Timeline() {
39 | const { timelineState, updateTimelineState } = useTimelineState();
40 | const { trackList, getSegment, updateSegment, segmentToTrack, insertTack, clearNoneSegmentTack, updateTrackActive } = useTrackList([
41 | {
42 | id: uuidv4(),
43 | active: false,
44 | type: TrackType.img,
45 | segment: [
46 | {
47 | id: uuidv4(),
48 | type: TrackType.img,
49 | start: 1,
50 | end: 2,
51 | dur: 1,
52 | source: {
53 | title: '综艺,符号,表情,星星,可爱',
54 | url: 'https://lf3-effectcdn-tos.byteeffecttos.com/obj/ies.fe.effect//9706ab28d2dad85d4bc8131c0c046a82'
55 | }
56 | },
57 | {
58 | id: uuidv4(),
59 | type: TrackType.text,
60 | start: 3,
61 | end: 4,
62 | dur: 1,
63 | source: {
64 | title: '小鸡',
65 | url: 'https://lf3-effectcdn-tos.byteeffecttos.com/obj/ies.fe.effect//911a4ebf43020b16bb3f9f8b7d91f4e9'
66 | }
67 | }
68 | ]
69 | },
70 | {
71 | id: uuidv4(),
72 | active: false,
73 | type: TrackType.text,
74 | segment: [
75 | {
76 | id: uuidv4(),
77 | type: TrackType.text,
78 | start: 3,
79 | end: 4,
80 | dur: 1,
81 | source: {
82 | title: '2843',
83 | url: 'https://lf3-effectcdn-tos.byteeffecttos.com/obj/ies.fe.effect//b716298a03337703057a6e320d37efd7'
84 | }
85 | }
86 | ]
87 | }
88 | ]);
89 | /** 选中片段 */
90 | const [selectSegment, setSelectSegment ] = useState<{
91 | id: string;
92 | drag: 'drag' | 'edge-front' | 'edge-rear' | '';
93 | top: number;
94 | left: number;
95 | width: number;
96 | }>({ id: '', drag: '', top: 0, left: 0, width: 0 });
97 | /** 拖拽横线 */
98 | const [ dragHorizontalLine, setDragHorizontalLine ] = useState({ active: true, top: 0, left: 0 });
99 | //
100 | const updateSelectSegment = (segment: Partial) => {
101 | setSelectSegment({ ...selectSegment, ...segment })
102 | };
103 | // 滚动元素的 div
104 | const $contentBody = useRef(null);
105 | const $trackList = useRef(null);
106 | // const $drag = useRef(null);
107 |
108 | function init() {
109 | const { secondWidth, totalTime } = timelineState
110 | const contentWidth = secondWidth * totalTime;
111 |
112 | updateTimelineState({
113 | contentWidth,
114 | scrollWidth: contentWidth + LeftGap + RightHide
115 | });
116 |
117 | console.log($trackList.current?.children);
118 | }
119 |
120 | useEffect(() => {
121 | init();
122 | }, [])
123 |
124 | /**
125 | * 通过scrollLeft计算nowTime值
126 | * @param dire 方向
127 | */
128 | function scrollToTime(dire: -1 | 1): number {
129 | timelineState.scrollLeft = $contentBody.current?.scrollLeft || 0;
130 | if (dire === 1) {
131 | // 右极限时间
132 | return (timelineState.scrollLeft - LeftGap + timelineState.visibleWidth) / timelineState.secondWidth;
133 | }
134 | return (timelineState.scrollLeft - LeftGap) / timelineState.secondWidth;
135 | }
136 |
137 |
138 | const sMpos: {
139 | id: string;
140 | action: 'drag' | 'edge-front' | 'edge-rear' | '';
141 | start: number;
142 | end: number;
143 | dur: number;
144 | /** 鼠标 down 下时位置对应时间 */
145 | time: number;
146 | /** 鼠标 down 下时对应需要基准时间 */
147 | eTime: number;
148 | /** 最小持续时长 */
149 | midDur: number;
150 | /** 鼠标 down 下时 X,Y 位置 */
151 | point: [number, number];
152 | downY: number;
153 | /** 鼠标 down 下时 场景页时长 */
154 | totalTime: number;
155 | /** 滚动条 滚动值 */
156 | scroll: number;
157 | /** 左极限 时间值 */
158 | ml: number;
159 | /** 右极限 时间值 -1 是没有极限 */
160 | mr: number;
161 | /** 是否有改变时间 */
162 | change: number;
163 | dragTarget: string | number;
164 | dragTargetType: 'move' | 'new' | '';
165 | dragTargetIndex: number;
166 | trackBBox: { id: string; index: number; rect: DOMRect }[];
167 | dragBox: DOMRect;
168 | } = {
169 | id: '',
170 | action: 'drag',
171 | dragTarget: '',
172 | dragTargetType: '',
173 | dragTargetIndex: -1,
174 | start: 0,
175 | end: 0,
176 | dur: 0,
177 | time: 0,
178 | eTime: 0,
179 | midDur: 0.1,
180 | point: [0, 0],
181 | downY: 0,
182 | totalTime: 0,
183 | scroll: 0,
184 | ml: 0,
185 | mr: 0,
186 | change: 0,
187 | dragBox: new DOMRect(0, 0),
188 | trackBBox: []
189 | }
190 |
191 | function onSelectionDown(e: PointerEvent): void {
192 | const dataset = (e.target as HTMLDivElement).dataset;
193 | const action = dataset.action || '';
194 | const getSegments = getSegment(dataset.id || '');
195 |
196 | if (action === '' || !getSegments || $trackList.current === null) return;
197 |
198 | const { segment, prev: prevS, next: nextS } = getSegments;
199 | const segRect = ((e.target as HTMLDivElement).parentNode as HTMLDivElement).getBoundingClientRect();
200 | updateSelectSegment({ id: dataset.id || '', drag: '' });
201 | updateTrackActive(dataset.trackid || '', true);
202 |
203 | sMpos.id = dataset.id || '';
204 | sMpos.action = action as 'drag' | 'edge-front' | 'edge-rear';
205 | sMpos.point = [e.clientX, e.clientY - segRect.top];
206 | sMpos.downY = e.clientY;
207 | sMpos.dragBox = new DOMRect(0, 0, 1550, 22); // TODO: 暂时定死轴的宽高
208 | sMpos.trackBBox = [...$trackList.current.children].map((el, index) => ({
209 | id: el.getAttribute('data-id') || '',
210 | index,
211 | rect: el.getBoundingClientRect()
212 | }));
213 | sMpos.dragTargetType = '';
214 | sMpos.dragTargetIndex = -1;
215 |
216 | sMpos.time = (e.pageX - timelineState.limitLeft) / timelineState.secondWidth + scrollToTime(-1);
217 | sMpos.start = segment.start;
218 | sMpos.end = segment.end;
219 | sMpos.dur = segment.dur;
220 |
221 | switch (sMpos.action) {
222 | case 'drag':
223 | {
224 | sMpos.eTime = segment.start;
225 | sMpos.ml = 0;
226 | sMpos.mr = -1;
227 |
228 | updateTimelineState({ dragTrackWrap: {
229 | active: false,
230 | x: sMpos.eTime * timelineState.secondWidth + timelineState.limitLeft,
231 | y: e.clientY - sMpos.point[1]
232 | } });
233 | }
234 | break;
235 | case 'edge-front':
236 | {
237 | sMpos.eTime = segment.start;
238 | sMpos.ml = prevS ? prevS.end : 0;
239 | sMpos.mr = Fix3(segment.end - Math.max(0.1, sMpos.midDur));
240 | }
241 | break
242 | case 'edge-rear':
243 | {
244 | console.log(nextS ? nextS.start : -1);
245 |
246 | sMpos.eTime = segment.end;
247 | sMpos.ml = segment.start + Math.max(0.1, sMpos.midDur);
248 | sMpos.mr = nextS ? nextS.start : -1;
249 | }
250 | break
251 | }
252 |
253 | document.addEventListener('pointermove', onSelectionMove, false);
254 | document.addEventListener('pointerup', onSelectionUp, false);
255 |
256 | }
257 |
258 | function onSelectionMove(e: PointerEvent) {
259 | e.preventDefault();
260 |
261 | const { secondWidth, limitLeft, scrollLeft } = timelineState
262 | const x = e.clientX;
263 | const time = scrollToTime(-1) + (e.pageX - limitLeft) / secondWidth;
264 |
265 | sMpos.change = x - sMpos.point[0];
266 |
267 | /** eTime 新的基准点 时间 */
268 | let eTime = Fix1(sMpos.eTime + (time - sMpos.time));
269 | if (eTime < sMpos.ml) {
270 | eTime = sMpos.ml;
271 | }
272 | if (sMpos.mr > 0 && eTime > sMpos.mr) {
273 | eTime = sMpos.mr;
274 | }
275 |
276 | switch (sMpos.action) {
277 | case 'drag':
278 | {
279 | sMpos.start = eTime;
280 | sMpos.end = eTime + sMpos.dur;
281 | const x = (limitLeft - scrollLeft) + (eTime * secondWidth),
282 | y = e.clientY - sMpos.point[1];
283 |
284 | sMpos.dragBox.x = x;
285 | sMpos.dragBox.y = y;
286 |
287 | updateTimelineState({ dragTrackWrap: { active: true, x, y }});
288 | updateSelectSegment({ id: sMpos.id, drag: sMpos.action});
289 |
290 | for (let i = 0; i < sMpos.trackBBox.length; i++) {
291 | const overlapRatio = Math.floor(calculateOverlapRatio(sMpos.dragBox, sMpos.trackBBox[i].rect) * 100);
292 |
293 | // 判断元素是否出现重叠
294 | if (overlapRatio >= OVERLAPRATIO) {
295 | sMpos.dragTarget = sMpos.trackBBox[i].id;
296 | sMpos.dragTargetIndex = sMpos.trackBBox[i].index;
297 | sMpos.dragTargetType = 'move';
298 | updateTrackActive(sMpos.trackBBox[i].id, true);
299 | break;
300 | }
301 | }
302 |
303 | const { id, rect, index } = sMpos.trackBBox[sMpos.dragTargetIndex];
304 | const overlapRatio = Math.floor(calculateOverlapRatio(sMpos.dragBox, rect) * 100 );
305 | if (overlapRatio < OVERLAPRATIO) {
306 |
307 | if (sMpos.dragBox.y < rect.y) {
308 | sMpos.dragTarget = index;
309 | sMpos.dragTargetType = 'new';
310 | updateTrackActive(id, false);
311 | break;
312 | }
313 | if ((sMpos.dragBox.y + 22) > (rect.y + rect.height)){
314 | console.log(index, 'bottom');
315 | sMpos.dragTarget = (index + 1);
316 | sMpos.dragTargetType = 'new';
317 | updateTrackActive(id, false);
318 | break;
319 | }
320 | }
321 | }
322 | break;
323 | case 'edge-front':
324 | {
325 | sMpos.start = eTime;
326 | sMpos.dur = sMpos.end - eTime;
327 | updateSelectSegment({
328 | id: sMpos.id,
329 | drag: sMpos.action,
330 | left: sMpos.start * secondWidth,
331 | width: sMpos.dur * secondWidth
332 | });
333 | }
334 | break;
335 | case 'edge-rear':
336 | {
337 | const dur = eTime - sMpos.start;
338 | sMpos.end = eTime;
339 | sMpos.dur = dur;
340 |
341 | updateSelectSegment({
342 | id: sMpos.id,
343 | drag: sMpos.action,
344 | left: sMpos.start * secondWidth,
345 | width: dur * secondWidth
346 | });
347 | }
348 | break;
349 | }
350 | }
351 |
352 | function onSelectionUp(e: PointerEvent) {
353 | e.preventDefault();
354 | document.removeEventListener('pointermove', onSelectionMove);
355 | document.removeEventListener('pointerup', onSelectionUp);
356 |
357 | switch (sMpos.action) {
358 | case 'drag':
359 | {
360 | updateSegment(sMpos.id, { start: sMpos.start, end: sMpos.end, dur: sMpos.end - sMpos.start });
361 | if (sMpos.dragTargetType === 'move') {
362 | segmentToTrack(sMpos.dragTarget as string, sMpos.id);
363 | clearNoneSegmentTack();
364 | }
365 | if (sMpos.dragTargetType === 'new') {
366 | insertTack(sMpos.dragTarget as number, sMpos.id);
367 | clearNoneSegmentTack();
368 | }
369 | }
370 | break;
371 | case 'edge-front':
372 | case 'edge-rear':
373 | {
374 | updateSegment(sMpos.id, { start: sMpos.start, end: sMpos.start + sMpos.dur, dur: sMpos.dur });
375 | }
376 | break;
377 | }
378 |
379 | updateSelectSegment({ id: sMpos.id, drag: ''});
380 | sMpos.action = '';
381 | }
382 |
383 | /**
384 | * 双指捏合缩放
385 | */
386 | function wheelEvent(e: SyntheticEvent) {
387 | if ((e as unknown as WheelEvent).ctrlKey) {
388 | // e.preventDefault();
389 | console.log('wheelEvent');
390 |
391 | // // let val = secondWidth.value - e.deltaY;
392 | // // val = val < 20 ? 20 : val > 340 ? 340 : val;
393 | // // secondWidth.value = Math.round(val);
394 | }
395 | }
396 |
397 | /**
398 | * 滚动事件修改自定义滚动块
399 | * @param e e
400 | */
401 | function scrollEvent() {
402 | // console.log('scrollEvent', ContentBody.current?.scrollLeft);
403 | updateTimelineState({ scrollLeft: $contentBody.current?.scrollLeft })
404 | // handleXLeft.value = (scrollLeft.value * visibleWidth.value) / scrollWidth.value;
405 | }
406 |
407 | return (
408 | <>
409 |
410 |
411 |
412 |
413 |
414 | {
415 | trackList.map(t => (
))
416 | }
417 |
418 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 | {/* --------------- 时间轴刻度 ----------- */}
440 |
441 |
442 | {/* --------------- 时间轴轨道容器 ----------- */}
443 | onSelectionDown(e as unknown as PointerEvent) }>
444 |
445 | {/* --------------- 时间轴轨道列表----------- */}
446 |
447 |
448 | {
449 | trackList.map((track, index) => {
450 | return (
451 |
456 |
457 |
458 |
459 | {
460 | track.segment.map((s, i) => {
461 | if (selectSegment.id === s.id && selectSegment.drag === 'drag') {
462 | return (
463 |
468 |
476 |
477 | )
478 | } else if (selectSegment.id === s.id && (selectSegment.drag === 'edge-front' || selectSegment.drag === 'edge-rear')){
479 | return (
)
487 | } {
488 | return (
)
496 | }
497 | })
498 | }
499 |
500 | )
501 | })
502 | }
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 | >
515 | )
516 | }
517 |
--------------------------------------------------------------------------------
/src/components/timelines/TimelineAxios.tsx:
--------------------------------------------------------------------------------
1 | /***
2 | * 时间轴刻度
3 | */
4 | export const TimelineAxios: React.FC<{ totalTimeInt: number, secondWidth: number }> = ({ totalTimeInt, secondWidth }) => {
5 | return (
6 |
7 |
8 | {[...Array(totalTimeInt)].map((_, index) => (
9 |
10 |
11 | {index}
12 |
13 |
14 | {
15 | [...Array(11)].map((_, i) => (
16 |
25 | { i === 5 ? (index + 0.5) : '' }
26 |
27 | ))
28 | }
29 |
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/timelines/TimelineContainer.css:
--------------------------------------------------------------------------------
1 | .timeline-container {
2 | background-color: var(--color-bg-2);
3 | height: 100%;
4 | position: relative;
5 | width: 100%
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/timelines/TimelineContainer.tsx:
--------------------------------------------------------------------------------
1 | import { TimelineTools } from "./TimelineTools";
2 | import { Timeline } from "./Timeline";
3 | import './TimelineContainer.css';
4 |
5 | export function TimelineContainer() {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
--------------------------------------------------------------------------------
/src/components/timelines/TimelineDragTrack.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 |
3 | export const TimelineDragTrackComponent: React.FC<{
4 | active: boolean,
5 | x: number,
6 | y: number,
7 | children?: React.ReactNode
8 | }> = (props) => {
9 | const { active, x, y } = props;
10 |
11 | return ReactDOM.createPortal(
12 | (
13 | { props.children }
14 |
),
15 | document.body
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/timelines/TimelineHooks.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { TimeLineTrack, TimelineSegment } from './type'
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | // 左则头
6 | export const LeftWidth = 80;
7 | // 左边素材边栏
8 | export const MaterialWidth = 388;
9 | // 时间轴左则空隙
10 | export const LeftGap = 16;
11 | /** 轴尾部空隙 */
12 | export const RightHide = 360;
13 |
14 | export const useTimelineState = () => {
15 | const [timelineState, setTimelineState] = useState<{
16 | /** 总时长 */
17 | totalTime: number;
18 | /** 可以滚动内容的宽度 */
19 | scrollWidth: number;
20 | /** 内容轴的宽度 */
21 | contentWidth: number;
22 | /** 秒 尺度的宽 */
23 | secondWidth: number;
24 | /** 当前时间 */
25 | nowTime: number;
26 | /** 滚动元素的左边位置 */
27 | limitLeft: number;
28 | /** 滚动元素的左边位置 */
29 | limitRight: number;
30 | /** 滚动值 */
31 | scrollLeft: number;
32 | /** 可以滚动内容的宽度 */
33 | visibleWidth: number;
34 | /** 拖拽容器 */
35 | dragTrackWrap: { active: boolean, x: number, y: number }
36 | }>({
37 | totalTime: 10,
38 | nowTime: 1,
39 | scrollWidth: 700,
40 | contentWidth: 700,
41 | secondWidth: 120,
42 | limitLeft: LeftWidth + MaterialWidth + LeftGap,
43 | limitRight: 0,
44 | scrollLeft: 0,
45 | visibleWidth: 700,
46 | dragTrackWrap: { active: false, x: 0, y: 0 }
47 | });
48 |
49 | const updateTimelineState = (state: Partial ) => {
50 | setTimelineState({
51 | ...timelineState,
52 | ...state
53 | })
54 | }
55 |
56 | return {
57 | timelineState,
58 | updateTimelineState
59 | }
60 | };
61 |
62 |
63 | export const useTrackList = (track: TimeLineTrack[] = []) => {
64 | const [trackList, setTrackList] = useState(track)
65 |
66 | function getSegment(id: string) {
67 | for (let i = 0; i < trackList.length; i++) {
68 | for (let j = 0; j < trackList[i].segment.length; j++) {
69 | if (trackList[i].segment[j].id === id) {
70 | return {
71 | prev: trackList[i].segment[j - 1],
72 | segment: trackList[i].segment[j],
73 | next: trackList[i].segment[j + 1]
74 | }
75 | }
76 | }
77 | }
78 | }
79 |
80 | function updateSegment(id: string, segment: Partial) {
81 | for (let i = 0; i < trackList.length; i++) {
82 | for (let j = 0; j < trackList[i].segment.length; j++) {
83 | if (trackList[i].segment[j].id === id) {
84 | trackList[i].segment[j] = { ...trackList[i].segment[j], ...segment }
85 | setTrackList([...trackList]);
86 | return;
87 | }
88 | }
89 | }
90 | }
91 |
92 | function segmentToTrack(trackId: string, segmentId: string) {
93 | if (!trackId || !segmentId) return;
94 |
95 | for (let i = 0; i < trackList.length; i++) {
96 | for (let j = 0; j < trackList[i].segment.length; j++) {
97 | if (trackList[i].segment[j].id !== segmentId) continue;
98 | const segment = trackList[i].segment.splice(j, 1)[0];
99 | const index = trackList.findIndex(t => t.id === trackId);
100 | if (index !== -1) {
101 | trackList[index].segment.push(segment);
102 | trackList[index].segment.sort((a, b) => a.start - b.start);
103 | setTrackList([...trackList]);
104 | }
105 | return;
106 | }
107 | }
108 | }
109 |
110 | function insertTack(index: number, segment: TimelineSegment | string) {
111 | let s: TimelineSegment | undefined;
112 | if (typeof segment === 'string') {
113 | for (let i = 0; i < trackList.length; i++) {
114 | for (let j = 0; j < trackList[i].segment.length; j++) {
115 | if (trackList[i].segment[j].id === segment) {
116 | s = trackList[i].segment.splice(j, 1)[0];
117 | }
118 | }
119 | }
120 | } else {
121 | s = segment;
122 | }
123 |
124 | if (!s) return;
125 |
126 | trackList.splice(index, 0, {
127 | id: uuidv4(),
128 | active: true,
129 | type: s.type,
130 | segment: [ s ]
131 | });
132 | setTrackList([...trackList]);
133 | }
134 |
135 | function clearNoneSegmentTack() {
136 | for (let i = 0; i < trackList.length; i++) {
137 | if (trackList[i].segment.length < 1) {
138 | trackList.splice(i, 1);
139 | setTrackList([...trackList]);
140 | }
141 | }
142 | }
143 |
144 | function updateTrackActive(id: string, active: boolean) {
145 | for (let i = 0; i < trackList.length; i++) {
146 | trackList[i].active = false;
147 | if (trackList[i].id === id) {
148 | trackList[i].active = active;
149 | }
150 | }
151 | setTrackList([...trackList]);
152 | }
153 |
154 | return {
155 | trackList,
156 | setTrackList,
157 | getSegment,
158 | updateSegment,
159 | updateTrackActive,
160 | segmentToTrack,
161 | insertTack,
162 | clearNoneSegmentTack
163 | }
164 | }
--------------------------------------------------------------------------------
/src/components/timelines/TimelineSegment.tsx:
--------------------------------------------------------------------------------
1 | import { TimeLineTrack, TimelineSegment } from './type';
2 |
3 | interface SegmentProps {
4 | active: boolean;
5 | track: TimeLineTrack
6 | s: TimelineSegment;
7 | i: number;
8 | width: number;
9 | left: number;
10 | }
11 |
12 | export const SegmentComponent: React.FC = ({ active, s, i, left, width, track}) => (
13 |
17 |
21 |
25 |
26 |
27 |

28 |
{ s.source.title }
29 |
30 |
31 |
32 | );
--------------------------------------------------------------------------------
/src/components/timelines/TimelineTools.css:
--------------------------------------------------------------------------------
1 | .timeline-tools {
2 | -moz-box-pack: start;
3 | -moz-box-align: stretch;
4 | -webkit-align-items: stretch;
5 | align-items: stretch;
6 | border-bottom: 2px solid var(--color-border-1);
7 | -webkit-flex-direction: row;
8 | flex-direction: row;
9 | height: 48px;
10 | -webkit-justify-content: flex-start;
11 | justify-content: flex-start;
12 | padding: 12px
13 | }
--------------------------------------------------------------------------------
/src/components/timelines/TimelineTools.tsx:
--------------------------------------------------------------------------------
1 | import './TimelineTools.css'
2 |
3 | export function TimelineTools() {
4 | return (
5 |
6 | )
7 | }
--------------------------------------------------------------------------------
/src/components/timelines/index.tsx:
--------------------------------------------------------------------------------
1 | import { TimelineContainer } from './TimelineContainer';
2 |
3 | export function TimelinesLayout() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | )
13 | }
--------------------------------------------------------------------------------
/src/components/timelines/type.ts:
--------------------------------------------------------------------------------
1 |
2 | export enum TrackType {
3 | text = 1,
4 | img = 2,
5 | audio = 3,
6 | video = 4,
7 | }
8 |
9 | export type TimelineSegment = {
10 | id: string;
11 | type: TrackType;
12 | start: number;
13 | end: number;
14 | dur: number;
15 | source: {
16 | title: string;
17 | url?: string;
18 | content?: string;
19 | };
20 | }
21 | export type TimeLineTrack = {
22 | id: string;
23 | active: boolean;
24 | type: TrackType;
25 | segment: TimelineSegment[]
26 | }
27 | /**
28 | * 用于计算的元素盒子数据
29 | * @property w 宽
30 | * @property h 高
31 | * @property x left
32 | * @property y top
33 | * @property r 旋转
34 | * @property cx 中心点x
35 | * @property cy 中心点y
36 | */
37 | export type BBox = {
38 | w: number;
39 | h: number;
40 | x: number;
41 | y: number;
42 | cx: number;
43 | cy: number;
44 | };
--------------------------------------------------------------------------------
/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.5rem;
27 | --color-white: hsla(0,0%,100%,.9);
28 | }
29 |
30 | .dark {
31 | --background: 240 10% 3.9%;
32 | --foreground: 0 0% 98%;
33 | --card: 240 10% 3.9%;
34 | --card-foreground: 0 0% 98%;
35 | --popover: 240 10% 3.9%;
36 | --popover-foreground: 0 0% 98%;
37 | --primary: 0 0% 98%;
38 | --primary-foreground: 240 5.9% 10%;
39 | --secondary: 240 3.7% 15.9%;
40 | --secondary-foreground: 0 0% 98%;
41 | --muted: 240 3.7% 15.9%;
42 | --muted-foreground: 240 5% 64.9%;
43 | --accent: 240 3.7% 15.9%;
44 | --accent-foreground: 0 0% 98%;
45 | --destructive: 0 62.8% 30.6%;
46 | --destructive-foreground: 0 0% 98%;
47 | --border: 240 3.7% 15.9%;
48 | --input: 240 3.7% 15.9%;
49 | --ring: 240 4.9% 83.9%;
50 |
51 | --color-black: #000;
52 | --color-border: #333335;
53 | --color-bg-1: #121212;
54 | --color-bg-2: #202023;
55 | --color-bg-3: rgba(48,48,51,.98);
56 | --color-bg-4: rgba(240,245,255,.06);
57 | --color-bg-5: rgba(240,245,255,.03);
58 | --color-bg-white: #fff;
59 | --color-text-1: hsla(0,0%,100%,.6);
60 | --color-text-2: hsla(0,0%,100%,.4);
61 | --color-text-3: hsla(0,0%,100%,.2);
62 | --color-text-4: #00c1cd;
63 | --color-fill-1: hsla(0,0%,100%,.8);
64 | --color-fill-2: hsla(0,0%,100%,.4);
65 | --color-fill-3: hsla(0,0%,100%,.14);
66 | --color-fill-4: rgba(240,240,255,.11);
67 | --color-border-1: hsla(0,0%,100%,.06);
68 | --color-border-2: hsla(0,0%,100%,.2);
69 | --color-border-3: hsla(0,0%,100%,.4);
70 | --color-border-4: #00c1cd;
71 | --color-primary-light-1: rgba(var(--primary-6),.2);
72 | --color-primary-light-2: rgba(var(--primary-6),.35);
73 | --color-primary-light-3: rgba(var(--primary-6),.5);
74 | --color-primary-light-4: rgba(var(--primary-6),.65);
75 | --color-secondary: rgba(var(--gray-9),.08);
76 | --color-secondary-hover: rgba(var(--gray-8),.16);
77 | --color-secondary-active: rgba(var(--gray-7),.24);
78 | --color-secondary-disabled: rgba(var(--gray-9),.08);
79 | --color-danger-light-1: rgba(var(--danger-6),.2);
80 | --color-danger-light-2: rgba(var(--danger-6),.35);
81 | --color-danger-light-3: rgba(var(--danger-6),.5);
82 | --color-danger-light-4: rgba(var(--danger-6),.65);
83 | --color-success-light-1: rgb(var(--success-6),.2);
84 | --color-success-light-2: rgb(var(--success-6),.35);
85 | --color-success-light-3: rgb(var(--success-6),.5);
86 | --color-success-light-4: rgb(var(--success-6),.65);
87 | --color-warning-light-1: rgb(var(--warning-6),.2);
88 | --color-warning-light-2: rgb(var(--warning-6),.35);
89 | --color-warning-light-3: rgb(var(--warning-6),.5);
90 | --color-warning-light-4: rgb(var(--warning-6),.65);
91 | --color-link-light-1: rgba(var(--link-6),.2);
92 | --color-link-light-2: rgba(var(--link-6),.35);
93 | --color-link-light-3: rgba(var(--link-6),.5);
94 | --color-link-light-4: rgba(var(--link-6),.65);
95 | --color-tooltip-bg: #373739;
96 | --color-spin-layer-bg: rgba(51,51,51,.6);
97 | --color-menu-dark-bg: #232324;
98 | --color-menu-light-bg: #232324;
99 | --color-menu-dark-hover: var(--color-fill-2);
100 | --color-mask-bg: rgba(23,23,26,.6);
101 | --color-text-dark: #000;
102 | --color-text-red: #fe2c55;
103 | --color-text-3-dark: hsla(0,0%,100%,.2);
104 | --color-text-0: #fff;
105 | --color-fill-5: #484952;
106 | --color-fill-6: rgba(0,0,0,.3);
107 | --color-fill-7: rgba(0,0,0,.7);
108 | --color-fill-video: #014a51;
109 | --color-fill-audio: #0e3058;
110 | --color-fill-text: #9c4937;
111 | --color-fill-effects: #2e5297;
112 | --color-fill-filter: #47418b;
113 | --color-fill-textselection: rgba(0,182,194,.3);
114 | --color-fill-red: #e95052;
115 | --color-fill-orange: #ff6b18;
116 | --color-fill-sticker: #f08a34;
117 | --color-fill-8: rgba(0,0,0,.8);
118 | --color-fill-9: #3e3e42;
119 | --color-fill-10: #323236;
120 | --color-fill-tooltip: rgba(48,48,51,.99);
121 | --color-fill-4-dark: rgba(240,240,255,.1);
122 | --color-fill-red-dark: #e95052;
123 | --color-fill-3-dark: hsla(0,0%,100%,.14);
124 | --color-fill-0: #fff;
125 | --color-border-5: #fe2c55;
126 | --color-border-1-dark: hsla(0,0%,100%,.06);
127 | --color-border-2-dark: hsla(0,0%,100%,.2);
128 | --color-bg-black: #000;
129 | --color-bg-3-dark: rgba(48,48,51,.98);
130 | }
131 | }
132 |
133 | @layer base {
134 | * {
135 | @apply border-border;
136 | }
137 | body {
138 | @apply bg-background text-foreground;
139 | }
140 | }
141 |
142 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App.tsx';
4 | import { ThemeProvider } from '@/components/theme-provider';
5 |
6 | import './globals.css';
7 |
8 | ReactDOM.createRoot(document.getElementById('root')!).render(
9 |
10 |
11 |
12 |
13 | ,
14 | );
15 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | prefix: '',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: '2rem',
15 | screens: {
16 | '2xl': '1400px',
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: 'hsl(var(--border))',
22 | input: 'hsl(var(--input))',
23 | ring: 'hsl(var(--ring))',
24 | background: 'hsl(var(--background))',
25 | foreground: 'hsl(var(--foreground))',
26 | grey: {
27 | 50: '#F4F4F5',
28 | 100: '#EBEDF0',
29 | 200: '#E4E4E7',
30 | 300: '#D4D4D8',
31 | 400: '#A1A1A9',
32 | 500: '#717179',
33 | 600: '#52525A',
34 | 700: '#3F3F45',
35 | 800: '#27272A',
36 | 900: '#1B1B1F',
37 | 1000: '#151515',
38 | surface: '#131313',
39 | },
40 | green: {
41 | 50: '#E8F5E7',
42 | 100: '#C8E7C4',
43 | 200: '#A4D79E',
44 | 300: '#80C976',
45 | 400: '#63BD58',
46 | 500: '#46B138',
47 | 600: '#3CA22F',
48 | 700: '#2F9024',
49 | 800: '#227F18',
50 | 900: '#006100',
51 | 950: '#006100',
52 | },
53 | primary: {
54 | DEFAULT: 'hsl(var(--primary))',
55 | foreground: 'hsl(var(--primary-foreground))',
56 | },
57 | secondary: {
58 | DEFAULT: 'hsl(var(--secondary))',
59 | foreground: 'hsl(var(--secondary-foreground))',
60 | },
61 | destructive: {
62 | DEFAULT: 'hsl(var(--destructive))',
63 | foreground: 'hsl(var(--destructive-foreground))',
64 | },
65 | muted: {
66 | DEFAULT: 'hsl(var(--muted))',
67 | foreground: 'hsl(var(--muted-foreground))',
68 | },
69 | accent: {
70 | DEFAULT: 'hsl(var(--accent))',
71 | foreground: 'hsl(var(--accent-foreground))',
72 | },
73 | popover: {
74 | DEFAULT: 'hsl(var(--popover))',
75 | foreground: 'hsl(var(--popover-foreground))',
76 | },
77 | card: {
78 | DEFAULT: 'hsl(var(--card))',
79 | foreground: 'hsl(var(--card-foreground))',
80 | },
81 | },
82 | borderRadius: {
83 | lg: 'var(--radius)',
84 | md: 'calc(var(--radius) - 2px)',
85 | sm: 'calc(var(--radius) - 4px)',
86 | },
87 | keyframes: {
88 | 'accordion-down': {
89 | from: { height: '0' },
90 | to: { height: 'var(--radix-accordion-content-height)' },
91 | },
92 | 'accordion-up': {
93 | from: { height: 'var(--radix-accordion-content-height)' },
94 | to: { height: '0' },
95 | },
96 | },
97 | animation: {
98 | 'accordion-down': 'accordion-down 0.2s ease-out',
99 | 'accordion-up': 'accordion-up 0.2s ease-out',
100 | },
101 | },
102 | },
103 | plugins: [require('tailwindcss-animate')],
104 | };
105 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./src/*"]
25 | }
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import path from 'path';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | base: './',
8 | plugins: [react()],
9 | resolve: {
10 | alias: {
11 | '@': path.resolve(__dirname, './src'),
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------