Sistema de design de site web
Gerencia a camada visual e de desempenho: gráficos mobile, modo escuro e os tokens de tema. Produz/mantém o arquivo BRAND.md (modelo: o skill new-website's templates/brand.md) e o bloco de tokens no templates/astro/src/styles/global.css desse skill. As regras de imagem estão abaixo (WebP/AVIF via astro:assets do Astro); o teste images.spec.ts as impõe. Para gerar imagens, use o skill image.
Modo escuro (primeira classe)
- Tokens, não cores fixas. Toda cor passa por propriedades CSS customizadas; o modo claro é o padrão
:root, as sobreposições do escuro ficam em:root[data-theme="dark"], mais um bloco@media (prefers-color-scheme: dark)para visitantes sem JavaScript. O starter já vem com essa estrutura exata. - Script No-FOUC no
<head>definedata-themea partir delocalStorage.themeou da preferência do sistema antes da primeira pintura (já configurado emBase.astro). O toggle escrevelocalStorage.theme(o starter não envia nenhum; se você adicionar um, mencione isso na seção 3 da página de privacidade). - Contraste em ambos os temas: cada par texto/fundo passa WCAG AA (4.5:1 corpo, 3:1 grande/UI). Uma cor que funciona no claro muitas vezes falha no escuro — o teste de a11y executa a matriz completa claro+escuro, portanto verifique ambos.
Gráficos mobile e responsivos
- Mobile first: projete a partir de 360px para cima; alvos de toque ≥ 44px; sem scroll horizontal. Verifique em 360 / 768 / 1280 (preview_resize).
- Formatos modernos: WebP (ou AVIF) em vez de JPEG/PNG para fotos.
- Tamanho conforme a exibição, não a origem. Nunca envie uma imagem de 2000px em um slot de 80px; gere variantes de largura e use
srcset/sizes(ou<picture>). - Sem layout shift: defina atributos explícitos de
width/heightpara que a caixa seja reservada antes do carregamento da imagem (protege CLS / LCP). O<Image>do Astro define eles automaticamente;images.spec.tsexige os atributos (umaspect-ratioapenas via CSS não satisfaz o teste). - Lazy vs eager:
loading="lazy"abaixo da dobra; a imagem LCP/hero carrega com eager comfetchpriority="high"(NÃO coloque lazy nela). - Texto alternativo em cada imagem (
alt=""apenas se for puramente decorativa). - Fontes: auto-hospede woff2 com
font-display: swap; pré-carregue apenas as 1–2 usadas acima da dobra. Evite layout shift devido a trocas tardias de fonte.
Responsividade de texto (cópia traduzida)
- Texto traduzido ocupa mais espaço. O alemão (e muitas outras línguas) comumente ocupa 20–35% a mais que o equivalente em inglês. Um botão, link de navegação ou cartão dimensionado para caber um placeholder em inglês vai silenciosamente transbordar, quebrar feio ou truncar no momento em que a cópia traduzida real substituir.
- Prefira larguras flexíveis. Use
min-width/ flex / gridauto/frpara qualquer elemento que contenha texto traduzido, não umawidthfixa dimensionada para a string em inglês. - Transbordamento ≠ largura: dimensionamento flexível não impede que um ÚNICO substantivo composto longo (
Datenschutzerklärung) estoure uma coluna estreita — isso exigehyphens: auto(correto por idioma via atributolangque Base.astro já define) maisoverflow-wrap: break-wordcomo fallback; oglobal.cssdo starter já envia ambos nobody. Células de tabela desativam a hifenização (veja Tabelas). - Não confie apenas em
white-space: nowrap+text-overflow: ellipsiscomo tratamento para links de navegação ou labels de botões — truncar um link de navegação geralmente não é uma UX aceitável (o visitante não consegue mais saber para onde vai). Tudo bem para conteúdo onde truncamento é uma troca aceita (por exemplo, uma célula de tabela), não para navegação. - Verifique com labels traduzidas reais, não placeholders em inglês. Quando um site envia
astro-i18n-setup'sLanguageSwitcher.astroou qualquer navegação bilíngue/multilíngue, verifique o layout em 360/768/1280 (preview_resize) usando a cópia real enviada em cada idioma — não apenas o placeholder em inglês.
Tabelas
Um estilo para cada tabela; uma página não carrega CSS de layout para uma tabela. Ele vive em src/styles/tables.css do starter (importado após global.css por Base.astro) e tests/tables.spec.ts o verifica. Um site feito antes desta seção copia tables.css, adiciona a linha de importação ao seu layout e primeiro remove o CSS de tabela próprio de cada página.
Dois tipos, diferenciados pelo que as células contêm:
stack(frases): uma tabela acima de 40rem; abaixo disso cada linha empilha, o cabeçalho da linha como título e cada célula sob o nome da coluna. Quatro colunas de prosa não podem dividir 320px.figures(números curtos,class="num"em uma célula numérica): permanece como tabela no celular e deve caber em 320px. Se não couber mais, torne-astack; uma barra de rolagem não é a solução.stack figuresjuntos é a aparência de figuras com o layout empilhado.
Marcação, toda tabela:
<div class="table-wrap stack" role="region" aria-label="O que a tabela mostra" tabindex="0">
<table role="table">
<thead role="rowgroup"><tr role="row">
<th scope="col" role="columnheader">{cols[0]}</th><th scope="col" role="columnheader">{cols[1]}</th>
</tr></thead>
<tbody role="rowgroup">{rows.map((r) => (
<tr role="row"><th scope="row" role="rowheader">{r.item}</th><td role="cell" data-label={cols[1]}>{r.text}</td></tr>
))}</tbody>
</table>
</div>
- Escreva os nomes das colunas uma vez, em uma lista, e use-a para o cabeçalho e para cada
data-label: o label de uma célula empilhada deve ser igual ao cabeçalho da coluna, e o teste compara os dois. Uma tabelafiguresnão precisa de funções por célula nem dedata-label. - Uma tabela empilhada não é mais
display: table. As funções são um fallback para um navegador que descarta a tabela quando o layout muda; que funcionem lá não é verificado. No Chromium, medido uma vez em uma tabela real, a árvore de acessibilidade manteve a tabela sem elas. class="tap"em um link que está sozinho em uma célula o torna um alvo de toque de 44px no celular.nowrapmantém um valor curto (1, 3, 6) em uma linha.long-head(comstack) alarga a primeira coluna quando os cabeçalhos das linhas são frases.- Palavras não são hifenizadas em células. O
hyphens: autodo corpo comprime uma primeira coluna estreita em fragmentos ("An-thropic"), então o bloco definehyphens: manualali. - Uma página cujas tabelas vêm de fora e não podem carregar a marcação vai para
TABLE_EXEMPTno spec, com a razão.PAGESpermanece intacto: o sitemap e as verificações dellms.txtprecisam de cada rota ativa nele.
O que tables.spec.ts verifica: cada tabela está em um .table-wrap de um tipo com sua região, label e tabindex, scope="col" em cada célula de cabeçalho e um <th scope="row"> como a primeira célula de cada linha, com <td> para o restante; uma tabela empilhada tem suas funções e um label igual ao cabeçalho em cada célula; em 360 e 320px nenhuma página e nenhuma tabela rola lateralmente; em 641px e acima uma tabela empilhada é novamente uma tabela com sua linha de cabeçalho aparecendo e nenhuma palavra quebrada (isso pega regras como overflow-wrap: anywhere que permitem que uma coluna comprima uma palavra); nenhuma célula tem hyphens: auto. Também testa o CSS em um fixture que adiciona à home page, com axe em ambos os temas, para que um site sem tabela ainda prove o bloco. O Chromium headless não hifeniza, então a guarda de hifenização lê o estilo computado, não o layout. Safari e VoiceOver não são verificados.
Ofuscação de email (ocultar endereços de crawlers)
Nunca coloque um email em texto simples ou mailto:seu@endereco no HTML fonte — raspadores o coletam para spam em horas. Renderize cada endereço através do componente EmailLink.astro (src/components/EmailLink.astro no skill new-website's templates/astro/, + seu helper lib/obfuscate.ts): o endereço é invertido + codificado em base64 no momento da construção e remontado no navegador, então o HTML servido não contém endereço legível. Visitantes sem JavaScript veem uma dica ainda utilizável "nome [at] dominio [dot] com".
<EmailLink address="[email protected]" subject="Consulta sobre projeto" text="Envie-nos um email" />
Regra: zero links mailto: crus e zero endereços @ literais no HTML construído — website-qa verifica isso. Mesma ideia para qualquer número de telefone que você não queira ser coletado.
Tokens de marca
Espelhe a paleta de BRAND.md no bloco de tokens e mantenha-os sincronizados. Defina: marca (fixa), e por tema heading/ink/muted/accent/link/line/bg/bg-soft/surface. A cor de destaque e a cor do link devem passar AA no fundo em ambos os temas, e é por isso que cada tema tem as suas próprias. O opcional website-brand-kit faz o espelhamento com uma verificação: sua leitura lê as tabelas de BRAND.md, segura cada cor de texto contra 4.5:1 em cada fundo em ambos os temas, e escreve os valores nos três blocos de cores de global.css (claro, escuro e o bloco escuro para visitantes sem JavaScript). Manualmente, altere todos os três blocos juntos.
Feito significa
BRAND.md preenchido e consistente com o bloco de tokens; imagens são WebP, dimensionadas, lazy/eager corretas, com alt; o teste de a11y (claro+escuro) está verde; o layout se mantém em 360/768/1280 sem deslocamento; toda tabela é stack ou figures e tables.spec.ts está verde. Os números de desempenho (FCP/LCP) são verificados em website-qa. O layout também se mantém em 360/768/1280 com cópia real (não placeholder) em cada idioma enviado, não apenas no locale padrão.