Redesign das páginas internas no novo design system (AAA) (#2)

* feat(design): redesign das páginas internas no novo design system

Padroniza todas as páginas e componentes internos nos tokens e primitivos
de app/globals.css, com contraste AAA (7:1) nos dois temas e alvos de toque
de 44px.

- Páginas reescritas: glossário, séries, série, seletor, comparador,
  diagnóstico, catálogo, component registry, frameworks, framework, sobre
  e a nova 404.
- Componentes: PageHero e SectionHead novos; BoardCard, CodeExample,
  PinRestrictions, SeriesTabMenu e ConnectionsDiagram refeitos, com abas
  e diálogo acessíveis.
- Remove emojis, cores avulsas por série, caixa alta com tracking, texto
  menor que 12px e os pares slate-*/dark:*.
- Comparador: "mostrar só as diferenças" e leitura de ?chips=.
- Corrige overflow em 320/390px e ajusta accent/copper do tema claro
  para fechar 7:1.
- scripts/auditar-temas.mjs passa a travar paleta crua, dark:, texto
  pequeno, emoji e contraste dos tokens.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc

* ci: roda lint, auditorias de design e build em cada PR

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Rayan GuerraandClaude Sonnet 5.5 authored and GitHub committed 2026-10-20 16:10:15 -03:00
1 parent 4f27f5858b
commit bc9ae5c036
34 files changed
+4911 -6874

No files matched your search

+148 -240
View File
@@ -2,43 +2,14 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useState, Fragment } from "react";
import PageHero from "@/components/PageHero";
import { useEffect, useState, Fragment } from "react";
import Link from "next/link";
import seriesData from "@/public/series.json";
import {
Scale,
GitCompare,
Check,
ChevronDown,
ChevronUp,
ArrowRight,
FileText,
ExternalLink,
ShoppingBag,
Cpu,
Network,
Database,
Sliders,
Activity,
HelpCircle,
Thermometer,
ShieldCheck,
Package
} from "lucide-react";
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
// Category to Icon mapping
const categoryIcons = {
"Geral": <HelpCircle className="w-4 h-4 text-purple-500" />,
"Processador": <Cpu className="w-4 h-4 text-blue-500" />,
"Conectividade": <Network className="w-4 h-4 text-indigo-500" />,
"Memória": <Database className="w-4 h-4 text-pink-500" />,
"Periféricos Digitais": <Sliders className="w-4 h-4 text-sky-500" />,
"Periféricos Analógicos": <Activity className="w-4 h-4 text-emerald-500" />,
"Segurança": <ShieldCheck className="w-4 h-4 text-teal-500" />,
"Disponibilidade e Ferramentas": <Package className="w-4 h-4 text-amber-500" />,
"Interfaces Especiais": <Activity className="w-4 h-4 text-cyan-500" />,
"Especificações Ambientais": <Thermometer className="w-4 h-4 text-red-500" />,
};
const MIN_SERIES = 2;
const MAX_SERIES = 4;
/* Lê "seguranca.ecc" além de chaves simples. */
function valorDe(objeto, caminho) {
@@ -47,6 +18,7 @@ function valorDe(objeto, caminho) {
export default function Comparacao() {
const [selectedSeries, setSelectedSeries] = useState(["ESP32", "ESP32-S3"]);
const [soDiferencas, setSoDiferencas] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState({
"Interfaces Especiais": true,
"Especificações Ambientais": true,
@@ -54,23 +26,24 @@ export default function Comparacao() {
const series = Object.entries(seriesData);
// O seletor envia as séries finalistas em ?chips=ESP32-S3,ESP32-C6.
useEffect(() => {
const chips = new URLSearchParams(window.location.search).get("chips");
if (!chips) return;
const validas = [...new Set(chips.split(","))].filter((k) => seriesData[k]).slice(0, MAX_SERIES);
if (validas.length >= MIN_SERIES) setSelectedSeries(validas);
}, []);
const toggleSeries = (seriesKey) => {
if (selectedSeries.includes(seriesKey)) {
if (selectedSeries.length > 2) {
setSelectedSeries(selectedSeries.filter(s => s !== seriesKey));
}
} else {
if (selectedSeries.length < 4) {
setSelectedSeries([...selectedSeries, seriesKey]);
}
if (selectedSeries.length > MIN_SERIES) setSelectedSeries(selectedSeries.filter((s) => s !== seriesKey));
} else if (selectedSeries.length < MAX_SERIES) {
setSelectedSeries([...selectedSeries, seriesKey]);
}
};
const toggleCategory = (category) => {
setCollapsedCategories({
...collapsedCategories,
[category]: !collapsedCategories[category]
});
setCollapsedCategories({ ...collapsedCategories, [category]: !collapsedCategories[category] });
};
const compareFields = [
@@ -153,85 +126,60 @@ export default function Comparacao() {
];
const renderValue = (value) => {
if (!value || value === "Não") {
return <span className="text-slate-500 dark:text-slate-400 font-medium select-none">Não</span>;
}
if (!value || value === "Não") return <span className="text-muted">Não</span>;
if (value === "Sim") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 shadow-xs">
<Check className="w-3.5 h-3.5" />
<span className="inline-flex items-center gap-1.5 font-medium text-ok">
<Check className="h-4 w-4" aria-hidden="true" />
Sim
</span>
);
}
return <span className="text-slate-800 dark:text-slate-200 font-semibold">{value}</span>;
return <span className="font-medium text-ink">{value}</span>;
};
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? "Não"));
const diferem = (key) => new Set(valores(key)).size > 1;
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-6 pt-16 pb-24 max-w-7xl mx-auto">
{/* Top Section */}
<section className="mb-16">
<div className="border-l-4 border-purple-500 pl-6 max-w-3xl">
<p className="text-xs font-bold text-purple-600 dark:text-purple-400 uppercase tracking-widest mb-2 select-none">
Análise Técnica Comparativa
</p>
<h1 className="text-4xl md:text-5xl font-display font-extrabold text-slate-800 dark:text-slate-100 mb-4 tracking-tight leading-tight">
Comparador de Séries ESP32
</h1>
<p className="text-base text-slate-500 dark:text-slate-400 leading-relaxed">
Coloque lado a lado as especificações completas de clock, memória, interfaces de rádio e periféricos analógicos para validar a engenharia e o custo-benefício de cada microcontrolador.
</p>
</div>
</section>
<main id="conteudo">
<PageHero
trail={[{ label: "Comparador" }]}
icon={Scale}
label="Até quatro séries lado a lado"
title="Comparador de séries"
lede="Coloque núcleos, rádio, memória, periféricos e segurança na mesma tabela e veja o que realmente muda de um chip para outro."
/>
{/* Model Selector Card */}
<section className="mb-12">
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-6 md:p-8 shadow-2xl relative overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<section className="shell pt-10 sm:pt-12" aria-labelledby="escolha-titulo">
<div className="panel p-5 sm:p-7">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 className="text-lg font-bold text-slate-800 dark:text-slate-100 mb-1">
Escolha as Séries
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400">
Selecione de 2 a até 4 modelos simultâneos para montar sua planilha de comparação.
<h2 id="escolha-titulo" className="text-base font-semibold text-ink">Escolha de {MIN_SERIES} a {MAX_SERIES} séries</h2>
<p role="status" aria-live="polite" className="mt-1 text-sm text-muted">
{selectedSeries.length} de {MAX_SERIES} selecionadas
{selectedSeries.length === MAX_SERIES && ". Remova uma para trocar."}
</p>
</div>
<div className="inline-flex items-center gap-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 px-3.5 py-1.5 rounded-full border border-purple-500/20 text-xs font-bold shadow-xs select-none">
<GitCompare className="w-3.5 h-3.5" />
<span>{selectedSeries.length} de 4 selecionados</span>
</div>
</div>
{/* Brand-colored dynamic button selector grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-3">
{series.map(([key, seriesItem]) => {
const isSelected = selectedSeries.includes(key);
<div className="mt-4 flex flex-wrap gap-2">
{series.map(([key]) => {
const marcada = selectedSeries.includes(key);
const bloqueada = !marcada && selectedSeries.length >= MAX_SERIES;
return (
<button
key={key}
type="button"
onClick={() => toggleSeries(key)}
style={{
borderColor: isSelected ? seriesItem.cor : undefined
}}
className={`p-4 border rounded-2xl transition-all duration-300 cursor-pointer active:scale-95 text-center flex flex-col items-center justify-center shadow-xs ${
isSelected
? 'bg-slate-100 dark:bg-slate-950 shadow-md scale-[1.02] border-2'
: 'border-slate-300 dark:border-slate-800/80 bg-white dark:bg-slate-900/30 hover:border-purple-400/50 hover:bg-purple-500/5'
}`}
aria-pressed={marcada}
aria-disabled={bloqueada}
className={`chip-btn ${bloqueada ? "opacity-50" : ""}`}
>
<div className="text-3xl mb-2 select-none transform hover:scale-110 transition-transform">{seriesItem.icone}</div>
<div className="text-xs font-bold text-slate-800 dark:text-slate-200">{key}</div>
{isSelected && (
<div
className="mt-2.5 w-2 h-2 rounded-full shrink-0 animate-pulse"
style={{ backgroundColor: seriesItem.cor }}
/>
)}
{marcada && <Check className="h-4 w-4" aria-hidden="true" />}
{key}
</button>
);
})}
@@ -239,160 +187,120 @@ export default function Comparacao() {
</div>
</section>
{/* Side by side interactive Table */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl overflow-hidden shadow-2xl">
<div className="overflow-x-auto scrollbar-thin">
<table className="w-full border-collapse">
<thead>
<tr className="bg-slate-850 dark:bg-slate-950 border-b border-slate-750 dark:border-slate-900">
<th className="px-6 py-5 text-left text-white font-bold text-xs uppercase tracking-widest sticky left-0 bg-slate-850 dark:bg-slate-950 z-20 shadow-xs border-r border-slate-700/30">
Especificação
</th>
{selectedSeries.map(seriesKey => {
const seriesItem = seriesData[seriesKey];
return (
<th key={seriesKey} className="px-6 py-5 text-center text-white font-bold text-xs uppercase tracking-widest min-w-48">
<div className="flex flex-col items-center gap-2 select-none">
<span className="text-3xl transform hover:scale-105 transition-transform">{seriesItem.icone}</span>
<span className="text-sm font-extrabold tracking-wide" style={{ color: seriesItem.cor }}>{seriesKey}</span>
</div>
<section className="shell mt-8" aria-labelledby="tabela-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="tabela-titulo" className="text-base font-semibold text-ink">Especificações</h2>
<label className="flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
<input
type="checkbox"
checked={soDiferencas}
onChange={(e) => setSoDiferencas(e.target.checked)}
className="h-5 w-5 cursor-pointer accent-[var(--accent)]"
/>
Mostrar só as diferenças
</label>
</div>
<div className="panel mt-3 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full min-w-[30rem] border-collapse text-sm">
<caption className="sr-only">Comparação das séries {selectedSeries.join(", ")}</caption>
<thead>
<tr className="border-b border-line bg-surface-2">
<th scope="col" className="sticky left-0 z-20 w-28 bg-surface-2 px-4 py-4 text-left font-semibold text-muted sm:w-52 sm:px-6">
Especificação
</th>
{selectedSeries.map((seriesKey) => (
<th key={seriesKey} scope="col" className="min-w-40 px-4 py-4 text-left sm:px-6">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center gap-1 font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
<ArrowUpRight className="h-4 w-4 text-muted" aria-hidden="true" />
</Link>
</th>
))}
</tr>
</thead>
<tbody>
{compareFields.map(({ category, fields }) => {
const visiveis = soDiferencas ? fields.filter(({ key }) => diferem(key)) : fields;
if (soDiferencas && visiveis.length === 0) return null;
const isCollapsed = collapsedCategories[category] && !soDiferencas;
return (
<Fragment key={category}>
<tr className="border-y border-line bg-surface-2/60">
<th scope="rowgroup" colSpan={selectedSeries.length + 1} className="p-0 text-left">
<button
type="button"
onClick={() => toggleCategory(category)}
aria-expanded={!isCollapsed}
className="flex min-h-12 w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left font-semibold text-ink sm:px-6"
>
<span className="sticky left-4 sm:left-6">{category}</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-muted transition-transform ${isCollapsed ? "" : "rotate-180"}`} aria-hidden="true" />
</button>
</th>
</tr>
{!isCollapsed &&
visiveis.map(({ key, label }) => (
<tr key={key} className="border-b border-line last:border-b-0 hover:bg-surface-2/50">
<th scope="row" className="sticky left-0 z-10 bg-surface px-4 py-3.5 text-left align-top font-medium text-muted sm:px-6">
{label}
</th>
{selectedSeries.map((seriesKey) => (
<td key={seriesKey} className="px-4 py-3.5 align-top sm:px-6">
{renderValue(valorDe(seriesData[seriesKey], key))}
</td>
))}
</tr>
))}
</Fragment>
);
})}
</tr>
</thead>
<tbody>
{compareFields.map(({ category, fields }) => {
const isCollapsed = collapsedCategories[category];
return (
<Fragment key={category}>
{/* Collapsible Category Accordion Header */}
<tr
onClick={() => toggleCategory(category)}
className="bg-slate-100/80 dark:bg-slate-950/60 border-y border-slate-250/60 dark:border-slate-800/80 cursor-pointer select-none hover:bg-slate-200/60 dark:hover:bg-slate-900/80 transition-colors"
>
<td
colSpan={selectedSeries.length + 1}
className="px-6 py-3.5 font-bold text-slate-800 dark:text-slate-200 text-xs uppercase tracking-wider"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{categoryIcons[category] || <HelpCircle className="w-4 h-4 text-purple-500" />}
<span>{category}</span>
</div>
{isCollapsed ? (
<ChevronDown className="w-4 h-4 opacity-60 text-purple-600 dark:text-purple-400" />
) : (
<ChevronUp className="w-4 h-4 opacity-60 text-purple-600 dark:text-purple-400" />
)}
</div>
</td>
</tr>
{/* Fields list within category (if expanded) */}
{!isCollapsed && fields.map(({ key, label }) => (
<tr
key={key}
className="border-b border-slate-200 dark:border-slate-800/40 hover:bg-purple-500/5 dark:hover:bg-purple-400/5 transition-colors group"
>
<td className="px-6 py-4 font-bold text-slate-600 dark:text-slate-400 text-xs bg-white dark:bg-slate-900/10 sticky left-0 border-r border-slate-150/60 dark:border-slate-800/60 shrink-0 z-10">
{label}
</td>
{selectedSeries.map(seriesKey => {
const seriesItem = seriesData[seriesKey];
const value = valorDe(seriesItem, key);
return (
<td key={seriesKey} className="px-6 py-4 text-center text-xs">
{renderValue(value)}
</td>
);
})}
</tr>
))}
</Fragment>
);
})}
</tbody>
</table>
</div>
</div>
{/* Professional CTA Banner for the Catalog */}
<div className="my-20 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-slate-950 dark:to-purple-950/20 border border-indigo-200 dark:border-slate-800/80 rounded-3xl p-8 md:p-12 shadow-2xl relative overflow-hidden">
<div className="absolute right-0 bottom-0 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
<div className="flex-1 max-w-2xl text-center md:text-left">
<div className="inline-flex items-center gap-2 bg-indigo-500/10 text-indigo-700 dark:bg-white/10 dark:text-slate-350 px-3 py-1 rounded-full mb-4 text-xs font-semibold select-none">
<ShoppingBag className="w-3.5 h-3.5" />
<span>Hardware Adicional</span>
</div>
<h3 className="text-2xl md:text-3xl font-display font-extrabold text-slate-900 dark:text-white mb-4">
Catálogo de Placas de Desenvolvimento
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Quer testar o chip selecionado na bancada? Compare placas de desenvolvimento com layouts de pinos mapeados, conexões de periféricos e links oficiais dos melhores fornecedores.
</p>
</tbody>
</table>
</div>
<Link
href="/catalogo"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all duration-300 shrink-0 shadow-md active:scale-95"
>
<span>Acessar Catálogo</span>
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
</section>
{/* Detailed technical datasheets section */}
<div className="mt-12">
<h3 className="text-xl font-display font-extrabold text-slate-800 dark:text-slate-100 mb-6 border-l-4 border-purple-500 pl-4 select-none">
Documentação Técnica Relacionada
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{selectedSeries.map(seriesKey => {
<section className="shell section" aria-labelledby="docs-titulo">
<h2 id="docs-titulo" className="section-title">Documentação oficial</h2>
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{selectedSeries.map((seriesKey) => {
const seriesItem = seriesData[seriesKey];
return (
<div
key={seriesKey}
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 p-5 rounded-2xl hover:border-purple-500/50 dark:hover:border-purple-400/50 hover:shadow-lg transition-all duration-300 flex flex-col justify-between"
>
<div className="flex gap-4 items-start mb-4">
<div className="text-4xl select-none">{seriesItem.icone}</div>
<div className="min-w-0">
<Link href={`/series/${seriesKey}`} className="no-underline">
<h4 className="text-sm font-extrabold text-slate-850 dark:text-slate-100 hover:text-purple-600 dark:hover:text-purple-400 transition-colors">
{seriesKey}
</h4>
</Link>
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider mt-1">Guia de Hardware</p>
</div>
</div>
<div key={seriesKey} className="card flex flex-col justify-between gap-5 p-5">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
</Link>
{seriesItem.datasheet ? (
<a
href={seriesItem.datasheet}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-1.5 w-full text-center py-2 bg-slate-100 dark:bg-slate-950 hover:bg-purple-500/10 dark:hover:bg-purple-400/10 border border-slate-300 dark:border-slate-800 rounded-xl text-xs font-bold text-slate-650 dark:text-slate-350 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
>
<span>Datasheet Oficial</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={seriesItem.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
Datasheet oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
) : (
<span className="inline-flex items-center justify-center gap-1.5 w-full text-center py-2 bg-slate-100 dark:bg-slate-950 border border-slate-300 dark:border-slate-800 rounded-xl text-xs font-bold text-slate-500 dark:text-slate-400 select-none">
<span>Datasheet ainda não publicado</span>
</span>
<span className="text-sm text-muted">Datasheet ainda não publicado</span>
)}
</div>
);
})}
</div>
</div>
<div className="panel mt-10 flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Quer testar na bancada?</h2>
<p className="mt-3 text-base text-muted">Veja as placas de desenvolvimento com pinagem, periféricos e link para a loja.</p>
</div>
<Link href="/catalogo" className="btn btn-primary shrink-0">
Ver catálogo de placas
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
</div>
);
}
}