feat: Add 5 immersive loading animations and multiple feature enhancements #433
484
src/ui/components/LoadingOverlay.tsx
Normal file
484
src/ui/components/LoadingOverlay.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
visible: boolean;
|
||||
minDuration?: number; // Minimum display duration in milliseconds
|
||||
message?: string;
|
||||
showLogo?: boolean;
|
||||
className?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export function LoadingOverlay({
|
||||
visible,
|
||||
minDuration = 800,
|
||||
message,
|
||||
showLogo = true,
|
||||
className,
|
||||
backgroundColor,
|
||||
}: LoadingOverlayProps) {
|
||||
const [isShowing, setIsShowing] = useState(false);
|
||||
const [isFadingOut, setIsFadingOut] = useState(false);
|
||||
const showStartTimeRef = useRef<number | null>(null);
|
||||
const minDurationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// Start showing immediately
|
||||
setIsShowing(true);
|
||||
setIsFadingOut(false);
|
||||
showStartTimeRef.current = Date.now();
|
||||
|
||||
// Clear any existing timer
|
||||
if (minDurationTimerRef.current) {
|
||||
clearTimeout(minDurationTimerRef.current);
|
||||
minDurationTimerRef.current = null;
|
||||
}
|
||||
} else if (isShowing) {
|
||||
// Calculate how long it has been showing
|
||||
const elapsed = showStartTimeRef.current
|
||||
? Date.now() - showStartTimeRef.current
|
||||
: 0;
|
||||
const remaining = Math.max(0, minDuration - elapsed);
|
||||
|
||||
if (remaining > 0) {
|
||||
// Wait for minimum duration before hiding
|
||||
minDurationTimerRef.current = setTimeout(() => {
|
||||
setIsFadingOut(true);
|
||||
// Wait for fade-out animation to complete
|
||||
setTimeout(() => {
|
||||
setIsShowing(false);
|
||||
setIsFadingOut(false);
|
||||
showStartTimeRef.current = null;
|
||||
}, 300); // Match fade-out duration
|
||||
}, remaining);
|
||||
} else {
|
||||
// Minimum duration already passed, hide immediately
|
||||
setIsFadingOut(true);
|
||||
setTimeout(() => {
|
||||
setIsShowing(false);
|
||||
setIsFadingOut(false);
|
||||
showStartTimeRef.current = null;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (minDurationTimerRef.current) {
|
||||
clearTimeout(minDurationTimerRef.current);
|
||||
minDurationTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [visible, isShowing, minDuration]);
|
||||
|
||||
if (!isShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes glitch-main {
|
||||
0%, 20%, 40%, 60%, 80%, 100% {
|
||||
transform: translate(0);
|
||||
filter: none;
|
||||
}
|
||||
2% {
|
||||
transform: translate(-8px, 0);
|
||||
filter: hue-rotate(90deg);
|
||||
}
|
||||
4% {
|
||||
transform: translate(-8px, 0);
|
||||
filter: hue-rotate(90deg);
|
||||
}
|
||||
22% {
|
||||
transform: translate(8px, 0) skew(-5deg);
|
||||
}
|
||||
24% {
|
||||
transform: translate(8px, 0) skew(-5deg);
|
||||
}
|
||||
42% {
|
||||
transform: translate(-6px, 0);
|
||||
filter: blur(2px);
|
||||
}
|
||||
44% {
|
||||
transform: translate(-6px, 0);
|
||||
filter: blur(2px);
|
||||
}
|
||||
62% {
|
||||
transform: translate(10px, 0) skew(8deg);
|
||||
filter: saturate(3);
|
||||
}
|
||||
64% {
|
||||
transform: translate(10px, 0) skew(8deg);
|
||||
filter: saturate(3);
|
||||
}
|
||||
82% {
|
||||
transform: translate(-4px, 0);
|
||||
}
|
||||
84% {
|
||||
transform: translate(-4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-before {
|
||||
0%, 100% {
|
||||
clip-path: polygon(0 0, 100% 0, 100% 5%, 0 5%);
|
||||
transform: translate(-8px, 0) skew(-3deg);
|
||||
}
|
||||
10% {
|
||||
clip-path: polygon(0 15%, 100% 15%, 100% 20%, 0 20%);
|
||||
transform: translate(6px, 0) skew(2deg);
|
||||
}
|
||||
20% {
|
||||
clip-path: polygon(0 35%, 100% 35%, 100% 40%, 0 40%);
|
||||
transform: translate(-4px, 0) skew(-1deg);
|
||||
}
|
||||
30% {
|
||||
clip-path: polygon(0 50%, 100% 50%, 100% 60%, 0 60%);
|
||||
transform: translate(10px, 0) skew(4deg);
|
||||
}
|
||||
40% {
|
||||
clip-path: polygon(0 70%, 100% 70%, 100% 75%, 0 75%);
|
||||
transform: translate(-6px, 0) skew(-2deg);
|
||||
}
|
||||
50% {
|
||||
clip-path: polygon(0 80%, 100% 80%, 100% 90%, 0 90%);
|
||||
transform: translate(8px, 0) skew(3deg);
|
||||
}
|
||||
60% {
|
||||
clip-path: polygon(0 10%, 100% 10%, 100% 15%, 0 15%);
|
||||
transform: translate(-7px, 0) skew(-3deg);
|
||||
}
|
||||
70% {
|
||||
clip-path: polygon(0 25%, 100% 25%, 100% 35%, 0 35%);
|
||||
transform: translate(5px, 0) skew(2deg);
|
||||
}
|
||||
80% {
|
||||
clip-path: polygon(0 45%, 100% 45%, 100% 55%, 0 55%);
|
||||
transform: translate(-9px, 0) skew(-4deg);
|
||||
}
|
||||
90% {
|
||||
clip-path: polygon(0 65%, 100% 65%, 100% 70%, 0 70%);
|
||||
transform: translate(7px, 0) skew(2deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-after {
|
||||
0%, 100% {
|
||||
clip-path: polygon(0 80%, 100% 80%, 100% 90%, 0 90%);
|
||||
transform: translate(7px, 0) skew(2deg);
|
||||
}
|
||||
10% {
|
||||
clip-path: polygon(0 10%, 100% 10%, 100% 20%, 0 20%);
|
||||
transform: translate(-5px, 0) skew(-3deg);
|
||||
}
|
||||
20% {
|
||||
clip-path: polygon(0 30%, 100% 30%, 100% 35%, 0 35%);
|
||||
transform: translate(8px, 0) skew(4deg);
|
||||
}
|
||||
30% {
|
||||
clip-path: polygon(0 50%, 100% 50%, 100% 65%, 0 65%);
|
||||
transform: translate(-6px, 0) skew(-2deg);
|
||||
}
|
||||
40% {
|
||||
clip-path: polygon(0 5%, 100% 5%, 100% 15%, 0 15%);
|
||||
transform: translate(9px, 0) skew(3deg);
|
||||
}
|
||||
50% {
|
||||
clip-path: polygon(0 70%, 100% 70%, 100% 80%, 0 80%);
|
||||
transform: translate(-7px, 0) skew(-4deg);
|
||||
}
|
||||
60% {
|
||||
clip-path: polygon(0 40%, 100% 40%, 100% 50%, 0 50%);
|
||||
transform: translate(6px, 0) skew(2deg);
|
||||
}
|
||||
70% {
|
||||
clip-path: polygon(0 20%, 100% 20%, 100% 30%, 0 30%);
|
||||
transform: translate(-8px, 0) skew(-3deg);
|
||||
}
|
||||
80% {
|
||||
clip-path: polygon(0 60%, 100% 60%, 100% 70%, 0 70%);
|
||||
transform: translate(5px, 0) skew(2deg);
|
||||
}
|
||||
90% {
|
||||
clip-path: polygon(0 0%, 100% 0%, 100% 10%, 0 10%);
|
||||
transform: translate(-10px, 0) skew(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
31.98%, 32.98%, 34.98%, 36.98% {
|
||||
opacity: 1;
|
||||
}
|
||||
32%, 34%, 36% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
32.8%, 34.8%, 36.8% {
|
||||
opacity: 1;
|
||||
}
|
||||
32.82%, 34.82%, 36.82% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
32.92%, 34.92%, 36.92% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rgb-shift {
|
||||
0%, 100% {
|
||||
text-shadow:
|
||||
0.05em 0 0 rgba(255, 0, 0, 0.75),
|
||||
-0.025em -0.05em 0 rgba(0, 255, 0, 0.75),
|
||||
0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
14% {
|
||||
text-shadow:
|
||||
0.05em 0 0 rgba(255, 0, 0, 0.75),
|
||||
-0.025em -0.05em 0 rgba(0, 255, 0, 0.75),
|
||||
0.025em 0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
15% {
|
||||
text-shadow:
|
||||
-0.05em -0.025em 0 rgba(255, 0, 0, 0.75),
|
||||
0.025em 0.025em 0 rgba(0, 255, 0, 0.75),
|
||||
-0.05em -0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
49% {
|
||||
text-shadow:
|
||||
-0.05em -0.025em 0 rgba(255, 0, 0, 0.75),
|
||||
0.025em 0.025em 0 rgba(0, 255, 0, 0.75),
|
||||
-0.05em -0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
50% {
|
||||
text-shadow:
|
||||
0.025em 0.05em 0 rgba(255, 0, 0, 0.75),
|
||||
0.05em 0 0 rgba(0, 255, 0, 0.75),
|
||||
0 -0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
99% {
|
||||
text-shadow:
|
||||
0.025em 0.05em 0 rgba(255, 0, 0, 0.75),
|
||||
0.05em 0 0 rgba(0, 255, 0, 0.75),
|
||||
0 -0.05em 0 rgba(0, 0, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.glitch-container {
|
||||
position: relative;
|
||||
animation: glitch-main 2s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
.glitch-text {
|
||||
position: relative;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
animation:
|
||||
flicker 4s infinite,
|
||||
rgb-shift 0.6s infinite;
|
||||
}
|
||||
|
||||
.glitch-text::before,
|
||||
.glitch-text::after {
|
||||
content: 'TERMIX';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.glitch-text::before {
|
||||
left: 0;
|
||||
text-shadow: -3px 0 #00ffff;
|
||||
animation: glitch-before 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
color: transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.glitch-text::after {
|
||||
left: 0;
|
||||
text-shadow: 3px 0 #ff00de;
|
||||
animation: glitch-after 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
color: transparent;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
@keyframes scan-line {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
100% {
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgba(0, 255, 255, 0.6) 40%,
|
||||
rgba(255, 255, 255, 0.9) 50%,
|
||||
rgba(255, 0, 222, 0.6) 60%,
|
||||
transparent
|
||||
);
|
||||
animation: scan-line 3s linear infinite;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes noise {
|
||||
0%, 100% { background-position: 0 0; }
|
||||
10% { background-position: -5% -10%; }
|
||||
20% { background-position: -15% 5%; }
|
||||
30% { background-position: 7% -25%; }
|
||||
40% { background-position: 20% 25%; }
|
||||
50% { background-position: -25% 10%; }
|
||||
60% { background-position: 15% 5%; }
|
||||
70% { background-position: 0% 15%; }
|
||||
80% { background-position: 25% 35%; }
|
||||
90% { background-position: -10% 10%; }
|
||||
}
|
||||
|
||||
.noise-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.05;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' /%3E%3C/svg%3E");
|
||||
animation: noise 1s steps(2) infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes glitch-blocks {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
33% {
|
||||
opacity: 0;
|
||||
}
|
||||
33.3% {
|
||||
opacity: 1;
|
||||
}
|
||||
33.6% {
|
||||
opacity: 0;
|
||||
}
|
||||
66% {
|
||||
opacity: 0;
|
||||
}
|
||||
66.3% {
|
||||
opacity: 1;
|
||||
}
|
||||
66.6% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.glitch-blocks {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
animation: glitch-blocks 5s infinite;
|
||||
}
|
||||
|
||||
.glitch-blocks::before,
|
||||
.glitch-blocks::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.glitch-blocks::before {
|
||||
top: 20%;
|
||||
animation: glitch-block-1 3s infinite;
|
||||
}
|
||||
|
||||
.glitch-blocks::after {
|
||||
top: 60%;
|
||||
animation: glitch-block-2 2.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch-block-1 {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
33% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
33.3% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
66% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glitch-block-2 {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
25.3% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center z-50 transition-opacity duration-300",
|
||||
isFadingOut ? "opacity-0" : "opacity-100",
|
||||
className
|
||||
)}
|
||||
style={{ backgroundColor: backgroundColor || "rgba(0, 0, 0, 0.92)" }}
|
||||
>
|
||||
<div className="noise-overlay"></div>
|
||||
<div className="glitch-blocks"></div>
|
||||
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<div className="glitch-container relative">
|
||||
{/* TERMIX Glitch Text */}
|
||||
<div
|
||||
className="glitch-text text-6xl font-bold tracking-wider select-none"
|
||||
style={{
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
textShadow: '0 0 30px rgba(255, 255, 255, 0.3), 0 0 60px rgba(0, 255, 255, 0.2)'
|
||||
}}
|
||||
>
|
||||
TERMIX
|
||||
</div>
|
||||
|
||||
{/* Scan line effect */}
|
||||
<div className="scan-line"></div>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-300 font-medium tracking-wide animate-pulse">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { FileItem } from "../../../types/index.js";
|
||||
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||
|
||||
interface CreateIntent {
|
||||
id: string;
|
||||
@@ -871,19 +872,8 @@ export function FileManagerGrid({
|
||||
onUndo,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-muted-foreground">{t("common.loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-dark-bg overflow-hidden">
|
||||
<div className="h-full flex flex-col bg-dark-bg overflow-hidden relative">
|
||||
<div className="flex-shrink-0 border-b border-dark-border">
|
||||
<div className="flex items-center gap-1 p-2 border-b border-dark-border">
|
||||
<button
|
||||
@@ -1330,6 +1320,13 @@ export function FileManagerGrid({
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
<LoadingOverlay
|
||||
visible={isLoading}
|
||||
minDuration={600}
|
||||
message={t("common.loading")}
|
||||
showLogo={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type StatsConfig,
|
||||
DEFAULT_STATS_CONFIG,
|
||||
} from "@/types/stats-widgets";
|
||||
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||
import {
|
||||
CpuWidget,
|
||||
MemoryWidget,
|
||||
@@ -443,17 +444,8 @@ export function Server({
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{metricsEnabled && showStatsUI && (
|
||||
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker p-4 max-h-[50vh] overflow-y-auto">
|
||||
{isLoadingMetrics && !metrics ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-gray-300">
|
||||
{t("serverStats.loadingMetrics")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : !metrics && serverStatus === "offline" ? (
|
||||
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker p-4 max-h-[50vh] overflow-y-auto relative">
|
||||
{!metrics && serverStatus === "offline" ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-red-500/20 flex items-center justify-center">
|
||||
@@ -476,6 +468,13 @@ export function Server({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LoadingOverlay
|
||||
visible={isLoadingMetrics && !metrics}
|
||||
minDuration={700}
|
||||
message={t("serverStats.loadingMetrics")}
|
||||
showLogo={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useCommandTracker } from "@/ui/hooks/useCommandTracker";
|
||||
import { useCommandHistory } from "@/ui/hooks/useCommandHistory";
|
||||
import { CommandHistoryDialog } from "./CommandHistoryDialog";
|
||||
import { CommandAutocomplete } from "./CommandAutocomplete";
|
||||
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||
|
||||
interface HostConfig {
|
||||
id?: number;
|
||||
@@ -1450,17 +1451,13 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
onSelect={handleAutocompleteSelect}
|
||||
/>
|
||||
|
||||
{isConnecting && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-gray-300">{t("terminal.connecting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<LoadingOverlay
|
||||
visible={isConnecting}
|
||||
minDuration={800}
|
||||
message={t("terminal.connecting")}
|
||||
backgroundColor={backgroundColor}
|
||||
showLogo={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user