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

+249 -474
View File
@@ -3,41 +3,46 @@
import { useState, useMemo } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import {
CATEGORIAS_COMPONENTES,
COMPONENTES_REGISTRY,
GUIA_COMPONENT_MANAGER
} from "@/lib/componentRegistryData";
import {
Layers,
Search,
Copy,
Check,
ExternalLink,
Sparkles,
Terminal,
Code2,
Plus,
Trash2,
Download,
Cpu,
ArrowUpRight,
Box,
CheckCircle2,
ChevronRight,
HelpCircle,
ArrowRight,
BookOpen,
ShieldCheck,
Zap,
Monitor,
Cloud,
Brain,
Home,
Check,
CircuitBoard,
Wrench
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");
@@ -119,520 +124,290 @@ export default function ComponentRegistryPage() {
URL.revokeObjectURL(url);
};
// Mapeamento de ícones de categoria
const renderIconeCategoria = (id) => {
switch (id) {
case "ui_display":
return <Monitor className="w-4 h-4" />;
case "cloud_conectividade":
return <Cloud className="w-4 h-4" />;
case "audio_voz_ia":
return <Brain className="w-4 h-4" />;
case "smart_home_mesh":
return <Home className="w-4 h-4" />;
case "drivers_perifericos":
return <Cpu className="w-4 h-4" />;
case "bsp_placas":
return <CircuitBoard className="w-4 h-4" />;
case "sistema_utilitarios":
return <Wrench className="w-4 h-4" />;
default:
return <Layers className="w-4 h-4" />;
}
const limparFiltros = () => {
setBusca("");
setCategoriaAtiva("todos");
setChipFiltro("todos");
};
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
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-4 md:px-6 pt-12 md:pt-16 pb-24 max-w-7xl mx-auto space-y-12">
{/* Top Hero Section */}
<section className="text-center select-none relative">
<div className="inline-flex items-center gap-2 bg-white/80 dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-6 border border-slate-300 dark:border-slate-800/80 shadow-xs">
<Box className="w-4 h-4 text-purple-500" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
ESP-IDF Component Manager
</span>
<span className="bg-purple-100 dark:bg-purple-950/80 text-purple-700 dark:text-purple-300 text-[10px] font-extrabold px-2 py-0.5 rounded-full">
Oficial
</span>
</div>
<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>
<h1 className="text-4xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 pb-2 pt-1 leading-tight md:leading-[1.18] tracking-tight">
ESP Component Registry
</h1>
<p className="text-sm md:text-base text-slate-600 dark:text-slate-350 max-w-3xl mx-auto leading-relaxed font-semibold">
O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF.
Adicione drivers de displays, reconhecimento de voz, pilhas industriais e bibliotecas
com comandos de um clique e geração de manifesto declarativo.
</p>
<div className="flex flex-wrap items-center justify-center gap-3 mt-6">
<a
href="https://components.espressif.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white text-xs font-bold px-4 py-2.5 rounded-xl shadow-md transition-all active:scale-95"
>
<span>Acessar Registry Oficial</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
<a
href="#manifesto-builder"
className="inline-flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 text-xs font-bold px-4 py-2.5 rounded-xl shadow-xs transition-all active:scale-95"
>
<Code2 className="w-3.5 h-3.5 text-purple-500" />
<span>Gerador de idf_component.yml</span>
</a>
</div>
</section>
{/* Interactive Search & Filter Bar */}
<section className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 border border-slate-300 dark:border-slate-800/80 shadow-xl space-y-5">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
{/* Search Input */}
<div className="relative w-full md:flex-1">
<Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<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
type="text"
id="busca-componente"
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar componentes por nome, namespace, funcionalidade ou tags (ex: lvgl, rainmaker, ws2812)..."
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 dark:bg-slate-950/70 border border-slate-250 dark:border-slate-800 rounded-2xl text-xs md:text-sm font-semibold text-slate-800 dark:text-slate-100 placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-purple-500 transition-all"
placeholder="Buscar por nome, namespace ou tag"
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
{busca && (
<button
onClick={() => setBusca("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
>
Limpar
</button>
)}
</div>
{/* Chip Filter */}
<div className="flex items-center gap-1.5 w-full md:w-auto overflow-x-auto pb-1 md:pb-0">
<span className="text-[11px] font-bold text-slate-500 uppercase tracking-wider shrink-0 mr-1">
SoC:
</span>
{chipsDisponiveis.map((chip) => (
<button
key={chip}
onClick={() => setChipFiltro(chip)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
chipFiltro === chip
? "bg-purple-600 text-white shadow-xs"
: "bg-slate-100 dark:bg-slate-800/80 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-700"
}`}
>
{chip === "todos" ? "Todos SoCs" : chip}
</button>
))}
<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>
{/* Category Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 border-t border-slate-200 dark:border-slate-800/60 pt-4">
{CATEGORIAS_COMPONENTES.map((cat) => {
const ativa = categoriaAtiva === cat.id;
const contagem =
cat.id === "todos"
? COMPONENTES_REGISTRY.length
: COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
return (
<button
key={cat.id}
onClick={() => setCategoriaAtiva(cat.id)}
className={`inline-flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
ativa
? "bg-purple-50 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/60 shadow-xs"
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
{renderIconeCategoria(cat.id)}
<span>{cat.nome}</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full ${
ativa
? "bg-purple-200 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{contagem}
</span>
</button>
);
})}
<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>
{/* Components Grid */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-purple-500" />
<span>Componentes Disponíveis ({componentesFiltrados.length})</span>
<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>
<span className="text-xs text-slate-500 font-semibold">
Clique em um comando para copiar para o terminal
</span>
<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="text-center py-16 bg-white dark:bg-slate-900/40 rounded-3xl border border-slate-300 dark:border-slate-800 p-8">
<Box className="w-12 h-12 text-slate-400 mx-auto mb-3 opacity-60" />
<p className="text-base font-bold text-slate-750 dark:text-slate-200">
Nenhum componente encontrado para esses filtros.
</p>
<p className="text-xs text-slate-500 mt-1">
Tente buscar por termos mais genéricos ou resetar a categoria.
</p>
<button
onClick={() => {
setBusca("");
setCategoriaAtiva("todos");
setChipFiltro("todos");
}}
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
>
Limpar Todos os Filtros
</button>
<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="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<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 (
<div
key={comp.id}
className={`bg-white dark:bg-slate-900/45 backdrop-blur-xl border rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-300 flex flex-col justify-between overflow-hidden relative group ${
selecionado
? "border-purple-500/80 ring-2 ring-purple-500/20"
: "border-slate-300 dark:border-slate-800/80 hover:-translate-y-1"
}`}
>
{/* Top Header Card */}
<div className="p-6 pb-4 border-b border-slate-150 dark:border-slate-850/40">
<div className="flex items-start justify-between gap-3 mb-2">
<div>
<div className="flex items-center gap-1.5 mb-1">
<span className="text-[10px] font-extrabold text-slate-400 uppercase tracking-wider">
{comp.namespace}/
</span>
<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="inline-flex items-center gap-1 bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-300 text-[9px] font-extrabold px-2 py-0.2 rounded-full border border-emerald-300 dark:border-emerald-800/60">
<ShieldCheck className="w-3 h-3" />
<span className="chip chip-accent px-2 py-0">
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
Oficial
</span>
)}
</div>
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{comp.name}
</h3>
<p className="text-xs font-semibold text-purple-600 dark:text-purple-400">
{comp.titulo}
</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="px-2.5 py-1 rounded-lg text-[10px] font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 shrink-0">
{comp.versao}
</span>
<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="text-xs text-slate-500 dark:text-slate-350 leading-relaxed mt-2.5 line-clamp-3">
{comp.descricao}
</p>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
{/* Tags */}
<div className="flex flex-wrap gap-1.5 mt-3.5">
<ul className="mt-4 flex flex-wrap gap-1.5">
{comp.tags.map((tag) => (
<span
key={tag}
className="text-[9px] font-bold bg-slate-100 dark:bg-slate-950/60 text-slate-600 dark:text-slate-400 px-2 py-0.5 rounded-md border border-slate-250 dark:border-slate-800"
>
{tag}
</span>
<li key={tag} className="chip">{tag}</li>
))}
</div>
</ul>
</div>
{/* Interactive CLI Action Box */}
<div className="p-6 py-4 bg-slate-50/50 dark:bg-slate-950/40 space-y-3">
<div>
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
<span className="flex items-center gap-1">
<Terminal className="w-3 h-3 text-purple-500" />
Comando CLI
</span>
{foiCopiado && (
<span className="text-emerald-500 font-bold flex items-center gap-0.5">
<Check className="w-3 h-3" /> Copiado!
</span>
)}
</div>
<button
onClick={() => copiarComando(comp.id, cliCmd)}
className="w-full text-left bg-slate-900 text-slate-100 p-2.5 rounded-xl font-mono text-[11px] hover:bg-slate-800 border border-slate-800 flex items-center justify-between group/cmd transition-colors cursor-pointer"
title="Clique para copiar comando"
>
<span className="truncate pr-2">{cliCmd}</span>
<Copy className="w-3.5 h-3.5 text-slate-400 group-hover/cmd:text-white shrink-0" />
</button>
</div>
{/* Compatible Chips */}
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-1">
SoCs Compatíveis:
</span>
<div className="flex flex-wrap gap-1">
{comp.chips.map((chip) => (
<span
key={chip}
className="text-[9px] font-semibold text-slate-650 dark:text-slate-350 bg-slate-200/60 dark:bg-slate-800 px-1.5 py-0.2 rounded"
>
{chip}
</span>
))}
</div>
</div>
</div>
{/* Bottom Actions */}
<div className="p-4 px-6 border-t border-slate-150 dark:border-slate-850/40 flex items-center justify-between text-xs font-bold">
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
<button
onClick={() => toggleSelecaoManifesto(comp.id)}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
selecionado
? "bg-purple-600 text-white shadow-xs"
: "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/40 hover:bg-purple-100"
}`}
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}`}
>
{selecionado ? (
<>
<CheckCircle2 className="w-3.5 h-3.5" />
<span>No Manifesto</span>
</>
) : (
<>
<Plus className="w-3.5 h-3.5" />
<span>Adicionar ao Manifesto</span>
</>
)}
<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>
<div className="flex items-center gap-2">
<a
href={comp.urlRegistry}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors p-1"
title="Ver página oficial no ESP Component Registry"
>
<ExternalLink className="w-4 h-4" />
</a>
</div>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
{comp.chips.join(", ")}
</p>
</div>
</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>
{/* Interactive Manifest Builder Section */}
<section
id="manifesto-builder"
className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-purple-300 dark:border-purple-800/60 shadow-2xl space-y-6"
>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pb-4 border-b border-slate-250 dark:border-slate-800">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 text-xs font-bold rounded-full mb-2">
<Code2 className="w-3.5 h-3.5" />
<span>Gerador de Manifesto Declarativo</span>
<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>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Construtor de `idf_component.yml`
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 font-semibold">
Adicione este arquivo na pasta <code>main/</code> do seu projeto ESP-IDF para
instalar todas as bibliotecas automaticamente na compilação.
</p>
</div>
<div className="flex items-center gap-2.5 w-full md:w-auto">
<button
onClick={copiarManifesto}
className="flex-1 md:flex-none inline-flex items-center justify-center gap-1.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-2.5 rounded-xl text-xs font-bold hover:opacity-90 shadow-md transition-all active:scale-95 cursor-pointer"
>
{manifestoCopiado ? (
<>
<Check className="w-4 h-4" />
<span>Manifesto Copiado!</span>
</>
) : (
<>
<Copy className="w-4 h-4" />
<span>Copiar YAML</span>
</>
)}
</button>
<button
onClick={baixarManifesto}
className="inline-flex items-center justify-center gap-1.5 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95 cursor-pointer"
title="Baixar arquivo idf_component.yml pronto"
>
<Download className="w-4 h-4" />
<span className="hidden sm:inline">Baixar .yml</span>
</button>
{componentesSelecionados.length > 0 && (
<button
onClick={() => setComponentesSelecionados([])}
className="p-2.5 text-slate-400 hover:text-rose-500 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
title="Limpar seleção"
>
<Trash2 className="w-4 h-4" />
<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>
)}
</div>
</div>
{/* YAML Output Terminal */}
<div className="relative">
<div className="bg-slate-950 text-slate-200 rounded-2xl p-4 md:p-6 font-mono text-xs md:text-sm overflow-x-auto border border-slate-800 shadow-inner">
<pre>{manifestoYaml}</pre>
</div>
</div>
{/* Selected components badges */}
<div className="flex flex-wrap items-center gap-2 pt-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider mr-2">
Componentes Inclusos ({componentesSelecionados.length}):
</span>
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
return (
<span
key={id}
className="inline-flex items-center gap-1.5 bg-purple-50 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-800 px-3 py-1 rounded-xl text-xs font-bold"
>
<span>{c.name}</span>
<button
onClick={() => toggleSelecaoManifesto(id)}
className="hover:text-rose-500 ml-1 cursor-pointer"
title="Remover"
>
×
<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>
</span>
);
})}
)}
</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>
{/* Educational Guide: O que é o Component Manager */}
<section className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800 shadow-xl space-y-8 select-none">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="p-2.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400">
<BookOpen className="w-5 h-5" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
{GUIA_COMPONENT_MANAGER.titulo}
</h2>
</div>
<p className="text-xs md:text-sm text-slate-600 dark:text-slate-350 leading-relaxed font-semibold max-w-4xl">
{GUIA_COMPONENT_MANAGER.resumo}
</p>
</div>
<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} />
{/* Vantagens Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
{GUIA_COMPONENT_MANAGER.vantagens.map((v, i) => (
<div
key={i}
className="bg-slate-50/70 dark:bg-slate-950/40 p-5 rounded-2xl border border-slate-200 dark:border-slate-800/80 space-y-2"
>
<div className="w-8 h-8 rounded-xl bg-purple-100 dark:bg-purple-900/40 text-purple-600 dark:text-purple-400 flex items-center justify-center text-xs font-extrabold">
0{i + 1}
</div>
<h3 className="text-sm font-bold text-slate-850 dark:text-slate-100">{v.titulo}</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
{v.descricao}
</p>
</div>
<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>
))}
</div>
</ul>
{/* Comandos Essenciais da CLI */}
<div className="space-y-3">
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
<Terminal className="w-4 h-4 text-purple-500" />
<span>Comandos Essenciais no Terminal</span>
</h3>
<div className="space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
<div
key={i}
className="bg-slate-900 text-slate-100 p-4 rounded-2xl border border-slate-800 flex flex-col md:flex-row md:items-center justify-between gap-3 font-mono text-xs"
>
<div>
<span className="text-purple-400 font-bold">$ {cmd.exemplo}</span>
<p className="text-[11px] text-slate-400 font-sans mt-1">
{cmd.explicacao}
</p>
</div>
<button
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
className="self-start md:self-auto inline-flex items-center gap-1 bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-lg text-[11px] font-bold text-slate-200 transition-colors shrink-0"
>
{copiadoId === `cmd-${i}` ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-400" />
<span>Copiado</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copiar</span>
</>
)}
</button>
<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>
))}
</div>
</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>
{/* Versioning Semantics Callout */}
<div className="bg-purple-50 dark:bg-purple-950/30 rounded-2xl p-5 border border-purple-200 dark:border-purple-800/40 text-xs space-y-2">
<h4 className="font-bold text-purple-900 dark:text-purple-300 flex items-center gap-1.5">
<HelpCircle className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span>Regras de Versionamento Semântico no ESP-IDF</span>
</h4>
<ul className="list-disc list-inside space-y-1 text-slate-750 dark:text-slate-350">
<li>
<code>^1.2.0</code>: Permite atualizações menores e correções (ex: 1.2.1, 1.3.0),
mas bloqueia grandes mudanças de API (2.0.0). <strong>(Recomendado)</strong>
</li>
<li>
<code>~1.2.0</code>: Permite apenas correções de bugs (ex: 1.2.1, 1.2.5), travando
na mesma versão menor.
</li>
<li>
<code>1.2.0</code>: Trava na versão exata indicada, sem receber atualizações.
</li>
<li>
<code>*</code>: Aceita qualquer versão disponível (não recomendado para produção).
</li>
<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>