版本三继续中

This commit is contained in:
北枳 2025-06-29 00:04:15 +08:00
parent 2dc9a34241
commit 8d85eee872
13 changed files with 3766 additions and 207 deletions

View File

@ -9,6 +9,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { debounce } from "lodash";
import { GlassIconButton } from "@/components/ui/glass-icon-button";
import { EditModal } from "@/components/ui/edit-modal";
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
const MOCK_SKETCH_URLS = [
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
@ -31,6 +32,12 @@ const MOCK_VIDEO_URLS = [
const MOCK_SKETCH_COUNT = 8;
const MOCK_FINAL_VIDEO = {
id: 'final-video',
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
thumbnail: 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
};
export default function WorkFlow() {
const [taskObject, setTaskObject] = useState<any>(null);
const [projectObject, setProjectObject] = useState<any>(null);
@ -48,6 +55,7 @@ export default function WorkFlow() {
const [scrollLeft, setScrollLeft] = useState(0);
const [showControls, setShowControls] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [activeEditTab, setActiveEditTab] = useState('1');
const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const mainVideoRef = useRef<HTMLVideoElement>(null);
@ -92,6 +100,32 @@ export default function WorkFlow() {
setCurrentStep('3');
// 获取绘制角色后,开始获取分镜视频
await getTaskVideo(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '4'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '4'
}));
setCurrentStep('4');
// 获取分镜视频后,开始获取背景音
await getTaskBackgroundAudio(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '5'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '5'
}));
setCurrentStep('5');
// 获取背景音后,开始获取最终成品
await getTaskFinalProduct(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 首先修改 taskObject 下的 taskStatus 为 '6'
setTaskObject((prev: any) => ({
...prev,
taskStatus: '6'
}));
setCurrentStep('6');
// 获取最终成品后,任务完成
});
}, []);
@ -109,6 +143,15 @@ export default function WorkFlow() {
}
}, [currentSketchIndex, taskSketch.length]);
const handleEditModalOpen = (tab: string) => {
// 停止循环播放
setIsPlaying(false);
// 停止分镜视频播放
setIsVideoPlaying(false);
setActiveEditTab(tab);
setIsEditModalOpen(true);
}
// 处理鼠标/触摸拖动事件
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true);
@ -156,7 +199,7 @@ export default function WorkFlow() {
taskDescription: "Task 1 Description",
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
taskProgress: 0,
mode: 'auto', // 托管模式、人工干预模式
mode: 'auto', // 全自动模式、人工干预模式
resolution: '1080p', // 1080p、2160p
taskCreatedAt: new Date().toISOString(),
taskUpdatedAt: new Date().toISOString(),
@ -202,6 +245,16 @@ export default function WorkFlow() {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
}
// 模拟接口请求 获取背景音
const getTaskBackgroundAudio = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
}
// 模拟接口请求 获取最终成品
const getTaskFinalProduct = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
}
// 模拟接口请求 每次获取一个分镜视频 轮询获取
const getTaskVideo = async (taskId: string) => {
setIsGeneratingVideo(true);
@ -209,7 +262,7 @@ export default function WorkFlow() {
// 模拟分批获取分镜视频
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟5秒延迟
const newVideo = {
id: `video-${i}`,
@ -242,80 +295,107 @@ export default function WorkFlow() {
// 缓存渲染的缩略图列表
const renderedSketches = useMemo(() =>
taskSketch.map((sketch, index) => (
<motion.div
key={sketch.id}
<div
key={`sketch-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && setCurrentSketchIndex(index)}
initial={false}
animate={{
scale: currentSketchIndex === index ? 1.05 : 1,
rotateY: currentSketchIndex === index ? 5 : 0,
rotateX: currentSketchIndex === index ? -5 : 0,
translateZ: currentSketchIndex === index ? '20px' : '0px',
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
>
<motion.img
className="w-full h-full object-cover"
src={sketch.url}
alt={`缩略图 ${index + 1}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
<ProgressiveReveal
{...presets.thumbnail}
delay={index * 0.1}
customVariants={{
hidden: {
opacity: 0,
scale: 0.95,
filter: "blur(10px)"
},
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: {
duration: 0.8,
ease: [0.23, 1, 0.32, 1], // cubic-bezier
opacity: { duration: 0.6, ease: "easeInOut" },
scale: { duration: 1, ease: "easeOut" },
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
}
}
}}
loadingBgConfig={{
...presets.thumbnail.loadingBgConfig,
glowOpacity: 0.4,
duration: 4
}}
>
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img
className="w-full h-full object-cover"
src={sketch.url}
alt={`缩略图 ${index + 1}`}
/>
</div>
</ProgressiveReveal>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
</div>
)), [taskSketch, currentSketchIndex, isDragging]
);
// 缓存渲染的视频缩略图列表
const renderedVideos = useMemo(() =>
taskVideos.map((video, index) => (
<motion.div
key={video.id}
<div
key={`video-${index}`}
className={`relative aspect-video rounded-lg overflow-hidden
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
onClick={() => !isDragging && setCurrentSketchIndex(index)}
initial={false}
animate={{
scale: currentSketchIndex === index ? 1.05 : 1,
rotateY: currentSketchIndex === index ? 5 : 0,
rotateX: currentSketchIndex === index ? -5 : 0,
translateZ: currentSketchIndex === index ? '20px' : '0px',
transition: {
type: "spring",
stiffness: 300,
damping: 20
}
}}
style={{
transformStyle: 'preserve-3d',
perspective: '1000px'
}}
>
<video
className="w-full h-full object-cover"
src={video.url}
muted
playsInline
loop
poster={taskSketch[index]?.url}
/>
<ProgressiveReveal
{...presets.thumbnail}
delay={index * 0.1}
customVariants={{
hidden: {
opacity: 0,
scale: 0.95,
filter: "blur(10px)"
},
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: {
duration: 0.8,
ease: [0.23, 1, 0.32, 1], // cubic-bezier
opacity: { duration: 0.6, ease: "easeInOut" },
scale: { duration: 1, ease: "easeOut" },
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
}
}
}}
loadingBgConfig={{
...presets.thumbnail.loadingBgConfig,
glowOpacity: 0.4,
duration: 4
}}
>
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<video
className="w-full h-full object-cover"
src={video.url}
muted
playsInline
loop
poster={taskSketch[index]?.url}
/>
</div>
</ProgressiveReveal>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
</div>
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
);
@ -353,6 +433,14 @@ export default function WorkFlow() {
// 处理视频播放/暂停
const toggleVideoPlay = useCallback(() => {
if (mainVideoRef.current) {
if (isVideoPlaying) {
mainVideoRef.current.pause();
} else {
// 从暂停位置继续播放
mainVideoRef.current.play();
}
}
setIsVideoPlaying(prev => !prev);
}, []);
@ -361,7 +449,10 @@ export default function WorkFlow() {
if (isVideoPlaying && taskVideos.length > 0) {
// 确保当前视频开始播放
if (mainVideoRef.current) {
mainVideoRef.current.play();
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
setIsVideoPlaying(false);
});
}
} else {
// 暂停当前视频
@ -384,12 +475,16 @@ export default function WorkFlow() {
// 当切换视频时重置视频播放
useEffect(() => {
if (mainVideoRef.current) {
// 只有在切换视频时才重置时间
mainVideoRef.current.currentTime = 0;
if (isVideoPlaying) {
mainVideoRef.current.play();
mainVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
setIsVideoPlaying(false);
});
}
}
}, [currentSketchIndex, isVideoPlaying]);
}, [currentSketchIndex]);
// 当切换到分镜草图模式时,停止视频播放
useEffect(() => {
@ -419,6 +514,12 @@ export default function WorkFlow() {
} else {
setCurrentLoadingText('分镜视频生成完成');
}
} else if (currentStep === '4') {
setCurrentLoadingText('正在生成背景音...');
} else if (currentStep === '5') {
setCurrentLoadingText('正在生成最终成品...');
} else {
setCurrentLoadingText('任务完成');
}
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
@ -463,16 +564,19 @@ export default function WorkFlow() {
);
}
if (currentStep === '3') {
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
return (
<div
className="relative w-full h-full"
className="relative w-full h-full rounded-lg"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskVideos[currentSketchIndex] ? (
<motion.div className="relative w-full h-full">
<motion.video
<ProgressiveReveal
className="w-full h-full rounded-lg"
{...presets.main}
>
<video
ref={mainVideoRef}
key={taskVideos[currentSketchIndex].url}
className="w-full h-full rounded-lg object-cover object-center"
@ -482,52 +586,54 @@ export default function WorkFlow() {
loop={false}
playsInline
poster={taskSketch[currentSketchIndex]?.url}
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
onEnded={() => {
if (isVideoPlaying) {
// 当前视频播放完成后,自动切换到下一个
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
}
}}
/>
{/* 播放进度指示器 */}
<AnimatePresence>
{isVideoPlaying && (
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
exit={{ scaleX: 0 }}
transition={{
duration: mainVideoRef.current?.duration || 6,
repeat: Infinity
}}
style={{ transformOrigin: "left" }}
/>
)}
</AnimatePresence>
</motion.div>
</ProgressiveReveal>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
{/* 动态渐变背景 */}
<motion.div
className="flex flex-col items-center gap-4"
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0 opacity-50"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<motion.div
className="flex flex-col items-center gap-4 relative z-10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
@ -548,13 +654,12 @@ export default function WorkFlow() {
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
className="text-sm text-white font-medium"
animate={{
opacity: [0.5, 1, 0.5],
opacity: [0.7, 1, 0.7],
}}
transition={{
duration: 2,
@ -583,7 +688,7 @@ export default function WorkFlow() {
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
onClick={() => handleEditModalOpen('3')}
/>
{/* <GlassIconButton
icon={FileText}
@ -621,40 +726,160 @@ export default function WorkFlow() {
);
}
// 展示最终成片
if (Number(currentStep) === 6) {
return (
<div className="relative w-full h-full rounded-lg overflow-hidden">
<ProgressiveReveal
className="w-full h-full rounded-lg"
customVariants={{
hidden: {
opacity: 0,
filter: "blur(20px)",
clipPath: "inset(0 50% 0 50%)"
},
visible: {
opacity: 1,
filter: "blur(0px)",
clipPath: "inset(0 0% 0 0%)",
transition: {
duration: 1.2,
ease: [0.43, 0.13, 0.23, 0.96], // 自定义缓动函数
opacity: { duration: 0.8, ease: "easeOut" },
filter: { duration: 0.6, ease: "easeOut" },
clipPath: { duration: 1, ease: "easeInOut", delay: 0.2 }
}
}
}}
loadingBgConfig={{
fromColor: 'from-emerald-300',
viaColor: 'via-cyan-400',
toColor: 'to-blue-500',
glowOpacity: 0.6,
duration: 6
}}
>
<div className="relative w-full h-full">
{/* 最终成片视频 */}
<video
className="w-full h-full object-cover rounded-lg"
src={MOCK_FINAL_VIDEO.url}
poster={MOCK_FINAL_VIDEO.thumbnail}
autoPlay
loop
muted
playsInline
/>
{/* 视频信息浮层 */}
<motion.div
className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1, duration: 0.6 }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<motion.div
className="w-2 h-2 rounded-full bg-emerald-500"
animate={{
scale: [1, 1.2, 1],
opacity: [1, 0.6, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<span className="text-sm font-medium text-white/90"></span>
</div>
<div className="flex items-center gap-2">
<GlassIconButton
icon={Edit3}
tooltip="编辑成片"
onClick={() => handleEditModalOpen('4')}
size="sm"
/>
</div>
</div>
</motion.div>
{/* 完成标记 */}
<motion.div
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
initial={{ opacity: 0, scale: 0.8, x: 20 }}
animate={{ opacity: 1, scale: 1, x: 0 }}
transition={{ delay: 1.2, duration: 0.6 }}
>
</motion.div>
</div>
</ProgressiveReveal>
</div>
);
}
return (
<div
className="relative w-full h-full"
className="relative w-full h-full rounded-lg"
onMouseEnter={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
>
{taskSketch[currentSketchIndex] ? (
<motion.img
key={currentSketchIndex}
src={taskSketch[currentSketchIndex].url}
alt={`分镜草图 ${currentSketchIndex + 1}`}
className="w-full h-full rounded-lg"
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 25
}
}}
/>
<ProgressiveReveal
className="w-full h-full rounded-lg"
{...presets.main}
>
<img
key={currentSketchIndex}
src={taskSketch[currentSketchIndex].url}
alt={`分镜草图 ${currentSketchIndex + 1}`}
className="w-full h-full rounded-lg object-cover"
/>
</ProgressiveReveal>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm rounded-lg overflow-hidden">
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
{/* 动态渐变背景 */}
<motion.div
className="flex flex-col items-center gap-4"
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0 opacity-50"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<motion.div
className="flex flex-col items-center gap-4 relative z-10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
@ -675,13 +900,12 @@ export default function WorkFlow() {
ease: "linear"
}}
>
<Loader2 className="w-8 h-8 text-blue-500 relative z-10" />
</motion.div>
</div>
<motion.p
className="text-sm text-white/70"
className="text-sm text-white font-medium"
animate={{
opacity: [0.5, 1, 0.5],
opacity: [0.7, 1, 0.7],
}}
transition={{
duration: 2,
@ -710,7 +934,7 @@ export default function WorkFlow() {
<GlassIconButton
icon={Edit3}
tooltip="编辑分镜"
onClick={() => setIsEditModalOpen(true)}
onClick={() => handleEditModalOpen('1')}
/>
{/* <GlassIconButton
icon={FileText}
@ -867,20 +1091,50 @@ export default function WorkFlow() {
onMouseUp={handleMouseUp}
onMouseLeave={() => setIsDragging(false)}
>
{currentStep === '3' ? (
{(Number(currentStep) > 2 && Number(currentStep) < 6) ? (
<>
{renderedVideos}
{isGeneratingVideo && taskVideos.length < taskSketch.length && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
className="relative aspect-video rounded-lg overflow-hidden"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
{/* 动态渐变背景 */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0 opacity-50"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
@ -901,7 +1155,6 @@ export default function WorkFlow() {
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
@ -911,20 +1164,69 @@ export default function WorkFlow() {
</motion.div>
)}
</>
) : Number(currentStep) === 6 ? (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 1.4 }}
>
<video
className="w-full h-full object-cover"
src={MOCK_FINAL_VIDEO.url}
poster={MOCK_FINAL_VIDEO.thumbnail}
muted
playsInline
loop
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<span className="text-sm font-medium text-white"></span>
</div>
</motion.div>
) : (
<>
{renderedSketches}
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
<motion.div
className="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-blue-500/20 via-purple-500/10 to-pink-500/20 backdrop-blur-sm"
className="relative aspect-video rounded-lg overflow-hidden"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
{/* 动态渐变背景 */}
<motion.div
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0 opacity-50"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
<motion.div
className="absolute -inset-4 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full opacity-20 blur-xl"
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 180, 360],
@ -945,7 +1247,6 @@ export default function WorkFlow() {
ease: "linear"
}}
>
<Loader2 className="w-6 h-6 text-blue-500 relative z-10" />
</motion.div>
</div>
</div>
@ -974,9 +1275,11 @@ export default function WorkFlow() {
<EditModal
isOpen={isEditModalOpen}
activeEditTab={activeEditTab}
onClose={() => setIsEditModalOpen(false)}
taskStatus={taskObject?.taskStatus || '1'}
taskSketch={taskSketch}
sketchVideo={taskVideos}
currentSketchIndex={currentSketchIndex}
onSketchSelect={setCurrentSketchIndex}
/>

View File

@ -0,0 +1,292 @@
import React, { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Library, Play, Pause, RefreshCw, Wand2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { GlassIconButton } from './glass-icon-button';
import { ReplaceCharacterModal } from './replace-character-modal';
interface CharacterTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
// 模拟角色数据
const MOCK_CHARACTERS = [
{
id: 1,
name: '雪 (YUKI)',
avatar: '/assets/3dr_chihiro.png',
voiceDescription: '年轻女性,温柔而坚定的声线,语速适中,带有轻微的感性色彩。',
characterDescription: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。',
voiceUrl: 'https://example.com/voice-sample.mp3'
},
{
id: 2,
name: '春 (HARU)',
avatar: '/assets/3dr_mono.png',
voiceDescription: '年轻男性,清澈而温和的声线,语速从容,带有知性的特质。',
characterDescription: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。',
voiceUrl: 'https://example.com/voice-sample.mp3'
},
];
export function CharacterTabContent({
taskSketch,
currentSketchIndex,
onSketchSelect
}: CharacterTabContentProps) {
const [selectedCharacterIndex, setSelectedCharacterIndex] = useState(0);
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [editingField, setEditingField] = useState<{
type: 'name' | 'voiceDescription' | 'characterDescription' | null;
value: string;
}>({ type: null, value: '' });
const audioRef = useRef<HTMLAudioElement>(null);
// 处理音频播放进度
const handleTimeUpdate = () => {
if (audioRef.current) {
const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100;
setProgress(progress);
}
};
// 处理播放/暂停
const togglePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
// 处理进度条点击
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (audioRef.current) {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
const time = (percentage / 100) * audioRef.current.duration;
audioRef.current.currentTime = time;
setProgress(percentage);
}
};
return (
<div className="flex flex-col gap-6">
{/* 上部分:角色缩略图 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="relative">
<div className="flex gap-4 overflow-x-auto p-2 hide-scrollbar">
{MOCK_CHARACTERS.map((character, index) => (
<motion.div
key={character.id}
className={cn(
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
'aspect-[9/16]',
selectedCharacterIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => setSelectedCharacterIndex(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img
src={character.avatar}
alt={character.name}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90 line-clamp-1">{character.name}</span>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
{/* 中间部分:替换角色 */}
<motion.div
className="p-4 rounded-lg bg-white/5"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h3 className="text-sm font-medium mb-2"></h3>
<div className="flex gap-4">
<motion.button
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
onClick={() => {
setActiveReplaceMethod('upload');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
onClick={() => {
setActiveReplaceMethod('library');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
</div>
</motion.div>
{/* 下部分:角色详情 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:角色信息 */}
<div className="space-y-4">
{/* 角色姓名 */}
<div className="space-y-2">
<label className="text-sm text-white/70"></label>
<input
type="text"
value={MOCK_CHARACTERS[selectedCharacterIndex].name}
onChange={(e) => console.log('name changed:', e.target.value)}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500"
/>
</div>
{/* 声音描述 */}
<div className="space-y-2">
<label className="text-sm text-white/70"></label>
<textarea
value={MOCK_CHARACTERS[selectedCharacterIndex].voiceDescription}
onChange={(e) => console.log('voice description changed:', e.target.value)}
className="w-full h-24 px-3 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 resize-none"
/>
</div>
{/* 声音预览 */}
<div className="p-4 rounded-lg bg-white/5 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-white/70"></span>
<GlassIconButton
icon={RefreshCw}
tooltip="重新生成声音"
onClick={() => console.log('regenerate voice')}
size="sm"
/>
</div>
<div className="relative">
<audio
ref={audioRef}
src={MOCK_CHARACTERS[selectedCharacterIndex].voiceUrl}
onTimeUpdate={handleTimeUpdate}
onEnded={() => setIsPlaying(false)}
/>
{/* 进度条 */}
<div
className="w-full h-1 bg-white/10 rounded-full cursor-pointer overflow-hidden"
onClick={handleProgressClick}
>
<motion.div
className="h-full bg-blue-500 rounded-full"
style={{ width: `${progress}%` }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
/>
</div>
{/* 播放控制 */}
<div className="mt-2 flex items-center gap-2">
<motion.button
className="p-2 rounded-full hover:bg-white/10"
onClick={togglePlay}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isPlaying ? (
<Pause className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)}
</motion.button>
<div className="text-xs text-white/50">
{audioRef.current ? (
`${Math.floor(audioRef.current.currentTime)}s / ${Math.floor(audioRef.current.duration)}s`
) : '0:00 / 0:00'}
</div>
</div>
</div>
</div>
</div>
{/* 右列:角色信息 */}
<div className="space-y-4">
{/* 角色描述 */}
<div className="space-y-2">
<label className="text-sm text-white/70"></label>
<textarea
value={MOCK_CHARACTERS[selectedCharacterIndex].characterDescription}
onChange={(e) => console.log('character description changed:', e.target.value)}
className="w-full h-24 px-3 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 resize-none"
/>
</div>
{/* 角色预览 */}
<div className="w-full max-w-[280px] mx-auto aspect-[9/16] rounded-lg overflow-hidden relative group">
<img
src={MOCK_CHARACTERS[selectedCharacterIndex].avatar}
alt={MOCK_CHARACTERS[selectedCharacterIndex].name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent
opacity-100 transition-opacity">
<div className="absolute bottom-4 left-4">
<GlassIconButton
icon={Wand2}
tooltip="重新生成角色形象"
onClick={() => console.log('regenerate character')}
/>
</div>
</div>
</div>
</div>
</motion.div>
{/* 替换角色弹窗 */}
<ReplaceCharacterModal
isOpen={isReplaceModalOpen}
activeReplaceMethod={activeReplaceMethod}
onClose={() => setIsReplaceModalOpen(false)}
onCharacterSelect={(character) => {
console.log('Selected character:', character);
setIsReplaceModalOpen(false);
// TODO: 处理角色选择逻辑
}}
/>
</div>
);
}

View File

@ -1,16 +1,22 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, FileText, Users, Video, Music, Scissors, Settings } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScriptTabContent } from './script-tab-content';
import { VideoTabContent } from './video-tab-content';
import { SettingsTabContent } from './settings-tab-content';
import { CharacterTabContent } from './character-tab-content';
import { MusicTabContent } from './music-tab-content';
interface EditModalProps {
isOpen: boolean;
onClose: () => void;
activeEditTab: string;
taskStatus: string;
taskSketch: any[];
sketchVideo: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
@ -19,20 +25,27 @@ const tabs = [
{ id: '1', label: '脚本', icon: FileText },
{ id: '2', label: '角色', icon: Users },
{ id: '3', label: '分镜视频', icon: Video },
{ id: '4', label: '背景音', icon: Music },
{ id: '5', label: '剪辑', icon: Scissors },
{ id: '4', label: '', icon: Music },
// { id: '5', label: '剪辑', icon: Scissors },
{ id: 'settings', label: '设置', icon: Settings },
];
export function EditModal({
isOpen,
onClose,
activeEditTab,
taskStatus,
taskSketch,
sketchVideo,
currentSketchIndex,
onSketchSelect
}: EditModalProps) {
const [activeTab, setActiveTab] = useState('1');
const [activeTab, setActiveTab] = useState(activeEditTab);
// 当 activeEditTab 改变时更新 activeTab
useEffect(() => {
setActiveTab(activeEditTab);
}, [activeEditTab]);
const isTabDisabled = (tabId: string) => {
if (tabId === 'settings') return false;
@ -49,6 +62,40 @@ export function EditModal({
onSketchSelect={onSketchSelect}
/>
);
case '2':
return (
<CharacterTabContent
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={onSketchSelect}
/>
);
case '3':
return (
<VideoTabContent
taskSketch={sketchVideo}
currentSketchIndex={currentSketchIndex}
onSketchSelect={onSketchSelect}
isPlaying={false}
/>
);
case '4':
return (
<MusicTabContent
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={onSketchSelect}
/>
);
case 'settings':
return (
<SettingsTabContent
onSettingChange={(key, value) => {
console.log('Setting changed:', key, value);
// TODO: 实现设置更新逻辑
}}
/>
);
default:
return (
<div className="min-h-[400px] flex items-center justify-center text-white/50">
@ -153,7 +200,7 @@ export function EditModal({
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"

View File

@ -0,0 +1,151 @@
'use client';
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface GenerateVideoModalProps {
isOpen: boolean;
onClose: () => void;
onGenerate: (params: { text: string; duration: string }) => void;
}
export function GenerateVideoModal({
isOpen,
onClose,
onGenerate
}: GenerateVideoModalProps) {
const [text, setText] = useState('');
const [duration, setDuration] = useState('5');
const [videoUrl, setVideoUrl] = useState('');
const handleGenerate = () => {
onGenerate({ text, duration });
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-2">
<button
className="p-1 hover:bg-white/10 rounded-full transition-colors"
onClick={onClose}
>
<ChevronDown className="w-5 h-5" />
</button>
<h2 className="text-lg font-medium"></h2>
</div>
</div>
{/* 主要内容区域 */}
<div className="p-6 space-y-6 h-[80vh] flex flex-col overflow-y-auto hidden-scrollbar">
{/* 文本输入区域 */}
<div className="space-y-2 flex-shrink-0">
<label className="text-sm text-white/70"></label>
<textarea
className="w-full h-32 p-4 bg-white/5 border border-white/10 rounded-lg
text-white placeholder-white/30 resize-none focus:outline-none focus:border-blue-500"
placeholder="描述你想要生成的视频场景..."
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
{/* 时长选择 */}
{/* <div className="space-y-2">
<label className="text-sm text-white/70"></label>
<div className="flex gap-2">
{['5', '10', '15', '30'].map((value) => (
<motion.button
key={value}
className={cn(
'px-4 py-2 rounded-lg transition-colors',
duration === value
? 'bg-blue-500 text-white'
: 'bg-white/5 hover:bg-white/10'
)}
onClick={() => setDuration(value)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{value}
</motion.button>
))}
</div>
</div> */}
{/* 生成按钮 */}
<div className="flex justify-end flex-shrink-0">
<motion.button
className="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleGenerate}
disabled={!text.trim()}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
{/* 生成视频预览 */}
<div className="flex justify-center flex-1 min-h-[450px]">
<div className="w-[80%] bg-white/5 rounded-lg">
<video src={videoUrl} className="w-full object-cover" autoPlay />
</div>
</div>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,326 @@
'use client';
import React, { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Library, Play, Pause, RefreshCw, Music2, Volume2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { GlassIconButton } from './glass-icon-button';
import { ReplaceMusicModal } from './replace-music-modal';
interface MusicTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
// 模拟音乐数据
const MOCK_MUSICS = [
{
id: 1,
name: 'Music 1 / Rocka',
duration: '00m : 21s : 000ms',
totalDuration: '01m : 35s : 765ms',
isLooped: true,
url: 'https://example.com/music1.mp3'
}
];
export function MusicTabContent({
taskSketch,
currentSketchIndex,
onSketchSelect
}: MusicTabContentProps) {
const [selectedMusicIndex, setSelectedMusicIndex] = useState(0);
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
const [activeMethod, setActiveMethod] = useState('upload');
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [volume, setVolume] = useState(75);
const audioRef = useRef<HTMLAudioElement>(null);
// 处理音频播放进度
const handleTimeUpdate = () => {
if (audioRef.current) {
const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100;
setProgress(progress);
}
};
// 处理播放/暂停
const togglePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
// 处理进度条点击
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (audioRef.current) {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
const time = (percentage / 100) * audioRef.current.duration;
audioRef.current.currentTime = time;
setProgress(percentage);
}
};
// 处理音量变化
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseInt(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume / 100;
}
};
return (
<div className="flex flex-col gap-6">
{/* 上部分:音乐列表 */}
<motion.div
className="space-y-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{MOCK_MUSICS.map((music, index) => (
<motion.div
key={music.id}
className={cn(
'group relative p-4 rounded-lg overflow-hidden cursor-pointer transition-colors w-[272px]',
selectedMusicIndex === index ? 'border border-blue-500 bg-blue-500/10' : ''
)}
onClick={() => setSelectedMusicIndex(index)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<Music2 className="w-8 h-8 text-white/70" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium truncate">{music.name}</h3>
</div>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* 中间部分:替换区 */}
<motion.div
className="p-4 rounded-lg bg-white/5"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h3 className="text-sm font-medium mb-2"></h3>
<div className="flex gap-4">
<motion.button
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
onClick={() => setIsReplaceModalOpen(true)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
onClick={() => setIsReplaceModalOpen(true)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
</div>
</motion.div>
{/* 下部分:音乐属性 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:音乐编辑选项 */}
<div className="space-y-6">
{/* Loop Music */}
<motion.div
className="space-y-2"
whileHover={{ scale: 1.01 }}
>
<div className="flex items-center justify-between">
<label className="text-sm text-white/70">Loop music</label>
<motion.div
className={cn(
"w-12 h-6 rounded-full p-1 cursor-pointer",
MOCK_MUSICS[selectedMusicIndex].isLooped ? "bg-blue-500" : "bg-white/10"
)}
onClick={() => console.log('loop toggled')}
layout
>
<motion.div
className="w-4 h-4 bg-white rounded-full"
layout
animate={{
x: MOCK_MUSICS[selectedMusicIndex].isLooped ? "100%" : "0%"
}}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
/>
</motion.div>
</div>
</motion.div>
{/* Trim */}
<motion.div
className="space-y-2"
whileHover={{ scale: 1.01 }}
>
<label className="text-sm text-white/70">Trim</label>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-white/50">from</span>
<input
type="text"
value="00 : 00"
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
focus:outline-none focus:border-blue-500"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-white/50">to</span>
<input
type="text"
value="01 : 35"
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
focus:outline-none focus:border-blue-500"
/>
</div>
</div>
</motion.div>
{/* Fade in & out */}
<motion.div
className="space-y-4"
whileHover={{ scale: 1.01 }}
>
<label className="text-sm text-white/70">Fade in & out (max 10s)</label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<span className="text-xs text-white/50">Fade in:</span>
<input
type="text"
value="0s"
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
focus:outline-none focus:border-blue-500"
/>
</div>
<div className="space-y-2">
<span className="text-xs text-white/50">Fade out:</span>
<input
type="text"
value="3s"
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
focus:outline-none focus:border-blue-500"
/>
</div>
</div>
</motion.div>
{/* Music volume */}
<motion.div
className="space-y-2"
whileHover={{ scale: 1.01 }}
>
<label className="text-sm text-white/70">Music volume</label>
<div className="flex items-center gap-3">
<Volume2 className="w-4 h-4 text-white/50" />
<div className="flex-1 h-1 bg-white/10 rounded-full relative">
<motion.div
className="absolute h-full bg-blue-500 rounded-full"
style={{ width: `${volume}%` }}
layout
/>
<input
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
className="absolute w-full h-full opacity-0 cursor-pointer"
/>
</div>
<span className="text-sm text-white/50 w-12 text-right">{volume}%</span>
</div>
</motion.div>
</div>
{/* 右列:音频预览 */}
<motion.div
className="p-6 rounded-lg bg-white/5 flex flex-col items-center justify-center gap-6"
whileHover={{ scale: 1.01 }}
>
<div className="w-24 h-24 rounded-full bg-white/10 flex items-center justify-center relative">
<motion.div
className="absolute w-full h-full rounded-full border-2 border-blue-500 border-t-transparent"
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
style={{ opacity: isPlaying ? 1 : 0 }}
/>
<motion.button
className="w-16 h-16 rounded-full bg-blue-500/20 hover:bg-blue-500/30
flex items-center justify-center transition-colors"
onClick={togglePlay}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isPlaying ? (
<Pause className="w-8 h-8 text-blue-500" />
) : (
<Play className="w-8 h-8 text-blue-500" />
)}
</motion.button>
</div>
<div className="w-full space-y-3">
<div
className="w-full h-1 bg-white/10 rounded-full cursor-pointer overflow-hidden"
onClick={handleProgressClick}
>
<motion.div
className="h-full bg-blue-500 rounded-full"
style={{ width: `${progress}%` }}
layout
/>
</div>
<div className="text-sm text-white/50 text-center">
{audioRef.current ? (
`${Math.floor(audioRef.current.currentTime)}s / ${Math.floor(audioRef.current.duration)}s`
) : '0:00 / 0:00'}
</div>
</div>
</motion.div>
</motion.div>
{/* 替换音乐弹窗 */}
<ReplaceMusicModal
isOpen={isReplaceModalOpen}
activeReplaceMethod={activeMethod}
onClose={() => setIsReplaceModalOpen(false)}
onMusicSelect={(music) => {
console.log('Selected music:', music);
setIsReplaceModalOpen(false);
// TODO: 处理音乐选择逻辑
}}
/>
</div>
);
}

View File

@ -0,0 +1,242 @@
'use client';
import React from 'react';
import { motion } from 'framer-motion';
interface ProgressiveRevealProps {
/** 要显示的内容 */
children: React.ReactNode;
/** 容器类名 */
className?: string;
/** 是否为视频内容 */
isVideo?: boolean;
/** 动画延迟时间(秒) */
delay?: number;
/** 渐进显示动画时长(秒) */
revealDuration?: number;
/** 模糊过渡动画时长(秒) */
blurDuration?: number;
/** 初始模糊度 */
initialBlur?: number;
/** 自定义动画变体 */
customVariants?: {
hidden: any;
visible: any;
};
/** 自定义过渡效果 */
customTransition?: any;
/** 是否显示加载背景 */
showLoadingBg?: boolean;
/** 加载背景配置 */
loadingBgConfig?: {
/** 渐变色起始颜色 */
fromColor?: string;
/** 渐变色中间颜色 */
viaColor?: string;
/** 渐变色结束颜色 */
toColor?: string;
/** 光效不透明度 */
glowOpacity?: number;
/** 背景动画持续时间 */
duration?: number;
};
}
export const ProgressiveReveal: React.FC<ProgressiveRevealProps> = ({
children,
className = '',
isVideo = false,
delay = 0,
revealDuration = 0.8,
blurDuration = 0.5,
initialBlur = 20,
customVariants,
customTransition,
showLoadingBg = true,
loadingBgConfig = {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.5,
duration: 5,
},
}) => {
// 默认动画变体
const defaultVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
};
// 默认过渡效果
const defaultTransition = {
duration: 0.3,
delay
};
return (
<motion.div
className={`relative overflow-hidden ${className}`}
initial="hidden"
animate="visible"
variants={customVariants || defaultVariants}
transition={customTransition || defaultTransition}
>
{/* 加载背景 */}
{showLoadingBg && (
<>
{/* 动态渐变背景 */}
<motion.div
className={`absolute inset-0 bg-gradient-to-r ${loadingBgConfig.fromColor} ${loadingBgConfig.viaColor} ${loadingBgConfig.toColor}`}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: loadingBgConfig.duration,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
}}
/>
{/* 动态光效 */}
<motion.div
className="absolute inset-0"
style={{
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
opacity: loadingBgConfig.glowOpacity,
}}
animate={{
scale: [1, 1.2, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
</>
)}
{/* 内容显示动画 */}
<motion.div
className="relative w-full h-full"
initial={{ clipPath: "inset(0 100% 0 0)" }}
animate={{ clipPath: "inset(0 0% 0 0)" }}
transition={{
duration: revealDuration,
ease: "easeInOut",
delay
}}
>
<motion.div
className="w-full h-full"
initial={{ filter: `blur(${initialBlur}px)` }}
animate={{ filter: "blur(0px)" }}
transition={{
duration: blurDuration,
delay: delay + revealDuration,
ease: "easeOut"
}}
>
{children}
</motion.div>
</motion.div>
</motion.div>
);
};
// 预设配置
export const presets = {
// 快速显示
fast: {
revealDuration: 0.4,
blurDuration: 0.3,
initialBlur: 10,
loadingBgConfig: {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.5,
duration: 3,
}
},
// 标准显示
standard: {
revealDuration: 0.8,
blurDuration: 0.5,
initialBlur: 20,
loadingBgConfig: {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.5,
duration: 5,
}
},
// 缓慢显示
slow: {
revealDuration: 1.2,
blurDuration: 0.7,
initialBlur: 30,
loadingBgConfig: {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.5,
duration: 7,
}
},
// 缩略图
thumbnail: {
revealDuration: 0.6,
blurDuration: 0.4,
initialBlur: 10,
loadingBgConfig: {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.3,
duration: 4,
}
},
// 主内容
main: {
revealDuration: 0.8,
blurDuration: 0.5,
initialBlur: 20,
loadingBgConfig: {
fromColor: 'from-cyan-300',
viaColor: 'via-sky-400',
toColor: 'to-blue-500',
glowOpacity: 0.5,
duration: 5,
}
},
// 温暖色调
warm: {
revealDuration: 0.8,
blurDuration: 0.5,
initialBlur: 20,
loadingBgConfig: {
fromColor: 'from-orange-300',
viaColor: 'via-red-400',
toColor: 'to-rose-500',
glowOpacity: 0.5,
duration: 5,
}
},
// 冷色调
cool: {
revealDuration: 0.8,
blurDuration: 0.5,
initialBlur: 20,
loadingBgConfig: {
fromColor: 'from-emerald-300',
viaColor: 'via-teal-400',
toColor: 'to-cyan-500',
glowOpacity: 0.5,
duration: 5,
}
},
};

View File

@ -0,0 +1,266 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Upload, Library, Wand2, Search, Image, Plus, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ReplaceCharacterModalProps {
isOpen: boolean;
activeReplaceMethod: string;
onClose: () => void;
onCharacterSelect: (character: any) => void;
}
export function ReplaceCharacterModal({
isOpen,
activeReplaceMethod,
onClose,
onCharacterSelect
}: ReplaceCharacterModalProps) {
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
setActiveMethod(activeReplaceMethod);
}, [activeReplaceMethod]);
// 模拟角色库数据
const mockLibraryCharacters = [
{
id: 1,
avatar: '/assets/3dr_chihiro.png',
name: '雪 (YUKI)',
style: '动漫风格'
},
{
id: 2,
avatar: '/assets/3dr_mono.png',
name: '春 (HARU)',
style: '写实风格'
},
{
id: 3,
avatar: '/assets/3dr_chihiro.png',
name: '夏 (NATSU)',
style: '二次元风格'
},
];
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// 处理文件上传
console.log('Uploading file:', file);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-2">
<button
className="p-1 hover:bg-white/10 rounded-full transition-colors"
onClick={onClose}
>
<ChevronDown className="w-5 h-5" />
</button>
<h2 className="text-lg font-medium"></h2>
</div>
</div>
{/* 主要内容区域 */}
<div className="p-6 space-y-6 h-[80vh]">
{/* 替换方式选择 */}
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'upload'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('upload')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'library'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('library')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
</div>
{/* 内容区域 */}
<AnimatePresence mode="wait">
{activeMethod === 'upload' && (
<motion.div
key="upload"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
>
<input
type="file"
accept="image/*"
className="hidden"
id="character-upload"
onChange={handleFileUpload}
/>
<label
htmlFor="character-upload"
className="flex flex-col items-center gap-4 cursor-pointer"
>
<motion.div
className="p-4 rounded-full bg-white/5"
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
whileTap={{ scale: 0.9 }}
>
<Image className="w-8 h-8 text-white/70" />
</motion.div>
<div className="text-center">
<p className="text-white/70"></p>
<p className="text-sm text-white/50 mt-2"> PNG, JPG, WEBP </p>
</div>
</label>
</motion.div>
)}
{activeMethod === 'library' && (
<motion.div
key="library"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-4"
>
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
<input
type="text"
placeholder="搜索角色..."
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* 角色列表 */}
<div className="grid grid-cols-4 gap-4">
{mockLibraryCharacters.map((character) => (
<motion.div
key={character.id}
className="group relative aspect-[9/16] rounded-lg overflow-hidden cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onCharacterSelect(character)}
>
<img
src={character.avatar}
alt={character.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent
opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-2">
<p className="text-sm font-medium">{character.name}</p>
<p className="text-xs text-white/70">{character.style}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
{activeMethod === 'generate' && (
<motion.div
key="generate"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center gap-4"
>
<motion.button
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
rounded-lg transition-colors"
onClick={() => console.log('Generate character')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Plus className="w-5 h-5" />
<span></span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,249 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Library, Search, Music2, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ReplaceMusicModalProps {
isOpen: boolean;
activeReplaceMethod: string;
onClose: () => void;
onMusicSelect: (music: any) => void;
}
export function ReplaceMusicModal({
isOpen,
activeReplaceMethod,
onClose,
onMusicSelect
}: ReplaceMusicModalProps) {
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
setActiveMethod(activeReplaceMethod);
}, [activeReplaceMethod]);
// 模拟音乐库数据
const mockLibraryMusics = [
{
id: 1,
name: 'Upbeat Pop',
duration: '02:35',
genre: '流行音乐',
url: 'https://example.com/music1.mp3'
},
{
id: 2,
name: 'Chill Lofi',
duration: '03:15',
genre: '轻音乐',
url: 'https://example.com/music2.mp3'
},
{
id: 3,
name: 'Epic Orchestra',
duration: '04:20',
genre: '管弦乐',
url: 'https://example.com/music3.mp3'
},
];
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// 处理文件上传
console.log('Uploading file:', file);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-2">
<button
className="p-1 hover:bg-white/10 rounded-full transition-colors"
onClick={onClose}
>
<ChevronDown className="w-5 h-5" />
</button>
<h2 className="text-lg font-medium"></h2>
</div>
</div>
{/* 主要内容区域 */}
<div className="p-6 space-y-6 h-[80vh]">
{/* 替换方式选择 */}
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'upload'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('upload')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'library'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('library')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
</div>
{/* 内容区域 */}
<AnimatePresence mode="wait">
{activeMethod === 'upload' && (
<motion.div
key="upload"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
>
<input
type="file"
accept="audio/*"
className="hidden"
id="music-upload"
onChange={handleFileUpload}
/>
<label
htmlFor="music-upload"
className="flex flex-col items-center gap-4 cursor-pointer"
>
<motion.div
className="p-4 rounded-full bg-white/5"
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
whileTap={{ scale: 0.9 }}
>
<Music2 className="w-8 h-8 text-white/70" />
</motion.div>
<div className="text-center">
<p className="text-white/70"></p>
<p className="text-sm text-white/50 mt-2"> MP3, WAV, M4A </p>
</div>
</label>
</motion.div>
)}
{activeMethod === 'library' && (
<motion.div
key="library"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-4"
>
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
<input
type="text"
placeholder="搜索音乐..."
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* 音乐列表 */}
<div className="space-y-2">
{mockLibraryMusics.map((music) => (
<motion.div
key={music.id}
className="group relative p-4 rounded-lg bg-white/5 hover:bg-white/10
cursor-pointer transition-colors"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
onClick={() => onMusicSelect(music)}
>
<div className="flex items-center gap-4">
<div className="flex-shrink-0">
<Music2 className="w-8 h-8 text-white/70" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium truncate">{music.name}</h3>
<span className="text-xs text-white/50">{music.duration}</span>
</div>
<p className="text-xs text-white/50 mt-1">{music.genre}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,282 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Upload, Library, Wand2, Search, FileVideo, Plus, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { GenerateVideoModal } from './generate-video-modal';
interface ReplaceVideoModalProps {
isOpen: boolean;
activeReplaceMethod: string;
onClose: () => void;
onVideoSelect: (video: any) => void;
}
export function ReplaceVideoModal({
isOpen,
activeReplaceMethod,
onClose,
onVideoSelect
}: ReplaceVideoModalProps) {
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
const [searchQuery, setSearchQuery] = useState('');
const [isGenerateModalOpen, setIsGenerateModalOpen] = React.useState(false);
useEffect(() => {
setActiveMethod(activeReplaceMethod);
}, [activeReplaceMethod]);
// 模拟素材库视频数据
const mockLibraryVideos = [
{ id: 1, url: 'https://example.com/video1.mp4', title: '烟花绽放', duration: '00:15' },
{ id: 2, url: 'https://example.com/video2.mp4', title: '城市夜景', duration: '00:20' },
{ id: 3, url: 'https://example.com/video3.mp4', title: '海浪声音', duration: '00:30' },
];
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// 处理文件上传
console.log('Uploading file:', file);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
<motion.div
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{
type: 'spring',
damping: 25,
stiffness: 200,
}}
>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-2">
<button
className="p-1 hover:bg-white/10 rounded-full transition-colors"
onClick={onClose}
>
<ChevronDown className="w-5 h-5" />
</button>
<h2 className="text-lg font-medium"></h2>
</div>
</div>
{/* 主要内容区域 */}
<div className="p-6 space-y-6 h-[80vh]">
{/* 替换方式选择 */}
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'upload'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('upload')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'library'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('library')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeMethod === 'generate'
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => setActiveMethod('generate')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Wand2 className="w-6 h-6" />
<span></span>
</motion.button>
</div>
{/* 内容区域 */}
<AnimatePresence mode="wait">
{activeMethod === 'upload' && (
<motion.div
key="upload"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-white/10 rounded-lg"
>
<input
type="file"
accept="video/*"
className="hidden"
id="video-upload"
onChange={handleFileUpload}
/>
<label
htmlFor="video-upload"
className="flex flex-col items-center gap-4 cursor-pointer"
>
<motion.div
className="p-4 rounded-full bg-white/5"
whileHover={{ scale: 1.1, backgroundColor: 'rgba(255,255,255,0.1)' }}
whileTap={{ scale: 0.9 }}
>
<FileVideo className="w-8 h-8 text-white/70" />
</motion.div>
<div className="text-center">
<p className="text-white/70"></p>
<p className="text-sm text-white/50 mt-2"> MP4, MOV, AVI </p>
</div>
</label>
</motion.div>
)}
{activeMethod === 'library' && (
<motion.div
key="library"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-4"
>
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-white/50" />
<input
type="text"
placeholder="搜索视频..."
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg
focus:outline-none focus:border-blue-500 transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* 视频列表 */}
<div className="grid grid-cols-3 gap-4">
{mockLibraryVideos.map((video) => (
<motion.div
key={video.id}
className="group relative aspect-video rounded-lg overflow-hidden cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onVideoSelect(video)}
>
<video
src={video.url}
className="w-full h-full object-cover"
muted
loop
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent
opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-2">
<p className="text-sm font-medium">{video.title}</p>
<p className="text-xs text-white/70">{video.duration}</p>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
{activeMethod === 'generate' && (
<motion.div
key="generate"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center gap-4"
>
<motion.button
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
rounded-lg transition-colors"
onClick={() => setIsGenerateModalOpen(true)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Plus className="w-5 h-5" />
<span></span>
</motion.button>
</motion.div>
)}
</AnimatePresence>
</div>
{/* 底部操作栏 */}
<div className="p-4 border-t border-white/10 bg-black/20">
<div className="flex justify-end gap-3">
<motion.button
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
>
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
</motion.button>
</div>
</div>
</motion.div>
</div>
{/* 生成视频弹窗 */}
<GenerateVideoModal
isOpen={isGenerateModalOpen}
onClose={() => setIsGenerateModalOpen(false)}
onGenerate={(params) => {
console.log('Generate video with params:', params);
setIsGenerateModalOpen(false);
}}
/>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,298 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Check, ChevronDown, Volume2, Music, Mic, Radio } from 'lucide-react';
import { cn } from '@/lib/utils';
interface SettingsTabContentProps {
onSettingChange?: (key: string, value: any) => void;
}
interface SettingOption {
label: string;
value: string;
}
const modeOptions: SettingOption[] = [
{ label: 'Auto Mode', value: 'auto' },
{ label: 'Manual Mode', value: 'manual' },
];
const resolutionOptions: SettingOption[] = [
{ label: '720P', value: '720p' },
{ label: '1080P', value: '1080p' },
];
const overlayOptions: SettingOption[] = [
{ label: 'None', value: 'none' },
{ label: 'Light Leaks', value: 'light_leaks' },
{ label: 'Film Grain', value: 'film_grain' },
{ label: 'Dust', value: 'dust' },
];
const transitionOptions: SettingOption[] = [
{ label: 'None', value: 'none' },
{ label: 'Dynamic Cut', value: 'dynamic_cut' },
{ label: 'Zoom Pop', value: 'zoom_pop' },
{ label: 'Smooth Fade', value: 'smooth_fade' },
];
const subtitleOptions: SettingOption[] = [
{ label: 'None', value: 'none' },
{ label: 'Hormozi', value: 'hormozi' },
{ label: 'Castor', value: 'castor' },
{ label: 'Modern', value: 'modern' },
];
const watermarkOptions: SettingOption[] = [
{ label: 'None', value: 'none' },
{ label: 'Simple', value: 'simple' },
{ label: 'Branded', value: 'branded' },
];
const textEffectOptions: SettingOption[] = [
{ label: 'Minimal', value: 'minimal' },
{ label: 'Plain', value: 'plain' },
{ label: 'Dramatic', value: 'dramatic' },
{ label: 'Corporate', value: 'corporate' },
];
export function SettingsTabContent({ onSettingChange }: SettingsTabContentProps) {
const [selectedMode, setSelectedMode] = useState('auto');
const [selectedResolution, setSelectedResolution] = useState('1080p');
const [selectedOverlay, setSelectedOverlay] = useState('light_leaks');
const [selectedTransition, setSelectedTransition] = useState('none');
const [selectedSubtitle, setSelectedSubtitle] = useState('hormozi');
const [selectedWatermark, setSelectedWatermark] = useState('none');
const [selectedTextEffect, setSelectedTextEffect] = useState('minimal');
const [sfxVolume, setSfxVolume] = useState(75);
const [mediaVolume, setMediaVolume] = useState(75);
const [musicVolume, setMusicVolume] = useState(80);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const handleDropdownToggle = (key: string) => {
setOpenDropdown(openDropdown === key ? null : key);
};
const renderDropdown = (
key: string,
label: string,
options: SettingOption[],
value: string,
onChange: (value: string) => void
) => (
<div className="relative">
<motion.button
className={cn(
"w-full px-4 py-2 rounded-lg border text-left flex items-center justify-between",
openDropdown === key
? "border-blue-500 bg-blue-500/10"
: "border-white/10 hover:border-white/20"
)}
onClick={() => handleDropdownToggle(key)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<span>{options.find(opt => opt.value === value)?.label || value}</span>
<motion.div
animate={{ rotate: openDropdown === key ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown className="w-4 h-4" />
</motion.div>
</motion.button>
<AnimatePresence>
{openDropdown === key && (
<motion.div
className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-white/10 bg-black/90 backdrop-blur-xl"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{options.map((option) => (
<motion.button
key={option.value}
className={cn(
"w-full px-4 py-2 text-left flex items-center justify-between hover:bg-white/5",
value === option.value && "text-blue-500"
)}
onClick={() => {
onChange(option.value);
handleDropdownToggle(key);
onSettingChange?.(key, option.value);
}}
whileHover={{ x: 4 }}
>
{option.label}
{value === option.value && <Check className="w-4 h-4" />}
</motion.button>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
const renderVolumeSlider = (
icon: React.ReactNode,
label: string,
value: number,
onChange: (value: number) => void
) => (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-white/70">
{icon}
<span>{label}</span>
</div>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={value}
className="flex-1 h-2 rounded-full bg-white/10 cursor-pointer
[&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
[&::-webkit-slider-thumb]:hover:scale-110"
onChange={(e) => {
const newValue = parseInt(e.target.value);
onChange(newValue);
onSettingChange?.(label, newValue);
}}
/>
<span className="w-12 text-right text-sm">{value}%</span>
</div>
</div>
);
return (
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="space-y-6">
{/* 工作模式 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Mode</label>
{renderDropdown('mode', 'Mode', modeOptions, selectedMode, setSelectedMode)}
</div>
{/* 分辨率 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Resolution</label>
{renderDropdown('resolution', 'Resolution', resolutionOptions, selectedResolution, setSelectedResolution)}
</div>
{/* 叠加效果 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Overlay Preset</label>
{renderDropdown('overlay', 'Overlay Preset', overlayOptions, selectedOverlay, setSelectedOverlay)}
</div>
{/* 转场设定 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Transition Preset</label>
{renderDropdown('transition', 'Transition Preset', transitionOptions, selectedTransition, setSelectedTransition)}
</div>
{/* 字幕风格 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Subtitle Preset</label>
{renderDropdown('subtitle', 'Subtitle Preset', subtitleOptions, selectedSubtitle, setSelectedSubtitle)}
</div>
{/* 贴纸预设 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Sticker Preset</label>
{renderDropdown('watermark', 'Sticker Preset', watermarkOptions, selectedWatermark, setSelectedWatermark)}
</div>
{/* 文字效果 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Text Preset</label>
{renderDropdown('textEffect', 'Text Preset', textEffectOptions, selectedTextEffect, setSelectedTextEffect)}
</div>
{/* 音效主音量 */}
<div className="space-y-2">
<label className="text-sm text-white/70">SFX Master Volume</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={sfxVolume}
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
[&::-webkit-slider-thumb]:hover:scale-110"
onChange={(e) => {
const newValue = parseInt(e.target.value);
setSfxVolume(newValue);
onSettingChange?.('SFX Master Volume', newValue);
}}
/>
<span className="w-12 text-right text-sm">{sfxVolume}%</span>
</div>
</div>
{/* 媒体音频主音量 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Media Audio Master Volume</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={mediaVolume}
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
[&::-webkit-slider-thumb]:hover:scale-110"
onChange={(e) => {
const newValue = parseInt(e.target.value);
setMediaVolume(newValue);
onSettingChange?.('Media Audio Master Volume', newValue);
}}
/>
<span className="w-12 text-right text-sm">{mediaVolume}%</span>
</div>
</div>
{/* 音乐主音量 */}
<div className="space-y-2">
<label className="text-sm text-white/70">Music Master Volume</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={musicVolume}
className="flex-1 h-1 rounded-full bg-white/10 cursor-pointer
[&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer
[&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:duration-200
[&::-webkit-slider-thumb]:hover:scale-110"
onChange={(e) => {
const newValue = parseInt(e.target.value);
setMusicVolume(newValue);
onSettingChange?.('Music Master Volume', newValue);
}}
/>
<span className="w-12 text-right text-sm">{musicVolume}%</span>
</div>
</div>
</div>
</motion.div>
);
}

View File

@ -0,0 +1,422 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Trash2, RefreshCw, Play, Pause, Volume2, VolumeX, Upload, Library, Wand2 } from 'lucide-react';
import { GlassIconButton } from './glass-icon-button';
import { cn } from '@/lib/utils';
import { ReplaceVideoModal } from './replace-video-modal';
interface VideoTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
isPlaying?: boolean;
}
export function VideoTabContent({
taskSketch = [],
currentSketchIndex = 0,
onSketchSelect,
isPlaying: externalIsPlaying = true
}: VideoTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const videosRef = useRef<HTMLDivElement>(null);
const videoPlayerRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
const [isMuted, setIsMuted] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false);
const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload');
// 监听外部播放状态变化
useEffect(() => {
setIsPlaying(externalIsPlaying);
}, [externalIsPlaying]);
// 确保 taskSketch 是数组
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
// 模拟视频数据
const mockVideos = sketches.map((_, index) => ({
id: `video-${index}`,
url: 'https://example.com/video.mp4', // 替换为实际视频URL
duration: '00:21',
description: `这是第 ${index + 1} 个分镜的视频描述,包含了场景中的主要动作和表现。`
}));
// 自动滚动到选中项
useEffect(() => {
if (thumbnailsRef.current && videosRef.current) {
const thumbnailContainer = thumbnailsRef.current;
const videoContainer = videosRef.current;
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
const thumbnailGap = 16; // gap-4 = 16px
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
const videoElement = videoContainer.children[currentSketchIndex] as HTMLElement;
const videoScrollPosition = videoElement?.offsetLeft ?? 0;
thumbnailContainer.scrollTo({
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
behavior: 'smooth'
});
videoContainer.scrollTo({
left: videoScrollPosition - videoContainer.clientWidth / 2 + videoElement?.clientWidth / 2,
behavior: 'smooth'
});
}
}, [currentSketchIndex]);
// 视频播放控制
useEffect(() => {
if (videoPlayerRef.current) {
if (isPlaying) {
videoPlayerRef.current.play().catch(() => {
// 处理自动播放策略限制
setIsPlaying(false);
});
} else {
// videoPlayerRef.current.pause();
}
}
}, [isPlaying, currentSketchIndex]);
// 更新进度条
const handleTimeUpdate = () => {
if (videoPlayerRef.current) {
const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100;
setProgress(progress);
}
};
// 如果没有数据,显示空状态
if (sketches.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<p></p>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* 上部分 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{/* 分镜缩略图行 */}
<div className="relative">
<div
ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar"
>
{sketches.map((sketch, index) => (
<motion.div
key={sketch.id || index}
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer',
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => onSketchSelect(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<video
src={sketch.url}
className="w-full h-full object-cover"
muted
loop
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
))}
</div>
</div>
{/* 视频描述行 - 单行滚动 */}
<div className="relative group">
<div
ref={videosRef}
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
>
{mockVideos.map((video, index) => {
const isActive = currentSketchIndex === index;
return (
<motion.div
key={video.id}
className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
)}
onClick={() => onSketchSelect(index)}
initial={false}
animate={{
scale: isActive ? 1.02 : 1,
}}
>
<div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap">
{video.description}
</span>
{index < mockVideos.length - 1 && (
<span className="text-white/20">|</span>
)}
</div>
</motion.div>
);
})}
</div>
{/* 渐变遮罩 */}
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</motion.div>
{/* 中间部分 替换视频 可上传、素材库选择、生成视频 */}
<motion.div
className="p-4 rounded-lg bg-white/5"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h3 className="text-sm font-medium mb-2"></h3>
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'upload' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('upload');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'library' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('library');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span></span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'generate' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('generate');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Wand2 className="w-6 h-6" />
<span></span>
</motion.button>
</div>
</motion.div>
{/* 下部分 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:编辑项 截取视频、设置转场、调节音量 */}
<div className="space-y-4">
{/* 视频截取 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2"></h3>
<div className="flex items-center gap-4">
<input
type="text"
placeholder="00:00"
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
text-center focus:outline-none focus:border-blue-500"
/>
<span className="text-white/50"></span>
<input
type="text"
placeholder="00:00"
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
text-center focus:outline-none focus:border-blue-500"
/>
</div>
</div>
{/* 转场设置 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2"></h3>
<div className="grid grid-cols-3 gap-2">
{['淡入淡出', '滑动', '缩放'].map((transition) => (
<motion.button
key={transition}
className="px-3 py-1 bg-white/5 hover:bg-white/10 rounded-lg text-sm"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{transition}
</motion.button>
))}
</div>
</div>
{/* 音量调节 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2"></h3>
<input
type="range"
min="0"
max="100"
className="w-full"
onChange={(e) => console.log('Volume:', e.target.value)}
/>
</div>
</div>
{/* 右列:视频预览和操作 */}
<div className="space-y-4">
{/* 选中的视频预览 */}
<motion.div
className="aspect-video rounded-lg overflow-hidden relative group"
layoutId={`video-preview-${currentSketchIndex}`}
>
<video
ref={videoPlayerRef}
src={sketches[currentSketchIndex]?.url}
className="w-full h-full object-cover"
loop
autoPlay
playsInline
muted={isMuted}
onTimeUpdate={handleTimeUpdate}
/>
{/* 视频控制层 */}
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent">
{/* 进度条 */}
<div className="w-full h-1 bg-white/20 rounded-full mb-4 cursor-pointer"
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
if (videoPlayerRef.current) {
videoPlayerRef.current.currentTime = (percentage / 100) * videoPlayerRef.current.duration;
}
}}
>
<motion.div
className="h-full bg-blue-500 rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
{/* 控制按钮 */}
<div className="flex items-center gap-4">
<motion.button
className="p-2 rounded-full hover:bg-white/10"
onClick={() => setIsPlaying(!isPlaying)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isPlaying ? (
<Pause className="w-5 h-5" />
) : (
<Play className="w-5 h-5" />
)}
</motion.button>
<motion.button
className="p-2 rounded-full hover:bg-white/10"
onClick={() => setIsMuted(!isMuted)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isMuted ? (
<VolumeX className="w-5 h-5" />
) : (
<Volume2 className="w-5 h-5" />
)}
</motion.button>
<span className="text-sm">{mockVideos[currentSketchIndex]?.duration}</span>
</div>
</div>
</div>
</motion.div>
{/* 操作按钮 */}
<div className="grid grid-cols-2 gap-2">
<motion.button
onClick={() => console.log('删除分镜')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500/10 hover:bg-red-500/20
text-red-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Trash2 className="w-4 h-4" />
<span></span>
</motion.button>
<motion.button
onClick={() => console.log('重新生成')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw className="w-4 h-4" />
<span></span>
</motion.button>
</div>
</div>
</motion.div>
{/* 替换视频弹窗 */}
<ReplaceVideoModal
isOpen={isReplaceModalOpen}
activeReplaceMethod={activeReplaceMethod}
onClose={() => setIsReplaceModalOpen(false)}
onVideoSelect={(video) => {
console.log('Selected video:', video);
setIsReplaceModalOpen(false);
}}
/>
</div>
);
}

821
package-lock.json generated
View File

@ -30,9 +30,9 @@
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
@ -1599,31 +1599,32 @@
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz",
"integrity": "sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==",
"version": "2.2.5",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.5.tgz",
"integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.6.0"
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
@ -1640,6 +1641,466 @@
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
"version": "1.2.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
@ -1663,21 +2124,22 @@
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
"integrity": "sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==",
"version": "1.3.5",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
"integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@ -1694,6 +2156,197 @@
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
@ -1931,6 +2584,39 @@
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@ -5093,14 +5779,6 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@ -7211,19 +7889,20 @@
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.1",
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@ -7268,20 +7947,20 @@
}
},
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
"version": "2.2.3",
"resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"invariant": "^2.2.4",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
@ -8399,9 +9078,10 @@
}
},
"node_modules/use-callback-ref": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
@ -8409,8 +9089,8 @@
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
@ -8473,9 +9153,10 @@
}
},
"node_modules/use-sidecar": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
@ -8484,8 +9165,8 @@
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {

View File

@ -31,9 +31,9 @@
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",