diff --git a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx index e76e6e83a2..eb2e0539fb 100644 --- a/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx +++ b/ui/app/(prowler)/_overview/_components/lighthouse-overview-banner.tsx @@ -1,8 +1,13 @@ +"use client"; + import { ArrowRight } from "lucide-react"; import Link from "next/link"; +import { useRef, useState } from "react"; import { LighthouseIcon } from "@/components/icons/Icons"; import { Card, CardContent } from "@/components/shadcn"; +import { useMountEffect } from "@/hooks/use-mount-effect"; +import { cn } from "@/lib/utils"; import type { LighthouseOverviewBannerHref } from "../_lib/lighthouse-banner"; @@ -13,6 +18,46 @@ interface LighthouseOverviewBannerProps { export function LighthouseOverviewBanner({ href, }: LighthouseOverviewBannerProps) { + const interactiveRef = useRef(null); + const curXRef = useRef(0); + const curYRef = useRef(0); + const tgXRef = useRef(0); + const tgYRef = useRef(0); + const [isSafari, setIsSafari] = useState(false); + + useMountEffect(() => { + setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)); + }); + + useMountEffect(() => { + let animationFrameId: number; + + const move = () => { + if (!interactiveRef.current) return; + + curXRef.current += (tgXRef.current - curXRef.current) / 20; + curYRef.current += (tgYRef.current - curYRef.current) / 20; + + interactiveRef.current.style.transform = `translate(${Math.round(curXRef.current)}px, ${Math.round(curYRef.current)}px)`; + + animationFrameId = requestAnimationFrame(move); + }; + + animationFrameId = requestAnimationFrame(move); + + return () => { + cancelAnimationFrame(animationFrameId); + }; + }); + + const handleMouseMove = (event: React.MouseEvent) => { + if (interactiveRef.current) { + const rect = interactiveRef.current.getBoundingClientRect(); + tgXRef.current = event.clientX - rect.left; + tgYRef.current = event.clientY - rect.top; + } + }; + return ( - + + + + + + + + + + +
+
+ +
+ +
+ +
+
+ +
diff --git a/ui/styles/globals.css b/ui/styles/globals.css index a1f9b65dc9..2d35e7b4a1 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -328,6 +328,43 @@ max-width: 100% !important; height: auto; } + + /* Lighthouse overview banner animated gradient layers */ + .lighthouse-banner-gradient-neutral { + background: radial-gradient( + circle at center, + var(--bg-neutral-tertiary) 0, + transparent 50% + ) + no-repeat; + } + + .lighthouse-banner-gradient-primary { + background: radial-gradient( + circle at center, + var(--bg-button-primary) 0, + transparent 50% + ) + no-repeat; + } + + .lighthouse-banner-gradient-primary-hover { + background: radial-gradient( + circle at center, + var(--bg-button-primary-hover) 0, + transparent 50% + ) + no-repeat; + } + + .lighthouse-banner-gradient-primary-press { + background: radial-gradient( + circle at center, + var(--bg-button-primary-press) 0, + transparent 50% + ) + no-repeat; + } } /* ===== UTILITY LAYER ===== */ diff --git a/ui/tailwind.config.js b/ui/tailwind.config.js index d6ce085a8b..2be6f0132b 100644 --- a/ui/tailwind.config.js +++ b/ui/tailwind.config.js @@ -140,11 +140,26 @@ module.exports = { "50%": { opacity: "1" }, "100%": { transform: "translateY(0)", opacity: "1" }, }, + first: { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + second: { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + third: { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, }, animation: { "collapsible-down": "collapsible-down 0.2s ease-out", "collapsible-up": "collapsible-up 0.2s ease-out", "drop-arrow": "dropArrow 0.6s ease-out infinite", + first: "first 20s linear infinite", + second: "second 30s linear infinite", + third: "third 25s linear infinite", }, screens: { "3xl": "1920px", // Add breakpoint to optimize layouts for large screens.