UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: отключение межстраничного лоудера

This commit is contained in:
DCCONSTRUCTIONS 2026-04-26 22:09:49 +03:00
parent 7ac9a3dbd3
commit 347d95709c
4 changed files with 13 additions and 248 deletions

View File

@ -4,12 +4,19 @@
* See the LICENSE file for details.
*/
import { NodedcProcessingLoader } from "./nodedc-processing-loader";
import { useTheme } from "next-themes";
// assets
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
export function LogoSpinner() {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight;
return (
<div className="flex items-center justify-center">
<NodedcProcessingLoader tone="white" variant="fluid" />
<img src={logoSrc} alt="logo" className="h-6 w-auto object-contain sm:h-11" />
</div>
);
}

View File

@ -4,23 +4,13 @@ type TNodedcProcessingLoaderProps = {
className?: string;
label?: string;
tone?: "accent" | "white";
variant?: "orbit" | "fluid";
};
export function NodedcProcessingLoader({
className,
label,
tone = "accent",
variant = "orbit",
}: TNodedcProcessingLoaderProps) {
export function NodedcProcessingLoader({ className, label, tone = "accent" }: TNodedcProcessingLoaderProps) {
return (
<div className={cn("flex flex-col items-center justify-center gap-5 text-center", className)}>
<span
className={cn(
"nodedc-processing-loader",
variant === "fluid" && "nodedc-processing-loader-fluid",
tone === "white" && "nodedc-processing-loader-white"
)}
className={cn("nodedc-processing-loader", tone === "white" && "nodedc-processing-loader-white")}
aria-hidden="true"
/>
{label && <div className="text-18 font-semibold text-primary">{label}</div>}

View File

@ -4,150 +4,6 @@
* See the LICENSE file for details.
*/
import { useEffect, useRef, useState } from "react";
import { BProgress } from "@bprogress/core";
import { useNavigation } from "react-router";
import { NodedcProcessingLoader } from "@/components/common/nodedc-processing-loader";
import "@bprogress/core/css";
/**
* Progress bar configuration options
*/
interface ProgressConfig {
/** Whether to show the loading spinner */
showSpinner: boolean;
/** Minimum progress percentage (0-1) */
minimum: number;
/** Animation speed in milliseconds */
speed: number;
/** Auto-increment speed in milliseconds */
trickleSpeed: number;
/** CSS easing function */
easing: string;
/** Enable auto-increment */
trickle: boolean;
/** Delay before showing progress bar in milliseconds */
delay: number;
/** Whether to disable the progress bar */
isDisabled?: boolean;
}
/**
* Configuration for the progress bar
*/
const PROGRESS_CONFIG: Readonly<ProgressConfig> = {
showSpinner: false,
minimum: 0.1,
speed: 400,
trickleSpeed: 800,
easing: "ease",
trickle: true,
delay: 0,
} as const;
/**
* Navigation Progress Bar Component
*
* Automatically displays a progress bar at the top of the page during React Router navigation.
* Integrates with React Router's useNavigation hook to monitor route changes.
*
* Note: Progress bar is disabled in production builds.
*
* @returns null - This component doesn't render any visible elements
*
* @example
* ```tsx
* function App() {
* return (
* <>
* <AppProgressBar />
* <Outlet />
* </>
* );
* }
* ```
*/
export default function AppProgressBar() {
const navigation = useNavigation();
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const startedRef = useRef<boolean>(false);
const [isLoaderVisible, setIsLoaderVisible] = useState(false);
// Initialize BProgress once on mount
useEffect(() => {
// Skip initialization in production builds
if (PROGRESS_CONFIG.isDisabled) {
return;
}
// Configure BProgress with our settings
BProgress.configure({
showSpinner: PROGRESS_CONFIG.showSpinner,
minimum: PROGRESS_CONFIG.minimum,
speed: PROGRESS_CONFIG.speed,
trickleSpeed: PROGRESS_CONFIG.trickleSpeed,
easing: PROGRESS_CONFIG.easing,
trickle: PROGRESS_CONFIG.trickle,
});
// Render the progress bar element in the DOM
BProgress.render(true);
// Cleanup on unmount
return () => {
if (BProgress.isStarted()) {
BProgress.done();
}
};
}, []);
// Handle navigation state changes
useEffect(() => {
// Skip navigation tracking in production builds
if (PROGRESS_CONFIG.isDisabled) {
return;
}
if (navigation.state === "idle") {
// Navigation complete - clear any pending timer
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
// Complete progress if it was started
if (startedRef.current) {
BProgress.done();
startedRef.current = false;
}
setIsLoaderVisible(false);
} else {
// Navigation in progress (loading or submitting)
// Only start if not already started and no timer pending
if (timerRef.current === null && !startedRef.current) {
timerRef.current = setTimeout((): void => {
if (!BProgress.isStarted()) {
BProgress.start();
startedRef.current = true;
}
setIsLoaderVisible(true);
timerRef.current = null;
}, PROGRESS_CONFIG.delay);
}
}
return () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, [navigation.state]);
if (!isLoaderVisible) return null;
return (
<div className="pointer-events-none fixed inset-0 z-[9999] grid place-items-center bg-black/10 backdrop-blur-[1.5px]">
<NodedcProcessingLoader tone="white" variant="fluid" />
</div>
);
export default function AppProgressBar(): null {
return null;
}

View File

@ -4006,92 +4006,4 @@
}
}
.nodedc-processing-loader-fluid {
width: 5rem;
aspect-ratio: 1;
padding: 0.625rem;
box-sizing: border-box;
display: grid;
background: transparent;
filter: blur(5px) contrast(15);
mix-blend-mode: screen;
}
.nodedc-processing-loader-fluid::before,
.nodedc-processing-loader-fluid::after {
content: "";
position: static;
grid-area: 1 / 1;
display: block;
width: auto;
height: auto;
margin: 0.3125rem;
border: 0;
border-radius: 9999px;
background: currentColor;
box-shadow: none;
offset-path: none;
-webkit-mask-size: 100% 20px, 100% 100%;
mask-size: 100% 20px, 100% 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-composite: destination-out;
mask-composite: exclude;
}
.nodedc-processing-loader-fluid::before {
-webkit-mask-image:
linear-gradient(#000 0 0),
linear-gradient(#000 0 0);
mask-image:
linear-gradient(#000 0 0),
linear-gradient(#000 0 0);
animation: nodedc-processing-fluid-shape 2s infinite;
}
.nodedc-processing-loader-fluid::after {
-webkit-mask-image: linear-gradient(#000 0 0);
mask-image: linear-gradient(#000 0 0);
animation:
nodedc-processing-fluid-shape 2s infinite,
nodedc-processing-fluid-jitter 0.5s infinite cubic-bezier(0.5, 200, 0.5, -200);
}
@keyframes nodedc-processing-fluid-shape {
0% {
-webkit-mask-position: 0 20%, 0 0;
mask-position: 0 20%, 0 0;
}
20% {
-webkit-mask-position: 0 80%, 0 0;
mask-position: 0 80%, 0 0;
}
40% {
-webkit-mask-position: 0 100%, 0 0;
mask-position: 0 100%, 0 0;
}
60% {
-webkit-mask-position: 0 0%, 0 0;
mask-position: 0 0%, 0 0;
}
80% {
-webkit-mask-position: 0 35%, 0 0;
mask-position: 0 35%, 0 0;
}
100% {
-webkit-mask-position: 0 0, 0 0;
mask-position: 0 0, 0 0;
}
}
@keyframes nodedc-processing-fluid-jitter {
100% {
transform: translate(0.1px);
}
}
}