Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
50 lines
1.3 KiB
TypeScript
50 lines
1.3 KiB
TypeScript
"use client";
|
|
|
|
import Image, { type StaticImageData } from "next/image";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface LazyRevealImageProps {
|
|
src: string | StaticImageData;
|
|
alt: string;
|
|
className?: string;
|
|
imageClassName?: string;
|
|
sizes?: string;
|
|
}
|
|
|
|
/** Imagem que entra com um fade curto quando termina de carregar (CSS em globals.css, .ds-lazy-reveal). */
|
|
export function LazyRevealImage({
|
|
src,
|
|
alt,
|
|
className = "",
|
|
imageClassName = "object-cover",
|
|
sizes = "(max-width: 1024px) 100vw, 50vw",
|
|
}: Readonly<LazyRevealImageProps>) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const [loaded, setLoaded] = useState(false);
|
|
|
|
// uma imagem que já estava no cache termina antes de a página hidratar: o onLoad não vem mais
|
|
useEffect(() => {
|
|
const image = ref.current?.querySelector("img");
|
|
if (image?.complete && image.naturalWidth > 0) setLoaded(true);
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={`ds-lazy-reveal relative ${className}`.trim()}
|
|
data-loaded={loaded ? "" : undefined}
|
|
>
|
|
<Image
|
|
src={src}
|
|
alt={alt}
|
|
fill
|
|
loading="lazy"
|
|
sizes={sizes}
|
|
placeholder={typeof src === "string" ? "empty" : "blur"}
|
|
onLoad={() => setLoaded(true)}
|
|
className={imageClassName}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|