forked from 77media/video-flow
加载样式的优化
This commit is contained in:
parent
140448c3a1
commit
d13f2a3363
@ -70,21 +70,24 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
|||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '奇幻冒险故事',
|
name: '奇幻冒险故事',
|
||||||
imageUrl: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
|
image_url: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
|
||||||
generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。',
|
generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。',
|
||||||
storyRole: [
|
storyRole: [
|
||||||
{
|
{
|
||||||
role_name: '艾莉娅',
|
role_name: '艾莉娅',
|
||||||
|
role_description: '一个勇敢的女孩,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_chihiro.png',
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role_name: '魔法师梅林',
|
role_name: '魔法师梅林',
|
||||||
|
role_description: '一个智慧的魔法师,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_mono.png',
|
photo_url: '/assets/3dr_mono.png',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role_name: '守护者龙',
|
role_name: '守护者龙',
|
||||||
|
role_description: '一个强大的守护者,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_howlbg.jpg',
|
photo_url: '/assets/3dr_howlbg.jpg',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
}
|
}
|
||||||
@ -93,39 +96,44 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
|||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
name: '科幻探索之旅',
|
name: '科幻探索之旅',
|
||||||
imageUrl: ['/assets/3dr_monobg.jpg'],
|
image_url: ['/assets/3dr_monobg.jpg'],
|
||||||
generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。',
|
generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。',
|
||||||
storyRole: [
|
storyRole: [
|
||||||
{
|
{
|
||||||
role_name: '船长凯特',
|
role_name: '船长凯特',
|
||||||
|
role_description: '一个勇敢的船长,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_chihiro.png',
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role_name: 'AI助手诺娃',
|
role_name: 'AI助手诺娃',
|
||||||
|
role_description: '一个强大的AI助手,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_mono.png',
|
photo_url: '/assets/3dr_mono.png',
|
||||||
voice_url:""
|
voice_url: ''
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
name: '温馨家庭喜剧',
|
name: '温馨家庭喜剧',
|
||||||
imageUrl: ['/assets/3dr_spirited.jpg'],
|
image_url: ['/assets/3dr_spirited.jpg'],
|
||||||
generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。',
|
generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。',
|
||||||
storyRole: [
|
storyRole: [
|
||||||
{
|
{
|
||||||
role_name: '妈妈莉莉',
|
role_name: '妈妈莉莉',
|
||||||
|
role_description: '一个温柔的妈妈,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_chihiro.png',
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role_name: '爸爸汤姆',
|
role_name: '爸爸汤姆',
|
||||||
|
role_description: '一个勇敢的爸爸,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_mono.png',
|
photo_url: '/assets/3dr_mono.png',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role_name: '孩子小杰',
|
role_name: '孩子小杰',
|
||||||
|
role_description: '一个聪明的孩子,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||||
photo_url: '/assets/3dr_howlbg.jpg',
|
photo_url: '/assets/3dr_howlbg.jpg',
|
||||||
voice_url:""
|
voice_url:""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,13 +137,15 @@ export interface StoryTemplateEntity {
|
|||||||
/** 故事模板名称 */
|
/** 故事模板名称 */
|
||||||
name: string;
|
name: string;
|
||||||
/** 故事模板图片 */
|
/** 故事模板图片 */
|
||||||
imageUrl: string[];
|
image_url: string[];
|
||||||
/** 故事模板概览*/
|
/** 故事模板概览*/
|
||||||
generateText: string;
|
generateText: string;
|
||||||
/**故事角色 */
|
/**故事角色 */
|
||||||
storyRole: {
|
storyRole: {
|
||||||
/**角色名 */
|
/**角色名 */
|
||||||
role_name: string;
|
role_name: string;
|
||||||
|
/**角色描述 */
|
||||||
|
role_description: string;
|
||||||
/**照片URL */
|
/**照片URL */
|
||||||
photo_url: string;
|
photo_url: string;
|
||||||
/**声音URL */
|
/**声音URL */
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Settings,
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Dropdown, Modal, Tooltip, Upload, Spin } from "antd";
|
import { Dropdown, Modal, Tooltip, Upload, Spin, Popconfirm } from "antd";
|
||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||||||
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
||||||
@ -30,6 +30,7 @@ import { createMovieProjectV1 } from "@/api/video_flow";
|
|||||||
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
||||||
import { ActionButton } from "../common/ActionButton";
|
import { ActionButton } from "../common/ActionButton";
|
||||||
import { HighlightEditor } from "../common/HighlightEditor";
|
import { HighlightEditor } from "../common/HighlightEditor";
|
||||||
|
import GlobalLoad from "../common/GlobalLoad";
|
||||||
|
|
||||||
/**模板故事模式弹窗组件 */
|
/**模板故事模式弹窗组件 */
|
||||||
const RenderTemplateStoryMode = ({
|
const RenderTemplateStoryMode = ({
|
||||||
@ -55,7 +56,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
} = useTemplateStoryServiceHook();
|
} = useTemplateStoryServiceHook();
|
||||||
|
|
||||||
// 本地加载状态,用于 UI 反馈
|
// 本地加载状态,用于 UI 反馈
|
||||||
const [localLoading, setLocalLoading] = useState(false);
|
const [localLoading, setLocalLoading] = useState(0);
|
||||||
|
|
||||||
// 组件挂载时获取模板列表
|
// 组件挂载时获取模板列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,9 +74,19 @@ const RenderTemplateStoryMode = ({
|
|||||||
// 处理确认操作
|
// 处理确认操作
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (!selectedTemplate) return;
|
if (!selectedTemplate) return;
|
||||||
|
let timer = setInterval(() => {
|
||||||
|
setLocalLoading((prev) => {
|
||||||
|
if (prev >= 95) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return 95;
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
try {
|
try {
|
||||||
setLocalLoading(true);
|
setLocalLoading(1);
|
||||||
|
// 假性的增加进度条
|
||||||
|
|
||||||
const projectId = await actionStory();
|
const projectId = await actionStory();
|
||||||
console.log("Story action created:", projectId);
|
console.log("Story action created:", projectId);
|
||||||
onClose();
|
onClose();
|
||||||
@ -86,7 +97,8 @@ const RenderTemplateStoryMode = ({
|
|||||||
console.error("Failed to create story action:", error);
|
console.error("Failed to create story action:", error);
|
||||||
// 这里可以添加 toast 提示
|
// 这里可以添加 toast 提示
|
||||||
} finally {
|
} finally {
|
||||||
setLocalLoading(false);
|
setLocalLoading(0);
|
||||||
|
clearInterval(timer);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +131,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
onClick={() => handleTemplateSelect(template)}
|
onClick={() => handleTemplateSelect(template)}
|
||||||
>
|
>
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
imageUrl={template.imageUrl[0]}
|
imageUrl={template.image_url[0]}
|
||||||
imageAlt={template.name}
|
imageAlt={template.name}
|
||||||
title={template.name}
|
title={template.name}
|
||||||
description={template.generateText}
|
description={template.generateText}
|
||||||
@ -150,7 +162,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer"
|
className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={selectedTemplate.imageUrl[0]}
|
src={selectedTemplate.image_url[0]}
|
||||||
alt={selectedTemplate.name}
|
alt={selectedTemplate.name}
|
||||||
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
||||||
/>
|
/>
|
||||||
@ -322,7 +334,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
</div> */}
|
</div> */}
|
||||||
<div className=" absolute bottom-0 right-0">
|
<div className=" absolute bottom-0 right-0">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isCreating={localLoading}
|
isCreating={localLoading>0}
|
||||||
handleCreateVideo={handleConfirm}
|
handleCreateVideo={handleConfirm}
|
||||||
icon={<Clapperboard className="w-5 h-5" />}
|
icon={<Clapperboard className="w-5 h-5" />}
|
||||||
/>
|
/>
|
||||||
@ -775,15 +787,21 @@ const PhotoStoryModal = ({
|
|||||||
uploadCharacterAvatarAndAnalyzeFeatures,
|
uploadCharacterAvatarAndAnalyzeFeatures,
|
||||||
} = useImageStoryServiceHook();
|
} = useImageStoryServiceHook();
|
||||||
const { loadingText } = useLoadScriptText(isLoading);
|
const { loadingText } = useLoadScriptText(isLoading);
|
||||||
const { uploadFile } = useUploadFile();
|
const [localLoading, setLocalLoading] = useState(0);
|
||||||
// 重置状态
|
// 重置状态
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
resetImageStory();
|
// resetImageStory();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 处理图片上传
|
// 处理图片上传
|
||||||
const handleImageUpload = async () => {
|
const handleImageUpload = async (e: any) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
if (!(target.tagName == "IMG" || e.target.dataset.alt =="image-upload-area")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
await triggerFileSelection();
|
await triggerFileSelection();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -820,6 +838,35 @@ const PhotoStoryModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAnalyzeImage = async () => {
|
||||||
|
let timeout = 100
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
setLocalLoading((prev) => {
|
||||||
|
if (prev >= 95) {
|
||||||
|
return 95;
|
||||||
|
}
|
||||||
|
return prev + 0.1;
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
|
try {
|
||||||
|
await uploadAndAnalyzeImage();
|
||||||
|
} finally {
|
||||||
|
timeout=10
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
setLocalLoading((prev) => {
|
||||||
|
if (prev >= 100) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@ -836,7 +883,7 @@ const PhotoStoryModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Spin spinning={isLoading} tip={loadingText}>
|
<GlobalLoad show={localLoading>0} progress={localLoading}>
|
||||||
<div className="rounded-2xl">
|
<div className="rounded-2xl">
|
||||||
{/* 弹窗头部 */}
|
{/* 弹窗头部 */}
|
||||||
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">
|
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">
|
||||||
@ -865,18 +912,33 @@ const PhotoStoryModal = ({
|
|||||||
alt="Story inspiration"
|
alt="Story inspiration"
|
||||||
className="w-full h-full object-cover rounded-lg"
|
className="w-full h-full object-cover rounded-lg"
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Clear all content !!! " placement="top">
|
<Popconfirm
|
||||||
|
title="Clear all content"
|
||||||
|
description="Are you sure you want to clear all content? This action cannot be undone."
|
||||||
|
onConfirm={() => {
|
||||||
|
resetImageStory();
|
||||||
|
}}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
showCancel={false}
|
||||||
|
okType="default"
|
||||||
|
placement="top"
|
||||||
|
classNames={{
|
||||||
|
root: "text-white event-pointer",
|
||||||
|
body: "text-white border rounded-lg bg-white/[0.04] [&_.ant-popconfirm-description]:!text-white [&_.ant-popconfirm-title]:!text-white [&_.ant-btn]:!text-white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
resetImageStory();
|
|
||||||
}}
|
}}
|
||||||
className="absolute top-1 right-1 w-4 h-4 bg-black/60 hover:bg-black/80 rounded-full flex items-center justify-center text-white/80 hover:text-white transition-colors"
|
className="absolute top-1 right-1 w-4 h-4 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-md flex items-center justify-center text-white/90 hover:text-white border border-white/20 hover:border-white/30 transition-all duration-200 z-10 shadow-sm hover:shadow-md"
|
||||||
data-alt="clear-all-button"
|
data-alt="clear-all-button"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-2.5 h-2.5" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-white/60">
|
<div className="text-center text-white/60">
|
||||||
@ -1035,7 +1097,7 @@ const PhotoStoryModal = ({
|
|||||||
>
|
>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
isCreating={isLoading}
|
isCreating={isLoading}
|
||||||
handleCreateVideo={uploadAndAnalyzeImage}
|
handleCreateVideo={handleAnalyzeImage}
|
||||||
icon={<Sparkles className="w-5 h-5" />}
|
icon={<Sparkles className="w-5 h-5" />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -1055,7 +1117,7 @@ const PhotoStoryModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
</GlobalLoad>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
138
components/common/GlobalLoad.tsx
Normal file
138
components/common/GlobalLoad.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { Spin } from "antd";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface GlobalLoadProps {
|
||||||
|
/** 是否显示加载状态 */
|
||||||
|
show: boolean;
|
||||||
|
/** 子元素内容 */
|
||||||
|
children: ReactNode;
|
||||||
|
/** 进度条数值 0-100,非必须 */
|
||||||
|
progress?: number;
|
||||||
|
/** 旋转圆环直径,非必须 */
|
||||||
|
spinnerDiameter?: number;
|
||||||
|
/** 进度条宽度,非必须 */
|
||||||
|
progressWidth?: number;
|
||||||
|
/** 是否显示旋转圆圈,默认显示 */
|
||||||
|
showSpinner?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局加载组件,支持标签包裹形式渲染子元素
|
||||||
|
* @param props - 组件属性
|
||||||
|
* @returns 加载组件
|
||||||
|
*/
|
||||||
|
export default function GlobalLoad({
|
||||||
|
show,
|
||||||
|
children,
|
||||||
|
progress,
|
||||||
|
spinnerDiameter,
|
||||||
|
progressWidth,
|
||||||
|
showSpinner = true,
|
||||||
|
}: GlobalLoadProps) {
|
||||||
|
if (!show) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义加载图标:组合两个组件
|
||||||
|
const customIndicator = (
|
||||||
|
<div
|
||||||
|
data-alt="custom-loading-indicator"
|
||||||
|
className="!w-max !h-max !flex flex-col items-center justify-center gap-4 -translate-x-1/2 -translate-y-1/2"
|
||||||
|
>
|
||||||
|
{showSpinner && <TailwindSpinner diameter={spinnerDiameter} />}
|
||||||
|
{progress && <TailwindLinearLoader progress={progress} width={progressWidth} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-alt="loading-container" className="relative">
|
||||||
|
<Spin spinning={true} tip="" indicator={customIndicator}>
|
||||||
|
{children}
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tailwind CSS loading spinner component for global loading overlay.
|
||||||
|
* @param diameter - Diameter of the spinner in pixels
|
||||||
|
* @returns {JSX.Element} - Spinner visual.
|
||||||
|
*/
|
||||||
|
export const TailwindSpinner = ({ diameter = 50 }: { diameter?: number }) => {
|
||||||
|
const radius = diameter / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-alt="loading-spinner" className="relative" style={{ width: `${diameter}px`, height: `${diameter}px` }}>
|
||||||
|
{/* 主旋转圆环 */}
|
||||||
|
<div
|
||||||
|
className="w-full h-full animate-spin"
|
||||||
|
style={{
|
||||||
|
width: `${diameter}px`,
|
||||||
|
height: `${diameter}px`,
|
||||||
|
borderRadius: `${radius}px`,
|
||||||
|
backgroundImage:
|
||||||
|
"linear-gradient(rgb(186, 66, 255) 35%, rgb(0, 225, 255))",
|
||||||
|
filter: "blur(1px)",
|
||||||
|
boxShadow:
|
||||||
|
"0px -5px 20px 0px rgb(186, 66, 255), 0px 5px 20px 0px rgb(0, 225, 255)",
|
||||||
|
animation: "spinning82341 1.7s linear infinite",
|
||||||
|
}}
|
||||||
|
data-alt="spinner-main"
|
||||||
|
/>
|
||||||
|
{/* 背景模糊圆环 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
width: `${diameter}px`,
|
||||||
|
height: `${diameter}px`,
|
||||||
|
borderRadius: `${radius}px`,
|
||||||
|
backgroundColor: "rgb(36, 36, 36)",
|
||||||
|
filter: "blur(10px)",
|
||||||
|
}}
|
||||||
|
data-alt="spinner-blur-bg"
|
||||||
|
/>
|
||||||
|
{/* 自定义动画关键帧 */}
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes spinning82341 {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tailwind CSS linear progress bar loader with animated light sweep.
|
||||||
|
* @param progress - Progress value from 0 to 100
|
||||||
|
* @param width - Width of the progress bar in pixels
|
||||||
|
* @returns {JSX.Element} - Linear loader visual.
|
||||||
|
*/
|
||||||
|
export const TailwindLinearLoader = ({
|
||||||
|
progress,
|
||||||
|
width = 160
|
||||||
|
}: {
|
||||||
|
progress: number;
|
||||||
|
width?: number;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
data-alt="linear-loader-container"
|
||||||
|
className="relative h-[2px] bg-gray-200 dark:bg-gray-700 overflow-hidden rounded-full"
|
||||||
|
style={{ width: `${width}px` }}
|
||||||
|
>
|
||||||
|
{/* Animated light sweep - position controlled by progress */}
|
||||||
|
<div
|
||||||
|
data-alt="linear-loader-light"
|
||||||
|
className="absolute top-0 h-full w-[70px]"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(87deg, rgba(0, 0, 0, 0) 0%, rgb(0, 204, 255) 40%, rgb(0, 204, 255) 60%, rgba(0, 0, 0, 0) 100%)",
|
||||||
|
left: `${progress - 10}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Keyframes for light sweep - no longer needed since position is controlled by progress */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
Loading…
x
Reference in New Issue
Block a user