feat: Add professional glitch-effect loading animation with minimum duration control
Implement unified LoadingOverlay component with advanced glitch effects: Animation features: - Glitch text effect with RGB chromatic aberration - Dynamic clip-path based text slicing animations - Random flicker and screen tearing effects - Horizontal glitch block artifacts - CRT-style scan line with color gradient - Fractal noise overlay for authenticity Technical improvements: - Minimum display duration (600-800ms) to prevent flickering - Smooth fade-in/fade-out transitions - Consistent TERMIX branding across all loading states - Multiple animation layers with different timing - Mix-blend-mode and advanced CSS filters Applied to: - SSH terminal connection loading - File manager directory loading - Server metrics loading Brand enhancement: - Uses TERMIX monospace typography - Cyberpunk-style visual effects - Professional loading experience - Stronger brand recognition 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
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";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { FileItem } from "../../../types/index.js";
|
import type { FileItem } from "../../../types/index.js";
|
||||||
|
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||||
|
|
||||||
interface CreateIntent {
|
interface CreateIntent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -871,19 +872,8 @@ export function FileManagerGrid({
|
|||||||
onUndo,
|
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 (
|
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-shrink-0 border-b border-dark-border">
|
||||||
<div className="flex items-center gap-1 p-2 border-b border-dark-border">
|
<div className="flex items-center gap-1 p-2 border-b border-dark-border">
|
||||||
<button
|
<button
|
||||||
@@ -1330,6 +1320,13 @@ export function FileManagerGrid({
|
|||||||
</div>,
|
</div>,
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={isLoading}
|
||||||
|
minDuration={600}
|
||||||
|
message={t("common.loading")}
|
||||||
|
showLogo={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
type StatsConfig,
|
type StatsConfig,
|
||||||
DEFAULT_STATS_CONFIG,
|
DEFAULT_STATS_CONFIG,
|
||||||
} from "@/types/stats-widgets";
|
} from "@/types/stats-widgets";
|
||||||
|
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||||
import {
|
import {
|
||||||
CpuWidget,
|
CpuWidget,
|
||||||
MemoryWidget,
|
MemoryWidget,
|
||||||
@@ -443,17 +444,8 @@ export function Server({
|
|||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{metricsEnabled && showStatsUI && (
|
{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">
|
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker p-4 max-h-[50vh] overflow-y-auto relative">
|
||||||
{isLoadingMetrics && !metrics ? (
|
{!metrics && serverStatus === "offline" ? (
|
||||||
<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="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="text-center">
|
<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">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={isLoadingMetrics && !metrics}
|
||||||
|
minDuration={700}
|
||||||
|
message={t("serverStats.loadingMetrics")}
|
||||||
|
showLogo={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { useCommandTracker } from "@/ui/hooks/useCommandTracker";
|
|||||||
import { useCommandHistory } from "@/ui/hooks/useCommandHistory";
|
import { useCommandHistory } from "@/ui/hooks/useCommandHistory";
|
||||||
import { CommandHistoryDialog } from "./CommandHistoryDialog";
|
import { CommandHistoryDialog } from "./CommandHistoryDialog";
|
||||||
import { CommandAutocomplete } from "./CommandAutocomplete";
|
import { CommandAutocomplete } from "./CommandAutocomplete";
|
||||||
|
import { LoadingOverlay } from "@/ui/components/LoadingOverlay";
|
||||||
|
|
||||||
interface HostConfig {
|
interface HostConfig {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -1450,17 +1451,13 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
onSelect={handleAutocompleteSelect}
|
onSelect={handleAutocompleteSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isConnecting && (
|
<LoadingOverlay
|
||||||
<div
|
visible={isConnecting}
|
||||||
className="absolute inset-0 flex items-center justify-center"
|
minDuration={800}
|
||||||
style={{ backgroundColor }}
|
message={t("terminal.connecting")}
|
||||||
>
|
backgroundColor={backgroundColor}
|
||||||
<div className="flex items-center gap-3">
|
showLogo={true}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user