fix(navegacao): foco em âncora de outra página, vitrine acessível e 3D mais leve

Foco cai no destino ao chegar de outra página; âncoras ignoram ?tipo=; movimento reduzido salta em todos os cliques; cronômetro da janela ampliada; degradê no topo de Projetos e legendas do mergulho com intervalos testados; vitrine acima da barra de cookies e com 2 paradas por projeto; reel-list.css no global; 3D montado em tarefas curtas, limitado a ~30 qps e estático com WebGL por software; pôsteres da vitrine de 330 para 158 KB; SC Plus regravado; scripts sem funções acima de complexidade 10.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 19:14:48 -03:00
1 parent 9e2d65a926
commit 204ebd5fa5
49 files changed
+2197 -606

No files matched your search

+38 -3
View File
@@ -1,5 +1,8 @@
"use client";
import Link from "next/link";
import type { ShowcaseItem } from "./ShowcaseProvider";
import { useRovingTabindex } from "./useRovingTabindex";
/** O site publicado abre em nova aba quando existe e não foi dado como fora do ar. */
export function canOpenSite(project: ShowcaseItem["project"]) {
@@ -10,19 +13,49 @@ export function canOpenSite(project: ShowcaseItem["project"]) {
* Ações de um projeto na vitrine: "Ver case" (contorno) leva ao estudo de caso; "Ampliar vídeo" (contorno,
* com ícone) abre a janela ampliada; "Abrir site" (link de texto, nova aba) só existe com `demoUrl`.
* Nenhuma é verde-limão: o preenchido de destaque é só da ação de contato.
*
* Com `grouped`, as ações são UMA parada no Tab (barra de ferramentas com roving tabindex: as setas e Home/End movem o
* foco entre elas): na vitrine fixa só o projeto em cena existe, e sem isto eram três paradas a mais por projeto.
*/
export function ProjectActions({
item,
onExpand,
grouped = false,
}: {
item: ShowcaseItem;
/** Sem a janela ampliada (antes de o navegador montar a página) o botão nem aparece. */
onExpand?: () => void;
/** As ações viram uma parada só no Tab (a vitrine fixa); na lista, cada uma é a sua. */
grouped?: boolean;
}) {
const { project } = item;
const site = canOpenSite(project);
// só as ações que existem podem ser a parada do grupo (um projeto sem site não tem "Abrir site")
const present = [
"case",
...(onExpand ? ["expand"] : []),
...(site ? ["site"] : []),
];
const { groupProps, itemProps } = useRovingTabindex("case", (key) =>
present.includes(key),
);
const roving = (key: string) => (grouped ? itemProps(key) : {});
return (
<div className="ds-reel-actions">
<Link className="ds-link ds-link-muted" href={`/cases/${project.id}`}>
<div
className="ds-reel-actions"
{...(grouped
? {
role: "toolbar",
"aria-label": `Ações de ${project.title}`,
...groupProps,
}
: {})}
>
<Link
className="ds-link ds-link-muted"
href={`/cases/${project.id}`}
{...roving("case")}
>
Ver case
<span className="sr-only"> de {project.title}</span>
</Link>
@@ -32,6 +65,7 @@ export function ProjectActions({
className="ds-link ds-link-muted ds-reel-expand"
onClick={onExpand}
aria-haspopup="dialog"
{...roving("expand")}
>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M9.5 2.5h4v4M6.5 13.5h-4v-4M13.5 2.5 9 7M2.5 13.5 7 9" />
@@ -41,12 +75,13 @@ export function ProjectActions({
<span className="sr-only"> de {project.title}</span>
</button>
)}
{canOpenSite(project) && (
{site && (
<a
className="ds-link ds-link-text"
href={project.demoUrl ?? undefined}
target="_blank"
rel="noopener noreferrer"
{...roving("site")}
>
Abrir site
<span className="sr-only">