Files
portifolio-evandrodevbr/app/components/ClientStrip.tsx
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento
- cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py):
  marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao
  cursor e, parada no topo, passa sozinha pelos estados com legenda
- três níveis de aparelho (full, lite, static); sem WebGL fica o pôster
- título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha."
- faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções
- rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa
  compartilhada entre a lista de cases e o case

Ofertas
- atendimento no WhatsApp com IA e nota fiscal para a reforma tributária
  (páginas de serviço, cartões da home, formulário e aviso com data de validade)
- página /ia atualizada, com fontes
- peças 3D em loop nos cartões e topos de página (3d/pecas.py)

Design
- hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta
- um destino por rótulo: "Falar sobre meu projeto" leva ao formulário,
  "Chamar no WhatsApp" abre o WhatsApp
- logo montado em blocos, 404 redesenhado, grades sem célula vazia
- ícones do PWA e manifesto novos

Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
2026-11-10 03:48:25 -03:00

101 lines
3.8 KiB
TypeScript

import HoverPrefetchLink from "./HoverPrefetchLink";
export type ClientLink = {
id: string;
name: string;
/** Ramo do cliente ("loja virtual"), sob o nome de 640 px em diante (lib/proof.ts). */
sector?: string;
};
/**
* Faixa "Já entregue para" do hero: só projetos de cliente (lib/proof.ts), cada um levando ao próprio case, e na
* mesma linha os anos de experiência (calculados em app/page.tsx). Sem clientes e sem anos, nada é desenhado.
* De 640 px em diante cada cliente é um par empilhado: o nome (link) sobre o ramo, em 13 px e apagado; abaixo disso,
* só os nomes. Os links têm 44 px de altura de toque no celular e 24 px a partir de 640 px, onde o ramo divide a
* altura do item; a rota só é pré-carregada quando a pessoa chega no link. Os pontos, o rótulo e os anos se alinham
* pela linha dos nomes (`sm:items-start`).
*
* O ponto separador vem ANTES de cada item, menos do primeiro, e só aparece ENTRE itens da mesma linha. Cada item
* (menos o primeiro) tem 16 px de margem negativa (`-ml-4`), que cobrem os 16 px de respiro (`pr-4`) do item
* anterior, e é nesses 16 px que o ponto (`w-4`) fica. Quando a lista quebra e um item abre a linha seguinte, o
* ponto dele cai na margem à esquerda da lista e o `overflow-x: clip` o esconde: nenhuma linha termina nem começa
* com "·". O recuo de 4 px da lista (`-ml-1 pl-1`) só dá espaço ao contorno de foco do primeiro link (2 px de
* contorno e 2 px de afastamento); o ponto, centrado na margem, fica 6 px além dele.
*/
export default function ClientStrip({
clients = [],
years,
className = "",
}: {
clients?: ClientLink[];
/** Anos de experiência, na mesma linha dos clientes. */
years?: number;
className?: string;
}) {
if (clients.length === 0 && !years) return null;
const items = [
...clients.map((client) => ({
key: client.id,
node: (
<span className="flex flex-col">
<HoverPrefetchLink
href={`/cases/${client.id}`}
className="inline-flex min-h-11 items-center self-start font-medium text-[#f4f2ed] no-underline underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--slash)] sm:min-h-6"
>
{client.name}
</HoverPrefetchLink>
{client.sector && (
<span className="text-[13px] leading-[18px] max-sm:hidden">
{client.sector}
</span>
)}
</span>
),
})),
...(years
? [
{
key: "years",
node: (
<p>
<span className="font-semibold text-[var(--slash)]">
{years}
</span>{" "}
anos de experiência
</p>
),
},
]
: []),
];
return (
<div
className={`flex flex-wrap items-center gap-x-sp-3 gap-y-sp-1 font-display text-[15px] leading-6 text-[rgba(244,242,237,.64)] sm:items-start ${className}`.trim()}
>
{clients.length > 0 && <p>Já entregue para:</p>}
{/* de 640 px em diante a lista fica ao lado do rótulo enquanto sobram 24rem, e quebra por dentro; menos que isso,
ela desce para baixo do rótulo e usa a largura toda */}
<ul className="-ml-1 flex flex-wrap items-center overflow-x-clip pl-1 sm:min-w-[min(24rem,100%)] sm:flex-1 sm:basis-0 sm:items-start sm:gap-y-sp-2">
{items.map((item, index) => (
<li
key={item.key}
className={`flex items-center pr-4 sm:items-start${index > 0 ? " -ml-4" : ""}`}
>
{index > 0 && (
<span
aria-hidden="true"
className="w-4 shrink-0 text-center text-[rgba(244,242,237,.4)]"
>
·
</span>
)}
{item.node}
</li>
))}
</ul>
</div>
);
}