add glowing effect component for tab tiles improve tab tile appearance with new glow effect add 'none' log level and simplify log level handling simplify tab configuration store by removing developer tabs remove useDebugStatus hook and related debug functionality remove system info endpoints no longer needed
193 lines
6.5 KiB
TypeScript
193 lines
6.5 KiB
TypeScript
import { memo, useCallback, useEffect, useRef } from 'react';
|
|
import { cn } from '~/utils/cn';
|
|
import { animate } from 'framer-motion';
|
|
|
|
interface GlowingEffectProps {
|
|
blur?: number;
|
|
inactiveZone?: number;
|
|
proximity?: number;
|
|
spread?: number;
|
|
variant?: 'default' | 'white';
|
|
glow?: boolean;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
movementDuration?: number;
|
|
borderWidth?: number;
|
|
}
|
|
|
|
const GlowingEffect = memo(
|
|
({
|
|
blur = 0,
|
|
inactiveZone = 0.7,
|
|
proximity = 0,
|
|
spread = 20,
|
|
variant = 'default',
|
|
glow = false,
|
|
className,
|
|
movementDuration = 2,
|
|
borderWidth = 1,
|
|
disabled = true,
|
|
}: GlowingEffectProps) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const lastPosition = useRef({ x: 0, y: 0 });
|
|
const animationFrameRef = useRef<number>(0);
|
|
|
|
const handleMove = useCallback(
|
|
(e?: MouseEvent | { x: number; y: number }) => {
|
|
if (!containerRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
}
|
|
|
|
animationFrameRef.current = requestAnimationFrame(() => {
|
|
const element = containerRef.current;
|
|
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
const { left, top, width, height } = element.getBoundingClientRect();
|
|
const mouseX = e?.x ?? lastPosition.current.x;
|
|
const mouseY = e?.y ?? lastPosition.current.y;
|
|
|
|
if (e) {
|
|
lastPosition.current = { x: mouseX, y: mouseY };
|
|
}
|
|
|
|
const center = [left + width * 0.5, top + height * 0.5];
|
|
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);
|
|
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
|
|
|
|
if (distanceFromCenter < inactiveRadius) {
|
|
element.style.setProperty('--active', '0');
|
|
return;
|
|
}
|
|
|
|
const isActive =
|
|
mouseX > left - proximity &&
|
|
mouseX < left + width + proximity &&
|
|
mouseY > top - proximity &&
|
|
mouseY < top + height + proximity;
|
|
|
|
element.style.setProperty('--active', isActive ? '1' : '0');
|
|
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
const currentAngle = parseFloat(element.style.getPropertyValue('--start')) || 0;
|
|
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;
|
|
|
|
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
|
|
const newAngle = currentAngle + angleDiff;
|
|
|
|
animate(currentAngle, newAngle, {
|
|
duration: movementDuration,
|
|
ease: [0.16, 1, 0.3, 1],
|
|
onUpdate: (value) => {
|
|
element.style.setProperty('--start', String(value));
|
|
},
|
|
});
|
|
});
|
|
},
|
|
[inactiveZone, proximity, movementDuration],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (disabled) {
|
|
return undefined;
|
|
}
|
|
|
|
const handleScroll = () => handleMove();
|
|
const handlePointerMove = (e: PointerEvent) => handleMove(e);
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
document.body.addEventListener('pointermove', handlePointerMove, {
|
|
passive: true,
|
|
});
|
|
|
|
return () => {
|
|
if (animationFrameRef.current) {
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
}
|
|
|
|
window.removeEventListener('scroll', handleScroll);
|
|
document.body.removeEventListener('pointermove', handlePointerMove);
|
|
};
|
|
}, [handleMove, disabled]);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={cn(
|
|
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
|
|
glow && 'opacity-100',
|
|
variant === 'white' && 'border-white',
|
|
disabled && '!block',
|
|
)}
|
|
/>
|
|
<div
|
|
ref={containerRef}
|
|
style={
|
|
{
|
|
'--blur': `${blur}px`,
|
|
'--spread': spread,
|
|
'--start': '0',
|
|
'--active': '0',
|
|
'--glowingeffect-border-width': `${borderWidth}px`,
|
|
'--repeating-conic-gradient-times': '5',
|
|
'--gradient':
|
|
variant === 'white'
|
|
? `repeating-conic-gradient(
|
|
from 236.84deg at 50% 50%,
|
|
var(--black),
|
|
var(--black) calc(25% / var(--repeating-conic-gradient-times))
|
|
)`
|
|
: `radial-gradient(circle, #9333ea 10%, #9333ea00 20%),
|
|
radial-gradient(circle at 40% 40%, #a855f7 5%, #a855f700 15%),
|
|
radial-gradient(circle at 60% 60%, #8b5cf6 10%, #8b5cf600 20%),
|
|
radial-gradient(circle at 40% 60%, #f63bdd 10%, #3b82f600 20%),
|
|
repeating-conic-gradient(
|
|
from 236.84deg at 50% 50%,
|
|
#9333ea 0%,
|
|
#a855f7 calc(25% / var(--repeating-conic-gradient-times)),
|
|
#8b5cf6 calc(50% / var(--repeating-conic-gradient-times)),
|
|
#f63bdd calc(75% / var(--repeating-conic-gradient-times)),
|
|
#9333ea calc(100% / var(--repeating-conic-gradient-times))
|
|
)`,
|
|
} as React.CSSProperties
|
|
}
|
|
className={cn(
|
|
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
|
|
glow && 'opacity-100',
|
|
blur > 0 && 'blur-[var(--blur)] ',
|
|
className,
|
|
disabled && '!hidden',
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'glow',
|
|
'rounded-[inherit]',
|
|
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
|
|
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
|
|
'after:[background:var(--gradient)] after:[background-attachment:fixed]',
|
|
'after:opacity-[var(--active)] after:transition-opacity after:duration-300',
|
|
'after:[mask-clip:padding-box,border-box]',
|
|
'after:[mask-composite:intersect]',
|
|
'after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
|
|
)}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
GlowingEffect.displayName = 'GlowingEffect';
|
|
|
|
export { GlowingEffect };
|