├── .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 |
6 |
7 |
8 |

属性面板

9 | 10 | 11 |
12 |
13 |
14 |
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 |
6 |
7 |
播放器
8 | 9 |
10 |
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 | Logo 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 | 103 | 109 | 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 | 140 | 146 | 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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 | --------------------------------------------------------------------------------