* 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>
420 lines
17 KiB
React
420 lines
17 KiB
React
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import Header from "@/components/Header";
|
|
import Footer from "@/components/Footer";
|
|
import PageHero from "@/components/PageHero";
|
|
import SectionHead from "@/components/SectionHead";
|
|
import {
|
|
CATEGORIAS_COMPONENTES,
|
|
COMPONENTES_REGISTRY,
|
|
GUIA_COMPONENT_MANAGER
|
|
} from "@/lib/componentRegistryData";
|
|
import {
|
|
ArrowUpRight,
|
|
Box,
|
|
Brain,
|
|
Check,
|
|
CircuitBoard,
|
|
Cloud,
|
|
Copy,
|
|
Cpu,
|
|
Download,
|
|
HelpCircle,
|
|
Home,
|
|
Layers,
|
|
Monitor,
|
|
Plus,
|
|
Search,
|
|
ShieldCheck,
|
|
Terminal,
|
|
Trash2,
|
|
Wrench,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
const ICONES_CATEGORIA = {
|
|
ui_display: Monitor,
|
|
cloud_conectividade: Cloud,
|
|
audio_voz_ia: Brain,
|
|
smart_home_mesh: Home,
|
|
drivers_perifericos: Cpu,
|
|
bsp_placas: CircuitBoard,
|
|
sistema_utilitarios: Wrench,
|
|
};
|
|
|
|
export default function ComponentRegistryPage() {
|
|
const [busca, setBusca] = useState("");
|
|
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
|
|
const [chipFiltro, setChipFiltro] = useState("todos");
|
|
const [copiadoId, setCopiadoId] = useState(null);
|
|
const [manifestoCopiado, setManifestoCopiado] = useState(false);
|
|
|
|
// Componentes selecionados no construtor de idf_component.yml
|
|
const [componentesSelecionados, setComponentesSelecionados] = useState([
|
|
"espressif-esp_lvgl_port",
|
|
"espressif-led_indicator"
|
|
]);
|
|
|
|
// Lista de chips únicos para filtro
|
|
const chipsDisponiveis = ["todos", "ESP32", "ESP32-S3", "ESP32-C6", "ESP32-P4", "ESP32-H2", "ESP32-C3"];
|
|
|
|
// Filtragem dos componentes
|
|
const componentesFiltrados = useMemo(() => {
|
|
return COMPONENTES_REGISTRY.filter((comp) => {
|
|
const matchCategoria = categoriaAtiva === "todos" || comp.categoria === categoriaAtiva;
|
|
const matchChip = chipFiltro === "todos" || (comp.chips && comp.chips.includes(chipFiltro));
|
|
const matchBusca =
|
|
busca.trim() === "" ||
|
|
comp.name.toLowerCase().includes(busca.toLowerCase()) ||
|
|
comp.namespace.toLowerCase().includes(busca.toLowerCase()) ||
|
|
comp.titulo.toLowerCase().includes(busca.toLowerCase()) ||
|
|
comp.descricao.toLowerCase().includes(busca.toLowerCase()) ||
|
|
comp.tags.some((t) => t.toLowerCase().includes(busca.toLowerCase()));
|
|
|
|
return matchCategoria && matchChip && matchBusca;
|
|
});
|
|
}, [busca, categoriaAtiva, chipFiltro]);
|
|
|
|
// Manipuladores de cópia
|
|
const copiarComando = (id, comando) => {
|
|
navigator.clipboard.writeText(comando);
|
|
setCopiadoId(id);
|
|
setTimeout(() => setCopiadoId(null), 2000);
|
|
};
|
|
|
|
const toggleSelecaoManifesto = (id) => {
|
|
setComponentesSelecionados((prev) =>
|
|
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
// Geração dinâmica do manifesto YAML
|
|
const manifestoYaml = useMemo(() => {
|
|
const selecionados = COMPONENTES_REGISTRY.filter((c) =>
|
|
componentesSelecionados.includes(c.id)
|
|
);
|
|
|
|
if (selecionados.length === 0) {
|
|
return `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
|
|
}
|
|
|
|
let yaml = `# main/idf_component.yml\n# Gerado via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/componentes)\n\ndependencies:\n`;
|
|
selecionados.forEach((c) => {
|
|
yaml += ` ${c.namespace}/${c.name}: "${c.versao}"\n`;
|
|
});
|
|
return yaml;
|
|
}, [componentesSelecionados]);
|
|
|
|
const copiarManifesto = () => {
|
|
navigator.clipboard.writeText(manifestoYaml);
|
|
setManifestoCopiado(true);
|
|
setTimeout(() => setManifestoCopiado(false), 2000);
|
|
};
|
|
|
|
const baixarManifesto = () => {
|
|
const blob = new Blob([manifestoYaml], { type: "text/yaml;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "idf_component.yml";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const limparFiltros = () => {
|
|
setBusca("");
|
|
setCategoriaAtiva("todos");
|
|
setChipFiltro("todos");
|
|
};
|
|
|
|
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
|
|
|
|
return (
|
|
<div className="site-canvas min-h-screen">
|
|
<Header />
|
|
|
|
<main id="conteudo">
|
|
<PageHero
|
|
trail={[{ label: "Component Registry" }]}
|
|
icon={Box}
|
|
label="ESP-IDF Component Manager"
|
|
title="ESP Component Registry"
|
|
lede="O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
|
|
>
|
|
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
|
Abrir o registro oficial
|
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
|
<span className="sr-only"> (abre em nova aba)</span>
|
|
</a>
|
|
<a href="#manifesto-builder" className="btn btn-ghost">
|
|
Gerar idf_component.yml
|
|
</a>
|
|
</PageHero>
|
|
|
|
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
|
|
<div className="panel p-5 sm:p-6">
|
|
<div className="relative">
|
|
<label htmlFor="busca-componente" className="sr-only">Buscar por nome, namespace ou tag</label>
|
|
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
|
|
<input
|
|
id="busca-componente"
|
|
type="search"
|
|
value={busca}
|
|
onChange={(e) => setBusca(e.target.value)}
|
|
placeholder="Buscar por nome, namespace ou tag"
|
|
autoComplete="off"
|
|
className="field pl-12 pr-4 text-base"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-5">
|
|
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
{CATEGORIAS_COMPONENTES.map((cat) => {
|
|
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
|
|
const contagem = cat.id === "todos" ? COMPONENTES_REGISTRY.length : COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
|
|
return (
|
|
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
|
|
<Icone className="h-4 w-4" aria-hidden="true" />
|
|
{cat.id === "todos" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
|
|
<span className="tabular-nums opacity-80">{contagem}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-5 border-t border-line pt-5">
|
|
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
{chipsDisponiveis.map((chip) => (
|
|
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
|
|
{chip === "todos" ? "Todos" : chip}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
|
|
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? "componente" : "componentes"}
|
|
</h2>
|
|
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
|
|
{totalFiltros > 0 && (
|
|
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
|
<X className="h-4 w-4" aria-hidden="true" />
|
|
Limpar filtros
|
|
</button>
|
|
)}
|
|
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
|
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{componentesFiltrados.length === 0 ? (
|
|
<div className="card mt-6 px-6 py-14 text-center">
|
|
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
|
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum componente encontrado</h3>
|
|
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
|
|
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
|
|
</div>
|
|
) : (
|
|
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{componentesFiltrados.map((comp) => {
|
|
const selecionado = componentesSelecionados.includes(comp.id);
|
|
const cliCmd = `idf.py add-dependency "${comp.namespace}/${comp.name}${comp.versao}"`;
|
|
const foiCopiado = copiadoId === comp.id;
|
|
|
|
return (
|
|
<article key={comp.id} className={`card flex flex-col overflow-hidden ${selecionado ? "border-accent ring-2 ring-accent/20" : ""}`}>
|
|
<div className="flex-1 p-5 sm:p-6">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
|
{comp.namespace}/
|
|
{comp.oficial && (
|
|
<span className="chip chip-accent px-2 py-0">
|
|
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
|
|
Oficial
|
|
</span>
|
|
)}
|
|
</p>
|
|
<h3 className="mt-1 break-words font-display text-xl font-semibold tracking-[-0.03em] text-ink">{comp.name}</h3>
|
|
<p className="mt-0.5 text-sm font-medium text-accent">{comp.titulo}</p>
|
|
</div>
|
|
<span className="shrink-0 rounded-md border border-line bg-surface-2 px-2 py-1 font-mono text-xs text-ink-soft">{comp.versao}</span>
|
|
</div>
|
|
|
|
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
|
|
|
|
<ul className="mt-4 flex flex-wrap gap-1.5">
|
|
{comp.tags.map((tag) => (
|
|
<li key={tag} className="chip">{tag}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => copiarComando(comp.id, cliCmd)}
|
|
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
|
|
aria-label={`Copiar comando para adicionar ${comp.name}`}
|
|
>
|
|
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
|
|
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
|
|
</button>
|
|
<p role="status" className="sr-only">{foiCopiado ? "Comando copiado" : ""}</p>
|
|
|
|
<p className="mt-3 text-xs text-muted">
|
|
<span className="font-medium text-ink-soft">Chips: </span>
|
|
{comp.chips.join(", ")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-2 border-t border-line p-4 sm:px-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleSelecaoManifesto(comp.id)}
|
|
aria-pressed={selecionado}
|
|
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
|
|
>
|
|
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
|
|
{selecionado ? "No manifesto" : "Adicionar ao manifesto"}
|
|
</button>
|
|
<a
|
|
href={comp.urlRegistry}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
|
|
aria-label={`Abrir ${comp.name} no registro oficial (nova aba)`}
|
|
>
|
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
|
</a>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section id="manifesto-builder" className="shell scroll-mt-28 pb-16" aria-labelledby="manifesto-titulo">
|
|
<div className="panel p-6 sm:p-9">
|
|
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
|
|
<div className="max-w-2xl">
|
|
<p className="eyebrow">Gerador de manifesto</p>
|
|
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
|
|
<p className="mt-3 text-sm leading-relaxed text-muted">
|
|
Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o
|
|
ESP-IDF baixa todas as bibliotecas na compilação.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
|
|
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
|
<span role="status">{manifestoCopiado ? "Copiado" : "Copiar YAML"}</span>
|
|
</button>
|
|
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
|
|
<Download className="h-4 w-4" aria-hidden="true" />
|
|
Baixar .yml
|
|
</button>
|
|
{componentesSelecionados.length > 0 && (
|
|
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
|
|
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
|
Limpar
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="code-panel mt-6">
|
|
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
|
|
</div>
|
|
|
|
{componentesSelecionados.length > 0 && (
|
|
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
|
|
{componentesSelecionados.map((id) => {
|
|
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
|
|
if (!c) return null;
|
|
return (
|
|
<li key={id} className="chip chip-accent gap-1 pr-1">
|
|
{c.name}
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleSelecaoManifesto(id)}
|
|
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
|
|
aria-label={`Remover ${c.name} do manifesto`}
|
|
>
|
|
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
|
|
<SectionHead id="guia-titulo" eyebrow="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
|
|
|
|
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
|
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
|
|
<li key={v.titulo} className="card p-5">
|
|
<h3 className="font-semibold text-ink">{v.titulo}</h3>
|
|
<p className="mt-2 text-sm leading-relaxed text-muted">{v.descricao}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
|
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
|
|
Comandos essenciais
|
|
</h3>
|
|
<ul className="mt-4 space-y-3">
|
|
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
|
|
<li key={cmd.exemplo} className="code-panel flex flex-col justify-between gap-3 p-4 md:flex-row md:items-center">
|
|
<div className="min-w-0">
|
|
<code className="block break-all font-mono text-sm text-[#5eead4]">$ {cmd.exemplo}</code>
|
|
<p className="mt-1 text-sm text-[#a6afbd]">{cmd.explicacao}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
|
|
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
|
|
>
|
|
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
|
{copiadoId === `cmd-${i}` ? "Copiado" : "Copiar"}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="callout mt-8">
|
|
<h3 className="flex items-center gap-2 font-semibold text-ink">
|
|
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
|
|
Versionamento semântico no ESP-IDF
|
|
</h3>
|
|
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
|
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
|
|
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
|
|
<li><code>1.2.0</code>: trava na versão exata.</li>
|
|
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|