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

+46 -78
View File
@@ -1,99 +1,67 @@
'use client';
"use client";
import { Award, CheckCircle, HelpCircle, ExternalLink } from 'lucide-react';
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
export default function BoardCard({ board }) {
const mainFields = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
const technicalFields = Object.keys(board).filter(
key => !mainFields.includes(key) && board[key]
);
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
const hasFeature = (value) => {
const yesValues = ['sim', 'opcional'];
return yesValues.includes(value?.toString().toLowerCase().trim());
};
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
// Filtrar apenas características que têm "Sim" ou "Opcional"
const availableFeatures = technicalFields.filter(field => hasFeature(board[field]));
const optionalFeatures = technicalFields.filter(field =>
board[field]?.toString().toLowerCase().trim() === 'opcional'
);
const valor = (campo) => board[campo]?.toString().toLowerCase().trim();
const recursos = technicalFields.filter((campo) => ["sim", "opcional"].includes(valor(campo)));
const lojaOficial = valor("Loja Oficial") === "sim";
return (
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-1.5 transition-all duration-300 overflow-hidden group flex flex-col h-full animate-fadeIn select-none">
{/* Image Container with premium glass background */}
<article className="card card-interactive flex h-full flex-col overflow-hidden">
{board.Imagem && (
<div
className="mx-auto w-full p-4 flex justify-center bg-white dark:bg-white border-b border-slate-200 dark:border-slate-850/40 shrink-0"
<div
className="flex shrink-0 justify-center border-b border-line bg-surface-2 p-4 [&_img]:h-44 [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain"
dangerouslySetInnerHTML={{ __html: board.Imagem }}
/>
)}
<div className="p-6 flex flex-col grow justify-between">
<div>
{/* Loja Oficial Badge */}
{board['Loja Oficial'] && board['Loja Oficial'].toLowerCase() === 'sim' && (
<div className="mb-2">
<span className="inline-flex items-center gap-1 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-wider">
<Award className="w-3.5 h-3.5 shrink-0" />
<span>Loja Oficial</span>
</span>
</div>
)}
<div className="flex grow flex-col p-5">
{lojaOficial && (
<span className="chip chip-accent mb-3 w-fit">
<Award className="h-3.5 w-3.5" aria-hidden="true" />
Loja oficial
</span>
)}
{/* Nome */}
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 mb-4 line-clamp-2 leading-snug group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
{board.Nome || 'Placa Genérica'}
</h3>
<h3 className="line-clamp-2 font-display text-lg font-semibold leading-snug tracking-[-0.02em] text-ink">
{board.Nome || "Placa genérica"}
</h3>
{/* Características Técnicas */}
{availableFeatures.length > 0 && (
<div className="mb-6">
<h4 className="text-[9px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest flex items-center gap-1.5 mb-2.5">
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
<span>Recursos Embarcados</span>
</h4>
<div className="flex flex-wrap gap-1.5">
{availableFeatures.map((field) => {
const isOptional = optionalFeatures.includes(field);
return (
<div
key={field}
className={`text-[9px] px-2 py-0.5 rounded-lg font-bold flex items-center gap-1 ${
isOptional
? 'bg-amber-500/10 dark:bg-amber-500/15 border border-amber-500/20 text-amber-600 dark:text-amber-400'
: 'bg-emerald-500/10 dark:bg-emerald-500/15 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
}`}
>
{isOptional ? (
<HelpCircle className="w-2.5 h-2.5 shrink-0" />
) : (
<CheckCircle className="w-2.5 h-2.5 shrink-0" />
)}
<span>{field}</span>
</div>
);
})}
</div>
</div>
)}
</div>
{recursos.length > 0 && (
<div className="mt-4">
<p className="text-xs font-medium text-muted">Recursos embarcados</p>
<ul className="mt-2 flex flex-wrap gap-1.5">
{recursos.map((campo) => {
const opcional = valor(campo) === "opcional";
return (
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
{opcional ? (
<CircleHelp className="h-3 w-3 text-gold" aria-hidden="true" />
) : (
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
)}
{campo}
{opcional && <span className="sr-only"> (opcional)</span>}
</li>
);
})}
</ul>
</div>
)}
{/* Link de Compra */}
{board.Link && (
<a
href={board.Link}
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-center px-4 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 active:scale-98 transition-all shadow-md mt-auto flex items-center justify-center gap-1.5"
>
<span>Adquirir no AliExpress</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
Ver no AliExpress
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}
</div>
</div>
</article>
);
}
+16
View File
@@ -0,0 +1,16 @@
export default function BookCover({ className = "" }) {
return (
<div
className={`book-cover dark:opacity-100 ${className}`}
role="img"
aria-label="Capa do livro Microcontroladores ESP32: conectividade, eficiência e desempenho, de Lucas Rayan Guerra"
>
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: "url('/livro/capa.jpg')" }}
aria-hidden="true"
/>
<div className="book-spine" aria-hidden="true" />
</div>
);
}
+41 -102
View File
@@ -1,19 +1,18 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { Code, Copy, CheckCircle, ExternalLink, HelpCircle } from 'lucide-react'
import { useEffect, useState } from "react";
import { Check, Copy, ExternalLink } from "lucide-react";
export default function CodeExample({ framework }) {
const examplesUrl = framework?.exemplos;
const color = framework?.cor || '#6B7280';
const [sample, setSample] = useState('');
const [sample, setSample] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
setSample('');
setSample("");
setError(null);
setLoading(false);
@@ -21,12 +20,11 @@ export default function CodeExample({ framework }) {
const exemploPath = framework.exemplo;
if (!exemploPath) {
const msg = `// Exemplos completos: ${examplesUrl || 'Ver repositório'}\n// Abra o link acima para ver exemplos e código completo.\n`;
setSample(msg);
setSample(`// Exemplos completos: ${examplesUrl || "ver repositório"}\n`);
return;
}
const url = exemploPath.startsWith('/') ? exemploPath : `/${exemploPath}`;
const url = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
let cancelled = false;
setLoading(true);
@@ -41,14 +39,14 @@ export default function CodeExample({ framework }) {
if (text && text.trim().length > 0) {
setSample(text);
} else {
setSample(`// Arquivo de exemplo está vazio. Consulte o repositório oficial.\n`);
setError('Arquivo vazio');
setSample("// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n");
setError("arquivo vazio");
}
})
.catch((err) => {
if (cancelled) return;
setSample(`// Não foi possível carregar o exemplo localmente.\n// Consulte o link do GitHub acima para exemplos funcionais.\n`);
setError(err.message || 'Falha ao carregar exemplo');
setSample("// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n");
setError(err.message || "falha ao carregar");
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -59,7 +57,7 @@ export default function CodeExample({ framework }) {
};
}, [framework?.exemplo, framework?.exemplos]);
const lines = (sample || '').split('\n');
const lines = (sample || "").replace(/\n$/, "").split("\n");
async function handleCopy() {
try {
@@ -70,109 +68,50 @@ export default function CodeExample({ framework }) {
}
return (
<div 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/80 shadow-2xl mb-8 animate-fadeIn">
<div className="md:flex items-center justify-between gap-6 mb-6">
<div className="select-none">
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 flex items-center gap-2">
<Code className="w-5 h-5" style={{ color }} />
<span>Demonstração de Código</span>
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 font-semibold">Trecho de inicialização rápida. Copie e personalize em seu projeto.</p>
<div className="card p-5 sm:p-7">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Exemplo de código</h2>
<p className="mt-1 text-sm text-muted">Trecho de partida em {framework?.linguagem}. Ajuste pinos e configuração à sua placa.</p>
</div>
<div className="flex flex-wrap items-center gap-2.5 mt-4 md:mt-0 select-none">
<span
className="inline-flex items-center text-[10px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider"
style={{
backgroundColor: `${color}18`,
color,
border: `1px solid ${color}25`
}}
>
{framework?.linguagem || 'Código'}
</span>
<button
onClick={handleCopy}
className="inline-flex items-center gap-1.5 bg-slate-50 dark:bg-slate-850 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-250 px-3.5 py-2 rounded-xl font-bold text-xs shadow-xs border border-slate-300 dark:border-slate-800/80 cursor-pointer transition active:scale-95"
title="Copiar código para a área de transferência"
>
{copied ? (
<>
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-600 dark:text-emerald-450">Copiado!</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copiar</span>
</>
)}
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={handleCopy} className="btn btn-ghost btn-sm">
{copied ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
<span role="status">{copied ? "Copiado" : "Copiar"}</span>
</button>
{examplesUrl && (
<a
href={examplesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white shadow-lg active:scale-95 transition hover:opacity-95"
style={{
background: `linear-gradient(90deg, ${color} 0%, rgba(147,51,234,1) 100%)`,
boxShadow: `0 8px 20px -8px ${color}`
}}
title="Abrir exemplos no GitHub"
>
<span>Mais Exemplos</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={examplesUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
Mais exemplos
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}
</div>
</div>
<div className="rounded-2xl overflow-hidden border border-slate-300 dark:border-slate-800/80 shadow-md">
<div className="select-none">
<div className="flex items-center justify-between bg-slate-100/80 dark:bg-slate-950/70 border-b border-slate-300 dark:border-slate-850/60 px-4 py-3.5">
<div className="text-xs font-bold text-slate-750 dark:text-slate-250 flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
<span className="ml-2 font-mono text-[10px] text-slate-500 dark:text-slate-400">{framework?.nome} example</span>
</div>
<div className="text-[9px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
{loading ? 'Buscando exemplo...' : (framework?.exemplo ? 'Local Source' : 'External GitHub')}
</div>
</div>
<div className="code-panel mt-5" aria-busy={loading}>
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2.5 text-xs text-[#a6afbd]">
<span className="font-mono">{framework?.nome}</span>
<span>{loading ? "Carregando exemplo…" : framework?.linguagem}</span>
</div>
<div className="flex bg-slate-950">
<pre
className="text-slate-200 p-5 w-full overflow-auto text-xs leading-relaxed scrollbar-thin select-text"
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace' }}
>
<pre tabIndex={0} aria-label={`Código de exemplo em ${framework?.linguagem}`}>
<code>
{lines.map((line, idx) => (
<div key={idx} className="flex">
<span className="text-slate-500 pr-4 text-right w-8 select-none border-r border-slate-900/60 mr-4 font-semibold">{idx + 1}</span>
<code className="whitespace-pre flex-1 text-left">{line === '' ? '\u00A0' : line}</code>
</div>
<span key={idx} className="flex">
<span className="mr-4 w-7 shrink-0 select-none text-right text-[#7d8794]">{idx + 1}</span>
<span className="min-w-0 whitespace-pre text-[#e9eef4]">{line || " "}</span>
</span>
))}
</pre>
</div>
</code>
</pre>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-3.5 mt-3 select-none">
<p className="text-[10px] font-bold text-red-600 dark:text-red-400">
Nota: Não foi possível carregar o arquivo de exemplo local ({error}). Use o botão "Mais Exemplos" para ver os códigos oficiais.
</p>
</div>
)}
<div className="mt-4 flex items-start gap-2 bg-slate-50/50 dark:bg-slate-950/20 border border-slate-150/40 dark:border-slate-850/40 p-4 rounded-2xl select-none">
<HelpCircle className="w-4.5 h-4.5 text-purple-500 shrink-0 mt-0.5" />
<p className="text-[10px] font-semibold text-slate-500 dark:text-slate-400 leading-relaxed">
<strong>Dica de Desenvolvimento:</strong> Personalize o trecho de inicialização com as pinagens e configurações corretas correspondentes à série do seu microcontrolador (ex: ESP32-S3, ESP32-C6). A documentação detalhada da API pode ser acessada na barra superior do framework.
<p role="alert" className="callout callout-warn mt-4">
Não foi possível carregar o exemplo local ({error}). Use “Mais exemplos” para ver o código oficial.
</p>
</div>
)}
</div>
);
}
}
+25
View File
@@ -0,0 +1,25 @@
export default function CodeSample({ label, language, code, note }) {
const lines = code.replace(/\n$/, "").split("\n");
return (
<figure className="min-w-0">
<figcaption className="mb-2.5 flex flex-wrap items-center justify-between gap-2 px-1">
<span className="text-xs font-semibold text-ink">{label}</span>
<span className="font-mono text-[0.6875rem] text-muted">{language}</span>
</figcaption>
<div className="code-panel rounded-2xl shadow-[0_18px_40px_-24px_rgba(0,0,0,0.5)]">
<pre>
<code>
{lines.map((line, index) => (
<span key={`${index}-${line}`} className="flex">
<span className="mr-4 w-6 shrink-0 select-none text-right text-[#7d8794]">{index + 1}</span>
<span className="min-w-0 whitespace-pre text-[#e9eef4]">{line || " "}</span>
</span>
))}
</code>
</pre>
</div>
{note && <p className="mt-2.5 px-1 text-xs leading-relaxed text-muted">{note}</p>}
</figure>
);
}
+266 -586
View File
@@ -1,22 +1,14 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import {
Search,
X,
Grid,
List,
Filter,
AlertTriangle,
Info,
Tag,
Zap,
Sliders,
CheckCircle,
ExternalLink
} from "lucide-react";
import { AlertTriangle, CheckCircle, List, Grid, Search, Sliders, X, Zap } from "lucide-react";
/*
* As cores abaixo são dado de visualização: identificam o tipo de função do
* pino no diagrama. O texto nunca fica sobre elas; elas aparecem só como borda
* ou ponto, e o rótulo usa as cores do tema (contraste AAA).
*/
const FILTER_CATEGORIES = [
{ id: "All", name: "Todos", color: "#8b5cf6" }, // purple
{ id: "GND", name: "Terra (GND)", color: "#64748b" }, // slate
@@ -52,11 +44,11 @@ const type_colors = {
export default function ConnectionsDiagram({ connections, serie }) {
const [activeFilter, setActiveFilter] = useState("All");
const [selectedConnection, setSelectedConnection] = useState(null);
const [hoveredConnection, setHoveredConnection] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState("grid");
const [showTooltip, setShowTooltip] = useState(null);
const [mounted, setMounted] = useState(false);
const dialogRef = useRef(null);
const gatilhoRef = useRef(null);
useEffect(() => {
setMounted(true);
@@ -64,7 +56,8 @@ export default function ConnectionsDiagram({ connections, serie }) {
const isConnectionVisible = (connection) => {
const matchesFilter = activeFilter === "All" || connection.categorias.includes(activeFilter);
const matchesSearch = searchQuery === "" ||
const matchesSearch =
searchQuery === "" ||
connection.nome.toLowerCase().includes(searchQuery.toLowerCase()) ||
connection.numero.toString().toLowerCase().includes(searchQuery.toLowerCase());
return matchesFilter && matchesSearch;
@@ -75,641 +68,328 @@ export default function ConnectionsDiagram({ connections, serie }) {
return filter?.color || "#64748b";
};
const visibleConnections = useMemo(() => {
return connections.filter(isConnectionVisible);
}, [connections, activeFilter, searchQuery]);
const nomeCategoria = (cat) => (cat === "Strapping" ? "Inicialização" : cat);
const activeCategories = useMemo(() => {
if (activeFilter === "All") return [];
return FILTER_CATEGORIES.filter(cat =>
visibleConnections.some(conn => conn.categorias.includes(cat.id))
);
}, [visibleConnections, activeFilter]);
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery]);
const abrir = (connection, evento) => {
gatilhoRef.current = evento.currentTarget;
setSelectedConnection(connection);
};
const fechar = () => {
setSelectedConnection(null);
gatilhoRef.current?.focus?.();
};
// Diálogo: Esc fecha, o foco entra nele, fica preso enquanto aberto e volta ao pino de origem.
useEffect(() => {
const handleEscape = (e) => {
if (!selectedConnection) return;
const dialogo = dialogRef.current;
const focaveis = () => dialogo?.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])') ?? [];
focaveis()[0]?.focus();
const aoTeclar = (e) => {
if (e.key === "Escape") {
setSelectedConnection(null);
setShowTooltip(null);
fechar();
return;
}
if (e.key !== "Tab") return;
const itens = [...focaveis()];
if (itens.length === 0) return;
const primeiro = itens[0];
const ultimo = itens[itens.length - 1];
if (e.shiftKey && document.activeElement === primeiro) {
e.preventDefault();
ultimo.focus();
} else if (!e.shiftKey && document.activeElement === ultimo) {
e.preventDefault();
primeiro.focus();
}
};
if (selectedConnection) {
document.addEventListener("keydown", handleEscape);
document.body.style.overflow = "hidden";
}
document.addEventListener("keydown", aoTeclar);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "unset";
document.removeEventListener("keydown", aoTeclar);
document.body.style.overflow = "";
};
}, [selectedConnection]);
useEffect(() => {
const handleKeyDown = (e) => {
if (selectedConnection) return;
if (e.key === "Tab") {
const buttons = document.querySelectorAll('[data-connection-button]');
const currentIndex = Array.from(buttons).findIndex(btn => btn === document.activeElement);
if (e.shiftKey && currentIndex > 0) {
e.preventDefault();
buttons[currentIndex - 1].focus();
} else if (!e.shiftKey && currentIndex < buttons.length - 1) {
e.preventDefault();
buttons[currentIndex + 1].focus();
}
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [selectedConnection]);
if (!connections || connections.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="w-20 h-20 mb-6 rounded-2xl bg-slate-100 dark:bg-slate-900 border border-slate-300 dark:border-slate-800 flex items-center justify-center shadow-lg">
<Sliders className="w-10 h-10 text-slate-500 dark:text-slate-400" />
</div>
<p className="text-lg font-bold text-slate-800 dark:text-slate-200">
Diagrama de conexões indisponível
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Este modelo ainda não possui um mapa detalhado de conexões e barramentos.
</p>
<div className="px-4 py-14 text-center">
<Sliders className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<p className="mt-4 font-display text-xl font-semibold text-ink">Diagrama de conexões indisponível</p>
<p className="mt-1 text-sm text-muted">Este modelo ainda não tem um mapa detalhado de conexões e barramentos.</p>
</div>
);
}
const numeroCurto = (c) => (typeof c.numero === "number" ? c.numero : String(c.numero).substring(0, 4));
return (
<div className="space-y-8 pb-8">
{/* Header info */}
<div className="space-y-3">
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-4 pb-3 border-b border-slate-300 dark:border-slate-800/65 flex items-center gap-3 select-none">
<Sliders className="w-5 h-5" style={{ color: serie.color }} />
<span>Diagrama de Conexões Interativo</span>
</h2>
<p className="text-sm text-slate-550 dark:text-slate-400 leading-relaxed">
Explore todas as conexões e multiplexação do SoC do {serie.nome_completo}. Utilize o painel de busca rápida ou filtre por categoria para acender conexões específicas e ver mapas de barramentos.
<div className="space-y-6">
<div>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Diagrama de conexões interativo</h2>
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">
Todas as conexões e a multiplexação do {serie.nome_completo}. Busque por nome ou número, filtre por função e abra um pino para ver os detalhes.
</p>
</div>
{/* Search Panel */}
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 p-5 space-y-4 shadow-xl">
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-slate-500 dark:text-slate-400" />
<input
type="text"
placeholder="Buscar por nome ou número da conexão (Ex: GPIO1, TXD, XTAL)..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-10 py-3 rounded-xl border border-slate-300 dark:border-slate-800 bg-white dark:bg-slate-950/40 focus:border-purple-500/80 focus:ring-4 focus:ring-purple-500/10 text-slate-800 dark:text-slate-200 transition-all outline-none text-sm font-semibold"
aria-label="Search connections"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
)}
</div>
{/* View Mode Toggle */}
<div className="flex gap-1.5 bg-slate-100 dark:bg-slate-950/80 rounded-xl p-1 shrink-0 border border-slate-250/30 dark:border-slate-850/30">
<button
onClick={() => setViewMode("grid")}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
viewMode === "grid"
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
}`}
aria-label="Grid view"
aria-pressed={viewMode === "grid"}
>
<Grid className="w-3.5 h-3.5" />
<span>Grade</span>
</button>
<button
onClick={() => setViewMode("list")}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
viewMode === "list"
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
}`}
aria-label="List view"
aria-pressed={viewMode === "list"}
>
<List className="w-3.5 h-3.5" />
<span>Lista</span>
</button>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<label htmlFor="busca-pino" className="sr-only">Buscar conexão por nome ou número</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-pino"
type="search"
placeholder="Buscar pino (ex.: GPIO1, TXD, XTAL)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
className="field pl-11 pr-4 text-sm"
/>
</div>
<div role="group" aria-label="Modo de exibição" className="flex gap-2">
<button type="button" onClick={() => setViewMode("grid")} aria-pressed={viewMode === "grid"} className="chip-btn">
<Grid className="h-4 w-4" aria-hidden="true" />
Grade
</button>
<button type="button" onClick={() => setViewMode("list")} aria-pressed={viewMode === "list"} className="chip-btn">
<List className="h-4 w-4" aria-hidden="true" />
Lista
</button>
</div>
</div>
{/* Filters Panel */}
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 p-5 shadow-xl">
<div className="flex items-center gap-3 mb-5 border-b border-slate-150/40 dark:border-slate-850/45 pb-3">
<Filter className="w-4 h-4 text-purple-500" />
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-100 select-none">
Filtre por Função do Pino
</h4>
<span className="ml-auto text-[10px] font-bold text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-950 px-3 py-1 rounded-full border border-slate-300 dark:border-slate-850/30 shadow-xs select-none">
{visibleConnections.length} de {connections.length} ativos
</span>
<div>
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-ink">Função do pino</h3>
<p role="status" aria-live="polite" className="text-sm text-muted">
{visibleConnections.length} de {connections.length} pinos
</p>
</div>
<div className="flex flex-wrap gap-2">
<div role="group" aria-label="Filtrar por função" className="mt-3 flex flex-wrap gap-2">
{FILTER_CATEGORIES.map((filter) => {
const isActive = activeFilter === filter.id;
const count = connections.filter(c =>
filter.id === "All" ? true : c.categorias.includes(filter.id)
).length;
if (count === 0 && !isActive) return null; // Hide filters that don't apply to this chip
const count = connections.filter((c) => (filter.id === "All" ? true : c.categorias.includes(filter.id))).length;
if (count === 0 && !isActive) return null; // filtros que não se aplicam a este chip
return (
<button
key={filter.id}
onClick={() => setActiveFilter(filter.id)}
aria-pressed={isActive}
aria-label={`Filter by ${filter.name}`}
className={`group relative px-4 py-2 rounded-xl text-xs font-extrabold transition-all duration-300 focus:outline-none cursor-pointer active:scale-95 ${
isActive
? "shadow-lg text-white"
: "bg-slate-50/70 dark:bg-slate-950/20 text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-850/30 border border-slate-300 dark:border-slate-850/40"
}`}
style={{
backgroundColor: isActive ? filter.color : undefined,
boxShadow: isActive ? `0 8px 15px -8px ${filter.color}` : undefined
}}
>
<span className="flex items-center gap-2">
<span>{filter.name}</span>
<span className={`text-[9px] px-1.5 py-0.5 rounded-full shrink-0 font-bold ${
isActive ? "bg-white/20 text-white" : "bg-slate-200/50 dark:bg-slate-800/80 text-slate-600 dark:text-slate-400"
}`}>
{count}
</span>
</span>
<button key={filter.id} type="button" onClick={() => setActiveFilter(filter.id)} aria-pressed={isActive} className="chip-btn">
{filter.id !== "All" && <span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: filter.color }} aria-hidden="true" />}
{filter.name}
<span className="tabular-nums opacity-80">{count}</span>
</button>
);
})}
</div>
{/* Active Categories badges */}
{activeFilter !== "All" && activeCategories.length > 0 && (
<div className="mt-5 pt-5 border-t border-slate-200 dark:border-slate-850/40 select-none">
<p className="text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-3 uppercase tracking-wider">Subcategorias ativadas por esse filtro:</p>
<div className="flex flex-wrap gap-2">
{activeCategories.map(cat => (
<span
key={cat.id}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-[10px] font-bold text-white shadow-xs"
style={{ backgroundColor: cat.color }}
>
<span className="w-1.5 h-1.5 rounded-full bg-white shrink-0"></span>
<span>{cat.name}</span>
</span>
))}
</div>
</div>
)}
</div>
{/* Diagram Workspace */}
{visibleConnections.length === 0 ? (
<div className="bg-slate-50/50 dark:bg-slate-900/10 backdrop-blur-xl rounded-2xl p-12 border-2 border-dashed border-slate-300 dark:border-slate-850/80 text-center shadow-inner">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 dark:bg-slate-950 flex items-center justify-center shadow-md">
<Search className="w-6 h-6 text-slate-400" />
</div>
<p className="text-base font-bold text-slate-800 dark:text-slate-200 mb-1 select-none">Nenhuma conexão encontrada</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Tente ajustar seus termos de pesquisa ou limpe os filtros rápidos.</p>
<button
onClick={() => {
setActiveFilter("All");
setSearchQuery("");
}}
className="mt-5 px-6 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl hover:opacity-90 transition-all font-bold text-xs uppercase tracking-wider cursor-pointer active:scale-95 shadow-lg"
>
Limpar Filtros e Busca
<div className="rounded-2xl border border-dashed border-line-strong px-6 py-12 text-center">
<Search className="mx-auto h-8 w-8 text-muted" aria-hidden="true" />
<p className="mt-3 font-semibold text-ink">Nenhuma conexão encontrada</p>
<p className="mt-1 text-sm text-muted">Ajuste a busca ou volte para todas as funções.</p>
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); }} className="btn btn-primary btn-sm mt-5">
Limpar busca e filtros
</button>
</div>
) : viewMode === "grid" ? (
/* Grid View Mode: Futuristic Electronic Chip Sockets */
<div className="bg-slate-100/50 dark:bg-slate-950/20 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-850/60 shadow-inner">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{visibleConnections.map((connection, index) => {
const isHovered = hoveredConnection === index;
const connectionId = `connection-${index}`;
const baseColor = type_colors[connection.tipo] || "#94a3b8";
return (
<div key={index} className="relative">
<button
id={connectionId}
data-connection-button
onClick={() => setSelectedConnection(connection)}
onMouseEnter={() => {
setHoveredConnection(index);
setShowTooltip(index);
}}
onMouseLeave={() => {
setHoveredConnection(null);
setShowTooltip(null);
}}
onFocus={() => setHoveredConnection(index)}
onBlur={() => setHoveredConnection(null)}
aria-label={`Pino ${connection.nome}, número ${connection.numero}`}
aria-describedby={showTooltip === index ? `tooltip-${index}` : undefined}
className="w-full flex flex-col items-center justify-center p-4 rounded-2xl border-2 transition-all duration-300 cursor-pointer active:scale-95 focus:outline-none"
style={{
backgroundColor: isHovered ? `${baseColor}24` : `${baseColor}0c`,
borderColor: isHovered ? baseColor : `${baseColor}40`,
boxShadow: isHovered ? `0 0 20px -5px ${baseColor}60` : 'none',
transform: isHovered ? "translateY(-4px)" : undefined,
}}
<ul className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
{visibleConnections.map((connection) => {
const baseColor = type_colors[connection.tipo] || "#94a3b8";
return (
<li key={`${connection.numero}-${connection.nome}`} className="relative">
<button
type="button"
onClick={(e) => abrir(connection, e)}
aria-haspopup="dialog"
aria-label={`Pino ${connection.nome}, número ${connection.numero}${connection.avisos ? ", com restrições de uso" : ""}`}
className="flex w-full flex-col items-center rounded-xl border border-line bg-surface p-3 transition-colors hover:bg-surface-2"
>
<span
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink"
style={{ borderColor: baseColor }}
>
{/* Pad Contact Core Circle */}
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white font-extrabold text-xs mb-3 shadow-lg select-none"
style={{ backgroundColor: baseColor, boxShadow: `0 4px 10px -3px ${baseColor}` }}
>
{typeof connection.numero === "number"
? connection.numero
: connection.numero.substring(0, 4)}
</div>
{/* Pad Name label */}
<div className="text-xs font-bold text-center text-slate-850 dark:text-slate-200 leading-tight mb-2 select-none group-hover:text-purple-600 transition-colors">
{connection.nome}
</div>
{/* Small Category dots indicator */}
<div className="flex flex-wrap gap-1 justify-center select-none">
{connection.categorias.slice(0, 5).map((cat) => (
<span
key={cat}
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ backgroundColor: getCategoryColor(cat) }}
title={cat === "Strapping" ? "Inicialização" : cat}
/>
))}
{connection.categorias.length > 5 && (
<span className="text-[9px] text-slate-500 dark:text-slate-400 font-bold ml-0.5 leading-none">
+{connection.categorias.length - 5}
{numeroCurto(connection)}
</span>
<span className="mt-2 break-all text-center text-sm font-medium leading-tight text-ink">{connection.nome}</span>
<span className="mt-2 flex flex-wrap justify-center gap-1" aria-hidden="true">
{connection.categorias.slice(0, 5).map((cat) => (
<span key={cat} className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} />
))}
</span>
</button>
{connection.avisos && (
<span className="pointer-events-none absolute -right-1.5 -top-1.5 grid h-6 w-6 place-items-center rounded-full border-2 border-surface bg-danger-soft text-danger" aria-hidden="true">
<AlertTriangle className="h-3 w-3" />
</span>
)}
</li>
);
})}
</ul>
) : (
<div className="overflow-x-auto rounded-xl border border-line">
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
<caption className="sr-only">Conexões do {serie.nome_completo}</caption>
<thead>
<tr className="border-b border-line bg-surface-2 text-muted">
<th scope="col" className="px-4 py-3 font-medium">Pino</th>
<th scope="col" className="px-4 py-3 font-medium">Nome</th>
<th scope="col" className="px-4 py-3 font-medium">Funções</th>
<th scope="col" className="px-4 py-3 font-medium">Uso</th>
</tr>
</thead>
<tbody className="divide-y divide-line">
{visibleConnections.map((connection) => {
const baseColor = type_colors[connection.tipo] || "#94a3b8";
return (
<tr key={`${connection.numero}-${connection.nome}`} className="hover:bg-surface-2/60">
<td className="px-4 py-2.5">
<button
type="button"
onClick={(e) => abrir(connection, e)}
aria-haspopup="dialog"
aria-label={`Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink hover:bg-surface-2"
style={{ borderColor: baseColor }}
>
{numeroCurto(connection)}
</button>
</td>
<td className="px-4 py-2.5 font-medium text-ink">{connection.nome}</td>
<td className="max-w-md px-4 py-2.5 text-muted">
<span className="line-clamp-2">{connection.funcoes.join(", ")}</span>
</td>
<td className="whitespace-nowrap px-4 py-2.5">
{connection.avisos ? (
<span className="inline-flex items-center gap-1.5 font-medium text-danger">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
Restrito
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-ok">
<CheckCircle className="h-4 w-4" aria-hidden="true" />
Livre
</span>
)}
</div>
{/* High-Voltage warnings tags */}
{connection.avisos && (
<div
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 shadow-lg ring-2 ring-white dark:ring-slate-950 animate-pulse"
aria-label="Possui restrições de uso"
>
<AlertTriangle className="w-3 h-3" />
</div>
)}
</button>
{/* Sleek Tooltip Popover */}
{showTooltip === index && (
<div
id={`tooltip-${index}`}
role="tooltip"
className="absolute z-40 bottom-full left-1/2 -translate-x-1/2 mb-3 p-3 bg-slate-950/90 dark:bg-slate-900/90 backdrop-blur-md text-white text-[10px] rounded-xl shadow-2xl max-w-xs pointer-events-none animate-fadeIn border border-slate-850/50"
style={{ minWidth: "180px" }}
>
<div className="font-extrabold text-sm mb-1">{connection.nome}</div>
<div className="text-slate-400 mb-2 font-semibold">Pino Físico {connection.numero}</div>
<div className="flex flex-wrap gap-1 mb-2">
{connection.categorias.map((cat) => (
<span
key={cat}
className="px-1.5 py-0.5 rounded text-[8px] font-bold"
style={{
backgroundColor: `${getCategoryColor(cat)}24`,
color: getCategoryColor(cat)
}}
>
{cat === "Strapping" ? "Inicialização" : cat}
</span>
))}
</div>
<div className="text-purple-400 font-bold text-[9px] uppercase tracking-wider">Clique para detalhes</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-slate-950/95 dark:border-t-slate-900/95"></div>
</div>
)}
</div>
);
})}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
/* List View Mode: Interactive Zebra Table */
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 overflow-hidden shadow-xl">
<div className="overflow-x-auto scrollbar-thin">
<table className="w-full text-left">
<thead>
<tr className="bg-slate-100/50 dark:bg-slate-950/50 border-b border-slate-300 dark:border-slate-850/60 select-none">
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Pino</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Nome do Pad</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Categorias</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Multiplexação de Funções</th>
<th className="px-6 py-4 text-center text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Restrições</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-850/30">
{visibleConnections.map((connection, index) => {
const baseColor = type_colors[connection.tipo] || "#94a3b8";
return (
<tr
key={index}
onClick={() => setSelectedConnection(connection)}
className="hover:bg-purple-500/5 dark:hover:bg-purple-400/5 cursor-pointer transition-colors group"
>
<td className="px-6 py-4 whitespace-nowrap">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-extrabold text-xs shadow-md select-none"
style={{ backgroundColor: baseColor }}
>
{typeof connection.numero === "number"
? connection.numero
: connection.numero.substring(0, 4)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-bold text-slate-800 dark:text-slate-200 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{connection.nome}
</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-wrap gap-1.5 select-none">
{connection.categorias.map((cat) => (
<span
key={cat}
className="inline-flex items-center px-2 py-0.5 rounded-md text-[9px] font-bold text-white shadow-xs"
style={{ backgroundColor: getCategoryColor(cat) }}
>
{cat === "Strapping" ? "Inicialização" : cat}
</span>
))}
</div>
</td>
<td className="px-6 py-4">
<div className="text-xs text-slate-500 dark:text-slate-400 font-semibold max-w-md line-clamp-2 leading-relaxed">
{connection.funcoes.join(", ")}
</div>
</td>
<td className="px-6 py-4 text-center whitespace-nowrap">
{connection.avisos ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-red-500/10 text-red-600 dark:text-red-400 text-[10px] font-bold uppercase border border-red-500/20 select-none">
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
<span>Restrito</span>
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold uppercase border border-emerald-500/20 select-none">
<CheckCircle className="w-3.5 h-3.5 shrink-0" />
<span>Livre</span>
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
{connections.some((c) => c.avisos) && (
<div className="callout callout-warn flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Notas sobre a pinagem</p>
<ul className="mt-1 list-disc space-y-1 pl-5">
<li>Pinos com o ícone de alerta têm papel especial no boot ou limitações analógicas. Abra o pino para ver os detalhes.</li>
<li>Isole os circuitos de toque capacitivo de pinos com ruído de RF acoplado.</li>
</ul>
</div>
</div>
)}
{/* Global Warning notice */}
{connections.some(c => c.avisos) && (
<div className="bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-2xl p-6 shadow-sm select-none">
<div className="flex gap-4">
<div className="shrink-0">
<div className="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center">
<AlertTriangle className="w-6 h-6" />
</div>
</div>
<div className="flex-1">
<p className="text-base font-bold text-amber-850 dark:text-amber-300 mb-1">
Notas Importantes sobre a Pinagem
</p>
<ul className="space-y-1.5 text-xs text-amber-800 dark:text-amber-400 leading-relaxed">
<li>• Pinos contendo tags de <strong>Avisos/Restrições</strong> (cor vermelha) possuem papéis especiais de Strapping na inicialização, ou limitações analógicas. Clique neles para ver detalhes.</li>
<li>• Sempre garanta isolamento de circuitos capacitivos de toque em pinos com ruído de RF acoplado.</li>
</ul>
</div>
</div>
</div>
)}
{/* Interactive Overlay Dialog Details Sheet */}
{selectedConnection && mounted && createPortal(
<div
className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fadeIn"
onClick={() => setSelectedConnection(null)}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="dialog-scrim fixed inset-0 z-50 flex items-center justify-center bg-code/80 p-4 backdrop-blur-sm" onClick={fechar}>
<div
className="bg-white/95 dark:bg-slate-950/95 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col animate-slideUp"
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="dialog-panel flex max-h-[85vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-line bg-surface shadow-[var(--shadow)]"
onClick={(e) => e.stopPropagation()}
>
{/* Dialog Header */}
<div className="sticky top-0 bg-white/50 dark:bg-slate-950/50 border-b border-slate-300 dark:border-slate-850/60 p-6 md:p-8 z-10">
<div className="flex items-start justify-between gap-6">
<div className="flex items-center gap-4 flex-1">
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-extrabold text-lg shadow-xl shrink-0"
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
>
{typeof selectedConnection.numero === "number"
? selectedConnection.numero
: selectedConnection.numero.substring(0, 4)}
</div>
<div className="min-w-0">
<h3 id="modal-title" className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 leading-none tracking-tight">
{selectedConnection.nome}
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
Pino Físico {selectedConnection.numero}
</p>
</div>
</div>
<button
onClick={() => setSelectedConnection(null)}
aria-label="Fechar"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 bg-slate-100 dark:bg-slate-900 rounded-xl shrink-0 cursor-pointer active:scale-95"
<div className="flex items-start justify-between gap-4 border-b border-line p-5 sm:p-7">
<div className="flex min-w-0 items-center gap-4">
<span
className="grid h-14 w-14 shrink-0 place-items-center rounded-full border-2 bg-surface text-lg font-semibold tabular-nums text-ink"
style={{ borderColor: type_colors[selectedConnection.tipo] || "#64748b" }}
>
<X className="w-5 h-5" />
</button>
{numeroCurto(selectedConnection)}
</span>
<div className="min-w-0">
<h3 id="modal-title" className="break-words font-display text-2xl font-semibold tracking-[-0.03em] text-ink">
{selectedConnection.nome}
</h3>
<p className="text-sm text-muted">Pino físico {selectedConnection.numero}</p>
</div>
</div>
<button type="button" onClick={fechar} aria-label="Fechar detalhes do pino" className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
{/* Dialog Body Contents */}
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-8 scrollbar-thin">
{/* Categories */}
<div className="flex-1 space-y-6 overflow-y-auto p-5 sm:p-7">
{selectedConnection.categorias.length > 0 && (
<div>
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
<Tag className="w-4 h-4 text-purple-500" />
<span>Categorias de Multiplexação</span>
</h4>
<div className="flex flex-wrap gap-2">
<section>
<h4 className="text-sm font-semibold text-ink">Categorias</h4>
<ul className="mt-3 flex flex-wrap gap-2">
{selectedConnection.categorias.map((cat) => (
<span
key={cat}
className="px-3 py-1.5 rounded-xl text-xs font-bold text-white shadow-md select-none"
style={{ backgroundColor: getCategoryColor(cat) }}
>
{cat === "Strapping" ? "Inicialização" : cat}
</span>
))}
</div>
</div>
)}
{/* Power Source */}
{selectedConnection.alimentacao && (
<div>
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
<Zap className="w-4 h-4 text-red-500" />
<span>Domínio de Alimentação Interna</span>
</h4>
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-300 dark:border-slate-850/50">
<ul className="space-y-2.5">
{selectedConnection.alimentacao.map((source, index) => (
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
<span
className="shrink-0 w-2 h-2 rounded-full"
style={{ backgroundColor: type_colors["power"] }}
/>
<span>{source}</span>
</li>
))}
</ul>
</div>
</div>
)}
{/* Function Multiplexation */}
<div>
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
<Sliders className="w-4 h-4 text-emerald-500" />
<span>Barramentos de Funções Disponíveis</span>
</h4>
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-300 dark:border-slate-850/50">
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{selectedConnection.funcoes.map((func, index) => (
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
<span
className="shrink-0 w-2 h-2 rounded-full"
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
/>
<span>{func}</span>
<li key={cat} className="chip">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} aria-hidden="true" />
{nomeCategoria(cat)}
</li>
))}
</ul>
</div>
</div>
</section>
)}
{selectedConnection.alimentacao && (
<section>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Zap className="h-4 w-4 text-danger" aria-hidden="true" />
Domínio de alimentação
</h4>
<ul className="tile mt-3 space-y-2">
{selectedConnection.alimentacao.map((source) => (
<li key={source} className="text-sm text-ink-soft">{source}</li>
))}
</ul>
</section>
)}
<section>
<h4 className="text-sm font-semibold text-ink">Funções disponíveis</h4>
<ul className="tile mt-3 grid gap-2.5 sm:grid-cols-2">
{selectedConnection.funcoes.map((func) => (
<li key={func} className="flex items-center gap-2.5 text-sm text-ink-soft">
<span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }} aria-hidden="true" />
{func}
</li>
))}
</ul>
</section>
{/* Alerts/Warnings */}
{selectedConnection.avisos && (
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-5 shadow-xs">
<div className="flex gap-4">
<div className="shrink-0">
<div className="w-10 h-10 rounded-xl bg-red-500/15 border border-red-500/25 flex items-center justify-center text-red-500">
<AlertTriangle className="w-5 h-5" />
</div>
</div>
<div className="flex-1">
<p className="text-sm font-bold text-red-900 dark:text-red-300 mb-2">
Avisos Importantes de Engenharia
</p>
{selectedConnection.avisos.map((aviso, index) => (
<div key={index} className="flex items-start gap-2.5 mb-1.5 text-xs text-red-800 dark:text-red-400 font-semibold leading-relaxed">
<span>•</span>
<span>{aviso}</span>
</div>
))}
</div>
</div>
</div>
<section className="callout callout-danger">
<h4 className="flex items-center gap-2 font-semibold text-ink">
<AlertTriangle className="h-4 w-4 text-danger" aria-hidden="true" />
Avisos de engenharia
</h4>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
{selectedConnection.avisos.map((aviso) => (
<li key={aviso}>{aviso}</li>
))}
</ul>
</section>
)}
</div>
{/* Dialog Footer */}
<div className="sticky bottom-0 bg-white/50 dark:bg-slate-950/50 border-t border-slate-300 dark:border-slate-850/60 p-5">
<button
onClick={() => setSelectedConnection(null)}
className="w-full py-3.5 rounded-xl font-bold text-white text-xs uppercase tracking-wider shadow-lg hover:opacity-90 active:scale-98 transition-all cursor-pointer"
style={{
backgroundColor: serie.color,
boxShadow: `0 8px 25px -8px ${serie.color}`
}}
aria-label="Close modal"
>
Confirmar e Fechar
</button>
<div className="border-t border-line p-4 sm:px-7">
<button type="button" onClick={fechar} className="btn btn-primary w-full">Fechar</button>
</div>
</div>
</div>,
document.body
)}
<style jsx>{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-fadeIn {
animation: fadeIn 0.2s ease-out;
}
.animate-slideUp {
animation: slideUp 0.3s ease-out;
}
`}</style>
</div>
);
}
}
+145 -143
View File
@@ -1,164 +1,166 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { ExternalLink } from "lucide-react";
import { ArrowUpRight } from "lucide-react";
import { SITE } from "@/lib/seo";
const COLUMNS = [
{
title: "Explorar",
links: [
{ href: "/series", label: "Séries ESP32" },
{ href: "/comparacao", label: "Comparador" },
{ href: "/catalogo", label: "Catálogo de placas" },
{ href: "/livro", label: "O livro" },
],
},
{
title: "Ferramentas",
links: [
{ href: "/seletor", label: "Seletor inteligente" },
{ href: "/diagnostico", label: "Diagnóstico" },
{ href: "/componentes", label: "Component Registry" },
{ href: "/frameworks", label: "Frameworks e SDKs" },
],
},
{
title: "Projeto",
links: [
{ href: "/sobre", label: "Sobre o ESPDocs" },
{ href: "/glossario", label: "Glossário" },
{ href: SITE.repositorio, label: "Repositório", external: true },
{ href: `${SITE.repositorio}/issues`, label: "Reportar um problema", external: true },
],
},
];
export default function Footer() {
const [year, setYear] = useState(2026);
useEffect(() => {
setYear(new Date().getFullYear());
}, []);
const year = new Date().getFullYear();
return (
<footer role="contentinfo" aria-label="Rodapé do site ESPDocs" className="bg-slate-50 dark:bg-slate-950 text-slate-600 dark:text-slate-400 mt-24 border-t border-slate-250/30 dark:border-slate-800/80">
<div className="max-w-7xl mx-auto px-6 py-12 md:py-16">
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-16">
{/* Coluna 1: Logo & Info */}
<div className="space-y-4">
<h3 className="text-2xl font-display font-bold text-slate-800 dark:text-slate-100">
ESPDocs
</h3>
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Plataforma brasileira e independente que centraliza e organiza informações técnicas sobre o ecossistema ESP32. Focada em clareza, usabilidade e agilidade no desenvolvimento de hardware.
<footer className="mt-20 border-t border-line md:mt-28" aria-label="Rodapé do ESPDocs">
<div className="shell py-14 md:py-20">
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]">
<div>
<Link href="/" className="inline-flex min-h-11 items-center gap-2.5" aria-label="ESPDocs, página inicial">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/marca/espdocs-marca.svg" alt="" aria-hidden="true" width={28} height={28} className="h-7 w-7" />
<span className="font-display text-2xl font-semibold tracking-[-0.045em] text-ink">ESPDocs</span>
</Link>
<p className="mt-5 max-w-sm text-sm leading-relaxed text-muted">
Documentação independente do ecossistema ESP32, escrita em português brasileiro.
Dados públicos, curadoria editorial e links para as fontes oficiais.
</p>
<div className="pt-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
aria-label="Ver repositório do ESPDocs no GitHub (abre em nova aba)"
className="inline-flex items-center gap-2 bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all cursor-pointer hover:shadow-xs active:scale-[0.98]"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
<span>Ver no GitHub</span>
<Link
href="/livro"
className="group mt-7 grid grid-cols-[3rem_1fr_auto] items-center gap-3.5 rounded-2xl border border-line bg-surface p-3.5 transition-colors duration-300 hover:border-line-strong"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={SITE.livroCapa}
alt=""
aria-hidden="true"
width={48}
height={72}
className="h-[72px] w-12 rounded-md object-cover shadow-md"
/>
<span className="min-w-0">
<span className="text-xs font-semibold text-accent">Livro novo</span>
<span className="mt-0.5 block font-display text-base font-semibold leading-tight text-ink">Microcontroladores ESP32</span>
<span className="mt-1 block text-xs text-muted">Conectividade, eficiência e desempenho</span>
</span>
<ArrowUpRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" />
</Link>
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-sm">
<a href={SITE.livroAmazon} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 text-muted transition-colors hover:text-accent">
Amazon
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
<a href={SITE.livroUiclap} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 text-muted transition-colors hover:text-accent">
UICLAP
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
</div>
</div>
{/* Coluna 2: Links Úteis */}
<nav aria-label="Links úteis" className="space-y-4">
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
Recursos
</h4>
<ul className="space-y-3 text-sm">
<li>
<Link
href="/componentes"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5 font-medium"
>
<span>ESP Component Registry</span>
</Link>
</li>
<li>
<Link
href="/diagnostico"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5 font-medium"
>
<span>Guia de Diagnóstico & Erros</span>
</Link>
</li>
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Abrir repositório no GitHub"
>
<span>Repositório Principal</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
</a>
</li>
<li>
<a
href="https://github.com/lucasrguerra/ESPDocs/issues"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Abrir issues do repositório"
>
<span>Reportar um Bug / Feedback</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
</a>
</li>
<li>
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
aria-label="Visitar Ciência Embarcada (abre em nova aba)"
>
<span className="font-semibold text-purple-600 dark:text-purple-400">Ciência Embarcada</span>
<ExternalLink className="w-3.5 h-3.5 opacity-60 text-purple-600 dark:text-purple-400" />
</a>
</li>
</ul>
</nav>
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
{COLUMNS.map((column) => (
<nav key={column.title} aria-label={column.title}>
<h2 className="text-sm font-semibold text-ink">{column.title}</h2>
<ul className="mt-2 space-y-0">
{column.links.map((link) => (
<li key={link.href}>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1 text-sm text-muted transition-colors hover:text-accent"
>
{link.label}
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a>
) : (
<Link href={link.href} className="inline-flex min-h-11 min-w-11 items-center text-sm text-muted transition-colors hover:text-accent">
{link.label}
</Link>
)}
</li>
))}
</ul>
</nav>
))}
{/* Coluna 3: Autor & Social */}
<div className="space-y-4">
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
Sobre o Autor
</h4>
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
Desenvolvido com dedicação por{' '}
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="font-semibold underline text-slate-700 dark:text-slate-200 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
aria-label="Perfil LinkedIn de Lucas Rayan Guerra (abre em nova aba)"
>
<div>
<h2 className="text-sm font-semibold text-ink">Autor</h2>
<p className="mt-4 text-sm leading-relaxed text-muted">
Lucas Rayan Guerra
</a>
, compartilhando eletrônica, IoT e computação no Brasil.
</p>
<div className="flex items-center gap-3 pt-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
aria-label="GitHub do projeto"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
</svg>
</a>
<a
href="https://linkedin.com/in/lucasrguerra"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
aria-label="LinkedIn do autor"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4.98 3.5a2.5 2.5 0 11.001 5.001A2.5 2.5 0 014.98 3.5zM3 8.98h4v12H3v-12zM9.5 8.98h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" fill="currentColor"/>
</svg>
</a>
<br />
<span className="text-xs text-faint">Ciência Embarcada</span>
</p>
<div className="mt-4 flex items-center gap-2">
<a
href={SITE.repositorio}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub do projeto (abre em nova aba)"
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0 0 12 .5z" />
</svg>
</a>
<a
href={SITE.autorLinkedin}
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn do autor (abre em nova aba)"
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001 2.5 2.5 0 0 1 0-5zM3 8.98h4v12H3v-12zm6.5 0h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" />
</svg>
</a>
</div>
</div>
</div>
</div>
{/* Divisor & Isenção */}
<div className="mt-12 pt-8 border-t border-slate-300 dark:border-slate-800/80 flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-slate-500 dark:text-slate-400">
<p className="text-center md:text-left leading-relaxed">
© {year} ESPDocs. Esta plataforma independente de código aberto não possui afiliação com a Espressif Systems.
</p>
<p className="text-center md:text-right shrink-0">
Feito para a comunidade maker 🇧🇷
<div className="mt-14 flex flex-col gap-4 border-t border-line pt-6 text-xs leading-relaxed text-faint md:flex-row md:items-center md:justify-between">
<p>© {year} ESPDocs. Projeto independente, sem afiliação com a Espressif Systems.</p>
<p className="flex flex-wrap items-center gap-x-5 gap-y-2">
<a href={SITE.blog} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent">
Ciência Embarcada
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a>
<a href={SITE.autorUrl} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent">
lucasrguerra.dev.br
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
</a>
<span>Fontes oficiais: Espressif Systems.</span>
</p>
</div>
</div>
</footer>
);
}
}
+268 -658
View File
@@ -1,689 +1,301 @@
"use client";
import { usePathname } from "next/navigation";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import {
Home,
Info,
BookOpen,
Cpu,
SlidersHorizontal,
Terminal,
Scale,
ShoppingBag,
AlertTriangle,
Menu,
X,
Sun,
Moon,
ChevronDown,
Sparkles,
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import {
ArrowRight,
ExternalLink,
Compass,
Layers,
Box
BookA,
BookOpen,
Box,
ChevronDown,
Cpu,
Library,
Menu,
Moon,
Scale,
Search,
SlidersHorizontal,
Sparkles,
Sun,
X,
AlertTriangle,
} from "lucide-react";
const MENUS = [
{
id: "hardware",
label: "Hardware",
items: [
{ href: "/series", label: "Séries ESP32", desc: "12 chips, pinagem e especificações", icon: Cpu },
{ href: "/comparacao", label: "Comparador", desc: "Até quatro séries lado a lado", icon: Scale },
{ href: "/catalogo", label: "Catálogo de placas", desc: "DevKits, pinos e onde comprar", icon: Box },
],
},
{
id: "tools",
label: "Ferramentas",
items: [
{ href: "/seletor", label: "Seletor inteligente", desc: "O chip certo para o seu projeto", icon: SlidersHorizontal },
{ href: "/diagnostico", label: "Diagnóstico", desc: "Brownout, bootloop, WDT e mais", icon: AlertTriangle },
{ href: "/componentes", label: "Component Registry", desc: "Drivers e manifests idf_component.yml", icon: Search },
],
},
{
id: "library",
label: "Biblioteca",
items: [
{ href: "/frameworks", label: "Frameworks e SDKs", desc: "ESP-IDF, Arduino, Rust e Matter", icon: BookOpen },
{ href: "/glossario", label: "Glossário técnico", desc: "Os termos que aparecem nos datasheets", icon: BookA },
{ href: "/sobre", label: "Sobre o projeto", desc: "Autoria, método e abertura", icon: Sparkles },
],
},
];
const MOBILE_LINKS = [
{ href: "/series", label: "Séries ESP32" },
{ href: "/comparacao", label: "Comparador" },
{ href: "/seletor", label: "Seletor inteligente" },
{ href: "/diagnostico", label: "Diagnóstico" },
{ href: "/frameworks", label: "Frameworks e SDKs" },
{ href: "/componentes", label: "Component Registry" },
{ href: "/glossario", label: "Glossário" },
{ href: "/catalogo", label: "Catálogo de placas" },
{ href: "/livro", label: "O livro" },
{ href: "/sobre", label: "Sobre" },
];
function isActivePath(pathname, href) {
return href === "/" ? pathname === "/" : pathname === href || String(pathname).startsWith(`${href}/`);
}
export default function Header() {
const [drawerOpen, setDrawerOpen] = useState(false);
const [hardwareDropdown, setHardwareDropdown] = useState(false);
const [toolsDropdown, setToolsDropdown] = useState(false);
const [docsDropdown, setDocsDropdown] = useState(false);
const [mounted, setMounted] = useState(false);
const pathname = usePathname();
const [mounted, setMounted] = useState(false);
const [openMenu, setOpenMenu] = useState(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const navRef = useRef(null);
const closeRef = useRef(null);
const menuButtonRef = useRef(null);
const hardwareRef = useRef(null);
const toolsRef = useRef(null);
const docsRef = useRef(null);
useEffect(() => setMounted(true), []);
useEffect(() => {
setMounted(true);
}, []);
setOpenMenu(null);
setDrawerOpen(false);
}, [pathname]);
// Close dropdowns on outside click
useEffect(() => {
function handleClickOutside(e) {
if (hardwareRef.current && !hardwareRef.current.contains(e.target)) {
setHardwareDropdown(false);
}
if (toolsRef.current && !toolsRef.current.contains(e.target)) {
setToolsDropdown(false);
}
if (docsRef.current && !docsRef.current.contains(e.target)) {
setDocsDropdown(false);
function onPointerDown(event) {
if (navRef.current && !navRef.current.contains(event.target)) setOpenMenu(null);
}
function onKeyDown(event) {
if (event.key === "Escape") {
setOpenMenu(null);
setDrawerOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Lock scroll when mobile menu is open
useEffect(() => {
if (drawerOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.body.style.overflow = "";
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, []);
useEffect(() => {
if (!drawerOpen) return;
const previous = document.body.style.overflow;
document.body.style.overflow = "hidden";
closeRef.current?.focus();
return () => {
document.body.style.overflow = previous;
menuButtonRef.current?.focus();
};
}, [drawerOpen]);
// Close menu on route change
useEffect(() => {
setDrawerOpen(false);
setHardwareDropdown(false);
setToolsDropdown(false);
setDocsDropdown(false);
}, [pathname]);
function isActive(href) {
if (href === "/") {
return pathname === "/";
}
return String(pathname).startsWith(href);
}
const popularChips = [
{ name: "ESP32", desc: "Clássico Dual-Core", href: "/series/ESP32" },
{ name: "ESP32-S3", desc: "IA & USB Nativo", href: "/series/ESP32-S3" },
{ name: "ESP32-C6", desc: "Wi-Fi 6 + Zigbee/Thread", href: "/series/ESP32-C6" },
{ name: "ESP32-P4", desc: "Alto Desempenho 400MHz", href: "/series/ESP32-P4" },
];
return (
<header className="bg-white/90 dark:bg-slate-950/80 backdrop-blur-md py-3.5 px-4 md:px-8 shadow-xs border-b border-slate-200 dark:border-slate-800/60 sticky top-0 z-50">
<div className="flex justify-between max-w-7xl mx-auto items-center gap-4 lg:gap-8">
{/* LOGO */}
<Link href="/" className="group shrink-0 flex items-center gap-2.5">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/marca/espdocs-marca.svg"
alt="ESPDocs Logo"
width={32}
height={32}
className="w-7 h-7 md:w-8 md:h-8 shrink-0 transition-transform duration-300 group-hover:scale-105"
/>
<h1 className="text-2xl md:text-3xl font-display font-extrabold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent transition-all duration-300 group-hover:scale-[1.02]">
ESPDocs
</h1>
</Link>
{/* Desktop Navigation */}
<nav className="hidden lg:flex items-center gap-1 xl:gap-2 flex-1 justify-center" aria-label="Menu principal">
{/* Link Início */}
<Link
href="/"
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 ${
isActive("/") && pathname === "/"
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
}`}
>
<Home className="w-4 h-4" />
<span>Início</span>
<header className="sticky top-0 z-50 pt-3 sm:pt-4">
<div className="shell">
<div className="nav-pill flex h-16 items-center justify-between gap-3 rounded-full px-3 sm:px-4">
<Link href="/" className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label="ESPDocs, página inicial">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/marca/espdocs-marca.svg"
alt=""
aria-hidden="true"
width={30}
height={30}
className="h-[30px] w-[30px]"
/>
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
</Link>
{/* Dropdown: Séries & Hardware */}
<div className="relative" ref={hardwareRef}>
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label="Navegação principal">
{MENUS.map((menu) => {
const active = menu.items.some((item) => isActivePath(pathname, item.href));
const expanded = openMenu === menu.id;
return (
<div key={menu.id} className="relative">
<button
type="button"
className={`nav-link ${active ? "is-active" : ""}`}
aria-expanded={expanded}
aria-controls={`menu-${menu.id}`}
onClick={() => setOpenMenu(expanded ? null : menu.id)}
>
{menu.label}
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${expanded ? "rotate-180" : ""}`} aria-hidden="true" />
</button>
{expanded && (
<div
id={`menu-${menu.id}`}
className="absolute left-0 top-full mt-3 w-[23rem] rounded-[1.5rem] border border-line bg-surface p-2 shadow-[0_24px_60px_-20px_rgba(15,23,42,0.35)] dark:shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)]"
>
{menu.items.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className="group flex items-start gap-3 rounded-2xl px-3 py-3 transition-colors hover:bg-surface-2"
aria-current={isActivePath(pathname, item.href) ? "page" : undefined}
>
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent-soft text-accent transition-colors group-hover:bg-surface">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-ink">{item.label}</span>
<span className="mt-0.5 block text-xs leading-relaxed text-muted">{item.desc}</span>
</span>
</Link>
);
})}
</div>
)}
</div>
);
})}
<Link href="/livro" className={`nav-link ${isActivePath(pathname, "/livro") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro") ? "page" : undefined}>
Livro
</Link>
<Link href="/sobre" className={`nav-link ${isActivePath(pathname, "/sobre") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre") ? "page" : undefined}>
Sobre
</Link>
</nav>
<div className="flex items-center gap-1.5">
<Link href="/seletor" className="btn btn-primary btn-sm hidden lg:inline-flex">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Selecionar chip
</Link>
<ThemeToggle />
<button
onClick={() => {
setHardwareDropdown(!hardwareDropdown);
setToolsDropdown(false);
setDocsDropdown(false);
}}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
isActive("/series") || isActive("/comparacao") || isActive("/catalogo")
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
}`}
aria-expanded={hardwareDropdown}
ref={menuButtonRef}
type="button"
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2 lg:hidden"
aria-label="Abrir menu de navegação"
aria-expanded={drawerOpen}
onClick={() => setDrawerOpen(true)}
>
<Cpu className="w-4 h-4" />
<span>Séries & Chips</span>
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${hardwareDropdown ? "rotate-180" : ""}`} />
<Menu className="h-5 w-5" aria-hidden="true" />
</button>
{hardwareDropdown && (
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
<div className="space-y-1 mb-2">
<Link
href="/series"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 shrink-0">
<Cpu className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 flex items-center gap-1.5">
Todas as 12 Séries
<span className="text-[10px] bg-blue-100 dark:bg-blue-950/60 text-blue-700 dark:text-blue-300 font-bold px-1.5 py-0.5 rounded-md">12</span>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Pinouts, especificações e restrições</div>
</div>
</Link>
<Link
href="/comparacao"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 shrink-0">
<Scale className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Comparador de Séries
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Compare até 4 modelos lado a lado</div>
</div>
</Link>
<Link
href="/catalogo"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400 shrink-0">
<ShoppingBag className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Catálogo de Placas
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Placas de dev, conectores e compras</div>
</div>
</Link>
</div>
{/* Atalhos Rápidos Séries Populares */}
<div className="pt-2 border-t border-slate-200 dark:border-slate-800/80">
<div className="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-2.5 mb-1.5">
Séries Populares
</div>
<div className="grid grid-cols-2 gap-1">
{popularChips.map((chip) => (
<Link
key={chip.name}
href={chip.href}
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors text-left"
>
<span className="text-xs font-bold text-slate-850 dark:text-slate-100 block truncate">
{chip.name}
</span>
<span className="text-[10px] text-slate-500 dark:text-slate-400 block truncate">
{chip.desc}
</span>
</Link>
))}
</div>
</div>
</div>
)}
</div>
{/* Dropdown: Ferramentas */}
<div className="relative" ref={toolsRef}>
<button
onClick={() => {
setToolsDropdown(!toolsDropdown);
setHardwareDropdown(false);
setDocsDropdown(false);
}}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
isActive("/seletor") || isActive("/diagnostico")
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
}`}
aria-expanded={toolsDropdown}
>
<Sparkles className="w-4 h-4" />
<span>Ferramentas</span>
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${toolsDropdown ? "rotate-180" : ""}`} />
</button>
{toolsDropdown && (
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
<div className="space-y-1">
<Link
href="/seletor"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 shrink-0">
<SlidersHorizontal className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 flex items-center gap-1.5">
Seletor Inteligente
<span className="text-[10px] bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 font-bold px-1.5 py-0.5 rounded-md">Quiz</span>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Descubra o chip ideal para o seu projeto</div>
</div>
</Link>
<Link
href="/diagnostico"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 shrink-0">
<AlertTriangle className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-amber-600 dark:group-hover:text-amber-400 flex items-center gap-1.5">
Diagnóstico de Erros
<span className="text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-700 dark:text-amber-300 font-bold px-1.5 py-0.5 rounded-md">30+ Erros</span>
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Solucione Brownout, Bootloops, WDT e falhas</div>
</div>
</Link>
</div>
</div>
)}
</div>
{/* Dropdown: Documentação */}
<div className="relative" ref={docsRef}>
<button
onClick={() => {
setDocsDropdown(!docsDropdown);
setHardwareDropdown(false);
setToolsDropdown(false);
}}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
isActive("/frameworks") || isActive("/componentes") || isActive("/glossario") || isActive("/sobre")
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
}`}
aria-expanded={docsDropdown}
>
<BookOpen className="w-4 h-4" />
<span>Documentação</span>
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${docsDropdown ? "rotate-180" : ""}`} />
</button>
{docsDropdown && (
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
<div className="space-y-1">
<Link
href="/frameworks"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-teal-50 dark:bg-teal-950/40 text-teal-600 dark:text-teal-400 shrink-0">
<Terminal className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Frameworks & SDKs
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">ESP-IDF, Arduino, Rust, RainMaker e mais</div>
</div>
</Link>
<Link
href="/componentes"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 shrink-0">
<Box className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Component Registry
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Catálogo de drivers e gerador idf_component.yml</div>
</div>
</Link>
<Link
href="/glossario"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-pink-50 dark:bg-pink-950/40 text-pink-600 dark:text-pink-400 shrink-0">
<BookOpen className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Glossário Técnico
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Conceitos essenciais de hardware e software</div>
</div>
</Link>
<Link
href="/sobre"
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
>
<div className="p-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-650 dark:text-slate-350 shrink-0">
<Info className="w-4 h-4" />
</div>
<div>
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
Sobre o Projeto
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">História, autor e Ciência Embarcada</div>
</div>
</Link>
</div>
</div>
)}
</div>
</nav>
{/* Right CTA + Theme + Full Drawer Button */}
<div className="flex items-center gap-2 md:gap-3">
{/* CTA Button Desktop */}
<Link
href="/seletor"
className="hidden sm:inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-2 rounded-xl font-bold text-xs shadow-sm hover:shadow-purple-500/20 hover:-translate-y-0.5 transition-all duration-200 active:scale-95 shrink-0 cursor-pointer"
>
<Compass className="w-3.5 h-3.5" />
<span>Seletor de ESP32</span>
</Link>
<div className="h-5 w-px bg-slate-200 dark:bg-slate-800 mx-1 hidden sm:block" />
{/* Theme Switcher Button */}
<ThemeToggle />
{/* Full Drawer Trigger Button */}
<button
onClick={() => setDrawerOpen(true)}
className="p-2 md:p-2.5 rounded-xl border border-slate-300 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-purple-500 dark:hover:border-purple-400 hover:text-purple-600 dark:hover:text-purple-400 transition-all cursor-pointer flex items-center gap-1.5"
aria-label="Abrir menu de navegação completo"
aria-expanded={drawerOpen}
>
<Menu className="w-5 h-5" />
<span className="text-xs font-bold hidden xl:inline">Menu</span>
</button>
</div>
</div>
{/* Full Screen / Side Drawer Portal */}
{mounted && createPortal(
<>
{drawerOpen && (
{mounted &&
createPortal(
<>
<div
className="fixed inset-0 bg-slate-950/60 dark:bg-black/80 backdrop-blur-xs z-[100] transition-opacity duration-300"
className={`fixed inset-0 z-[100] bg-black/45 backdrop-blur-sm transition-opacity duration-200 ${
drawerOpen ? "opacity-100" : "hidden"
}`}
onClick={() => setDrawerOpen(false)}
aria-hidden="true"
/>
)}
<div
className={`fixed top-0 right-0 h-screen w-96 max-w-[88vw] bg-white dark:bg-slate-900 shadow-2xl z-[101] transform transition-transform duration-300 ease-in-out flex flex-col border-l border-slate-200 dark:border-slate-800 ${
drawerOpen ? "translate-x-0" : "translate-x-full"
}`}
>
{/* Drawer Header */}
<div className="flex items-center justify-between p-5 border-b border-slate-200 dark:border-slate-800">
<div className="flex items-center gap-2.5">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/marca/espdocs-marca.svg" alt="" aria-hidden="true" width={28} height={28} className="w-7 h-7" />
<div
role="dialog"
aria-modal="true"
aria-label="Menu de navegação"
aria-hidden={!drawerOpen}
inert={!drawerOpen}
className={`fixed inset-y-0 right-0 z-[101] flex w-[min(23rem,94vw)] flex-col rounded-l-[2rem] border-l border-line bg-canvas transition-transform duration-300 ${
drawerOpen ? "translate-x-0" : "hidden"
}`}
>
<div className="flex items-start justify-between px-5 py-5">
<div>
<h2 className="text-lg font-display font-extrabold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
Navegação ESPDocs
</h2>
<p className="text-[11px] text-slate-500 dark:text-slate-400">Guia Técnico em Português</p>
<p className="text-xs font-semibold text-muted">Navegação</p>
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">Para onde ir</p>
</div>
<button
ref={closeRef}
type="button"
onClick={() => setDrawerOpen(false)}
className="grid h-10 w-10 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
aria-label="Fechar menu"
>
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div className="px-4 pb-2">
<Link
href="/livro"
onClick={() => setDrawerOpen(false)}
className="group grid grid-cols-[3.25rem_1fr_auto] items-center gap-3 rounded-2xl bg-[#0b0d10] p-3 text-[#f4f6f8] ring-1 ring-white/10"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/livro/capa.jpg"
alt=""
aria-hidden="true"
width={52}
height={78}
className="h-[78px] w-[52px] rounded-md object-cover shadow-lg"
/>
<span className="min-w-0">
<span className="text-[0.6875rem] font-semibold text-[#75cdc1]">Livro novo</span>
<span className="mt-1 block font-display text-lg font-semibold leading-tight">Microcontroladores ESP32</span>
<span className="mt-1 block text-xs text-[#a6afbd]">Conectividade, eficiência e desempenho</span>
</span>
<ArrowRight className="h-4 w-4 shrink-0 text-[#75cdc1] transition-transform duration-300 group-hover:translate-x-1" aria-hidden="true" />
</Link>
</div>
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel">
{MOBILE_LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setDrawerOpen(false)}
className="group flex min-h-11 items-center justify-between gap-3 rounded-xl px-3 text-sm font-medium text-ink-soft transition-colors hover:bg-surface-2 hover:text-ink"
aria-current={isActivePath(pathname, link.href) ? "page" : undefined}
>
{link.label}
<ArrowRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" />
</Link>
))}
</nav>
<div className="p-4 pt-2">
<Link href="/seletor" onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Selecionar meu chip
</Link>
</div>
<button
onClick={() => setDrawerOpen(false)}
className="p-2 text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 transition-all cursor-pointer"
aria-label="Fechar menu lateral"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Quick Action Highlights */}
<div className="p-4 bg-slate-50 dark:bg-slate-950/50 border-b border-slate-200 dark:border-slate-800 space-y-2">
<Link
href="/seletor"
onClick={() => setDrawerOpen(false)}
className="flex items-center justify-between p-3 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm hover:shadow-md transition-all group"
>
<div className="flex items-center gap-2.5">
<Compass className="w-4 h-4 shrink-0" />
<div className="text-left">
<div className="font-bold text-xs">Seletor Inteligente</div>
<div className="text-[10px] text-purple-100">Encontre o ESP32 certo</div>
</div>
</div>
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
<Link
href="/diagnostico"
onClick={() => setDrawerOpen(false)}
className="flex items-center justify-between p-3 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm hover:shadow-md transition-all group"
>
<div className="flex items-center gap-2.5">
<AlertTriangle className="w-4 h-4 shrink-0" />
<div className="text-left">
<div className="font-bold text-xs">Diagnóstico de Falhas</div>
<div className="text-[10px] text-amber-100">Resolva Brownout, bootloop e erros</div>
</div>
</div>
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</div>
{/* Drawer Categorized Menu */}
<nav className="flex-1 overflow-y-auto p-5 space-y-6" aria-label="Navegação móvel completa">
{/* Seção 1: Hardware & Microcontroladores */}
<div>
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
Hardware & Séries
</div>
<div className="space-y-1">
<Link
href="/series"
onClick={() => setDrawerOpen(false)}
className={`flex items-center justify-between p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/series") && pathname === "/series"
? "bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<div className="flex items-center gap-2.5">
<Cpu className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<span>Todas as Séries</span>
</div>
<span className="text-[11px] bg-slate-200 dark:bg-slate-800 text-slate-650 dark:text-slate-350 px-2 py-0.5 rounded-full font-bold">
12
</span>
</Link>
<Link
href="/comparacao"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/comparacao")
? "bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<Scale className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
<span>Comparar Séries</span>
</Link>
<Link
href="/catalogo"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/catalogo")
? "bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<ShoppingBag className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
<span>Catálogo de Placas</span>
</Link>
</div>
{/* Atalhos Rápidos com Cores */}
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-800/80">
<div className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2 px-1">
Acesso Rápido a Chips
</div>
<div className="grid grid-cols-2 gap-1.5">
{popularChips.map((chip) => (
<Link
key={chip.name}
href={chip.href}
onClick={() => setDrawerOpen(false)}
className="p-2 rounded-xl bg-slate-50 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/60 dark:border-slate-750 transition-colors"
>
<span className="text-xs font-bold text-slate-850 dark:text-slate-100 block">
{chip.name}
</span>
<span className="text-[10px] text-slate-500 dark:text-slate-400 block truncate">
{chip.desc}
</span>
</Link>
))}
</div>
</div>
</div>
{/* Seção 2: Ferramentas & Diagnóstico */}
<div>
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
Ferramentas Interativas
</div>
<div className="space-y-1">
<Link
href="/seletor"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/seletor")
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<SlidersHorizontal className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span>Seletor Inteligente</span>
</Link>
<Link
href="/diagnostico"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/diagnostico")
? "bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span>Diagnóstico & Solução de Erros</span>
</Link>
</div>
</div>
{/* Seção 3: Documentação & Referência */}
<div>
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
Guias & Documentação
</div>
<div className="space-y-1">
<Link
href="/frameworks"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/frameworks")
? "bg-teal-50 dark:bg-teal-950/40 text-teal-600 dark:text-teal-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<Terminal className="w-4 h-4 text-teal-600 dark:text-teal-400" />
<span>Frameworks & SDKs</span>
</Link>
<Link
href="/componentes"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/componentes")
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<Box className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span>Component Registry</span>
</Link>
<Link
href="/glossario"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/glossario")
? "bg-pink-50 dark:bg-pink-950/40 text-pink-600 dark:text-pink-400 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<BookOpen className="w-4 h-4 text-pink-600 dark:text-pink-400" />
<span>Glossário Técnico</span>
</Link>
</div>
</div>
{/* Seção 4: Institucional & Comunidade */}
<div>
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
Comunidade & Projeto
</div>
<div className="space-y-1">
<Link
href="/sobre"
onClick={() => setDrawerOpen(false)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
isActive("/sobre")
? "bg-slate-100 dark:bg-slate-800 text-slate-850 dark:text-slate-100 font-bold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
<Info className="w-4 h-4 text-slate-500 dark:text-slate-400" />
<span>Sobre a Plataforma</span>
</Link>
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-2.5 rounded-xl font-medium text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors group"
>
<div className="flex items-center gap-2.5">
<Layers className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span>Blog Ciência Embarcada</span>
</div>
<ExternalLink className="w-3.5 h-3.5 text-slate-500 dark:text-slate-400 group-hover:text-purple-600 dark:group-hover:text-purple-400" />
</a>
</div>
</div>
</nav>
{/* Drawer Footer */}
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/50 flex items-center justify-between">
<div>
<p className="text-xs font-bold text-slate-700 dark:text-slate-300">
ESPDocs • 100% em Português
</p>
<p className="text-[10px] text-slate-500 dark:text-slate-400">
12 Séries • 8 Frameworks • 30+ Erros
</p>
</div>
<ThemeToggle />
</div>
</div>
</>
, document.body
)}
</>,
document.body
)}
</header>
);
}
/* ThemeToggle Component */
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
@@ -691,22 +303,20 @@ function ThemeToggle() {
useEffect(() => setMounted(true), []);
if (!mounted) {
return (
<div className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 animate-pulse border border-transparent" />
);
return <span className="h-11 w-11 rounded-full border border-line bg-surface-2" aria-hidden="true" />;
}
const dark = resolvedTheme === "dark";
return (
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-800/80 hover:text-purple-600 dark:hover:text-purple-400 border border-slate-200 dark:border-slate-800 shadow-xs flex items-center justify-center transition-all duration-200 cursor-pointer active:scale-95 shrink-0"
aria-label="Alternar tema claro/escuro"
type="button"
onClick={() => setTheme(dark ? "light" : "dark")}
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
aria-label={dark ? "Ativar tema claro" : "Ativar tema escuro"}
title={dark ? "Tema claro" : "Tema escuro"}
>
{resolvedTheme === "dark" ? (
<Sun className="w-4 h-4 md:w-5 md:h-5 text-yellow-500" />
) : (
<Moon className="w-4 h-4 md:w-5 md:h-5 text-indigo-600" />
)}
{dark ? <Sun className="h-4 w-4" aria-hidden="true" /> : <Moon className="h-4 w-4" aria-hidden="true" />}
</button>
);
}
}
+47
View File
@@ -0,0 +1,47 @@
import Link from "next/link";
/**
* Abertura padrão das páginas internas: trilha, rótulo, título, frase de apoio
* e ações. Mesma voz da /livro e da home, para que nenhuma página pareça
* de outro produto.
*/
export default function PageHero({ trail = [], icon: Icon, label, title, lede, align = "left", children }) {
const center = align === "center";
return (
<section className="shell pt-8 sm:pt-12">
{trail.length > 0 && (
<nav aria-label="Trilha de navegação" className="flex flex-wrap items-center gap-2 text-sm text-muted">
<Link href="/" className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
Início
</Link>
{trail.map((item, i) => (
<span key={item.label} className="flex min-h-11 items-center gap-2">
<span aria-hidden="true" className="text-line-strong">/</span>
{item.href && i < trail.length - 1 ? (
<Link href={item.href} className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
{item.label}
</Link>
) : (
<span aria-current="page" className="text-ink">
{item.label}
</span>
)}
</span>
))}
</nav>
)}
<div className={`${trail.length ? "mt-8 sm:mt-10" : ""} max-w-3xl ${center ? "mx-auto text-center" : ""}`}>
{label && (
<span className="chip chip-accent">
{Icon && <Icon className="h-3.5 w-3.5" aria-hidden="true" />}
{label}
</span>
)}
<h1 className={`page-title ${label ? "mt-5" : ""}`}>{title}</h1>
{lede && <p className={`lede mt-5 max-w-2xl ${center ? "mx-auto" : ""}`}>{lede}</p>}
{children && <div className={`mt-7 flex flex-wrap gap-3 ${center ? "justify-center" : ""}`}>{children}</div>}
</div>
</section>
);
}
+32 -47
View File
@@ -23,7 +23,7 @@ const GRUPOS = [
id: "entrada",
titulo: "Somente entrada",
icone: ArrowDownToLine,
cor: "text-sky-500",
cor: "text-accent",
explicacao:
"Aceitam apenas leitura. Não podem ser configurados como saída e não possuem resistores de pull-up ou pull-down internos. Se precisar de um, coloque no circuito.",
combina: (avisos) => /apenas de entrada|input only/.test(avisos),
@@ -32,7 +32,7 @@ const GRUPOS = [
id: "flash",
titulo: "Ligados à memória flash/PSRAM",
icone: HardDrive,
cor: "text-rose-500",
cor: "text-copper",
explicacao:
"Já estão conectados à memória de onde o firmware é lido. Usá-los como GPIO comum normalmente trava o chip ou corrompe a execução. Em módulos com flash interna eles nem chegam ao encapsulamento.",
combina: (avisos) =>
@@ -43,7 +43,7 @@ const GRUPOS = [
id: "strapping",
titulo: "Strapping (modo de boot)",
icone: Power,
cor: "text-amber-500",
cor: "text-gold",
explicacao:
"O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Dá para usá-los depois do boot, mas o circuito externo não pode forçar o nível errado durante o reset, senão a placa não inicia ou entra em modo de gravação sozinha.",
combina: (avisos, cats) =>
@@ -54,7 +54,7 @@ const GRUPOS = [
id: "debug",
titulo: "Gravação e depuração",
icone: Bug,
cor: "text-violet-500",
cor: "text-accent",
explicacao:
"São a UART de gravação e as linhas de JTAG. Continuam utilizáveis como GPIO, mas ao reaproveitá-los você perde o caminho de gravar e depurar a placa sem desconectar o que estiver ali.",
combina: (avisos, cats) => /debugging/.test(avisos) || cats.includes("JTAG"),
@@ -63,7 +63,7 @@ const GRUPOS = [
id: "usb",
titulo: "USB nativo",
icone: Usb,
cor: "text-teal-500",
cor: "text-accent",
explicacao:
"Saem de fábrica ligados ao USB Serial/JTAG interno. Para usá-los como GPIO é preciso desativar essa função, o que também remove a gravação por USB direto.",
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/.test(avisos),
@@ -72,7 +72,7 @@ const GRUPOS = [
id: "eletrico",
titulo: "Não podem ficar flutuando",
icone: Zap,
cor: "text-orange-500",
cor: "text-gold",
explicacao:
"Precisam de nível definido por hardware: pull-up externo ou circuito RC de atraso. Deixá-los soltos causa reset aleatório ou inicialização instável.",
combina: (avisos) => /flutuando/.test(avisos),
@@ -101,7 +101,7 @@ function derivarGrupos(conexoes) {
}).filter((g) => g.pinos.length > 0);
}
export default function PinRestrictions({ conexoes, serieKey, cor }) {
export default function PinRestrictions({ conexoes, serieKey }) {
if (!conexoes?.length) return null;
const grupos = derivarGrupos(conexoes);
@@ -110,69 +110,54 @@ export default function PinRestrictions({ conexoes, serieKey, cor }) {
const totalPinos = new Set(grupos.flatMap((g) => g.pinos)).size;
return (
<div 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/80 shadow-2xl mb-12">
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-3 pb-3 border-b border-slate-200 dark:border-slate-800/65 flex items-center gap-3">
<AlertTriangle className="w-5 h-5" style={{ color: cor }} />
<span>Pinos com Restrição de Uso</span>
<section id="restricoes" aria-labelledby="restricoes-titulo" className="card mt-5 scroll-mt-28 p-5 sm:p-7">
<h2 id="restricoes-titulo" className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-gold-soft text-gold">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
</span>
Pinos com restrição de uso
</h2>
<p className="text-sm text-slate-650 dark:text-slate-350 leading-relaxed mb-8">
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
Nem todo pino do {serieKey} está livre para uso geral. {totalPinos} deles têm alguma
condição: já estão ocupados por memória, são lidos durante o boot ou só funcionam
como entrada. Vale conferir antes de fechar o esquemático.
</p>
<div className="grid md:grid-cols-2 gap-5">
<div className="mt-6 grid gap-4 md:grid-cols-2">
{grupos.map(({ id, titulo, icone: Icone, cor: corIcone, explicacao, pinos }) => (
<div
key={id}
className="bg-slate-50 dark:bg-slate-950/40 rounded-2xl p-5 border border-slate-200 dark:border-slate-800/60"
>
<div className="flex items-center gap-2.5 mb-3">
<Icone className={`w-4 h-4 shrink-0 ${corIcone}`} />
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-200">{titulo}</h3>
<span className="ml-auto text-[10px] font-bold text-slate-500 dark:text-slate-400 tabular">
<div key={id} className="tile">
<div className="flex items-center gap-2.5">
<Icone className={`h-4 w-4 shrink-0 ${corIcone}`} aria-hidden="true" />
<h3 className="text-sm font-semibold text-ink">{titulo}</h3>
<span className="ml-auto text-xs text-muted tabular-nums">
{pinos.length} {pinos.length === 1 ? "pino" : "pinos"}
</span>
</div>
<div className="flex flex-wrap gap-1.5 mb-3.5">
<ul className="mt-3 flex flex-wrap gap-1.5">
{pinos.map((p) => (
<span
key={p}
className="px-2 py-1 rounded-lg text-[10px] font-bold font-mono"
style={{
backgroundColor: `${cor}18`,
color: cor,
border: `1px solid ${cor}30`,
}}
>
<li key={p} className="rounded-md border border-line bg-surface px-2 py-1 font-mono text-xs text-ink-soft">
{p}
</span>
</li>
))}
</div>
</ul>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
{explicacao}
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{explicacao}</p>
</div>
))}
</div>
<div className="mt-7 pt-6 border-t border-slate-200 dark:border-slate-800/65 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed max-w-xl">
<div className="mt-6 flex flex-col justify-between gap-4 border-t border-line pt-5 sm:flex-row sm:items-center">
<p className="max-w-xl text-sm leading-relaxed text-muted">
Estes grupos vêm dos mesmos dados do diagrama de conexões. Lá você vê pino a pino
todas as funções, o domínio de alimentação e os avisos completos de cada um.
todas as funções, o domínio de alimentação e os avisos completos.
</p>
<Link
href="#connections"
className="inline-flex items-center justify-center gap-2 shrink-0 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider text-white transition-opacity hover:opacity-90"
style={{ backgroundColor: cor }}
>
<span>Abrir diagrama de pinos</span>
<ArrowRight className="w-4 h-4" />
<Link href="#connections" className="btn btn-ghost shrink-0">
Abrir diagrama de pinos
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
);
}
+15
View File
@@ -0,0 +1,15 @@
/** Título de seção: rótulo curto + título + frase de apoio, alinhado à esquerda. */
export default function SectionHead({ id, eyebrow, title, lede, children }) {
return (
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-2xl">
{eyebrow && <p className="eyebrow">{eyebrow}</p>}
<h2 id={id} className={`section-title ${eyebrow ? "mt-3" : ""}`}>
{title}
</h2>
{lede && <p className="mt-3 text-base leading-relaxed text-muted">{lede}</p>}
</div>
{children && <div className="shrink-0">{children}</div>}
</div>
);
}
+52 -78
View File
@@ -1,11 +1,11 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { ChevronDown } from 'lucide-react';
import { useEffect, useRef, useState } from "react";
export default function SeriesTabMenu({ tabs, color }) {
const availableTabs = tabs.filter(tab => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'specs');
export default function SeriesTabMenu({ tabs }) {
const availableTabs = tabs.filter((tab) => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id);
const refs = useRef({});
// Troca manual limpa o hash. Sem isso, depois de abrir o diagrama pelo link e
// mudar de aba na mão, o hash continuaria em #connections e um novo clique no
@@ -13,7 +13,7 @@ export default function SeriesTabMenu({ tabs, color }) {
const trocarAba = (id) => {
setActiveTab(id);
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname + window.location.search);
history.replaceState(null, "", window.location.pathname + window.location.search);
}
};
@@ -21,93 +21,67 @@ export default function SeriesTabMenu({ tabs, color }) {
// É o que permite a seção de restrições apontar para cá.
useEffect(() => {
const abrirPeloHash = () => {
const alvo = window.location.hash.replace('#', '');
if (alvo && availableTabs.some(t => t.id === alvo)) {
const alvo = window.location.hash.replace("#", "");
if (alvo && availableTabs.some((t) => t.id === alvo)) {
setActiveTab(alvo);
// Nenhum elemento tem id="connections", então o navegador não rola
// sozinho: levamos até as abas na mão.
const semAnimacao = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.getElementById('diagrama-abas')?.scrollIntoView({
behavior: semAnimacao ? 'auto' : 'smooth',
block: 'start',
const semAnimacao = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.getElementById("diagrama-abas")?.scrollIntoView({
behavior: semAnimacao ? "auto" : "smooth",
block: "start",
});
}
};
abrirPeloHash();
window.addEventListener('hashchange', abrirPeloHash);
return () => window.removeEventListener('hashchange', abrirPeloHash);
window.addEventListener("hashchange", abrirPeloHash);
return () => window.removeEventListener("hashchange", abrirPeloHash);
}, [availableTabs]);
const activeTabContent = availableTabs.find(tab => tab.id === activeTab)?.content;
// Setas, Home e End movem o foco entre as abas, como no padrão WAI-ARIA.
const aoTeclar = (evento, indice) => {
const total = availableTabs.length;
const destino =
evento.key === "ArrowRight" ? (indice + 1) % total
: evento.key === "ArrowLeft" ? (indice - 1 + total) % total
: evento.key === "Home" ? 0
: evento.key === "End" ? total - 1
: null;
if (destino === null) return;
evento.preventDefault();
const id = availableTabs[destino].id;
trocarAba(id);
refs.current[id]?.focus();
};
if (availableTabs.length === 0) return null;
const ativa = availableTabs.find((tab) => tab.id === activeTab) ?? availableTabs[0];
return (
<div className="mb-12" id="diagrama-abas">
{/* Desktop Tabs */}
<div className="hidden md:flex bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-xl py-2 px-3 mb-6 border border-slate-300 dark:border-slate-800/80 gap-3">
{availableTabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => trocarAba(tab.id)}
className={`flex-1 flex items-center justify-center gap-2 py-3 px-5 rounded-xl font-bold text-xs uppercase tracking-wider transition-all duration-300 cursor-pointer active:scale-98 ${
isActive
? 'text-white shadow-lg scale-[1.02]'
: 'text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-800/40 hover:text-slate-800 dark:hover:text-slate-100'
}`}
style={
isActive
? { backgroundColor: color, boxShadow: `0 10px 20px -10px ${color}` }
: {}
}
>
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Mobile Dropdown Tab Selector */}
<div className="md:hidden mb-6">
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg border border-slate-300 dark:border-slate-800/80 overflow-hidden">
<div id="diagrama-abas" className="scroll-mt-28">
<div role="tablist" aria-label="Placa e diagrama" className="no-scrollbar flex gap-2 overflow-x-auto pb-1">
{availableTabs.map((tab, indice) => (
<button
onClick={() => {
const dropdown = document.getElementById('mobile-tab-dropdown');
dropdown.classList.toggle('hidden');
}}
className="w-full flex items-center justify-between py-4 px-6 font-bold text-sm uppercase tracking-wider text-white transition-all duration-300"
style={{ backgroundColor: color }}
key={tab.id}
ref={(el) => (refs.current[tab.id] = el)}
type="button"
role="tab"
id={`aba-${tab.id}`}
aria-selected={ativa.id === tab.id}
aria-controls={`painel-${tab.id}`}
tabIndex={ativa.id === tab.id ? 0 : -1}
onClick={() => trocarAba(tab.id)}
onKeyDown={(e) => aoTeclar(e, indice)}
className="chip-btn shrink-0"
>
<div className="flex items-center gap-3">
<span>
{availableTabs.find(tab => tab.id === activeTab)?.label}
</span>
</div>
<ChevronDown className="w-4 h-4 shrink-0 transition-transform duration-300" />
{tab.label}
</button>
<div id="mobile-tab-dropdown" className="hidden">
{availableTabs.map((tab) => (
tab.id !== activeTab && (
<button
key={tab.id}
onClick={() => {
trocarAba(tab.id);
document.getElementById('mobile-tab-dropdown').classList.add('hidden');
}}
className="w-full flex items-center gap-3 py-4 px-6 text-slate-700 dark:text-slate-300 hover:bg-slate-150/40 dark:hover:bg-slate-800/30 transition-colors border-t border-slate-300 dark:border-slate-800/60 font-semibold text-xs uppercase tracking-wider text-left"
>
<span>{tab.label}</span>
</button>
)
))}
</div>
</div>
))}
</div>
<div className="animate-fadeIn">
{activeTabContent}
<div role="tabpanel" id={`painel-${ativa.id}`} aria-labelledby={`aba-${ativa.id}`} className="mt-4">
{ativa.content}
</div>
</div>
);
}
}