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:
Evandro Fonseca Junior committed 2026-10-14 03:32:23 -03:00
1 parent 8d07b8c6bb
commit c9a0b80656
48 files changed
+7210 -1241

No files matched your search

+19 -20
View File
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import {
type ChangeEvent,
type CSSProperties,
@@ -13,6 +14,7 @@ import {
useState,
} from "react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { isMobileViewport, isSaveData } from "@/lib/device";
import {
chapterFraction,
chapterIndexAt,
@@ -56,12 +58,17 @@ export function ProjectLightbox({
const pendingFraction = useRef<number | null>(null);
const pressedOnBackdrop = useRef(false);
// celular abaixo de 768 px (lib/device.ts); a troca de aparelho continua à escolha do visitante
const [device, setDevice] = useState<Device>(() =>
window.matchMedia("(max-width: 639px)").matches ? "mobile" : "desktop",
isMobileViewport() ? "mobile" : "desktop",
);
// o vídeo de cada aparelho/projeto nasce tocando só se o visitante não pausou e não pediu menos movimento
// "economizar dados": o arquivo só chega quando o visitante pede (play), não antes
const [saveData] = useState(isSaveData);
const [autoplay, setAutoplay] = useState(
() => !window.matchMedia("(prefers-reduced-motion: reduce)").matches,
() =>
!window.matchMedia("(prefers-reduced-motion: reduce)").matches &&
!saveData,
);
const wantsToPlay = useRef(autoplay);
const [playing, setPlaying] = useState(false);
@@ -199,7 +206,6 @@ export function ProjectLightbox({
};
const video = (
// biome-ignore lint/a11y/useMediaCaption: demonstração sem áudio; a descrição está no título e nos capítulos
<video
key={`${config.id}-${device}`}
ref={videoRef}
@@ -208,7 +214,7 @@ export function ProjectLightbox({
muted
loop
playsInline
preload="auto"
preload={saveData ? "metadata" : "auto"}
poster={assets.poster}
width={assets.width}
height={assets.height}
@@ -249,9 +255,9 @@ export function ProjectLightbox({
{String(index + 1).padStart(2, "0")} /{" "}
{String(items.length).padStart(2, "0")}
</p>
<h3 id={titleId} className="ds-lb-title">
<h2 id={titleId} className="ds-lb-title">
{project.title}
</h3>
</h2>
</div>
<fieldset className="ds-lb-toggle">
@@ -371,25 +377,18 @@ export function ProjectLightbox({
</ol>
<div className="ds-lb-links">
<Link className="ds-link ds-link-muted" href={`/cases/${project.id}`}>
Ver case
<span className="sr-only"> de {project.title}</span>
</Link>
{canOpenSite && (
<a
className="ds-link ds-link-accent"
className="ds-link ds-link-text"
href={project.demoUrl ?? undefined}
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
Abrir projeto
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}
{project.githubUrl && (
<a
className="ds-link ds-link-muted"
href={project.githubUrl}
target="_blank"
rel="noreferrer"
>
Ver codigo
Abrir site
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}