perf(movimento): uma transição, 3D leve e vídeo certo por aparelho
Passagens com fade e 32 px, cena anterior termina em opacidade 0, sem visibility nem clip-path. 3D do hero como fundo ambiente, sob demanda e desligado em toque, aparelho fraco, saveData e movimento reduzido. Lenis fora do toque e salto direto em âncoras distantes. Vitrine só de clientes, até 3 capítulos, fixa só no desktop e carrossel no celular; vídeo só do projeto em cena com preload metadata. Rótulos mono a partir de 12 px e limão só no contato.
This commit is contained in:
1 parent
8d07b8c6bb
commit
c9a0b80656
48 files changed
+7210
-1241
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { type CSSProperties, useId, useRef } from "react";
|
||||
import { type Device, getShowcaseAssets } from "@/lib/project-showcase";
|
||||
import { BrowserFrame, displayUrl } from "./BrowserFrame";
|
||||
import { PhoneFrame } from "./PhoneFrame";
|
||||
import { ProjectActions } from "./ProjectActions";
|
||||
import { type ShowcaseItem, useShowcaseOpen } from "./ShowcaseProvider";
|
||||
import { useNearViewport } from "./useShowcaseMedia";
|
||||
|
||||
function ListItem({
|
||||
item,
|
||||
device,
|
||||
onExpand,
|
||||
}: {
|
||||
item: ShowcaseItem;
|
||||
/** `null` no servidor e até montar: o item mostra só o texto e os links. */
|
||||
device: Device | null;
|
||||
onExpand?: () => void;
|
||||
}) {
|
||||
const { config, project } = item;
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const near = useNearViewport(ref);
|
||||
const assets = device ? getShowcaseAssets(config.id, device) : null;
|
||||
// só pôster (nunca vídeo): a imagem entra quando o item chega a ~1 tela da janela
|
||||
const poster =
|
||||
assets && near ? (
|
||||
<Image
|
||||
src={assets.poster}
|
||||
alt={`Captura de tela de ${project.title}`}
|
||||
width={assets.width}
|
||||
height={assets.height}
|
||||
sizes={device === "mobile" ? "(max-width: 767px) 70vw" : "50vw"}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
className="ds-reel-item"
|
||||
data-device={device ?? undefined}
|
||||
style={{ "--accent": config.accent } as CSSProperties}
|
||||
>
|
||||
{device && (
|
||||
<div className="ds-reel-item-media">
|
||||
{device === "mobile" ? (
|
||||
<PhoneFrame className="ds-reel-item-phone">{poster}</PhoneFrame>
|
||||
) : (
|
||||
<BrowserFrame url={displayUrl(project)}>
|
||||
<div className="ds-reel-item-screen">{poster}</div>
|
||||
</BrowserFrame>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="ds-reel-caption">
|
||||
<p className="ds-kicker">
|
||||
{project.category} · {project.status}
|
||||
</p>
|
||||
<h3 className="ds-reel-title">{project.title}</h3>
|
||||
{project.subtitle && <p className="ds-reel-sub">{project.subtitle}</p>}
|
||||
<ProjectActions item={item} onExpand={onExpand} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projetos em lista simples: sem rolagem presa, sem vídeo que acompanha a rolagem. No celular (abaixo de 768 px)
|
||||
* é um carrossel por toque (rolagem horizontal com encaixe, o próximo projeto aparece pela borda); em telas
|
||||
* maiores, uma grade. Serve também a quem pede menos movimento e a janelas baixas demais para a vitrine fixa.
|
||||
*
|
||||
* É o que o servidor entrega (texto e links, sem mídia): sem JavaScript os projetos continuam todos ali, e
|
||||
* `pending` esconde a lista por CSS só onde a vitrine fixa vai assumir o lugar (largura e altura de desktop).
|
||||
* Pôster e "Ampliar vídeo" entram depois que o navegador monta; vídeo, só dentro da janela ampliada.
|
||||
*/
|
||||
export function ProjectList({
|
||||
items,
|
||||
device,
|
||||
pending = false,
|
||||
}: {
|
||||
items: ShowcaseItem[];
|
||||
device: Device | null;
|
||||
pending?: boolean;
|
||||
}) {
|
||||
const open = useShowcaseOpen();
|
||||
const headingId = useId();
|
||||
return (
|
||||
<section
|
||||
className="ds-reel-list"
|
||||
aria-labelledby={headingId}
|
||||
data-pending={pending ? "" : undefined}
|
||||
>
|
||||
<h2 id={headingId} className="sr-only">
|
||||
Projetos de clientes
|
||||
</h2>
|
||||
<ul className="ds-reel-items">
|
||||
{items.map((item) => (
|
||||
<ListItem
|
||||
key={item.config.id}
|
||||
item={item}
|
||||
device={device}
|
||||
onExpand={device ? () => open(item.config.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user