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

+189 -432
View File
@@ -3,31 +3,37 @@
import { useState, useMemo, useRef } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import { categoriasGlossario } from "@/lib/glossarioData";
import {
ArrowRight,
BookOpen,
Search,
Check,
Code2,
Cpu,
Database,
Wifi,
HelpCircle,
LayoutGrid,
Lightbulb,
Link2,
BatteryCharging,
RotateCcw,
Search,
ShieldCheck,
Sliders,
Sparkles,
BatteryCharging,
Code2,
Link2,
Check,
ArrowRight,
Lightbulb,
HelpCircle,
Wifi,
X,
SlidersHorizontal,
Filter,
RotateCcw,
Tag,
ShieldCheck,
} from "lucide-react";
const BUSCAS_POPULARES = ["GPIO", "ULP", "PSRAM", "Strapping Pins", "Deep Sleep", "OTA", "Matter", "Secure Boot", "Wi-Fi 6", "MIPI"];
const ICONES = { Cpu, Database, Wifi, Sliders, Sparkles, BatteryCharging, Code2, ShieldCheck };
/** "Processamento, CPU & Silício" vira "Processamento": cabe no filtro sem truncar. */
const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
export default function GlossarioPage() {
const [busca, setBusca] = useState("");
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
@@ -35,67 +41,30 @@ export default function GlossarioPage() {
const [copiadoSlug, setCopiadoSlug] = useState(null);
const searchInputRef = useRef(null);
// Sugestões populares para busca rápida
const buscasPopulares = [
{ rotulo: "GPIO", emoji: "🔌" },
{ rotulo: "ULP", emoji: "⚡" },
{ rotulo: "PSRAM", emoji: "💾" },
{ rotulo: "Strapping Pins", emoji: "🚀" },
{ rotulo: "Deep Sleep", emoji: "🔋" },
{ rotulo: "OTA", emoji: "📡" },
{ rotulo: "Matter", emoji: "🌐" },
{ rotulo: "Secure Boot", emoji: "🔒" },
{ rotulo: "Wi-Fi 6", emoji: "📶" },
{ rotulo: "MIPI", emoji: "✨" },
];
const todosOsTermos = useMemo(
() =>
categoriasGlossario.flatMap((cat) =>
cat.termos.map((t) => ({
...t,
categoriaId: cat.id,
categoriaNome: cat.nome,
categoriaIcone: cat.icone,
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
primeiraLetra: t.termo.charAt(0).toUpperCase(),
}))
),
[]
);
// Mapeamento de ícones por nome
const iconMap = {
Cpu: <Cpu className="w-4 h-4" />,
Database: <Database className="w-4 h-4" />,
Wifi: <Wifi className="w-4 h-4" />,
Sliders: <Sliders className="w-4 h-4" />,
Sparkles: <Sparkles className="w-4 h-4" />,
BatteryCharging: <BatteryCharging className="w-4 h-4" />,
Code2: <Code2 className="w-4 h-4" />,
ShieldCheck: <ShieldCheck className="w-4 h-4" />,
};
// Lista de todos os termos com metadados de categoria
const todosOsTermos = useMemo(() => {
return categoriasGlossario.flatMap((cat) =>
cat.termos.map((t) => ({
...t,
categoriaId: cat.id,
categoriaNome: cat.nome,
categoriaCor: cat.cor,
categoriaIcone: cat.icone,
categoriaEmoji: cat.emoji,
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
primeiraLetra: t.termo.charAt(0).toUpperCase(),
}))
);
}, []);
// Todas as letras do alfabeto de A a Z com indicador de existência de termos
const alfabeto = useMemo(() => {
const letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const setDisponivel = new Set(
todosOsTermos.map((t) => t.termo.charAt(0).toUpperCase())
);
return letras.map((letra) => ({
letra,
temTermos: setDisponivel.has(letra),
}));
const disponiveis = new Set(todosOsTermos.map((t) => t.primeiraLetra));
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letra) => ({ letra, temTermos: disponiveis.has(letra) }));
}, [todosOsTermos]);
// Filtragem dinâmica
const termosFiltrados = useMemo(() => {
const termoBusca = busca.toLowerCase().trim();
return todosOsTermos.filter((item) => {
const matchCategoria =
categoriaSelecionada === "todas" ||
item.categoriaId === categoriaSelecionada;
const matchCategoria = categoriaSelecionada === "todas" || item.categoriaId === categoriaSelecionada;
const matchLetra = !letraFiltro || item.primeiraLetra === letraFiltro;
const matchBusca =
!termoBusca ||
@@ -103,15 +72,11 @@ export default function GlossarioPage() {
item.definicao.toLowerCase().includes(termoBusca) ||
item.exemplo.toLowerCase().includes(termoBusca) ||
item.categoriaNome.toLowerCase().includes(termoBusca);
return matchCategoria && matchLetra && matchBusca;
});
}, [todosOsTermos, categoriaSelecionada, letraFiltro, busca]);
const temFiltroAtivo =
busca.trim() !== "" ||
categoriaSelecionada !== "todas" ||
letraFiltro !== null;
const temFiltroAtivo = busca.trim() !== "" || categoriaSelecionada !== "todas" || letraFiltro !== null;
const limparTodosFiltros = () => {
setBusca("");
@@ -121,365 +86,178 @@ export default function GlossarioPage() {
};
const copiarLinkTermo = (slug) => {
const url = `${window.location.origin}/glossario#${slug}`;
navigator.clipboard.writeText(url);
navigator.clipboard.writeText(`${window.location.origin}/glossario#${slug}`);
setCopiadoSlug(slug);
setTimeout(() => setCopiadoSlug(null), 2000);
};
const catAtivaInfo = categoriasGlossario.find(
(c) => c.id === categoriaSelecionada
);
const total = termosFiltrados.length;
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/30 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 sm:px-6 pt-12 pb-24 max-w-7xl mx-auto">
{/* Hero Section */}
<section className="text-center mb-10 select-none">
<div className="inline-flex items-center gap-2 bg-white dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-5 border border-slate-250 dark:border-slate-800/80 shadow-xs">
<BookOpen className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
Enciclopédia & Conceitos do ESP32
</span>
</div>
<main id="conteudo">
<PageHero
trail={[{ label: "Glossário" }]}
icon={BookOpen}
label={`${todosOsTermos.length} termos explicados`}
title="Glossário técnico do ESP32"
lede="Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
/>
<h1 className="text-3xl sm:text-5xl 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-4 leading-tight tracking-tight">
Glossário Técnico
</h1>
<p className="text-sm sm:text-base text-slate-600 dark:text-slate-400 max-w-2xl mx-auto leading-relaxed font-normal">
Consulte definições diretas, parâmetros de hardware e exemplos práticos dos termos fundamentais do ecossistema ESP32.
</p>
</section>
{/* 1. PAINEL INTEGRADO DE BUSCA & FILTRAGEM (REDESIGN COMPLETO) */}
<section className="mb-10 bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-slate-200 dark:border-slate-800 p-5 sm:p-7 shadow-sm transition-all">
{/* Barra de Pesquisa Principal */}
<div className="relative mb-5">
<div className="absolute left-4 top-1/2 -translate-y-1/2 p-1.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400 pointer-events-none">
<Search className="w-4 h-4 sm:w-5 sm:h-5" />
</div>
<input
ref={searchInputRef}
type="text"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Pesquisar por termo, sigla ou conceito (ex: GPIO, ULP, PSRAM, OTA, Deep Sleep)..."
className="w-full bg-slate-50 dark:bg-slate-950/70 pl-13 sm:pl-14 pr-24 sm:pr-28 py-3.5 sm:py-4 rounded-2xl border border-slate-250 dark:border-slate-800 text-sm sm:text-base font-medium placeholder:text-slate-400 dark:placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all shadow-inner"
/>
{/* Lado Direito da Barra de Pesquisa: Botão Limpar ou Tag de Contagem */}
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
{busca ? (
<button
type="button"
onClick={() => {
setBusca("");
searchInputRef.current?.focus();
}}
className="text-xs px-2.5 py-1 rounded-xl bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-700 font-semibold flex items-center gap-1 transition-colors cursor-pointer"
>
<X className="w-3.5 h-3.5" />
<span>Limpar</span>
</button>
) : (
<span className="hidden sm:inline-flex text-[11px] font-bold text-slate-500 dark:text-slate-400 bg-slate-200/60 dark:bg-slate-800/80 px-2.5 py-1 rounded-lg border border-slate-250 dark:border-slate-700">
{termosFiltrados.length} {termosFiltrados.length === 1 ? "termo" : "termos"}
</span>
)}
</div>
</div>
{/* Atalhos de Pesquisa Rápida (Tags Populares) */}
<div className="flex items-center gap-2 mb-6 overflow-x-auto pb-1.5 scrollbar-none text-xs">
<span className="text-slate-500 dark:text-slate-400 font-bold shrink-0 flex items-center gap-1 text-[11px] uppercase tracking-wider">
<Tag className="w-3 h-3" />
Populares:
</span>
{buscasPopulares.map((item) => (
<button
key={item.rotulo}
type="button"
onClick={() => setBusca(item.rotulo)}
className={`px-2.5 py-1 rounded-lg font-medium transition-all shrink-0 cursor-pointer border ${
busca.toLowerCase() === item.rotulo.toLowerCase()
? "bg-purple-600 text-white border-purple-600 shadow-xs"
: "bg-slate-100/80 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-750 hover:bg-purple-50 dark:hover:bg-purple-950/40 hover:border-purple-300 dark:hover:border-purple-800"
}`}
>
<span className="mr-1">{item.emoji}</span>
<span>{item.rotulo}</span>
</button>
))}
</div>
{/* Seletor de Categorias em Grid/Flex Moderno */}
<div className="space-y-2 mb-5">
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
<span className="flex items-center gap-1.5">
<Filter className="w-3.5 h-3.5 text-purple-500" />
Filtrar por Categoria
</span>
{categoriaSelecionada !== "todas" && (
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className="text-purple-600 dark:text-purple-400 hover:underline cursor-pointer lowercase first-letter:uppercase text-[11px]"
>
Ver todas ({todosOsTermos.length})
</button>
)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-9 gap-2">
{/* Botão Todas */}
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
categoriaSelecionada === "todas"
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
}`}
>
<span className="text-base">✨</span>
<div className="flex items-center justify-between w-full">
<span className="truncate">Todas</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
categoriaSelecionada === "todas"
? "bg-white/20 text-white"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{todosOsTermos.length}
</span>
</div>
</button>
{/* Categorias Individuais */}
{categoriasGlossario.map((cat) => {
const ativa = categoriaSelecionada === cat.id;
return (
<button
key={cat.id}
type="button"
onClick={() => setCategoriaSelecionada(cat.id)}
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
ativa
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
}`}
>
<span className="text-base">{cat.emoji}</span>
<div className="flex items-center justify-between w-full">
<span className="truncate">{cat.nome.split(" ")[0]}</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
ativa
? "bg-white/20 text-white"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{cat.termos.length}
</span>
</div>
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<div className="panel p-5 sm:p-7">
<div className="relative">
<label htmlFor="busca-glossario" className="sr-only">
Buscar termo, sigla ou conceito
</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-glossario"
ref={searchInputRef}
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar termo, sigla ou conceito"
autoComplete="off"
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{busca && (
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
<X className="h-3.5 w-3.5" aria-hidden="true" />
Limpar
</button>
);
})}
</div>
</div>
{/* Scrubber de Índice Alfabético (A-Z) */}
<div className="pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider shrink-0">
<span className="text-[11px]">Índice A-Z:</span>
)}
</div>
</div>
<div className="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-none flex-1 justify-start sm:justify-end">
<button
type="button"
onClick={() => setLetraFiltro(null)}
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
letraFiltro === null
? "bg-purple-600 text-white shadow-xs"
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
}`}
>
Todos
</button>
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="mr-1 text-sm text-muted">Mais buscados</span>
{BUSCAS_POPULARES.map((rotulo) => (
<button
key={rotulo}
type="button"
onClick={() => setBusca(rotulo)}
aria-pressed={busca.toLowerCase() === rotulo.toLowerCase()}
className="chip-btn"
>
{rotulo}
</button>
))}
</div>
{alfabeto.map(({ letra, temTermos }) => {
const ativo = letraFiltro === letra;
if (!temTermos) {
<div className="mt-7 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaSelecionada("todas")} aria-pressed={categoriaSelecionada === "todas"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
Todas
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
</button>
{categoriasGlossario.map((cat) => {
const Icone = ICONES[cat.icone] ?? Cpu;
return (
<span
key={letra}
className="w-6 h-6 flex items-center justify-center text-[11px] font-semibold text-slate-500 dark:text-slate-450 select-none shrink-0"
<button
key={cat.id}
type="button"
onClick={() => setCategoriaSelecionada(cat.id)}
aria-pressed={categoriaSelecionada === cat.id}
className="chip-btn"
>
{letra}
</span>
<Icone className="h-4 w-4" aria-hidden="true" />
{nomeCurto(cat.nome)}
<span className="tabular-nums opacity-80">{cat.termos.length}</span>
</button>
);
}
return (
})}
</div>
</div>
<div className="mt-6 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Ordem alfabética</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label="Filtrar pela letra inicial">
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
Todas
</button>
{alfabeto.map(({ letra, temTermos }) => (
<button
key={letra}
type="button"
onClick={() => setLetraFiltro(ativo ? null : letra)}
className={`w-6 h-6 rounded-md flex items-center justify-center text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
ativo
? "bg-purple-600 text-white scale-110 shadow-xs"
: "text-slate-700 dark:text-slate-300 hover:bg-purple-100 dark:hover:bg-purple-950/60"
}`}
disabled={!temTermos}
onClick={() => setLetraFiltro(letraFiltro === letra ? null : letra)}
aria-pressed={letraFiltro === letra}
className="chip-btn min-w-11 justify-center px-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-surface"
>
{letra}
</button>
);
})}
))}
</div>
</div>
</div>
{/* Fita de Filtros Ativos (Aparece quando algum filtro está ativo) */}
{temFiltroAtivo && (
<div className="mt-4 pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span className="text-slate-500 dark:text-slate-400 font-semibold mr-1">
Filtros ativos:
</span>
{busca && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Busca: <strong>"{busca}"</strong>
<button
type="button"
onClick={() => setBusca("")}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
{categoriaSelecionada !== "todas" && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Categoria: <strong>{catAtivaInfo?.nome}</strong>
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
{letraFiltro && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Letra: <strong>{letraFiltro}</strong>
<button
type="button"
onClick={() => setLetraFiltro(null)}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
</div>
<button
type="button"
onClick={limparTodosFiltros}
className="text-xs font-semibold text-red-600 dark:text-red-400 hover:underline flex items-center gap-1 cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
<span>Redefinir filtros</span>
</button>
</div>
)}
</section>
{/* 2. GRID DE CARDS DO GLOSSÁRIO */}
<section>
{termosFiltrados.length === 0 ? (
<div className="text-center py-16 bg-white dark:bg-slate-900 rounded-3xl border border-slate-250 dark:border-slate-800 shadow-sm p-8">
<HelpCircle className="w-12 h-12 text-slate-400 mx-auto mb-3" />
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">
Nenhum termo encontrado
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1 max-w-md mx-auto">
Não encontramos resultados para sua pesquisa com os filtros selecionados.
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
{total} {total === 1 ? "termo" : "termos"}
{temFiltroAtivo && <span className="font-normal text-muted"> com os filtros atuais</span>}
</h2>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Redefinir filtros
</button>
)}
</div>
{total === 0 ? (
<div className="card mt-6 px-6 py-14 text-center">
<HelpCircle 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 termo encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias.
</p>
<button
type="button"
onClick={limparTodosFiltros}
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold hover:bg-purple-700 cursor-pointer shadow-xs inline-flex items-center gap-1.5"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>Limpar todos os filtros</span>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Limpar filtros
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="mt-6 grid gap-4 md:grid-cols-2">
{termosFiltrados.map((item) => {
const Icone = ICONES[item.categoriaIcone] ?? Cpu;
return (
<article
key={item.slug}
id={item.slug}
className="bg-white dark:bg-slate-900/90 rounded-2xl border border-slate-250 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-500/40 p-5 sm:p-6 transition-all duration-200 shadow-xs flex flex-col justify-between scroll-mt-24 group"
>
<div className="space-y-3">
{/* Header do Card */}
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 text-xs px-2.5 py-0.5 rounded-full font-semibold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
<span>{item.categoriaEmoji}</span>
<span>{item.categoriaNome}</span>
</span>
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
title="Copiar link direto para este termo"
className="text-xs text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 inline-flex items-center gap-1 p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
>
{copiadoSlug === item.slug ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-500 font-semibold text-[11px]">
Copiado!
</span>
</>
) : (
<Link2 className="w-3.5 h-3.5" />
)}
</button>
</div>
{/* Título do Termo */}
<h2 className="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{item.termo}
</h2>
{/* Definição */}
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed font-normal">
<TextoComLinks texto={item.definicao} />
</p>
<article key={item.slug} id={item.slug} className="card flex scroll-mt-28 flex-col p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<span className="chip">
<Icone className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{nomeCurto(item.categoriaNome)}
</span>
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
aria-label={`Copiar link para ${item.termo}`}
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"
>
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
{/* Caixa de Exemplo Prático */}
<div className="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/80">
<div className="p-3.5 rounded-xl bg-purple-50/70 dark:bg-purple-950/20 border border-purple-100 dark:border-purple-900/30 text-xs leading-relaxed space-y-1">
<span className="font-bold text-purple-700 dark:text-purple-400 flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
<Lightbulb className="w-3.5 h-3.5" />
Exemplo Prático
</span>
<p className="text-slate-700 dark:text-slate-300">
<TextoComLinks texto={item.exemplo} />
</p>
</div>
<h3 className="mt-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{item.termo}</h3>
<p className="mt-2 text-[0.9375rem] leading-relaxed text-muted">
<TextoComLinks texto={item.definicao} />
</p>
<div className="callout mt-5">
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
Na prática
</p>
<p className="mt-1">
<TextoComLinks texto={item.exemplo} />
</p>
</div>
</article>
);
@@ -488,41 +266,20 @@ export default function GlossarioPage() {
)}
</section>
{/* 3. CTA & PRÓXIMOS PASSOS */}
<section className="mt-16 bg-gradient-to-br from-slate-900 to-slate-950 text-white rounded-3xl p-6 sm:p-10 border border-slate-800 shadow-xl">
<div className="text-center max-w-2xl mx-auto space-y-4">
<div className="inline-flex items-center gap-2 bg-purple-500/20 text-purple-400 border border-purple-500/30 px-3.5 py-1.5 rounded-full text-xs font-bold">
<Sparkles className="w-4 h-4" />
<span>Continue Explorando</span>
</div>
<h2 className="text-2xl sm:text-3xl font-display font-extrabold">
Pronto para aplicar na prática?
</h2>
<p className="text-xs sm:text-sm text-slate-400 leading-relaxed">
Agora que você domina os conceitos técnicos, compare as especificações das séries ou use o seletor inteligente para encontrar o microcontrolador ideal para seu projeto.
<section className="shell pb-20 md:pb-28">
<div className="panel p-8 text-center sm:p-12">
<h2 className="section-title mx-auto max-w-2xl">Do conceito ao chip certo.</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto.
</p>
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
<Link
href="/series"
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white text-xs font-bold transition-all shadow-md hover:shadow-lg inline-flex items-center gap-2"
>
<span>Explorar Séries ESP32</span>
<ArrowRight className="w-4 h-4" />
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto">
<Sliders className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
</Link>
<Link
href="/seletor"
className="px-5 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold border border-slate-700 transition-all inline-flex items-center gap-2"
>
<span>Seletor Inteligente</span>
</Link>
<Link
href="/diagnostico"
className="px-5 py-2.5 rounded-xl bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-xs font-bold border border-amber-500/40 transition-all inline-flex items-center gap-2"
>
<span>Guia de Diagnóstico & Erros</span>
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
Explorar as séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
@@ -564,7 +321,7 @@ function TextoComLinks({ texto }) {
<Link
key={i}
href={`/series/${parte}`}
className="font-bold text-purple-600 dark:text-purple-400 hover:underline inline-flex items-center mx-0.5"
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
{parte}
</Link>