* 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>
307 lines
12 KiB
React
307 lines
12 KiB
React
"use client";
|
|
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import PageHero from "@/components/PageHero";
|
|
import { useEffect, useState, Fragment } from "react";
|
|
import Link from "next/link";
|
|
import seriesData from "@/public/series.json";
|
|
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
|
|
|
|
const MIN_SERIES = 2;
|
|
const MAX_SERIES = 4;
|
|
|
|
/* Lê "seguranca.ecc" além de chaves simples. */
|
|
function valorDe(objeto, caminho) {
|
|
return caminho.split(".").reduce((atual, parte) => atual?.[parte], objeto);
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
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 > 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] });
|
|
};
|
|
|
|
const compareFields = [
|
|
{ category: "Geral", fields: [
|
|
{ key: "nome_completo", label: "Nome Completo" },
|
|
{ key: "descricao", label: "Descrição" },
|
|
]},
|
|
{ category: "Disponibilidade e Ferramentas", fields: [
|
|
{ key: "esp_idf_minimo", label: "ESP-IDF mínimo" },
|
|
{ key: "arduino_core", label: "Core Arduino" },
|
|
{ key: "depuracao", label: "Depuração / JTAG" },
|
|
{ key: "encapsulamento", label: "Encapsulamento" },
|
|
{ key: "modulos", label: "Módulos disponíveis" },
|
|
]},
|
|
{ category: "Processador", fields: [
|
|
{ key: "arquitetura", label: "Arquitetura" },
|
|
{ key: "nucleos", label: "Núcleos" },
|
|
{ key: "frequencia", label: "Frequência" },
|
|
{ key: "coprocessador_ulp", label: "Coprocessador ULP" },
|
|
{ key: "aceleradores_ia", label: "Aceleradores de IA" },
|
|
]},
|
|
{ category: "Conectividade", fields: [
|
|
{ key: "wifi", label: "Wi-Fi" },
|
|
{ key: "bluetooth", label: "Bluetooth" },
|
|
{ key: "zigbee_thread", label: "Zigbee/Thread" },
|
|
{ key: "matter", label: "Matter" },
|
|
{ key: "ethernet", label: "Ethernet" },
|
|
]},
|
|
{ category: "Memória", fields: [
|
|
{ key: "memoria_sram", label: "SRAM" },
|
|
{ key: "memoria_sram_rtc", label: "SRAM RTC" },
|
|
{ key: "memoria_rom", label: "ROM" },
|
|
{ key: "flash_externa", label: "Flash Externa" },
|
|
{ key: "psram_externa", label: "PSRAM Externa" },
|
|
]},
|
|
{ category: "Periféricos Digitais", fields: [
|
|
{ key: "gpio", label: "GPIO" },
|
|
{ key: "uart", label: "UART" },
|
|
{ key: "spi", label: "SPI" },
|
|
{ key: "i2c", label: "I²C" },
|
|
{ key: "i2s", label: "I²S" },
|
|
{ key: "pwm", label: "PWM" },
|
|
{ key: "can", label: "CAN" },
|
|
{ key: "usb", label: "USB" },
|
|
{ key: "depuracao", label: "Depuração / JTAG" },
|
|
{ key: "sdio", label: "SDIO" },
|
|
]},
|
|
{ category: "Periféricos Analógicos", fields: [
|
|
{ key: "adc", label: "ADC" },
|
|
{ key: "dac", label: "DAC" },
|
|
{ key: "touch", label: "Touch Capacitivo" },
|
|
{ key: "sensor_hall", label: "Sensor Hall" },
|
|
{ key: "sensor_temperatura", label: "Sensor de Temperatura" },
|
|
]},
|
|
{ category: "Segurança", fields: [
|
|
{ key: "seguranca.aes", label: "AES" },
|
|
{ key: "seguranca.sha", label: "SHA" },
|
|
{ key: "seguranca.rsa", label: "RSA / MPI" },
|
|
{ key: "seguranca.ecc", label: "ECC (curvas elípticas)" },
|
|
{ key: "seguranca.ecdsa", label: "ECDSA em hardware" },
|
|
{ key: "seguranca.hmac", label: "HMAC" },
|
|
{ key: "seguranca.assinatura_digital", label: "Assinatura Digital (DS)" },
|
|
{ key: "seguranca.key_manager", label: "Key Manager" },
|
|
{ key: "seguranca.rng", label: "Gerador aleatório (RNG)" },
|
|
{ key: "seguranca.criptografia_flash", label: "Criptografia de flash" },
|
|
{ key: "seguranca.secure_boot", label: "Secure Boot" },
|
|
{ key: "seguranca.protecao_dpa", label: "Proteção contra DPA" },
|
|
]},
|
|
{ category: "Interfaces Especiais", fields: [
|
|
{ key: "lcd", label: "LCD" },
|
|
{ key: "camera", label: "Câmera" },
|
|
{ key: "mipi_csi", label: "MIPI CSI" },
|
|
{ key: "mipi_dsi", label: "MIPI DSI" },
|
|
{ key: "ethernet_mac", label: "Ethernet MAC" },
|
|
]},
|
|
{ category: "Especificações Ambientais", fields: [
|
|
{ key: "temperatura_operacao", label: "Temp. Operação" },
|
|
{ key: "temperatura_armazenamento", label: "Temp. Armazenamento" },
|
|
]},
|
|
];
|
|
|
|
const renderValue = (value) => {
|
|
if (!value || value === "Não") return <span className="text-muted">Não</span>;
|
|
if (value === "Sim") {
|
|
return (
|
|
<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="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="site-canvas min-h-screen">
|
|
<Header />
|
|
|
|
<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."
|
|
/>
|
|
|
|
<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 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>
|
|
<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)}
|
|
aria-pressed={marcada}
|
|
aria-disabled={bloqueada}
|
|
className={`chip-btn ${bloqueada ? "opacity-50" : ""}`}
|
|
>
|
|
{marcada && <Check className="h-4 w-4" aria-hidden="true" />}
|
|
{key}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<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>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<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="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="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="text-sm text-muted">Datasheet ainda não publicado</span>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|