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.
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
"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"]) {
|
|
return Boolean(project.demoUrl) && project.demoAvailability !== "unavailable";
|
|
}
|
|
|
|
/**
|
|
* 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"
|
|
{...(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>
|
|
{onExpand && (
|
|
<button
|
|
type="button"
|
|
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" />
|
|
</svg>
|
|
{/* no celular o botão é só o ícone (CSS): o rótulo continua para o leitor de tela */}
|
|
<span className="ds-reel-expand-label">Ampliar vídeo</span>
|
|
<span className="sr-only"> de {project.title}</span>
|
|
</button>
|
|
)}
|
|
{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">
|
|
{" "}
|
|
de {project.title} (abre em nova aba)
|
|
</span>
|
|
</a>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|