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:
1 parent
4f27f5858b
commit
bc9ae5c036
34 files changed
+4911
-6874
No files matched your search
+46
-78
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user