Redesign das páginas internas no novo design system (AAA) (#2)

* feat(design): redesign das páginas internas no novo design system

Padroniza todas as páginas e componentes internos nos tokens e primitivos
de app/globals.css, com contraste AAA (7:1) nos dois temas e alvos de toque
de 44px.

- Páginas reescritas: glossário, séries, série, seletor, comparador,
  diagnóstico, catálogo, component registry, frameworks, framework, sobre
  e a nova 404.
- Componentes: PageHero e SectionHead novos; BoardCard, CodeExample,
  PinRestrictions, SeriesTabMenu e ConnectionsDiagram refeitos, com abas
  e diálogo acessíveis.
- Remove emojis, cores avulsas por série, caixa alta com tracking, texto
  menor que 12px e os pares slate-*/dark:*.
- Comparador: "mostrar só as diferenças" e leitura de ?chips=.
- Corrige overflow em 320/390px e ajusta accent/copper do tema claro
  para fechar 7:1.
- scripts/auditar-temas.mjs passa a travar paleta crua, dark:, texto
  pequeno, emoji e contraste dos tokens.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc

* ci: roda lint, auditorias de design e build em cada PR

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Rayan GuerraandClaude Sonnet 5.5 authored and GitHub committed 2026-10-20 16:10:15 -03:00
1 parent 4f27f5858b
commit bc9ae5c036
34 files changed
+4365 -6328

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
verificar:
name: Lint, design e build
runs-on: ubuntu-latest
timeout-minutes: 15
env:
# O Puppeteer só é usado em scripts locais (slides, capturas); não precisa do Chrome na CI.
PUPPETEER_SKIP_DOWNLOAD: "true"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
# eslint + auditoria do design system (tokens, contraste AAA) + auditoria de páginas
- run: npm run check
- run: npm run build
+114 -181
View File
@@ -3,17 +3,9 @@
import { useState, useEffect } from 'react';
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import BoardCard from "@/components/BoardCard";
import {
Search,
Filter,
ShoppingCart,
CheckCircle,
AlertTriangle,
RefreshCw,
Layers,
ChevronDown
} from 'lucide-react';
import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react";
export default function PlacasPage() {
const [boards, setBoards] = useState([]);
@@ -24,11 +16,14 @@ export default function PlacasPage() {
const [searchTerm, setSearchTerm] = useState('');
const [activeFilters, setActiveFilters] = useState({});
const [filterOptions, setFilterOptions] = useState({});
const [filtrosAbertos, setFiltrosAbertos] = useState(false);
const excludedColumns = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
const excludedColumns = ['Nome', 'Descrição', 'Link', 'Imagem', 'Loja Oficial'];
useEffect(() => {
fetchBoards();
// No desktop os filtros ficam sempre à vista; no celular começam recolhidos.
setFiltrosAbertos(window.matchMedia('(min-width: 1024px)').matches);
}, []);
useEffect(() => {
@@ -147,18 +142,33 @@ export default function PlacasPage() {
const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0);
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h));
const hero = (
<PageHero
trail={[{ label: "Catálogo de placas" }]}
icon={ShoppingCart}
label="DevKits e módulos reais"
title="Catálogo de placas ESP32"
lede="Compare placas oficiais e alternativas para o seu projeto: interface USB, sensores integrados e link direto para a loja."
/>
);
if (loading) {
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 flex flex-col justify-between">
<div className="site-canvas min-h-screen">
<Header />
<main className="max-w-7xl mx-auto px-6 py-24 flex-1 flex items-center justify-center">
<div className="text-center select-none animate-pulse">
<RefreshCw className="w-12 h-12 text-purple-500 animate-spin mx-auto mb-4" />
<p className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Consultando banco de placas...</p>
<main id="conteudo">
{hero}
<section className="shell section pt-10" aria-busy="true" aria-label="Carregando placas">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card h-72 animate-pulse bg-surface-2" />
))}
</div>
<p role="status" className="mt-6 text-center text-sm text-muted">Consultando o banco de placas…</p>
</section>
</main>
<Footer />
</div>
);
@@ -166,194 +176,139 @@ export default function PlacasPage() {
if (error) {
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 flex flex-col justify-between">
<div className="site-canvas min-h-screen">
<Header />
<main className="max-w-7xl mx-auto px-6 py-24 flex-1 flex items-center justify-center">
<div className="text-center bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-8 max-w-md shadow-2xl select-none">
<div className="w-12 h-12 bg-red-500/10 border border-red-500/20 rounded-2xl flex items-center justify-center text-red-500 mx-auto mb-4">
<AlertTriangle className="w-6 h-6" />
</div>
<p className="text-sm font-bold text-red-600 dark:text-red-400 mb-2 uppercase tracking-widest">Falha ao sincronizar dados</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-6 leading-relaxed">{error}</p>
<button
onClick={fetchBoards}
className="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 active:scale-95 transition-all shadow-md cursor-pointer"
>
<RefreshCw className="w-4 h-4" />
<span>Tentar Novamente</span>
<main id="conteudo">
{hero}
<section className="shell section pt-10">
<div role="alert" className="callout callout-danger flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-danger" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Não foi possível carregar as placas</p>
<p className="mt-1">{error}. Verifique a conexão e tente de novo.</p>
<button type="button" onClick={fetchBoards} className="btn btn-primary btn-sm mt-4">
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Tentar novamente
</button>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
const filterableColumns = headers.filter(h => !excludedColumns.includes(h));
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<Header />
<main id="conteudo" className="max-w-7xl mx-auto px-6 pt-16 pb-24">
{/* Top Hero Heading Block */}
<section className="text-center mb-16 select-none">
<div className="inline-flex items-center gap-2 bg-white dark:bg-slate-900/50 backdrop-blur-md px-4 py-2 rounded-full mb-6 border border-slate-300 dark:border-slate-800/60 shadow-xs">
<ShoppingCart className="w-4 h-4 text-purple-500" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-350 uppercase tracking-wider">Encontre sua placa ideal</span>
</div>
<h1 className="text-4xl md:text-6xl font-display leading-tight font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 tracking-tight">
Catálogo de Placas ESP32
</h1>
<p className="text-sm md:text-base text-slate-500 dark:text-slate-400 max-w-2xl mx-auto leading-relaxed mb-6 font-semibold">
Compare e selecione placas oficiais e alternativas de desenvolvimento do microcontrolador ESP32 para seus projetos.
</p>
<div className="inline-flex items-center gap-2 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-4 py-2 rounded-full border border-emerald-500/20">
<CheckCircle className="w-4 h-4 shrink-0" />
<p className="text-[10px] font-bold uppercase tracking-wider">
Links de lojas integradas de fontes verificadas e confiáveis
</p>
</div>
</section>
<div className="grid lg:grid-cols-4 gap-8">
{/* Left Filters Sidebar */}
<div className="lg:col-span-1">
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl shadow-xl overflow-hidden sticky top-4">
{/* Sidebar Banner Header */}
<div className="bg-gradient-to-r from-blue-600 to-purple-600 p-6 text-white select-none">
<div className="flex justify-between items-center">
const filtros = (
<div className="space-y-6">
<div>
<h2 className="text-base font-display font-extrabold flex items-center gap-2">
<Filter className="w-4.5 h-4.5" />
<span>Filtros de Busca</span>
</h2>
{activeFilterCount > 0 && (
<p className="text-[10px] text-blue-150 font-bold uppercase tracking-wider mt-1.5 animate-pulse">
{activeFilterCount} selecionado(s)
</p>
)}
</div>
{activeFilterCount > 0 && (
<button
onClick={clearFilters}
className="bg-white/20 hover:bg-white/30 text-white px-3 py-1.5 rounded-xl font-bold text-[10px] uppercase tracking-wider transition-all hover:scale-105 active:scale-95 cursor-pointer"
>
Limpar
</button>
)}
</div>
</div>
{/* Sidebar Filters Area */}
<div className="p-6 space-y-6">
{/* Text search by name */}
<div>
<label className="block text-[9px] font-bold text-slate-500 dark:text-slate-400 mb-2.5 uppercase tracking-widest select-none">
Buscar Placa
<label htmlFor="busca-placa" className="text-sm font-semibold text-ink">
Buscar placa
</label>
<div className="relative">
<div className="relative mt-2">
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
type="text"
placeholder="Digite o nome da placa..."
id="busca-placa"
type="search"
placeholder="Nome ou descrição"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-3 pl-10 border border-slate-300 dark:border-slate-800 rounded-2xl focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-500 focus:outline-none transition-colors text-xs bg-slate-50/50 dark:bg-slate-950/20 text-slate-800 dark:text-slate-100"
className="field pl-10 text-sm"
/>
<Search className="absolute left-3.5 top-3.5 w-4 h-4 text-slate-500 dark:text-slate-400" />
</div>
</div>
<div className="border-t border-slate-200 dark:border-slate-850/45 my-2"></div>
{/* Dinamic filters container with customized scrollbar */}
<div className="space-y-4 max-h-[calc(100vh-450px)] overflow-y-auto pr-2 custom-scrollbar">
{filterableColumns.map((column) => {
const filterData = filterOptions[column];
if (!filterData) return null;
const isActive = activeFilters[column]?.length > 0;
if (filterData.isBinary) {
return (
<div key={column} className={`pb-3 border-b border-slate-200 dark:border-slate-850/30 last:border-b-0 ${isActive ? 'bg-purple-500/5 dark:bg-purple-400/5 p-3 rounded-2xl border border-purple-500/10 dark:border-purple-400/10' : ''}`}>
{filterData.isBinary ? (
// Binary filter (checkbox inside title wrapper)
<label className="flex justify-between items-center cursor-pointer p-1 rounded-lg transition-all group select-none">
<span className="text-xs font-bold text-slate-700 dark:text-slate-350 group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
<label key={column} className="flex min-h-11 cursor-pointer items-center justify-between gap-3 border-t border-line pt-4 text-sm font-medium text-ink-soft">
{column}
</span>
<input
type="checkbox"
checked={activeFilters[column]?.includes('binary-enabled') || false}
onChange={() => handleFilterToggle(column, 'binary-enabled')}
className="w-4.5 h-4.5 rounded-md border border-slate-300 dark:border-slate-750 text-purple-650 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-all cursor-pointer bg-white/50 dark:bg-slate-950/30"
checked={activeFilters[column]?.includes("binary-enabled") || false}
onChange={() => handleFilterToggle(column, "binary-enabled")}
className="h-5 w-5 shrink-0 cursor-pointer accent-[var(--accent)]"
/>
</label>
) : (
// Standard options filter with dropdown listing
<>
<h3 className="font-bold text-slate-700 dark:text-slate-300 mb-3.5 text-xs uppercase tracking-widest flex items-center gap-2 select-none">
<span>{column}</span>
{isActive && (
<span className="bg-purple-650 text-white text-[9px] px-2 py-0.5 rounded-full font-bold select-none">
{activeFilters[column].length}
</span>
)}
</h3>
);
}
<div className="space-y-2.5 max-h-44 overflow-y-auto pr-2 custom-scrollbar">
return (
<fieldset key={column} className="border-t border-line pt-4">
<legend className="flex items-center gap-2 pr-2 text-sm font-semibold text-ink">
{column}
{activeFilters[column]?.length > 0 && <span className="chip chip-accent px-2 py-0">{activeFilters[column].length}</span>}
</legend>
<div className="mt-2 max-h-44 space-y-0.5 overflow-y-auto pr-1">
{filterData.values.map((option) => (
<label
key={option}
className="flex items-center gap-3 cursor-pointer hover:bg-purple-500/5 dark:hover:bg-purple-400/5 p-1.5 rounded-lg transition-all group select-none"
>
<label key={option} className="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2 text-sm text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<input
type="checkbox"
checked={activeFilters[column]?.includes(option) || false}
onChange={() => handleFilterToggle(column, option)}
className="w-4 h-4 rounded border border-slate-350 dark:border-slate-750 text-purple-650 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-all cursor-pointer bg-white/50 dark:bg-slate-950/30"
className="h-4 w-4 shrink-0 cursor-pointer accent-[var(--accent)]"
/>
<span className="text-xs font-semibold text-slate-600 dark:text-slate-400 group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
{option}
</span>
</label>
))}
</div>
</>
)}
</div>
</fieldset>
);
})}
</div>
</div>
</div>
</div>
);
{/* Right Catalog Boards Grid */}
<div className="lg:col-span-3">
<div className="mb-6 flex justify-between items-center select-none">
<p className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
Total Encontrado: <span className="text-purple-650 dark:text-purple-400">{filteredBoards.length}</span> Placa(s)
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
{hero}
<section className="shell section pt-10 sm:pt-12" aria-label="Placas">
<div className="grid gap-8 lg:grid-cols-[17rem_1fr] lg:gap-10">
<aside className="lg:sticky lg:top-28 lg:self-start">
<details className="panel" open={filtrosAbertos} onToggle={(e) => setFiltrosAbertos(e.currentTarget.open)}>
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 px-5 py-3 lg:cursor-default">
<span className="flex items-center gap-2 text-sm font-semibold text-ink">
<SlidersHorizontal className="h-4 w-4 text-accent" aria-hidden="true" />
Filtros
{activeFilterCount > 0 && <span className="chip chip-accent px-2 py-0">{activeFilterCount}</span>}
</span>
{activeFilterCount > 0 && (
<button
type="button"
onClick={(e) => { e.preventDefault(); clearFilters(); }}
className="text-sm font-medium text-accent hover:text-accent-strong"
>
Limpar
</button>
)}
</summary>
<div className="border-t border-line p-5 lg:max-h-[calc(100vh-11rem)] lg:overflow-y-auto">{filtros}</div>
</details>
</aside>
<div>
<p role="status" aria-live="polite" className="text-sm text-muted">
<strong className="font-semibold text-ink">{filteredBoards.length}</strong>{" "}
{filteredBoards.length === 1 ? "placa encontrada" : "placas encontradas"}
</p>
</div>
{filteredBoards.length === 0 ? (
<div className="text-center py-20 bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-8 shadow-md select-none">
<Search className="w-12 h-12 text-slate-450 mx-auto mb-4" />
<p className="text-base font-display font-extrabold text-slate-800 dark:text-slate-200 mb-1">Nenhuma Placa Correspondente</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Tente ajustar ou redefinir seus filtros laterais para expandir a busca.</p>
<div className="card mt-4 px-6 py-14 text-center">
<Search className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h2 className="mt-4 font-display text-xl font-semibold text-ink">Nenhuma placa com esses filtros</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Remova alguns filtros ou busque por outro nome.</p>
<button type="button" onClick={clearFilters} className="btn btn-primary mt-6">
Limpar filtros
</button>
</div>
) : (
<div className="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredBoards.map((board, index) => (
<BoardCard key={index} board={board} />
))}
@@ -361,32 +316,10 @@ export default function PlacasPage() {
)}
</div>
</div>
</section>
</main>
<Footer />
<style dangerouslySetInnerHTML={{ __html: `
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: #334155;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
`}} />
</div>
);
}
+123 -215
View File
@@ -2,43 +2,14 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { useState, Fragment } from "react";
import PageHero from "@/components/PageHero";
import { useEffect, useState, Fragment } from "react";
import Link from "next/link";
import seriesData from "@/public/series.json";
import {
Scale,
GitCompare,
Check,
ChevronDown,
ChevronUp,
ArrowRight,
FileText,
ExternalLink,
ShoppingBag,
Cpu,
Network,
Database,
Sliders,
Activity,
HelpCircle,
Thermometer,
ShieldCheck,
Package
} from "lucide-react";
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
// Category to Icon mapping
const categoryIcons = {
"Geral": <HelpCircle className="w-4 h-4 text-purple-500" />,
"Processador": <Cpu className="w-4 h-4 text-blue-500" />,
"Conectividade": <Network className="w-4 h-4 text-indigo-500" />,
"Memória": <Database className="w-4 h-4 text-pink-500" />,
"Periféricos Digitais": <Sliders className="w-4 h-4 text-sky-500" />,
"Periféricos Analógicos": <Activity className="w-4 h-4 text-emerald-500" />,
"Segurança": <ShieldCheck className="w-4 h-4 text-teal-500" />,
"Disponibilidade e Ferramentas": <Package className="w-4 h-4 text-amber-500" />,
"Interfaces Especiais": <Activity className="w-4 h-4 text-cyan-500" />,
"Especificações Ambientais": <Thermometer className="w-4 h-4 text-red-500" />,
};
const MIN_SERIES = 2;
const MAX_SERIES = 4;
/* Lê "seguranca.ecc" além de chaves simples. */
function valorDe(objeto, caminho) {
@@ -47,6 +18,7 @@ function valorDe(objeto, caminho) {
export default function Comparacao() {
const [selectedSeries, setSelectedSeries] = useState(["ESP32", "ESP32-S3"]);
const [soDiferencas, setSoDiferencas] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState({
"Interfaces Especiais": true,
"Especificações Ambientais": true,
@@ -54,23 +26,24 @@ export default function Comparacao() {
const series = Object.entries(seriesData);
// O seletor envia as séries finalistas em ?chips=ESP32-S3,ESP32-C6.
useEffect(() => {
const chips = new URLSearchParams(window.location.search).get("chips");
if (!chips) return;
const validas = [...new Set(chips.split(","))].filter((k) => seriesData[k]).slice(0, MAX_SERIES);
if (validas.length >= MIN_SERIES) setSelectedSeries(validas);
}, []);
const toggleSeries = (seriesKey) => {
if (selectedSeries.includes(seriesKey)) {
if (selectedSeries.length > 2) {
setSelectedSeries(selectedSeries.filter(s => s !== seriesKey));
}
} else {
if (selectedSeries.length < 4) {
if (selectedSeries.length > MIN_SERIES) setSelectedSeries(selectedSeries.filter((s) => s !== seriesKey));
} else if (selectedSeries.length < MAX_SERIES) {
setSelectedSeries([...selectedSeries, seriesKey]);
}
}
};
const toggleCategory = (category) => {
setCollapsedCategories({
...collapsedCategories,
[category]: !collapsedCategories[category]
});
setCollapsedCategories({ ...collapsedCategories, [category]: !collapsedCategories[category] });
};
const compareFields = [
@@ -153,85 +126,60 @@ export default function Comparacao() {
];
const renderValue = (value) => {
if (!value || value === "Não") {
return <span className="text-slate-500 dark:text-slate-400 font-medium select-none">Não</span>;
}
if (!value || value === "Não") return <span className="text-muted">Não</span>;
if (value === "Sim") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 shadow-xs">
<Check className="w-3.5 h-3.5" />
<span className="inline-flex items-center gap-1.5 font-medium text-ok">
<Check className="h-4 w-4" aria-hidden="true" />
Sim
</span>
);
}
return <span className="text-slate-800 dark:text-slate-200 font-semibold">{value}</span>;
return <span className="font-medium text-ink">{value}</span>;
};
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? "Não"));
const diferem = (key) => new Set(valores(key)).size > 1;
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-6 pt-16 pb-24 max-w-7xl mx-auto">
<main id="conteudo">
<PageHero
trail={[{ label: "Comparador" }]}
icon={Scale}
label="Até quatro séries lado a lado"
title="Comparador de séries"
lede="Coloque núcleos, rádio, memória, periféricos e segurança na mesma tabela e veja o que realmente muda de um chip para outro."
/>
{/* Top Section */}
<section className="mb-16">
<div className="border-l-4 border-purple-500 pl-6 max-w-3xl">
<p className="text-xs font-bold text-purple-600 dark:text-purple-400 uppercase tracking-widest mb-2 select-none">
Análise Técnica Comparativa
</p>
<h1 className="text-4xl md:text-5xl font-display font-extrabold text-slate-800 dark:text-slate-100 mb-4 tracking-tight leading-tight">
Comparador de Séries ESP32
</h1>
<p className="text-base text-slate-500 dark:text-slate-400 leading-relaxed">
Coloque lado a lado as especificações completas de clock, memória, interfaces de rádio e periféricos analógicos para validar a engenharia e o custo-benefício de cada microcontrolador.
</p>
</div>
</section>
{/* Model Selector Card */}
<section className="mb-12">
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-6 md:p-8 shadow-2xl relative overflow-hidden">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<section className="shell pt-10 sm:pt-12" aria-labelledby="escolha-titulo">
<div className="panel p-5 sm:p-7">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 className="text-lg font-bold text-slate-800 dark:text-slate-100 mb-1">
Escolha as Séries
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400">
Selecione de 2 a até 4 modelos simultâneos para montar sua planilha de comparação.
<h2 id="escolha-titulo" className="text-base font-semibold text-ink">Escolha de {MIN_SERIES} a {MAX_SERIES} séries</h2>
<p role="status" aria-live="polite" className="mt-1 text-sm text-muted">
{selectedSeries.length} de {MAX_SERIES} selecionadas
{selectedSeries.length === MAX_SERIES && ". Remova uma para trocar."}
</p>
</div>
<div className="inline-flex items-center gap-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 px-3.5 py-1.5 rounded-full border border-purple-500/20 text-xs font-bold shadow-xs select-none">
<GitCompare className="w-3.5 h-3.5" />
<span>{selectedSeries.length} de 4 selecionados</span>
</div>
</div>
{/* Brand-colored dynamic button selector grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-3">
{series.map(([key, seriesItem]) => {
const isSelected = selectedSeries.includes(key);
<div className="mt-4 flex flex-wrap gap-2">
{series.map(([key]) => {
const marcada = selectedSeries.includes(key);
const bloqueada = !marcada && selectedSeries.length >= MAX_SERIES;
return (
<button
key={key}
type="button"
onClick={() => toggleSeries(key)}
style={{
borderColor: isSelected ? seriesItem.cor : undefined
}}
className={`p-4 border rounded-2xl transition-all duration-300 cursor-pointer active:scale-95 text-center flex flex-col items-center justify-center shadow-xs ${
isSelected
? 'bg-slate-100 dark:bg-slate-950 shadow-md scale-[1.02] border-2'
: 'border-slate-300 dark:border-slate-800/80 bg-white dark:bg-slate-900/30 hover:border-purple-400/50 hover:bg-purple-500/5'
}`}
aria-pressed={marcada}
aria-disabled={bloqueada}
className={`chip-btn ${bloqueada ? "opacity-50" : ""}`}
>
<div className="text-3xl mb-2 select-none transform hover:scale-110 transition-transform">{seriesItem.icone}</div>
<div className="text-xs font-bold text-slate-800 dark:text-slate-200">{key}</div>
{isSelected && (
<div
className="mt-2.5 w-2 h-2 rounded-full shrink-0 animate-pulse"
style={{ backgroundColor: seriesItem.cor }}
/>
)}
{marcada && <Check className="h-4 w-4" aria-hidden="true" />}
{key}
</button>
);
})}
@@ -239,74 +187,71 @@ export default function Comparacao() {
</div>
</section>
{/* Side by side interactive Table */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl overflow-hidden shadow-2xl">
<div className="overflow-x-auto scrollbar-thin">
<table className="w-full border-collapse">
<section className="shell mt-8" aria-labelledby="tabela-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="tabela-titulo" className="text-base font-semibold text-ink">Especificações</h2>
<label className="flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
<input
type="checkbox"
checked={soDiferencas}
onChange={(e) => setSoDiferencas(e.target.checked)}
className="h-5 w-5 cursor-pointer accent-[var(--accent)]"
/>
Mostrar só as diferenças
</label>
</div>
<div className="panel mt-3 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full min-w-[30rem] border-collapse text-sm">
<caption className="sr-only">Comparação das séries {selectedSeries.join(", ")}</caption>
<thead>
<tr className="bg-slate-850 dark:bg-slate-950 border-b border-slate-750 dark:border-slate-900">
<th className="px-6 py-5 text-left text-white font-bold text-xs uppercase tracking-widest sticky left-0 bg-slate-850 dark:bg-slate-950 z-20 shadow-xs border-r border-slate-700/30">
<tr className="border-b border-line bg-surface-2">
<th scope="col" className="sticky left-0 z-20 w-28 bg-surface-2 px-4 py-4 text-left font-semibold text-muted sm:w-52 sm:px-6">
Especificação
</th>
{selectedSeries.map(seriesKey => {
const seriesItem = seriesData[seriesKey];
return (
<th key={seriesKey} className="px-6 py-5 text-center text-white font-bold text-xs uppercase tracking-widest min-w-48">
<div className="flex flex-col items-center gap-2 select-none">
<span className="text-3xl transform hover:scale-105 transition-transform">{seriesItem.icone}</span>
<span className="text-sm font-extrabold tracking-wide" style={{ color: seriesItem.cor }}>{seriesKey}</span>
</div>
{selectedSeries.map((seriesKey) => (
<th key={seriesKey} scope="col" className="min-w-40 px-4 py-4 text-left sm:px-6">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center gap-1 font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
<ArrowUpRight className="h-4 w-4 text-muted" aria-hidden="true" />
</Link>
</th>
);
})}
))}
</tr>
</thead>
<tbody>
{compareFields.map(({ category, fields }) => {
const isCollapsed = collapsedCategories[category];
const visiveis = soDiferencas ? fields.filter(({ key }) => diferem(key)) : fields;
if (soDiferencas && visiveis.length === 0) return null;
const isCollapsed = collapsedCategories[category] && !soDiferencas;
return (
<Fragment key={category}>
{/* Collapsible Category Accordion Header */}
<tr
<tr className="border-y border-line bg-surface-2/60">
<th scope="rowgroup" colSpan={selectedSeries.length + 1} className="p-0 text-left">
<button
type="button"
onClick={() => toggleCategory(category)}
className="bg-slate-100/80 dark:bg-slate-950/60 border-y border-slate-250/60 dark:border-slate-800/80 cursor-pointer select-none hover:bg-slate-200/60 dark:hover:bg-slate-900/80 transition-colors"
aria-expanded={!isCollapsed}
className="flex min-h-12 w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left font-semibold text-ink sm:px-6"
>
<td
colSpan={selectedSeries.length + 1}
className="px-6 py-3.5 font-bold text-slate-800 dark:text-slate-200 text-xs uppercase tracking-wider"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{categoryIcons[category] || <HelpCircle className="w-4 h-4 text-purple-500" />}
<span>{category}</span>
</div>
{isCollapsed ? (
<ChevronDown className="w-4 h-4 opacity-60 text-purple-600 dark:text-purple-400" />
) : (
<ChevronUp className="w-4 h-4 opacity-60 text-purple-600 dark:text-purple-400" />
)}
</div>
</td>
<span className="sticky left-4 sm:left-6">{category}</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-muted transition-transform ${isCollapsed ? "" : "rotate-180"}`} aria-hidden="true" />
</button>
</th>
</tr>
{/* Fields list within category (if expanded) */}
{!isCollapsed && fields.map(({ key, label }) => (
<tr
key={key}
className="border-b border-slate-200 dark:border-slate-800/40 hover:bg-purple-500/5 dark:hover:bg-purple-400/5 transition-colors group"
>
<td className="px-6 py-4 font-bold text-slate-600 dark:text-slate-400 text-xs bg-white dark:bg-slate-900/10 sticky left-0 border-r border-slate-150/60 dark:border-slate-800/60 shrink-0 z-10">
{!isCollapsed &&
visiveis.map(({ key, label }) => (
<tr key={key} className="border-b border-line last:border-b-0 hover:bg-surface-2/50">
<th scope="row" className="sticky left-0 z-10 bg-surface px-4 py-3.5 text-left align-top font-medium text-muted sm:px-6">
{label}
</th>
{selectedSeries.map((seriesKey) => (
<td key={seriesKey} className="px-4 py-3.5 align-top sm:px-6">
{renderValue(valorDe(seriesData[seriesKey], key))}
</td>
{selectedSeries.map(seriesKey => {
const seriesItem = seriesData[seriesKey];
const value = valorDe(seriesItem, key);
return (
<td key={seriesKey} className="px-6 py-4 text-center text-xs">
{renderValue(value)}
</td>
);
})}
))}
</tr>
))}
</Fragment>
@@ -316,80 +261,43 @@ export default function Comparacao() {
</table>
</div>
</div>
</section>
{/* Professional CTA Banner for the Catalog */}
<div className="my-20 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-slate-950 dark:to-purple-950/20 border border-indigo-200 dark:border-slate-800/80 rounded-3xl p-8 md:p-12 shadow-2xl relative overflow-hidden">
<div className="absolute right-0 bottom-0 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
<div className="flex-1 max-w-2xl text-center md:text-left">
<div className="inline-flex items-center gap-2 bg-indigo-500/10 text-indigo-700 dark:bg-white/10 dark:text-slate-350 px-3 py-1 rounded-full mb-4 text-xs font-semibold select-none">
<ShoppingBag className="w-3.5 h-3.5" />
<span>Hardware Adicional</span>
</div>
<h3 className="text-2xl md:text-3xl font-display font-extrabold text-slate-900 dark:text-white mb-4">
Catálogo de Placas de Desenvolvimento
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Quer testar o chip selecionado na bancada? Compare placas de desenvolvimento com layouts de pinos mapeados, conexões de periféricos e links oficiais dos melhores fornecedores.
</p>
</div>
<Link
href="/catalogo"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all duration-300 shrink-0 shadow-md active:scale-95"
>
<span>Acessar Catálogo</span>
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
{/* Detailed technical datasheets section */}
<div className="mt-12">
<h3 className="text-xl font-display font-extrabold text-slate-800 dark:text-slate-100 mb-6 border-l-4 border-purple-500 pl-4 select-none">
Documentação Técnica Relacionada
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{selectedSeries.map(seriesKey => {
<section className="shell section" aria-labelledby="docs-titulo">
<h2 id="docs-titulo" className="section-title">Documentação oficial</h2>
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{selectedSeries.map((seriesKey) => {
const seriesItem = seriesData[seriesKey];
return (
<div
key={seriesKey}
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 p-5 rounded-2xl hover:border-purple-500/50 dark:hover:border-purple-400/50 hover:shadow-lg transition-all duration-300 flex flex-col justify-between"
>
<div className="flex gap-4 items-start mb-4">
<div className="text-4xl select-none">{seriesItem.icone}</div>
<div className="min-w-0">
<Link href={`/series/${seriesKey}`} className="no-underline">
<h4 className="text-sm font-extrabold text-slate-850 dark:text-slate-100 hover:text-purple-600 dark:hover:text-purple-400 transition-colors">
<div key={seriesKey} className="card flex flex-col justify-between gap-5 p-5">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
</h4>
</Link>
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider mt-1">Guia de Hardware</p>
</div>
</div>
{seriesItem.datasheet ? (
<a
href={seriesItem.datasheet}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-1.5 w-full text-center py-2 bg-slate-100 dark:bg-slate-950 hover:bg-purple-500/10 dark:hover:bg-purple-400/10 border border-slate-300 dark:border-slate-800 rounded-xl text-xs font-bold text-slate-650 dark:text-slate-350 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
>
<span>Datasheet Oficial</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={seriesItem.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
Datasheet oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
) : (
<span className="inline-flex items-center justify-center gap-1.5 w-full text-center py-2 bg-slate-100 dark:bg-slate-950 border border-slate-300 dark:border-slate-800 rounded-xl text-xs font-bold text-slate-500 dark:text-slate-400 select-none">
<span>Datasheet ainda não publicado</span>
</span>
<span className="text-sm text-muted">Datasheet ainda não publicado</span>
)}
</div>
);
})}
</div>
<div className="panel mt-10 flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Quer testar na bancada?</h2>
<p className="mt-3 text-base text-muted">Veja as placas de desenvolvimento com pinagem, periféricos e link para a loja.</p>
</div>
<Link href="/catalogo" className="btn btn-primary shrink-0">
Ver catálogo de placas
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
+211 -436
View File
@@ -3,41 +3,46 @@
import { useState, useMemo } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import {
CATEGORIAS_COMPONENTES,
COMPONENTES_REGISTRY,
GUIA_COMPONENT_MANAGER
} from "@/lib/componentRegistryData";
import {
Layers,
Search,
Copy,
Check,
ExternalLink,
Sparkles,
Terminal,
Code2,
Plus,
Trash2,
Download,
Cpu,
ArrowUpRight,
Box,
CheckCircle2,
ChevronRight,
HelpCircle,
ArrowRight,
BookOpen,
ShieldCheck,
Zap,
Monitor,
Cloud,
Brain,
Home,
Check,
CircuitBoard,
Wrench
Cloud,
Copy,
Cpu,
Download,
HelpCircle,
Home,
Layers,
Monitor,
Plus,
Search,
ShieldCheck,
Terminal,
Trash2,
Wrench,
X,
} from "lucide-react";
const ICONES_CATEGORIA = {
ui_display: Monitor,
cloud_conectividade: Cloud,
audio_voz_ia: Brain,
smart_home_mesh: Home,
drivers_perifericos: Cpu,
bsp_placas: CircuitBoard,
sistema_utilitarios: Wrench,
};
export default function ComponentRegistryPage() {
const [busca, setBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
@@ -119,520 +124,290 @@ export default function ComponentRegistryPage() {
URL.revokeObjectURL(url);
};
// Mapeamento de ícones de categoria
const renderIconeCategoria = (id) => {
switch (id) {
case "ui_display":
return <Monitor className="w-4 h-4" />;
case "cloud_conectividade":
return <Cloud className="w-4 h-4" />;
case "audio_voz_ia":
return <Brain className="w-4 h-4" />;
case "smart_home_mesh":
return <Home className="w-4 h-4" />;
case "drivers_perifericos":
return <Cpu className="w-4 h-4" />;
case "bsp_placas":
return <CircuitBoard className="w-4 h-4" />;
case "sistema_utilitarios":
return <Wrench className="w-4 h-4" />;
default:
return <Layers className="w-4 h-4" />;
}
const limparFiltros = () => {
setBusca("");
setCategoriaAtiva("todos");
setChipFiltro("todos");
};
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-4 md:px-6 pt-12 md:pt-16 pb-24 max-w-7xl mx-auto space-y-12">
{/* Top Hero Section */}
<section className="text-center select-none relative">
<div className="inline-flex items-center gap-2 bg-white/80 dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-6 border border-slate-300 dark:border-slate-800/80 shadow-xs">
<Box className="w-4 h-4 text-purple-500" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
ESP-IDF Component Manager
</span>
<span className="bg-purple-100 dark:bg-purple-950/80 text-purple-700 dark:text-purple-300 text-[10px] font-extrabold px-2 py-0.5 rounded-full">
Oficial
</span>
</div>
<h1 className="text-4xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 pb-2 pt-1 leading-tight md:leading-[1.18] tracking-tight">
ESP Component Registry
</h1>
<p className="text-sm md:text-base text-slate-600 dark:text-slate-350 max-w-3xl mx-auto leading-relaxed font-semibold">
O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF.
Adicione drivers de displays, reconhecimento de voz, pilhas industriais e bibliotecas
com comandos de um clique e geração de manifesto declarativo.
</p>
<div className="flex flex-wrap items-center justify-center gap-3 mt-6">
<a
href="https://components.espressif.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white text-xs font-bold px-4 py-2.5 rounded-xl shadow-md transition-all active:scale-95"
<main id="conteudo">
<PageHero
trail={[{ label: "Component Registry" }]}
icon={Box}
label="ESP-IDF Component Manager"
title="ESP Component Registry"
lede="O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
>
<span>Acessar Registry Oficial</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
Abrir o registro oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
<a
href="#manifesto-builder"
className="inline-flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 text-xs font-bold px-4 py-2.5 rounded-xl shadow-xs transition-all active:scale-95"
>
<Code2 className="w-3.5 h-3.5 text-purple-500" />
<span>Gerador de idf_component.yml</span>
<a href="#manifesto-builder" className="btn btn-ghost">
Gerar idf_component.yml
</a>
</div>
</section>
</PageHero>
{/* Interactive Search & Filter Bar */}
<section className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 border border-slate-300 dark:border-slate-800/80 shadow-xl space-y-5">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
{/* Search Input */}
<div className="relative w-full md:flex-1">
<Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<div className="panel p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-componente" className="sr-only">Buscar por nome, namespace ou tag</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
type="text"
id="busca-componente"
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar componentes por nome, namespace, funcionalidade ou tags (ex: lvgl, rainmaker, ws2812)..."
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 dark:bg-slate-950/70 border border-slate-250 dark:border-slate-800 rounded-2xl text-xs md:text-sm font-semibold text-slate-800 dark:text-slate-100 placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-purple-500 transition-all"
placeholder="Buscar por nome, namespace ou tag"
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
{busca && (
<button
onClick={() => setBusca("")}
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
>
Limpar
</button>
)}
</div>
{/* Chip Filter */}
<div className="flex items-center gap-1.5 w-full md:w-auto overflow-x-auto pb-1 md:pb-0">
<span className="text-[11px] font-bold text-slate-500 uppercase tracking-wider shrink-0 mr-1">
SoC:
</span>
{chipsDisponiveis.map((chip) => (
<button
key={chip}
onClick={() => setChipFiltro(chip)}
className={`px-3 py-1.5 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
chipFiltro === chip
? "bg-purple-600 text-white shadow-xs"
: "bg-slate-100 dark:bg-slate-800/80 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-700"
}`}
>
{chip === "todos" ? "Todos SoCs" : chip}
</button>
))}
</div>
</div>
{/* Category Tabs */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 border-t border-slate-200 dark:border-slate-800/60 pt-4">
<div className="mt-5">
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
<div className="mt-3 flex flex-wrap gap-2">
{CATEGORIAS_COMPONENTES.map((cat) => {
const ativa = categoriaAtiva === cat.id;
const contagem =
cat.id === "todos"
? COMPONENTES_REGISTRY.length
: COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
const contagem = cat.id === "todos" ? COMPONENTES_REGISTRY.length : COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
return (
<button
key={cat.id}
onClick={() => setCategoriaAtiva(cat.id)}
className={`inline-flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
ativa
? "bg-purple-50 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/60 shadow-xs"
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/60"
}`}
>
{renderIconeCategoria(cat.id)}
<span>{cat.nome}</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full ${
ativa
? "bg-purple-200 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{contagem}
</span>
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
<Icone className="h-4 w-4" aria-hidden="true" />
{cat.id === "todos" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
<span className="tabular-nums opacity-80">{contagem}</span>
</button>
);
})}
</div>
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
<div className="mt-3 flex flex-wrap gap-2">
{chipsDisponiveis.map((chip) => (
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
{chip === "todos" ? "Todos" : chip}
</button>
))}
</div>
</div>
</div>
</section>
{/* Components Grid */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-purple-500" />
<span>Componentes Disponíveis ({componentesFiltrados.length})</span>
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? "componente" : "componentes"}
</h2>
<span className="text-xs text-slate-500 font-semibold">
Clique em um comando para copiar para o terminal
</span>
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
{totalFiltros > 0 && (
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<X className="h-4 w-4" aria-hidden="true" />
Limpar filtros
</button>
)}
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
</a>
</div>
</div>
{componentesFiltrados.length === 0 ? (
<div className="text-center py-16 bg-white dark:bg-slate-900/40 rounded-3xl border border-slate-300 dark:border-slate-800 p-8">
<Box className="w-12 h-12 text-slate-400 mx-auto mb-3 opacity-60" />
<p className="text-base font-bold text-slate-750 dark:text-slate-200">
Nenhum componente encontrado para esses filtros.
</p>
<p className="text-xs text-slate-500 mt-1">
Tente buscar por termos mais genéricos ou resetar a categoria.
</p>
<button
onClick={() => {
setBusca("");
setCategoriaAtiva("todos");
setChipFiltro("todos");
}}
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
>
Limpar Todos os Filtros
</button>
<div className="card mt-6 px-6 py-14 text-center">
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum componente encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{componentesFiltrados.map((comp) => {
const selecionado = componentesSelecionados.includes(comp.id);
const cliCmd = `idf.py add-dependency "${comp.namespace}/${comp.name}${comp.versao}"`;
const foiCopiado = copiadoId === comp.id;
return (
<div
key={comp.id}
className={`bg-white dark:bg-slate-900/45 backdrop-blur-xl border rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-300 flex flex-col justify-between overflow-hidden relative group ${
selecionado
? "border-purple-500/80 ring-2 ring-purple-500/20"
: "border-slate-300 dark:border-slate-800/80 hover:-translate-y-1"
}`}
>
{/* Top Header Card */}
<div className="p-6 pb-4 border-b border-slate-150 dark:border-slate-850/40">
<div className="flex items-start justify-between gap-3 mb-2">
<div>
<div className="flex items-center gap-1.5 mb-1">
<span className="text-[10px] font-extrabold text-slate-400 uppercase tracking-wider">
<article key={comp.id} className={`card flex flex-col overflow-hidden ${selecionado ? "border-accent ring-2 ring-accent/20" : ""}`}>
<div className="flex-1 p-5 sm:p-6">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="flex flex-wrap items-center gap-2 text-xs text-muted">
{comp.namespace}/
</span>
{comp.oficial && (
<span className="inline-flex items-center gap-1 bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-300 text-[9px] font-extrabold px-2 py-0.2 rounded-full border border-emerald-300 dark:border-emerald-800/60">
<ShieldCheck className="w-3 h-3" />
<span className="chip chip-accent px-2 py-0">
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
Oficial
</span>
)}
</div>
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{comp.name}
</h3>
<p className="text-xs font-semibold text-purple-600 dark:text-purple-400">
{comp.titulo}
</p>
<h3 className="mt-1 break-words font-display text-xl font-semibold tracking-[-0.03em] text-ink">{comp.name}</h3>
<p className="mt-0.5 text-sm font-medium text-accent">{comp.titulo}</p>
</div>
<span className="shrink-0 rounded-md border border-line bg-surface-2 px-2 py-1 font-mono text-xs text-ink-soft">{comp.versao}</span>
</div>
<span className="px-2.5 py-1 rounded-lg text-[10px] font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 shrink-0">
{comp.versao}
</span>
</div>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
<p className="text-xs text-slate-500 dark:text-slate-350 leading-relaxed mt-2.5 line-clamp-3">
{comp.descricao}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-1.5 mt-3.5">
<ul className="mt-4 flex flex-wrap gap-1.5">
{comp.tags.map((tag) => (
<span
key={tag}
className="text-[9px] font-bold bg-slate-100 dark:bg-slate-950/60 text-slate-600 dark:text-slate-400 px-2 py-0.5 rounded-md border border-slate-250 dark:border-slate-800"
>
{tag}
</span>
<li key={tag} className="chip">{tag}</li>
))}
</div>
</ul>
</div>
{/* Interactive CLI Action Box */}
<div className="p-6 py-4 bg-slate-50/50 dark:bg-slate-950/40 space-y-3">
<div>
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
<span className="flex items-center gap-1">
<Terminal className="w-3 h-3 text-purple-500" />
Comando CLI
</span>
{foiCopiado && (
<span className="text-emerald-500 font-bold flex items-center gap-0.5">
<Check className="w-3 h-3" /> Copiado!
</span>
)}
</div>
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
<button
type="button"
onClick={() => copiarComando(comp.id, cliCmd)}
className="w-full text-left bg-slate-900 text-slate-100 p-2.5 rounded-xl font-mono text-[11px] hover:bg-slate-800 border border-slate-800 flex items-center justify-between group/cmd transition-colors cursor-pointer"
title="Clique para copiar comando"
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
aria-label={`Copiar comando para adicionar ${comp.name}`}
>
<span className="truncate pr-2">{cliCmd}</span>
<Copy className="w-3.5 h-3.5 text-slate-400 group-hover/cmd:text-white shrink-0" />
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
</button>
<p role="status" className="sr-only">{foiCopiado ? "Comando copiado" : ""}</p>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
{comp.chips.join(", ")}
</p>
</div>
{/* Compatible Chips */}
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-1">
SoCs Compatíveis:
</span>
<div className="flex flex-wrap gap-1">
{comp.chips.map((chip) => (
<span
key={chip}
className="text-[9px] font-semibold text-slate-650 dark:text-slate-350 bg-slate-200/60 dark:bg-slate-800 px-1.5 py-0.2 rounded"
>
{chip}
</span>
))}
</div>
</div>
</div>
{/* Bottom Actions */}
<div className="p-4 px-6 border-t border-slate-150 dark:border-slate-850/40 flex items-center justify-between text-xs font-bold">
<div className="flex items-center justify-between gap-2 border-t border-line p-4 sm:px-6">
<button
type="button"
onClick={() => toggleSelecaoManifesto(comp.id)}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
selecionado
? "bg-purple-600 text-white shadow-xs"
: "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/40 hover:bg-purple-100"
}`}
aria-pressed={selecionado}
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
>
{selecionado ? (
<>
<CheckCircle2 className="w-3.5 h-3.5" />
<span>No Manifesto</span>
</>
) : (
<>
<Plus className="w-3.5 h-3.5" />
<span>Adicionar ao Manifesto</span>
</>
)}
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
{selecionado ? "No manifesto" : "Adicionar ao manifesto"}
</button>
<div className="flex items-center gap-2">
<a
href={comp.urlRegistry}
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors p-1"
title="Ver página oficial no ESP Component Registry"
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
aria-label={`Abrir ${comp.name} no registro oficial (nova aba)`}
>
<ExternalLink className="w-4 h-4" />
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
</div>
</div>
</article>
);
})}
</div>
)}
</section>
{/* Interactive Manifest Builder Section */}
<section
id="manifesto-builder"
className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-purple-300 dark:border-purple-800/60 shadow-2xl space-y-6"
>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pb-4 border-b border-slate-250 dark:border-slate-800">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 text-xs font-bold rounded-full mb-2">
<Code2 className="w-3.5 h-3.5" />
<span>Gerador de Manifesto Declarativo</span>
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Construtor de `idf_component.yml`
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 font-semibold">
Adicione este arquivo na pasta <code>main/</code> do seu projeto ESP-IDF para
instalar todas as bibliotecas automaticamente na compilação.
<section id="manifesto-builder" className="shell scroll-mt-28 pb-16" aria-labelledby="manifesto-titulo">
<div className="panel p-6 sm:p-9">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div className="max-w-2xl">
<p className="eyebrow">Gerador de manifesto</p>
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o
ESP-IDF baixa todas as bibliotecas na compilação.
</p>
</div>
<div className="flex items-center gap-2.5 w-full md:w-auto">
<button
onClick={copiarManifesto}
className="flex-1 md:flex-none inline-flex items-center justify-center gap-1.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-2.5 rounded-xl text-xs font-bold hover:opacity-90 shadow-md transition-all active:scale-95 cursor-pointer"
>
{manifestoCopiado ? (
<>
<Check className="w-4 h-4" />
<span>Manifesto Copiado!</span>
</>
) : (
<>
<Copy className="w-4 h-4" />
<span>Copiar YAML</span>
</>
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
<span role="status">{manifestoCopiado ? "Copiado" : "Copiar YAML"}</span>
</button>
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
<Download className="h-4 w-4" aria-hidden="true" />
Baixar .yml
</button>
{componentesSelecionados.length > 0 && (
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
<Trash2 className="h-4 w-4" aria-hidden="true" />
Limpar
</button>
)}
</button>
</div>
</div>
<button
onClick={baixarManifesto}
className="inline-flex items-center justify-center gap-1.5 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95 cursor-pointer"
title="Baixar arquivo idf_component.yml pronto"
>
<Download className="w-4 h-4" />
<span className="hidden sm:inline">Baixar .yml</span>
</button>
<div className="code-panel mt-6">
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
</div>
{componentesSelecionados.length > 0 && (
<button
onClick={() => setComponentesSelecionados([])}
className="p-2.5 text-slate-400 hover:text-rose-500 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
title="Limpar seleção"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* YAML Output Terminal */}
<div className="relative">
<div className="bg-slate-950 text-slate-200 rounded-2xl p-4 md:p-6 font-mono text-xs md:text-sm overflow-x-auto border border-slate-800 shadow-inner">
<pre>{manifestoYaml}</pre>
</div>
</div>
{/* Selected components badges */}
<div className="flex flex-wrap items-center gap-2 pt-2">
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider mr-2">
Componentes Inclusos ({componentesSelecionados.length}):
</span>
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
return (
<span
key={id}
className="inline-flex items-center gap-1.5 bg-purple-50 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-800 px-3 py-1 rounded-xl text-xs font-bold"
>
<span>{c.name}</span>
<li key={id} className="chip chip-accent gap-1 pr-1">
{c.name}
<button
type="button"
onClick={() => toggleSelecaoManifesto(id)}
className="hover:text-rose-500 ml-1 cursor-pointer"
title="Remover"
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
aria-label={`Remover ${c.name} do manifesto`}
>
×
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</span>
</li>
);
})}
</ul>
)}
</div>
</section>
{/* Educational Guide: O que é o Component Manager */}
<section className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800 shadow-xl space-y-8 select-none">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="p-2.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400">
<BookOpen className="w-5 h-5" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
{GUIA_COMPONENT_MANAGER.titulo}
</h2>
</div>
<p className="text-xs md:text-sm text-slate-600 dark:text-slate-350 leading-relaxed font-semibold max-w-4xl">
{GUIA_COMPONENT_MANAGER.resumo}
</p>
</div>
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
<SectionHead id="guia-titulo" eyebrow="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
{/* Vantagens Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
{GUIA_COMPONENT_MANAGER.vantagens.map((v, i) => (
<div
key={i}
className="bg-slate-50/70 dark:bg-slate-950/40 p-5 rounded-2xl border border-slate-200 dark:border-slate-800/80 space-y-2"
>
<div className="w-8 h-8 rounded-xl bg-purple-100 dark:bg-purple-900/40 text-purple-600 dark:text-purple-400 flex items-center justify-center text-xs font-extrabold">
0{i + 1}
</div>
<h3 className="text-sm font-bold text-slate-850 dark:text-slate-100">{v.titulo}</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
{v.descricao}
</p>
</div>
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
<li key={v.titulo} className="card p-5">
<h3 className="font-semibold text-ink">{v.titulo}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{v.descricao}</p>
</li>
))}
</div>
</ul>
{/* Comandos Essenciais da CLI */}
<div className="space-y-3">
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
<Terminal className="w-4 h-4 text-purple-500" />
<span>Comandos Essenciais no Terminal</span>
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
Comandos essenciais
</h3>
<div className="space-y-3">
<ul className="mt-4 space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
<div
key={i}
className="bg-slate-900 text-slate-100 p-4 rounded-2xl border border-slate-800 flex flex-col md:flex-row md:items-center justify-between gap-3 font-mono text-xs"
>
<div>
<span className="text-purple-400 font-bold">$ {cmd.exemplo}</span>
<p className="text-[11px] text-slate-400 font-sans mt-1">
{cmd.explicacao}
</p>
<li key={cmd.exemplo} className="code-panel flex flex-col justify-between gap-3 p-4 md:flex-row md:items-center">
<div className="min-w-0">
<code className="block break-all font-mono text-sm text-[#5eead4]">$ {cmd.exemplo}</code>
<p className="mt-1 text-sm text-[#a6afbd]">{cmd.explicacao}</p>
</div>
<button
type="button"
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
className="self-start md:self-auto inline-flex items-center gap-1 bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-lg text-[11px] font-bold text-slate-200 transition-colors shrink-0"
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
>
{copiadoId === `cmd-${i}` ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-400" />
<span>Copiado</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copiar</span>
</>
)}
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `cmd-${i}` ? "Copiado" : "Copiar"}
</button>
</div>
</li>
))}
</div>
</div>
</ul>
{/* Versioning Semantics Callout */}
<div className="bg-purple-50 dark:bg-purple-950/30 rounded-2xl p-5 border border-purple-200 dark:border-purple-800/40 text-xs space-y-2">
<h4 className="font-bold text-purple-900 dark:text-purple-300 flex items-center gap-1.5">
<HelpCircle className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span>Regras de Versionamento Semântico no ESP-IDF</span>
</h4>
<ul className="list-disc list-inside space-y-1 text-slate-750 dark:text-slate-350">
<li>
<code>^1.2.0</code>: Permite atualizações menores e correções (ex: 1.2.1, 1.3.0),
mas bloqueia grandes mudanças de API (2.0.0). <strong>(Recomendado)</strong>
</li>
<li>
<code>~1.2.0</code>: Permite apenas correções de bugs (ex: 1.2.1, 1.2.5), travando
na mesma versão menor.
</li>
<li>
<code>1.2.0</code>: Trava na versão exata indicada, sem receber atualizações.
</li>
<li>
<code>*</code>: Aceita qualquer versão disponível (não recomendado para produção).
</li>
<div className="callout mt-8">
<h3 className="flex items-center gap-2 font-semibold text-ink">
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
Versionamento semântico no ESP-IDF
</h3>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: trava na versão exata.</li>
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
</ul>
</div>
</section>
+256 -622
View File
File diff suppressed because it is too large. Load diff
+74 -233
View File
@@ -2,27 +2,11 @@ import { paginaMeta, jsonLd } from "@/lib/seo";
import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import { notFound } from "next/navigation";
import CodeExample from "@/components/CodeExample";
import frameworksData from "@/public/frameworks.json";
import {
ArrowLeft,
BookOpen,
Code,
Layers,
Zap,
Compass,
CheckCircle,
Terminal,
FileText,
Sliders,
Award,
Target,
Box,
Sparkles,
ExternalLink,
ArrowRight
} from "lucide-react";
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
export default async function FrameworkDetail({ params }) {
const { key } = await params;
@@ -37,7 +21,7 @@ export default async function FrameworkDetail({ params }) {
const isEspIdf = key === "ESP-IDF";
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
{/* Ficha técnica do framework e trilha de navegação para o Google */}
<script
type="application/ld+json"
@@ -56,250 +40,107 @@ export default async function FrameworkDetail({ params }) {
<Header />
<main className="px-4 md:px-6 pt-12 md:pt-16 pb-24 max-w-7xl mx-auto">
{/* Back navigation link */}
<div className="mb-8">
<Link
href="/frameworks"
className="inline-flex items-center gap-2 text-xs font-bold text-slate-500 dark:text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors uppercase tracking-wider mb-6"
<main id="conteudo">
<PageHero
trail={[{ label: "Frameworks e SDKs", href: "/frameworks" }, { label: framework.nome }]}
icon={Layers}
label={framework.tipo}
title={framework.nome}
lede={framework.descricao}
>
<ArrowLeft className="w-3.5 h-3.5" />
<span>Voltar para Frameworks</span>
</Link>
{/* Top Info Banner Panel */}
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 shadow-2xl border relative overflow-hidden"
style={{ borderColor: `${framework.cor}40` }}
>
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-8">
<div className="flex items-center gap-5">
<span className="text-6xl select-none filter drop-shadow-md">{framework.icone}</span>
<div>
<h1 className="text-4xl font-display font-extrabold text-slate-850 dark:text-slate-100 tracking-tight leading-none mb-3">
{framework.nome}
</h1>
<p className="text-sm font-semibold text-slate-500 dark:text-slate-400 mb-4">{framework.nome_completo}</p>
<span
className="inline-block px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest select-none"
style={{ backgroundColor: `${framework.cor}18`, color: framework.cor, border: `1px solid ${framework.cor}30` }}
>
{framework.tipo}
</span>
</div>
</div>
{/* Call to Actions CTA Grid */}
<div className="grid sm:grid-cols-2 lg:grid-cols-1 gap-3 w-full lg:w-auto">
{framework.documentacao && (
<a
href={framework.documentacao}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 transition-opacity shadow-lg"
>
<BookOpen className="w-4 h-4" />
<span>Documentação Oficial</span>
<a href={framework.documentacao} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Documentação oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{framework.repositorio && (
<a
href={framework.repositorio}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/20 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:bg-purple-500/20 transition-all shadow-xs"
>
<Code className="w-4 h-4" />
<span>Repositório no GitHub</span>
<a href={framework.repositorio} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
Repositório no GitHub
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</div>
</div>
</PageHero>
<p className="text-sm text-slate-600 dark:text-slate-200 mt-6 leading-relaxed max-w-4xl font-semibold">
{framework.descricao}
</p>
<section className="shell mt-10" aria-label="Resumo">
<dl className="grid gap-3 md:grid-cols-3">
<div className="tile">
<dt className="text-xs font-medium text-muted">Linguagem</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.linguagem}</dd>
<dd className="mt-0.5 text-xs text-muted">{framework.nome_completo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">Categoria</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.tipo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">Objetivo principal</dt>
<dd className="mt-1 text-sm font-medium leading-relaxed text-ink">{framework.funcao_principal}</dd>
</div>
</dl>
</section>
{/* Framework Core Specifications Grid */}
<div className="grid md:grid-cols-3 gap-6 mb-8 select-none animate-fadeIn">
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: framework.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
<Code className="w-3.5 h-3.5 text-blue-500" />
<span>LINGUAGEM DE DESENVOLVIMENTO</span>
</h3>
<p className="text-2xl font-extrabold text-slate-800 dark:text-slate-100">{framework.linguagem}</p>
<p className="text-xs text-slate-400 mt-1">Sintaxe e ecossistema</p>
</div>
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: framework.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5 flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-purple-500" />
<span>CATEGORIA DO FRAMEWORK</span>
</h3>
<p className="text-2xl font-extrabold text-slate-800 dark:text-slate-100">{framework.tipo}</p>
<p className="text-xs text-slate-400 mt-1">Classificação de arquitetura</p>
</div>
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: framework.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1.5 flex items-center gap-1.5">
<Target className="w-3.5 h-3.5 text-emerald-500" />
<span>OBJETIVO PRINCIPAL</span>
</h3>
<p className="text-sm font-bold text-slate-700 dark:text-slate-200 leading-relaxed mt-1.5">{framework.funcao_principal}</p>
</div>
</div>
{/* side-by-side details block */}
<div className="grid lg:grid-cols-3 gap-6 mb-8 items-start">
{/* Left specs card */}
<div className="lg:col-span-2 bg-white dark:bg-slate-900/45 backdrop-blur-xl rounded-3xl p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl space-y-8">
<div>
<h2
className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 mb-6 pb-3 border-b flex items-center gap-2.5 select-none"
style={{ borderBottomColor: `${framework.cor}30` }}
>
<Zap className="w-5 h-5" style={{ color: framework.cor }} />
<span>Características de Destaque</span>
</h2>
<ul className="space-y-3">
{features.map((feature, i) => (
<li key={i} className="flex items-start gap-2.5 text-xs font-semibold text-slate-650 dark:text-slate-200">
<CheckCircle className="w-4 h-4 text-emerald-500 dark:text-emerald-450 mt-0.5 shrink-0" />
<span>{feature}</span>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label="Recursos e aplicações">
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Recursos de destaque</h2>
<ul className="mt-5 space-y-3">
{features.map((feature) => (
<li key={feature} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{feature}
</li>
))}
</ul>
</div>
<div>
<h2
className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 mb-6 pb-3 border-b flex items-center gap-2.5 select-none"
style={{ borderBottomColor: `${framework.cor}30` }}
>
<Compass className="w-5 h-5" style={{ color: framework.cor }} />
<span>Casos de Uso Recomendados</span>
</h2>
<div className="grid md:grid-cols-2 gap-3.5">
{useCases.map((useCase, i) => (
<div key={i} className="bg-slate-50/50 dark:bg-slate-900/60 p-4 rounded-2xl border border-slate-150/40 dark:border-slate-800/80 text-xs font-bold text-slate-700 dark:text-slate-200 flex items-center gap-2 hover:bg-purple-500/5 dark:hover:bg-purple-400/5 transition-colors select-none">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: framework.cor }} />
<span>{useCase}</span>
</div>
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Quando usar</h2>
<ul className="mt-5 space-y-2.5">
{useCases.map((useCase) => (
<li key={useCase} className="tile text-sm font-medium text-ink-soft">
{useCase}
</li>
))}
</ul>
</div>
</div>
</div>
</section>
{/* Right summary metadata card */}
<div className="bg-white dark:bg-slate-900/45 backdrop-blur-xl rounded-3xl p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl select-none">
<h2
className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 mb-6 pb-3 border-b flex items-center gap-2.5"
style={{ borderBottomColor: `${framework.cor}30` }}
>
<FileText className="w-5 h-5" style={{ color: framework.cor }} />
<span>Resumo Técnico</span>
</h2>
<div className="space-y-4 text-xs font-bold text-slate-650 dark:text-slate-200">
<div className="flex justify-between items-center py-2 border-b border-slate-200 dark:border-slate-850/45">
<span className="text-slate-500 dark:text-slate-400 uppercase tracking-wider text-[9px]">Nome Oficial</span>
<span className="text-slate-800 dark:text-slate-200 font-extrabold">{framework.nome}</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-slate-200 dark:border-slate-850/45">
<span className="text-slate-500 dark:text-slate-400 uppercase tracking-wider text-[9px]">Linguagem</span>
<span className="text-slate-800 dark:text-slate-200 font-extrabold">{framework.linguagem}</span>
</div>
<div className="flex justify-between items-center py-2 border-b border-slate-200 dark:border-slate-850/45">
<span className="text-slate-500 dark:text-slate-400 uppercase tracking-wider text-[9px]">Tipo / Suite</span>
<span className="text-slate-800 dark:text-slate-200 font-extrabold">{framework.tipo}</span>
</div>
<div className="flex justify-between items-center py-2">
<span className="text-slate-500 dark:text-slate-400 uppercase tracking-wider text-[9px]">Cor Temática</span>
<div className="flex items-center gap-2">
<code className="text-[10px] text-slate-500 uppercase">{framework.cor}</code>
<span className="inline-block w-4 h-4 rounded-md shadow-xs border border-white/20" style={{ backgroundColor: framework.cor }} />
</div>
</div>
</div>
</div>
</div>
{/* Special Section: ESP Component Registry Callout for ESP-IDF */}
{isEspIdf && (
<div className="bg-gradient-to-r from-purple-900/90 to-indigo-900/90 rounded-3xl p-6 md:p-8 text-white shadow-2xl border border-purple-500/40 mb-12 relative overflow-hidden">
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 relative z-10">
<div className="space-y-2 max-w-2xl">
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-500/20 border border-purple-400/30 text-purple-300 text-xs font-bold">
<Box className="w-3.5 h-3.5" />
<span>Arquitetura Moderna ESP-IDF</span>
</div>
<h3 className="text-2xl font-display font-extrabold text-white">
Gerenciamento Modular com ESP Component Registry
</h3>
<p className="text-xs md:text-sm text-slate-200 leading-relaxed font-medium">
Nos projetos ESP-IDF v4.4+ e v5.x+, as bibliotecas não precisam mais ser adicionadas como submódulos Git.
Utilize o <code>idf_component.yml</code> e o comando <code>idf.py add-dependency</code> para baixar automaticamente drivers de displays (LVGL), pilhas industriais (Modbus), RainMaker e mais.
<section className="shell mt-8">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
ESP-IDF 4.4 ou superior
</span>
<h2 className="section-title mt-4">Módulos com o ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
Nos projetos ESP-IDF v4.4+ e v5.x, as bibliotecas não precisam mais entrar como submódulos Git. Use o{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> e o comando{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> para baixar drivers de display (LVGL), pilhas industriais (Modbus), RainMaker e mais.
</p>
</div>
<Link
href="/componentes"
className="inline-flex items-center gap-2 bg-white text-purple-900 hover:bg-slate-100 font-extrabold text-xs uppercase tracking-wider px-6 py-3.5 rounded-xl shadow-lg transition-all active:scale-95 shrink-0"
>
<Sparkles className="w-4 h-4 text-purple-600" />
<span>Explorar Componentes ESP-IDF</span>
<ArrowRight className="w-4 h-4 text-purple-600" />
<Link href="/componentes" className="btn btn-primary shrink-0">
Explorar componentes
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
)}
{/* Coding examples container */}
{framework.exemplo && (
<div className="mt-8">
<section className="shell mt-8">
<CodeExample framework={framework} />
</div>
</section>
)}
{/* Technical documentation double CTAs */}
<div className="flex flex-wrap gap-4 mt-8 select-none">
{framework.documentacao && (
<a
href={framework.documentacao}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 transition-opacity shadow-lg"
>
<BookOpen className="w-4 h-4" />
<span>Documentação do Desenvolvedor</span>
</a>
)}
{framework.repositorio && (
<a
href={framework.repositorio}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/20 px-6 py-3.5 rounded-xl font-bold text-xs uppercase tracking-wider hover:bg-purple-500/20 transition-all shadow-xs"
>
<Code className="w-4 h-4" />
<span>Código Fonte no GitHub</span>
</a>
)}
<div className="shell section pt-10">
<Link href="/frameworks" className="inline-flex min-h-11 items-center gap-2 text-sm font-medium text-accent hover:text-accent-strong">
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden="true" />
Ver todos os frameworks
</Link>
</div>
</main>
+104 -280
View File
@@ -1,309 +1,133 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import Link from "next/link";
import frameworksData from "@/public/frameworks.json";
import {
Cpu,
Target,
Zap,
Compass,
ExternalLink,
GraduationCap,
ArrowRight,
Code,
Layers,
Sparkles,
Music,
Camera,
Brain,
Home,
Box,
Cloud,
Radio,
ShieldCheck,
Mic
} from "lucide-react";
import { ArrowRight, ArrowUpRight, Box, Brain, Cloud, GraduationCap, Layers, Mic, Music, Sparkles } from "lucide-react";
const GUIAS = [
{
icon: GraduationCap,
titulo: "Estou começando",
texto: "Arduino ou MicroPython são os caminhos mais rápidos para testar sensores e APIs. Para produto, vá de ESP-IDF ou Rust.",
links: [
["Arduino", "/frameworks/Arduino"],
["MicroPython", "/frameworks/MicroPython"],
["Rust", "/frameworks/Rust-ESP"],
],
},
{
icon: Cloud,
titulo: "Nuvem e casa conectada",
texto: "ESP-Matter para interoperabilidade, ESP-RainMaker para nuvem pronta e Zigbee para redes locais de baixo consumo.",
links: [
["RainMaker", "/frameworks/ESP-RainMaker"],
["Matter", "/frameworks/ESP-Matter"],
["Zigbee", "/frameworks/ESP-Zigbee-SDK"],
],
},
{
icon: Sparkles,
titulo: "Áudio, voz e IA",
texto: "ESP-ADF para áudio, ESP-SR para reconhecimento de fala offline e ESP-NN para acelerar TinyML.",
links: [
["Áudio", "/frameworks/ESP-ADF", Music],
["Voz", "/frameworks/ESP-SR", Mic],
["IA", "/frameworks/ESP-NN", Brain],
],
},
];
export default function Frameworks() {
const frameworks = Object.entries(frameworksData);
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-4 md:px-6 pt-12 md:pt-16 pb-24 max-w-7xl mx-auto space-y-12">
{/* Top Hero Section */}
<section className="text-center select-none">
<div className="inline-flex items-center gap-2 bg-white/80 dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-6 border border-slate-300 dark:border-slate-800/80 shadow-xs">
<Layers className="w-4 h-4 text-purple-500" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-350 uppercase tracking-wider">
Ecossistema de Desenvolvimento
<main id="conteudo">
<PageHero
trail={[{ label: "Frameworks e SDKs" }]}
icon={Layers}
label={`${frameworks.length} frameworks e SDKs`}
title="Frameworks e SDKs do ESP32"
lede="Do C/C++ nativo com FreeRTOS ao Rust, passando por nuvem IoT, reconhecimento de voz, áudio e Matter. Escolha pelo que você quer construir."
/>
<section className="shell pt-10 sm:pt-12" aria-label="Por onde começar">
<div className="grid gap-4 md:grid-cols-3">
{GUIAS.map(({ icon: Icon, titulo, texto, links }) => (
<div key={titulo} className="card flex flex-col p-5 sm:p-6">
<span className="grid h-10 w-10 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<span className="bg-purple-100 dark:bg-purple-950/80 text-purple-700 dark:text-purple-300 text-[10px] font-extrabold px-2 py-0.5 rounded-full">
14 SDKs
</span>
</div>
<h1 className="text-4xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 pb-2 pt-1 leading-tight md:leading-[1.18] tracking-tight">
Frameworks & SDKs ESP32
</h1>
<p className="text-sm md:text-base text-slate-600 dark:text-slate-400 max-w-3xl mx-auto leading-relaxed font-semibold">
Descubra o ecossistema completo de software do ESP32. Do desenvolvimento nativo C/C++
com FreeRTOS ao ecossistema moderno em Rust, passando por nuvem IoT, reconhecimento de voz local,
áudio de alta fidelidade e o padrão universal Matter.
</p>
</section>
{/* Highlight Callout: ESP Component Registry */}
<section className="bg-gradient-to-r from-purple-900/90 via-indigo-900/90 to-slate-900/90 text-white rounded-3xl p-6 md:p-8 shadow-2xl border border-purple-500/30 relative overflow-hidden flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
<div className="absolute -right-12 -top-12 w-64 h-64 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="space-y-2 max-w-3xl relative z-10">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-purple-500/20 text-purple-300 text-xs font-bold rounded-full border border-purple-400/30">
<Box className="w-3.5 h-3.5 text-purple-300" />
<span>Arquitetura Modular</span>
</div>
<h2 className="text-2xl md:text-3xl font-display font-extrabold text-white">
ESP Component Registry & IDF Component Manager
</h2>
<p className="text-xs md:text-sm text-slate-300 leading-relaxed font-medium">
Desenvolva de forma modular: instale drivers de displays (LVGL), pilhas industriais (Modbus),
recursos de nuvem (RainMaker) e pacotes de suporte a placas (BSPs) com uma única linha no seu manifesto <code>idf_component.yml</code>.
</p>
</div>
<div className="relative z-10 shrink-0 w-full lg:w-auto">
<Link
href="/componentes"
className="inline-flex items-center justify-center gap-2 w-full lg:w-auto bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-400 hover:to-pink-400 text-white font-bold text-xs uppercase tracking-wider px-6 py-3.5 rounded-2xl shadow-lg transition-all active:scale-95"
>
<Sparkles className="w-4 h-4" />
<span>Explorar Component Registry</span>
<ArrowRight className="w-4 h-4" />
<h2 className="mt-4 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{titulo}</h2>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{texto}</p>
<ul className="mt-5 flex flex-wrap gap-2 border-t border-line pt-4">
{links.map(([rotulo, href, Extra]) => (
<li key={rotulo}>
<Link href={href} className="chip-btn">
{Extra && <Extra className="h-4 w-4" aria-hidden="true" />}
{rotulo}
</Link>
</div>
</section>
{/* Cards Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{frameworks.map(([key, framework]) => (
<Link
key={key}
href={`/frameworks/${encodeURIComponent(key)}`}
className="group flex"
>
<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 transition-all duration-300 hover:-translate-y-1.5 flex flex-col justify-between overflow-hidden w-full"
style={{ borderTop: `4px solid ${framework.cor}` }}
>
<div className="p-6 pb-4 border-b border-slate-200 dark:border-slate-850/40 flex-1">
<div className="flex items-center justify-between mb-4">
<span className="text-5xl select-none filter drop-shadow-md">{framework.icone}</span>
<div className="flex flex-col gap-1 items-end select-none">
<span
className="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider"
style={{
backgroundColor: `${framework.cor}18`,
color: framework.cor,
border: `1px solid ${framework.cor}25`
}}
>
{framework.linguagem}
</span>
<span className="text-[9px] font-bold text-slate-600 dark:text-slate-350">
{framework.tipo}
</span>
</div>
</div>
<h2 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
{framework.nome}
</h2>
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider mb-4 leading-none select-none">
{framework.nome_completo}
</p>
<p className="text-xs text-slate-500 dark:text-slate-300 leading-relaxed line-clamp-3">
{framework.descricao}
</p>
</div>
<div className="p-6 py-5 space-y-4 flex-1 flex flex-col justify-between select-none">
<div className="space-y-4">
{/* Main Feature */}
<div>
<h4 className="text-[9px] font-bold text-slate-500 dark:text-slate-400 mb-1.5 uppercase tracking-widest flex items-center gap-1.5">
<Target className="w-3.5 h-3.5 text-blue-500" />
<span>Objetivo Principal</span>
</h4>
<p className="text-xs text-slate-650 dark:text-slate-200 font-medium leading-relaxed line-clamp-2">
{framework.funcao_principal}
</p>
</div>
{/* Key Specs */}
<div>
<h4 className="text-[9px] font-bold text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-widest flex items-center gap-1.5">
<Zap className="w-3.5 h-3.5 text-amber-500" />
<span>Recursos Chave</span>
</h4>
<ul className="space-y-1">
{framework.caracteristicas.slice(0, 3).map((carac, idx) => (
<li key={idx} className="text-xs text-slate-600 dark:text-slate-300 flex items-start gap-1.5">
<span className="text-[8px] mt-1 shrink-0" style={{ color: framework.cor }}>■</span>
<span className="line-clamp-1">{carac}</span>
</li>
))}
</ul>
</div>
))}
</div>
</section>
{/* Use Cases tags */}
<div>
<h4 className="text-[9px] font-bold text-slate-500 dark:text-slate-400 mb-2 uppercase tracking-widest flex items-center gap-1.5">
<Compass className="w-3.5 h-3.5 text-emerald-500" />
<span>Aplicações Ideais</span>
</h4>
<div className="flex flex-wrap gap-1.5">
{framework.casos_uso.slice(0, 3).map((caso, idx) => (
<span key={idx} className="text-[9px] font-bold bg-slate-100 dark:bg-slate-900/60 text-slate-650 dark:text-slate-200 px-2.5 py-1 rounded-md border border-slate-300 dark:border-slate-800/80">
{caso}
<section className="shell mt-10">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
Arquitetura modular
</span>
))}
</div>
<h2 className="section-title mt-4">ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
Instale drivers de display (LVGL), pilhas industriais (Modbus), recursos de nuvem e pacotes de placa
(BSPs) com uma linha no manifesto <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code>.
</p>
</div>
<Link href="/componentes" className="btn btn-primary shrink-0">
Explorar componentes
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
{/* CTA Link */}
<div className="pt-4 border-t border-slate-200 dark:border-slate-850/40 mt-auto flex items-center justify-between text-xs font-bold text-slate-700 dark:text-slate-300 group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
<span>Acessar Detalhes e Exemplos</span>
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1.5 transition-transform" />
</div>
</div>
<section className="shell section" aria-labelledby="todos-titulo">
<SectionHead id="todos-titulo" eyebrow="Todos" title="Todos os frameworks" lede="Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais." />
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{frameworks.map(([key, framework]) => (
<Link key={key} href={`/frameworks/${encodeURIComponent(key)}`} className="card card-interactive group flex flex-col p-5 sm:p-6">
<div className="flex flex-wrap items-start justify-between gap-2">
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">{framework.nome}</h3>
<span className="chip max-w-full">{framework.linguagem}</span>
</div>
<p className="mt-1 text-sm text-muted">{framework.tipo}</p>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{framework.descricao}</p>
<ul className="mt-5 flex flex-1 flex-wrap content-start gap-1.5">
{framework.casos_uso.slice(0, 3).map((caso) => (
<li key={caso} className="chip max-w-full">
<span className="truncate">{caso}</span>
</li>
))}
</ul>
<span className="mt-5 inline-flex items-center gap-1 border-t border-line pt-4 text-sm font-medium text-accent">
Ver detalhes e exemplo
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
))}
</div>
{/* Bottom Informative Cards Section */}
<div className="grid md:grid-cols-3 gap-6 select-none">
{/* Card 1: Beginner Guide */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl shadow-xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 flex flex-col justify-between relative overflow-hidden">
<div>
<div className="flex items-center gap-3.5 mb-5">
<div className="w-12 h-12 bg-blue-500/10 border border-blue-500/20 rounded-2xl flex items-center justify-center text-blue-500">
<GraduationCap className="w-6 h-6" />
</div>
<div>
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Iniciante no ESP32?
</h3>
<p className="text-[10px] font-bold text-blue-500 uppercase tracking-wider leading-none">Guia de Aprendizado</p>
</div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed mb-6 font-semibold">
Se você está iniciando a sua jornada, o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">Arduino Core</strong> ou&nbsp;
<strong className="text-slate-750 dark:text-slate-300">MicroPython</strong>&nbsp;
são ideais para testar sensores e APIs. Para produtos comerciais, adote o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-IDF</strong> ou&nbsp;
<strong className="text-slate-750 dark:text-slate-300">Rust on ESP32</strong>.
</p>
</div>
<div className="flex flex-wrap gap-2 pt-4 border-t border-slate-200 dark:border-slate-850/40 mt-auto">
<Link href="/frameworks/Arduino" className="inline-flex items-center gap-1 px-3 py-2 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-xl text-xs font-bold hover:bg-blue-500/20 transition-all">
<span>Arduino</span>
</Link>
<Link href="/frameworks/MicroPython" className="inline-flex items-center gap-1 px-3 py-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/20 rounded-xl text-xs font-bold hover:bg-purple-500/20 transition-all">
<span>MicroPython</span>
</Link>
<Link href="/frameworks/Rust-ESP" className="inline-flex items-center gap-1 px-3 py-2 bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-xl text-xs font-bold hover:bg-amber-500/20 transition-all">
<span>Rust</span>
</Link>
</div>
</div>
{/* Card 2: Cloud & Smart Home */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl shadow-xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 flex flex-col justify-between relative overflow-hidden">
<div>
<div className="flex items-center gap-3.5 mb-5">
<div className="w-12 h-12 bg-indigo-500/10 border border-indigo-500/20 rounded-2xl flex items-center justify-center text-indigo-500">
<Cloud className="w-6 h-6" />
</div>
<div>
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Cloud & Smart Home
</h3>
<p className="text-[10px] font-bold text-indigo-500 uppercase tracking-wider leading-none">Conectividade Avançada</p>
</div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed mb-6 font-semibold">
Crie dispositivos interoperáveis com&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-Matter</strong>, implemente produtos de nuvem rápida com&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-RainMaker</strong> ou redes locais com&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-NOW</strong> e&nbsp;
<strong className="text-slate-750 dark:text-slate-300">Zigbee 3.0</strong>.
</p>
</div>
<div className="flex flex-wrap gap-2 pt-4 border-t border-slate-200 dark:border-slate-850/40 mt-auto">
<Link href="/frameworks/ESP-RainMaker" className="inline-flex items-center gap-1 px-3 py-2 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-xl text-xs font-bold hover:bg-blue-500/20 transition-all">
<span>RainMaker</span>
</Link>
<Link href="/frameworks/ESP-Matter" className="inline-flex items-center gap-1 px-3 py-2 bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-xl text-xs font-bold hover:bg-amber-500/20 transition-all">
<span>Matter</span>
</Link>
<Link href="/frameworks/ESP-Zigbee-SDK" className="inline-flex items-center gap-1 px-3 py-2 bg-pink-500/10 text-pink-600 dark:text-pink-400 border border-pink-500/20 rounded-xl text-xs font-bold hover:bg-pink-500/20 transition-all">
<span>Zigbee</span>
</Link>
</div>
</div>
{/* Card 3: Specialized Edge AI */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl shadow-xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 flex flex-col justify-between relative overflow-hidden">
<div>
<div className="flex items-center gap-3.5 mb-5">
<div className="w-12 h-12 bg-purple-500/10 border border-purple-500/20 rounded-2xl flex items-center justify-center text-purple-500">
<Sparkles className="w-6 h-6" />
</div>
<div>
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Áudio, Voz & IA
</h3>
<p className="text-[10px] font-bold text-purple-500 uppercase tracking-wider leading-none">Domínios Especializados</p>
</div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed mb-6 font-semibold">
Processe áudio com o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-ADF</strong>, reconheça fala offline com o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-SR</strong>, processe vídeo com o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-WHO</strong> e acelere TinyML com o&nbsp;
<strong className="text-slate-750 dark:text-slate-300">ESP-NN</strong>.
</p>
</div>
<div className="flex flex-wrap gap-2 pt-4 border-t border-slate-200 dark:border-slate-850/40 mt-auto">
<Link href="/frameworks/ESP-ADF" className="inline-flex items-center gap-1 px-3 py-2 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-xl text-xs font-bold hover:bg-blue-500/20 transition-all">
<Music className="w-3.5 h-3.5" />
<span>Áudio</span>
</Link>
<Link href="/frameworks/ESP-SR" className="inline-flex items-center gap-1 px-3 py-2 bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-500/20 rounded-xl text-xs font-bold hover:bg-indigo-500/20 transition-all">
<Mic className="w-3.5 h-3.5" />
<span>Voz</span>
</Link>
<Link href="/frameworks/ESP-NN" className="inline-flex items-center gap-1 px-3 py-2 bg-teal-500/10 text-teal-600 dark:text-teal-400 border border-teal-500/20 rounded-xl text-xs font-bold hover:bg-teal-500/20 transition-all">
<Brain className="w-3.5 h-3.5" />
<span>IA</span>
</Link>
</div>
</div>
</div>
</section>
</main>
<Footer />
+727 -40
View File
@@ -1,75 +1,762 @@
@import "tailwindcss";
@theme {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-display: var(--font-outfit), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-sans: var(--font-manrope), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-display: var(--font-space-grotesk), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, Menlo, monospace;
/* Premium dark mode palette accents */
--color-brand-purple: #8b5cf6;
--color-brand-blue: #3b82f6;
--color-brand-pink: #ec4899;
--radius-md: 0.625rem;
--radius-lg: 0.875rem;
--radius-xl: 1.25rem;
--radius-2xl: 1.75rem;
--radius-3xl: 2.25rem;
/* ---------------------------------------------------------------------
Meios-tons de slate.
/* Escalas legadas remapeadas para a identidade atual. As telas internas
ainda usam estes nomes; a cor passa a ser consistente sem reescrever
cada tabela de uma vez. */
--color-brand-purple: #0b5c55;
--color-brand-blue: #0b5c55;
--color-brand-pink: #8a3b29;
A escala do Tailwind anda de 100 em 100 (50, 100, 200 ... 950), então
classes como text-slate-350 ou border-slate-850, usadas em cerca de 190
pontos do projeto, não geravam CSS nenhum: o elemento herdava a cor do pai e
ficava diferente em cada tema, sem padrão.
--color-blue-50: #eaf8f5;
--color-blue-100: #d2f0ea;
--color-blue-200: #a8e1d8;
--color-blue-300: #75cdc1;
--color-blue-400: #43b3a6;
--color-blue-500: #1f9588;
--color-blue-600: #0b5c55;
--color-blue-700: #094b45;
--color-blue-800: #083c37;
--color-blue-900: #072f2b;
--color-blue-950: #041f1c;
Cada valor abaixo é o ponto médio exato entre os dois degraus oficiais
vizinhos, então a intenção original de cada classe é preservada.
--------------------------------------------------------------------- */
--color-slate-150: #eaeef4; /* entre 100 e 200 */
--color-slate-250: #d6dee8; /* entre 200 e 300 */
--color-slate-350: #b0bccc; /* entre 300 e 400 */
--color-slate-450: #7c8ca2; /* entre 400 e 500 */
--color-slate-550: #56647a; /* entre 500 e 600 */
--color-slate-650: #3d4b5f; /* entre 600 e 700 */
--color-slate-750: #283548; /* entre 700 e 800 */
--color-slate-850: #162032; /* entre 800 e 900 */
--color-purple-50: #fbf0ec;
--color-purple-100: #f7e1da;
--color-purple-200: #eec3b5;
--color-purple-300: #e19f89;
--color-purple-400: #d07a60;
--color-purple-500: #b95a3f;
--color-purple-600: #8a3b29;
--color-purple-700: #723122;
--color-purple-800: #5c281c;
--color-purple-900: #472016;
--color-purple-950: #2a130d;
--color-indigo-50: #f3f5f8;
--color-indigo-100: #e7eaf0;
--color-indigo-200: #d0d6df;
--color-indigo-300: #aeb7c5;
--color-indigo-400: #8793a4;
--color-indigo-500: #697586;
--color-indigo-600: #515c6c;
--color-indigo-700: #414a58;
--color-indigo-800: #333b46;
--color-indigo-900: #262c35;
--color-indigo-950: #171b21;
--color-pink-50: #fbf0ec;
--color-pink-100: #f7e1da;
--color-pink-200: #eec3b5;
--color-pink-300: #e19f89;
--color-pink-400: #d07a60;
--color-pink-500: #b95a3f;
--color-pink-600: #8a3b29;
--color-pink-700: #723122;
--color-pink-800: #5c281c;
--color-pink-900: #472016;
--color-pink-950: #2a130d;
--color-teal-50: #eaf8f5;
--color-teal-100: #d2f0ea;
--color-teal-200: #a8e1d8;
--color-teal-300: #75cdc1;
--color-teal-400: #43b3a6;
--color-teal-500: #1f9588;
--color-teal-600: #0b5c55;
--color-teal-700: #094b45;
--color-teal-800: #083c37;
--color-teal-900: #072f2b;
--color-teal-950: #041f1c;
--color-emerald-50: #eaf8f5;
--color-emerald-100: #d2f0ea;
--color-emerald-200: #a8e1d8;
--color-emerald-300: #75cdc1;
--color-emerald-400: #43b3a6;
--color-emerald-500: #1f9588;
--color-emerald-600: #0b5c55;
--color-emerald-700: #094b45;
--color-emerald-800: #083c37;
--color-emerald-900: #072f2b;
--color-emerald-950: #041f1c;
--color-amber-50: #fdf6e7;
--color-amber-100: #faebc8;
--color-amber-200: #f2d894;
--color-amber-300: #e7bd5c;
--color-amber-400: #d6a23a;
--color-amber-500: #b88427;
--color-amber-600: #93681d;
--color-amber-700: #765318;
--color-amber-800: #5e4315;
--color-amber-900: #493512;
--color-amber-950: #2b1f0c;
}
/*
* Tokens semânticos. O tema troca por .dark no <html>, aplicado antes da
* pintura pelo next-themes. Todos os pares de texto usados no site ficam em
* 7:1 ou mais (WCAG AAA), inclusive sobre surface e surface-2.
*/
:root {
color-scheme: light;
--canvas: #f5f6f8;
--canvas-deep: #eef0f3;
--surface: #ffffff;
--surface-2: #eef0f3;
--ink: #0b0d10;
--ink-soft: #262c35;
--muted: #434a55;
--faint: #484f5b;
--line: #e1e4e9;
--line-strong: #c5cad3;
--accent: #0a5852;
--accent-strong: #094b45;
--accent-contrast: #ffffff;
--accent-soft: #ebf8f5;
--copper: #823626;
--copper-soft: #fbf0ec;
--teal: #0a5852;
--teal-soft: #ebf8f5;
--gold: #5e4300;
--gold-soft: #fdf6e7;
--ok: #0f5a35;
--ok-soft: #e8f6ee;
--danger: #9b1c1c;
--danger-soft: #fdeded;
--code-bg: #07090d;
--code-ink: #f4f6f8;
--glow-a: rgb(13 148 136 / 0.1);
--glow-b: rgb(56 130 246 / 0.07);
--nav-bg: rgb(255 255 255 / 0.74);
--hairline: inset 0 1px 0 rgb(255 255 255 / 0.9);
--shadow-ink: rgb(15 23 42 / 0.07);
--shadow: 0 2px 4px var(--shadow-ink), 0 14px 32px -8px var(--shadow-ink);
}
.dark {
color-scheme: dark;
--canvas: #07090d;
--canvas-deep: #040609;
--surface: #0e1218;
--surface-2: #151b23;
--ink: #f4f6f8;
--ink-soft: #d4dae2;
--muted: #a6afbd;
--faint: #a6afbd;
--line: #1c232d;
--line-strong: #2e3846;
--accent: #2dd4bf;
--accent-strong: #5eead4;
--accent-contrast: #06201d;
--accent-soft: #10302c;
--copper: #ffb39b;
--copper-soft: #331a14;
--teal: #2dd4bf;
--teal-soft: #10302c;
--gold: #fdb022;
--gold-soft: #2b1f0c;
--ok: #5fe0a2;
--ok-soft: #0f2a1c;
--danger: #ff9b9b;
--danger-soft: #331517;
--code-bg: #040609;
--code-ink: #f4f6f8;
--glow-a: rgb(45 212 191 / 0.1);
--glow-b: rgb(56 140 248 / 0.08);
--nav-bg: rgb(14 18 24 / 0.72);
--hairline: inset 0 1px 0 rgb(255 255 255 / 0.05);
--shadow-ink: rgb(0 0 0 / 0.45);
--shadow: 0 4px 10px var(--shadow-ink), 0 24px 48px -12px var(--shadow-ink);
}
@theme inline {
--color-canvas: var(--canvas);
--color-canvas-deep: var(--canvas-deep);
--color-surface: var(--surface);
--color-surface-2: var(--surface-2);
--color-ink: var(--ink);
--color-ink-soft: var(--ink-soft);
--color-muted: var(--muted);
--color-faint: var(--faint);
--color-line: var(--line);
--color-line-strong: var(--line-strong);
--color-accent: var(--accent);
--color-accent-strong: var(--accent-strong);
--color-accent-contrast: var(--accent-contrast);
--color-accent-soft: var(--accent-soft);
--color-copper: var(--copper);
--color-copper-soft: var(--copper-soft);
--color-teal: var(--teal);
--color-teal-soft: var(--teal-soft);
--color-gold: var(--gold);
--color-gold-soft: var(--gold-soft);
--color-ok: var(--ok);
--color-ok-soft: var(--ok-soft);
--color-danger: var(--danger);
--color-danger-soft: var(--danger-soft);
--color-code: var(--code-bg);
}
/* Enable class-based dark mode selector in Tailwind v4 */
@variant dark (&:where(.dark, .dark *));
@layer base {
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
*,
::before,
::after {
border-color: var(--line);
}
body {
@apply bg-slate-50 text-slate-900;
@apply bg-canvas text-ink;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* next-themes aplica a classe .dark no <html>, não no <body>.
O seletor body.dark nunca casava, então o fundo escuro do body vinha
só das classes utilitárias do layout. */
.dark body {
@apply bg-slate-950 text-slate-50;
/* Duas poças de luz fixas: dão profundidade sem textura nem gradiente
decorativo dentro do conteúdo. */
html::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
radial-gradient(60rem 36rem at 50% -12%, var(--glow-a), transparent 70%),
radial-gradient(44rem 30rem at 100% 28%, var(--glow-b), transparent 70%);
}
h1,
h2,
h3 {
letter-spacing: -0.03em;
text-wrap: balance;
overflow-wrap: break-word;
}
p,
li,
dd {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
/* Seleção de texto: o texto precisa de contraste contra o realce em cada
tema. Roxo claro sobre fundo claro era ilegível no tema claro. */
::selection {
@apply bg-purple-500/30 text-purple-950;
background: var(--accent);
color: var(--accent-contrast);
}
.dark ::selection {
@apply bg-purple-500/40 text-purple-50;
:where(a, button, input, select, textarea, summary):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: 6px;
}
/* Scrollbar customization for a premium, non-default OS look */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
background: var(--canvas);
}
::-webkit-scrollbar-thumb {
@apply bg-slate-300 dark:bg-slate-800 rounded-full hover:bg-slate-400 dark:hover:bg-slate-700 transition-colors;
background: var(--line-strong);
border-radius: 999px;
border: 3px solid var(--canvas);
}
::-webkit-scrollbar-thumb:hover {
background: var(--faint);
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}
@layer components {
.shell {
width: 100%;
max-width: 76rem;
margin-inline: auto;
padding-inline: clamp(1.25rem, 4vw, 2rem);
}
.section {
padding-block: clamp(3.5rem, 8vw, 7rem);
}
.site-canvas {
background: transparent;
color: var(--ink);
}
.hairline {
border-color: var(--line);
}
.display,
.page-title {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.04em;
line-height: 1.03;
color: var(--ink);
text-wrap: balance;
}
.display {
font-size: clamp(2.5rem, 1.7rem + 3.8vw, 4.5rem);
}
.page-title {
font-size: clamp(2.25rem, 1.5rem + 3.2vw, 3.75rem);
}
.section-title {
font-family: var(--font-display);
font-size: clamp(1.625rem, 1.2rem + 1.6vw, 2.5rem);
font-weight: 600;
letter-spacing: -0.035em;
line-height: 1.1;
color: var(--ink);
text-wrap: balance;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-sans);
font-size: 0.875rem;
line-height: 1.4;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
color: var(--accent);
}
.bench-label {
font-family: var(--font-sans);
font-size: 0.75rem;
font-weight: 600;
line-height: 1.4;
letter-spacing: 0;
text-transform: none;
color: var(--muted);
}
.lede {
font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.3125rem);
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.muted-copy {
color: var(--muted);
}
.total {
font-family: var(--font-display);
font-variant-numeric: tabular-nums;
letter-spacing: -0.03em;
}
/* min-width: 0 evita que um filho largo (chip, URL) estique a célula do grid. */
.card,
.panel {
min-width: 0;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-xl);
box-shadow: var(--hairline);
}
.card-flat {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-xl);
}
.card-quiet {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
}
.card-interactive {
transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s, box-shadow 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.card-interactive:hover {
transform: translateY(-3px);
border-color: var(--line-strong);
box-shadow: var(--shadow);
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-2xl);
box-shadow: var(--hairline);
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
border-bottom: 1px solid var(--line);
padding: 1rem 1.25rem;
}
.panel-dark {
background: #0b0d10;
color: #f4f6f8;
border: 1px solid #1c232d;
border-radius: var(--radius-2xl);
box-shadow: var(--shadow);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 2.75rem;
padding: 0.5rem 1.375rem;
border-radius: 9999px;
border: 1px solid transparent;
font-family: var(--font-sans);
font-size: 0.9375rem;
font-weight: 600;
line-height: 1.2;
text-transform: none;
letter-spacing: 0;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: transform 0.35s cubic-bezier(0.34, 1.4, 0.64, 1), background-color 0.2s, border-color 0.2s, box-shadow 0.3s, color 0.2s;
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: var(--accent);
color: var(--accent-contrast);
border-color: var(--accent);
}
.btn-primary:hover {
background: var(--accent-strong);
border-color: var(--accent-strong);
box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 12%, transparent);
}
.btn-ink {
background: var(--ink);
color: var(--canvas);
border-color: var(--ink);
}
.btn-ink:hover {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 12%, transparent), var(--shadow);
}
.btn-accent {
background: var(--copper);
color: #fffaf7;
border-color: var(--copper);
}
.btn-accent:hover {
background: color-mix(in srgb, var(--copper) 86%, black);
border-color: color-mix(in srgb, var(--copper) 86%, black);
}
.dark .btn-accent {
color: #1d0b06;
}
.btn-ghost {
background: var(--surface);
color: var(--ink);
border-color: var(--line-strong);
}
.btn-ghost:hover {
background: var(--surface-2);
}
.btn-sm {
min-height: 2.75rem;
padding: 0.375rem 1rem;
font-size: 0.875rem;
}
.btn-icon {
width: 2.75rem;
padding: 0;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border: 1px solid var(--line);
border-radius: 9999px;
background: var(--surface);
color: var(--muted);
font-family: var(--font-sans);
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0;
line-height: 1.3;
}
.chip-accent {
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 24%, transparent);
color: var(--accent);
}
.dark .chip-accent {
color: var(--accent);
}
/* Chip que funciona como controle (filtro, alternância). */
.chip-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 2.75rem;
padding: 0.375rem 1rem;
border: 1px solid var(--line-strong);
border-radius: 9999px;
background: var(--surface);
color: var(--ink-soft);
font-size: 0.875rem;
font-weight: 500;
line-height: 1.2;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}
.chip-btn:hover {
background: var(--surface-2);
}
.chip-btn[aria-pressed="true"],
.chip-btn[aria-selected="true"],
.chip-btn.is-active {
background: var(--ink);
border-color: var(--ink);
color: var(--canvas);
}
/* Bloco de destaque dentro do conteúdo: dica, aviso, exemplo. */
.callout {
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: var(--radius-lg);
background: var(--surface-2);
padding: 0.875rem 1rem;
font-size: 0.875rem;
line-height: 1.6;
color: var(--ink-soft);
}
.callout-warn {
border-left-color: var(--gold);
}
.callout-danger {
border-left-color: var(--danger);
}
.callout-ok {
border-left-color: var(--ok);
}
/* Número ou dado em destaque dentro de uma grade de fatos. */
.tile {
border-radius: var(--radius-xl);
background: var(--surface-2);
padding: 0.875rem 1rem;
}
.nav-pill {
background: var(--nav-bg);
-webkit-backdrop-filter: blur(22px) saturate(1.8);
backdrop-filter: blur(22px) saturate(1.8);
border: 1px solid var(--line);
box-shadow: var(--hairline), var(--shadow);
}
.nav-link {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 2.75rem;
padding: 0.5rem 0.875rem;
border: 0;
border-radius: 9999px;
font-family: var(--font-sans);
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
color: var(--muted);
text-decoration: none;
transition: color 0.2s, background-color 0.2s;
}
.nav-link:hover {
color: var(--ink);
background: var(--surface-2);
}
.nav-link[aria-current="page"],
.nav-link.is-active {
color: var(--ink);
background: var(--surface-2);
}
.field {
width: 100%;
min-height: 2.875rem;
background: var(--surface);
border: 1px solid var(--line-strong);
border-radius: var(--radius-lg);
color: var(--ink);
transition: border-color 0.2s, box-shadow 0.2s;
}
.field::placeholder {
color: var(--muted);
opacity: 0.75;
}
.field:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 15%, transparent);
}
.rule {
height: 1px;
background: var(--line);
border: 0;
}
.code-panel {
background: var(--code-bg);
color: var(--code-ink);
border: 1px solid #1c232d;
border-radius: var(--radius-xl);
overflow: auto;
}
.code-panel pre {
margin: 0;
padding: 1.1rem 1.25rem;
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.75;
tab-size: 2;
}
.book-cover {
position: relative;
aspect-ratio: 2 / 3;
width: 100%;
border-radius: 0.5rem 1rem 1rem 0.5rem;
background: linear-gradient(145deg, #1a1c21 0%, #0b0d10 60%, #5e2418 140%);
color: #f8fafc;
box-shadow:
inset 12px 0 18px -16px rgba(0, 0, 0, 0.9),
inset -1px 0 0 rgba(255, 255, 255, 0.14),
0 40px 80px -32px rgba(2, 4, 10, 0.6);
overflow: hidden;
isolation: isolate;
background-size: cover;
background-position: center;
}
.book-cover::after {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 10px;
background: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(255, 255, 255, 0.1), transparent);
}
.book-spine {
position: absolute;
inset: 0 auto 0 0;
width: 4px;
background: rgba(255, 247, 239, 0.3);
}
.dialog-scrim {
animation: dialog-fade 0.2s ease-out;
}
.dialog-panel {
animation: dialog-rise 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes dialog-fade {
from { opacity: 0; }
}
@keyframes dialog-rise {
from { opacity: 0; transform: translateY(16px); }
}
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
}
+147 -390
View File
@@ -3,31 +3,37 @@
import { useState, useMemo, useRef } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import { categoriasGlossario } from "@/lib/glossarioData";
import {
ArrowRight,
BookOpen,
Search,
Check,
Code2,
Cpu,
Database,
Wifi,
HelpCircle,
LayoutGrid,
Lightbulb,
Link2,
BatteryCharging,
RotateCcw,
Search,
ShieldCheck,
Sliders,
Sparkles,
BatteryCharging,
Code2,
Link2,
Check,
ArrowRight,
Lightbulb,
HelpCircle,
Wifi,
X,
SlidersHorizontal,
Filter,
RotateCcw,
Tag,
ShieldCheck,
} from "lucide-react";
const BUSCAS_POPULARES = ["GPIO", "ULP", "PSRAM", "Strapping Pins", "Deep Sleep", "OTA", "Matter", "Secure Boot", "Wi-Fi 6", "MIPI"];
const ICONES = { Cpu, Database, Wifi, Sliders, Sparkles, BatteryCharging, Code2, ShieldCheck };
/** "Processamento, CPU & Silício" vira "Processamento": cabe no filtro sem truncar. */
const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
export default function GlossarioPage() {
const [busca, setBusca] = useState("");
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
@@ -35,67 +41,30 @@ export default function GlossarioPage() {
const [copiadoSlug, setCopiadoSlug] = useState(null);
const searchInputRef = useRef(null);
// Sugestões populares para busca rápida
const buscasPopulares = [
{ rotulo: "GPIO", emoji: "🔌" },
{ rotulo: "ULP", emoji: "⚡" },
{ rotulo: "PSRAM", emoji: "💾" },
{ rotulo: "Strapping Pins", emoji: "🚀" },
{ rotulo: "Deep Sleep", emoji: "🔋" },
{ rotulo: "OTA", emoji: "📡" },
{ rotulo: "Matter", emoji: "🌐" },
{ rotulo: "Secure Boot", emoji: "🔒" },
{ rotulo: "Wi-Fi 6", emoji: "📶" },
{ rotulo: "MIPI", emoji: "✨" },
];
// Mapeamento de ícones por nome
const iconMap = {
Cpu: <Cpu className="w-4 h-4" />,
Database: <Database className="w-4 h-4" />,
Wifi: <Wifi className="w-4 h-4" />,
Sliders: <Sliders className="w-4 h-4" />,
Sparkles: <Sparkles className="w-4 h-4" />,
BatteryCharging: <BatteryCharging className="w-4 h-4" />,
Code2: <Code2 className="w-4 h-4" />,
ShieldCheck: <ShieldCheck className="w-4 h-4" />,
};
// Lista de todos os termos com metadados de categoria
const todosOsTermos = useMemo(() => {
return categoriasGlossario.flatMap((cat) =>
const todosOsTermos = useMemo(
() =>
categoriasGlossario.flatMap((cat) =>
cat.termos.map((t) => ({
...t,
categoriaId: cat.id,
categoriaNome: cat.nome,
categoriaCor: cat.cor,
categoriaIcone: cat.icone,
categoriaEmoji: cat.emoji,
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
primeiraLetra: t.termo.charAt(0).toUpperCase(),
}))
),
[]
);
}, []);
// Todas as letras do alfabeto de A a Z com indicador de existência de termos
const alfabeto = useMemo(() => {
const letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const setDisponivel = new Set(
todosOsTermos.map((t) => t.termo.charAt(0).toUpperCase())
);
return letras.map((letra) => ({
letra,
temTermos: setDisponivel.has(letra),
}));
const disponiveis = new Set(todosOsTermos.map((t) => t.primeiraLetra));
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letra) => ({ letra, temTermos: disponiveis.has(letra) }));
}, [todosOsTermos]);
// Filtragem dinâmica
const termosFiltrados = useMemo(() => {
const termoBusca = busca.toLowerCase().trim();
return todosOsTermos.filter((item) => {
const matchCategoria =
categoriaSelecionada === "todas" ||
item.categoriaId === categoriaSelecionada;
const matchCategoria = categoriaSelecionada === "todas" || item.categoriaId === categoriaSelecionada;
const matchLetra = !letraFiltro || item.primeiraLetra === letraFiltro;
const matchBusca =
!termoBusca ||
@@ -103,15 +72,11 @@ export default function GlossarioPage() {
item.definicao.toLowerCase().includes(termoBusca) ||
item.exemplo.toLowerCase().includes(termoBusca) ||
item.categoriaNome.toLowerCase().includes(termoBusca);
return matchCategoria && matchLetra && matchBusca;
});
}, [todosOsTermos, categoriaSelecionada, letraFiltro, busca]);
const temFiltroAtivo =
busca.trim() !== "" ||
categoriaSelecionada !== "todas" ||
letraFiltro !== null;
const temFiltroAtivo = busca.trim() !== "" || categoriaSelecionada !== "todas" || letraFiltro !== null;
const limparTodosFiltros = () => {
setBusca("");
@@ -121,366 +86,179 @@ export default function GlossarioPage() {
};
const copiarLinkTermo = (slug) => {
const url = `${window.location.origin}/glossario#${slug}`;
navigator.clipboard.writeText(url);
navigator.clipboard.writeText(`${window.location.origin}/glossario#${slug}`);
setCopiadoSlug(slug);
setTimeout(() => setCopiadoSlug(null), 2000);
};
const catAtivaInfo = categoriasGlossario.find(
(c) => c.id === categoriaSelecionada
);
const total = termosFiltrados.length;
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/30 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-4 sm:px-6 pt-12 pb-24 max-w-7xl mx-auto">
{/* Hero Section */}
<section className="text-center mb-10 select-none">
<div className="inline-flex items-center gap-2 bg-white dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-5 border border-slate-250 dark:border-slate-800/80 shadow-xs">
<BookOpen className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
Enciclopédia & Conceitos do ESP32
</span>
</div>
<h1 className="text-3xl sm:text-5xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-4 leading-tight tracking-tight">
Glossário Técnico
</h1>
<p className="text-sm sm:text-base text-slate-600 dark:text-slate-400 max-w-2xl mx-auto leading-relaxed font-normal">
Consulte definições diretas, parâmetros de hardware e exemplos práticos dos termos fundamentais do ecossistema ESP32.
</p>
</section>
{/* 1. PAINEL INTEGRADO DE BUSCA & FILTRAGEM (REDESIGN COMPLETO) */}
<section className="mb-10 bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-slate-200 dark:border-slate-800 p-5 sm:p-7 shadow-sm transition-all">
{/* Barra de Pesquisa Principal */}
<div className="relative mb-5">
<div className="absolute left-4 top-1/2 -translate-y-1/2 p-1.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400 pointer-events-none">
<Search className="w-4 h-4 sm:w-5 sm:h-5" />
</div>
<input
ref={searchInputRef}
type="text"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Pesquisar por termo, sigla ou conceito (ex: GPIO, ULP, PSRAM, OTA, Deep Sleep)..."
className="w-full bg-slate-50 dark:bg-slate-950/70 pl-13 sm:pl-14 pr-24 sm:pr-28 py-3.5 sm:py-4 rounded-2xl border border-slate-250 dark:border-slate-800 text-sm sm:text-base font-medium placeholder:text-slate-400 dark:placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all shadow-inner"
<main id="conteudo">
<PageHero
trail={[{ label: "Glossário" }]}
icon={BookOpen}
label={`${todosOsTermos.length} termos explicados`}
title="Glossário técnico do ESP32"
lede="Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
/>
{/* Lado Direito da Barra de Pesquisa: Botão Limpar ou Tag de Contagem */}
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
{busca ? (
<button
type="button"
onClick={() => {
setBusca("");
searchInputRef.current?.focus();
}}
className="text-xs px-2.5 py-1 rounded-xl bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-700 font-semibold flex items-center gap-1 transition-colors cursor-pointer"
>
<X className="w-3.5 h-3.5" />
<span>Limpar</span>
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<div className="panel p-5 sm:p-7">
<div className="relative">
<label htmlFor="busca-glossario" className="sr-only">
Buscar termo, sigla ou conceito
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-glossario"
ref={searchInputRef}
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar termo, sigla ou conceito"
autoComplete="off"
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{busca && (
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
<X className="h-3.5 w-3.5" aria-hidden="true" />
Limpar
</button>
) : (
<span className="hidden sm:inline-flex text-[11px] font-bold text-slate-500 dark:text-slate-400 bg-slate-200/60 dark:bg-slate-800/80 px-2.5 py-1 rounded-lg border border-slate-250 dark:border-slate-700">
{termosFiltrados.length} {termosFiltrados.length === 1 ? "termo" : "termos"}
</span>
)}
</div>
</div>
{/* Atalhos de Pesquisa Rápida (Tags Populares) */}
<div className="flex items-center gap-2 mb-6 overflow-x-auto pb-1.5 scrollbar-none text-xs">
<span className="text-slate-500 dark:text-slate-400 font-bold shrink-0 flex items-center gap-1 text-[11px] uppercase tracking-wider">
<Tag className="w-3 h-3" />
Populares:
</span>
{buscasPopulares.map((item) => (
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="mr-1 text-sm text-muted">Mais buscados</span>
{BUSCAS_POPULARES.map((rotulo) => (
<button
key={item.rotulo}
key={rotulo}
type="button"
onClick={() => setBusca(item.rotulo)}
className={`px-2.5 py-1 rounded-lg font-medium transition-all shrink-0 cursor-pointer border ${
busca.toLowerCase() === item.rotulo.toLowerCase()
? "bg-purple-600 text-white border-purple-600 shadow-xs"
: "bg-slate-100/80 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-750 hover:bg-purple-50 dark:hover:bg-purple-950/40 hover:border-purple-300 dark:hover:border-purple-800"
}`}
onClick={() => setBusca(rotulo)}
aria-pressed={busca.toLowerCase() === rotulo.toLowerCase()}
className="chip-btn"
>
<span className="mr-1">{item.emoji}</span>
<span>{item.rotulo}</span>
{rotulo}
</button>
))}
</div>
{/* Seletor de Categorias em Grid/Flex Moderno */}
<div className="space-y-2 mb-5">
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
<span className="flex items-center gap-1.5">
<Filter className="w-3.5 h-3.5 text-purple-500" />
Filtrar por Categoria
</span>
{categoriaSelecionada !== "todas" && (
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className="text-purple-600 dark:text-purple-400 hover:underline cursor-pointer lowercase first-letter:uppercase text-[11px]"
>
Ver todas ({todosOsTermos.length})
<div className="mt-7 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaSelecionada("todas")} aria-pressed={categoriaSelecionada === "todas"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
Todas
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
</button>
)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-9 gap-2">
{/* Botão Todas */}
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
categoriaSelecionada === "todas"
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
}`}
>
<span className="text-base">✨</span>
<div className="flex items-center justify-between w-full">
<span className="truncate">Todas</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
categoriaSelecionada === "todas"
? "bg-white/20 text-white"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{todosOsTermos.length}
</span>
</div>
</button>
{/* Categorias Individuais */}
{categoriasGlossario.map((cat) => {
const ativa = categoriaSelecionada === cat.id;
const Icone = ICONES[cat.icone] ?? Cpu;
return (
<button
key={cat.id}
type="button"
onClick={() => setCategoriaSelecionada(cat.id)}
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
ativa
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
}`}
aria-pressed={categoriaSelecionada === cat.id}
className="chip-btn"
>
<span className="text-base">{cat.emoji}</span>
<div className="flex items-center justify-between w-full">
<span className="truncate">{cat.nome.split(" ")[0]}</span>
<span
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
ativa
? "bg-white/20 text-white"
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
}`}
>
{cat.termos.length}
</span>
</div>
<Icone className="h-4 w-4" aria-hidden="true" />
{nomeCurto(cat.nome)}
<span className="tabular-nums opacity-80">{cat.termos.length}</span>
</button>
);
})}
</div>
</div>
{/* Scrubber de Índice Alfabético (A-Z) */}
<div className="pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider shrink-0">
<span className="text-[11px]">Índice A-Z:</span>
</div>
<div className="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-none flex-1 justify-start sm:justify-end">
<button
type="button"
onClick={() => setLetraFiltro(null)}
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
letraFiltro === null
? "bg-purple-600 text-white shadow-xs"
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
}`}
>
Todos
<div className="mt-6 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Ordem alfabética</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label="Filtrar pela letra inicial">
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
Todas
</button>
{alfabeto.map(({ letra, temTermos }) => {
const ativo = letraFiltro === letra;
if (!temTermos) {
return (
<span
key={letra}
className="w-6 h-6 flex items-center justify-center text-[11px] font-semibold text-slate-500 dark:text-slate-450 select-none shrink-0"
>
{letra}
</span>
);
}
return (
{alfabeto.map(({ letra, temTermos }) => (
<button
key={letra}
type="button"
onClick={() => setLetraFiltro(ativo ? null : letra)}
className={`w-6 h-6 rounded-md flex items-center justify-center text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
ativo
? "bg-purple-600 text-white scale-110 shadow-xs"
: "text-slate-700 dark:text-slate-300 hover:bg-purple-100 dark:hover:bg-purple-950/60"
}`}
disabled={!temTermos}
onClick={() => setLetraFiltro(letraFiltro === letra ? null : letra)}
aria-pressed={letraFiltro === letra}
className="chip-btn min-w-11 justify-center px-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-surface"
>
{letra}
</button>
);
})}
))}
</div>
</div>
{/* Fita de Filtros Ativos (Aparece quando algum filtro está ativo) */}
{temFiltroAtivo && (
<div className="mt-4 pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-1.5 text-xs">
<span className="text-slate-500 dark:text-slate-400 font-semibold mr-1">
Filtros ativos:
</span>
{busca && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Busca: <strong>"{busca}"</strong>
<button
type="button"
onClick={() => setBusca("")}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
{categoriaSelecionada !== "todas" && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Categoria: <strong>{catAtivaInfo?.nome}</strong>
<button
type="button"
onClick={() => setCategoriaSelecionada("todas")}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
{letraFiltro && (
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
Letra: <strong>{letraFiltro}</strong>
<button
type="button"
onClick={() => setLetraFiltro(null)}
className="hover:text-purple-900 dark:hover:text-purple-100"
>
<X className="w-3 h-3" />
</button>
</span>
)}
</div>
<button
type="button"
onClick={limparTodosFiltros}
className="text-xs font-semibold text-red-600 dark:text-red-400 hover:underline flex items-center gap-1 cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
<span>Redefinir filtros</span>
</button>
</div>
)}
</section>
{/* 2. GRID DE CARDS DO GLOSSÁRIO */}
<section>
{termosFiltrados.length === 0 ? (
<div className="text-center py-16 bg-white dark:bg-slate-900 rounded-3xl border border-slate-250 dark:border-slate-800 shadow-sm p-8">
<HelpCircle className="w-12 h-12 text-slate-400 mx-auto mb-3" />
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">
Nenhum termo encontrado
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1 max-w-md mx-auto">
Não encontramos resultados para sua pesquisa com os filtros selecionados.
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
{total} {total === 1 ? "termo" : "termos"}
{temFiltroAtivo && <span className="font-normal text-muted"> com os filtros atuais</span>}
</h2>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Redefinir filtros
</button>
)}
</div>
{total === 0 ? (
<div className="card mt-6 px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum termo encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias.
</p>
<button
type="button"
onClick={limparTodosFiltros}
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold hover:bg-purple-700 cursor-pointer shadow-xs inline-flex items-center gap-1.5"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>Limpar todos os filtros</span>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Limpar filtros
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="mt-6 grid gap-4 md:grid-cols-2">
{termosFiltrados.map((item) => {
const Icone = ICONES[item.categoriaIcone] ?? Cpu;
return (
<article
key={item.slug}
id={item.slug}
className="bg-white dark:bg-slate-900/90 rounded-2xl border border-slate-250 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-500/40 p-5 sm:p-6 transition-all duration-200 shadow-xs flex flex-col justify-between scroll-mt-24 group"
>
<div className="space-y-3">
{/* Header do Card */}
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 text-xs px-2.5 py-0.5 rounded-full font-semibold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
<span>{item.categoriaEmoji}</span>
<span>{item.categoriaNome}</span>
<article key={item.slug} id={item.slug} className="card flex scroll-mt-28 flex-col p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<span className="chip">
<Icone className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{nomeCurto(item.categoriaNome)}
</span>
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
title="Copiar link direto para este termo"
className="text-xs text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 inline-flex items-center gap-1 p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
aria-label={`Copiar link para ${item.termo}`}
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
>
{copiadoSlug === item.slug ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-500 font-semibold text-[11px]">
Copiado!
</span>
</>
) : (
<Link2 className="w-3.5 h-3.5" />
)}
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
{/* Título do Termo */}
<h2 className="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{item.termo}
</h2>
{/* Definição */}
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed font-normal">
<h3 className="mt-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{item.termo}</h3>
<p className="mt-2 text-[0.9375rem] leading-relaxed text-muted">
<TextoComLinks texto={item.definicao} />
</p>
</div>
{/* Caixa de Exemplo Prático */}
<div className="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/80">
<div className="p-3.5 rounded-xl bg-purple-50/70 dark:bg-purple-950/20 border border-purple-100 dark:border-purple-900/30 text-xs leading-relaxed space-y-1">
<span className="font-bold text-purple-700 dark:text-purple-400 flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
<Lightbulb className="w-3.5 h-3.5" />
Exemplo Prático
</span>
<p className="text-slate-700 dark:text-slate-300">
<div className="callout mt-5">
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
Na prática
</p>
<p className="mt-1">
<TextoComLinks texto={item.exemplo} />
</p>
</div>
</div>
</article>
);
})}
@@ -488,41 +266,20 @@ export default function GlossarioPage() {
)}
</section>
{/* 3. CTA & PRÓXIMOS PASSOS */}
<section className="mt-16 bg-gradient-to-br from-slate-900 to-slate-950 text-white rounded-3xl p-6 sm:p-10 border border-slate-800 shadow-xl">
<div className="text-center max-w-2xl mx-auto space-y-4">
<div className="inline-flex items-center gap-2 bg-purple-500/20 text-purple-400 border border-purple-500/30 px-3.5 py-1.5 rounded-full text-xs font-bold">
<Sparkles className="w-4 h-4" />
<span>Continue Explorando</span>
</div>
<h2 className="text-2xl sm:text-3xl font-display font-extrabold">
Pronto para aplicar na prática?
</h2>
<p className="text-xs sm:text-sm text-slate-400 leading-relaxed">
Agora que você domina os conceitos técnicos, compare as especificações das séries ou use o seletor inteligente para encontrar o microcontrolador ideal para seu projeto.
<section className="shell pb-20 md:pb-28">
<div className="panel p-8 text-center sm:p-12">
<h2 className="section-title mx-auto max-w-2xl">Do conceito ao chip certo.</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto.
</p>
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
<Link
href="/series"
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white text-xs font-bold transition-all shadow-md hover:shadow-lg inline-flex items-center gap-2"
>
<span>Explorar Séries ESP32</span>
<ArrowRight className="w-4 h-4" />
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto">
<Sliders className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
</Link>
<Link
href="/seletor"
className="px-5 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold border border-slate-700 transition-all inline-flex items-center gap-2"
>
<span>Seletor Inteligente</span>
</Link>
<Link
href="/diagnostico"
className="px-5 py-2.5 rounded-xl bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-xs font-bold border border-amber-500/40 transition-all inline-flex items-center gap-2"
>
<span>Guia de Diagnóstico & Erros</span>
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
Explorar as séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
@@ -564,7 +321,7 @@ function TextoComLinks({ texto }) {
<Link
key={i}
href={`/series/${parte}`}
className="font-bold text-purple-600 dark:text-purple-400 hover:underline inline-flex items-center mx-0.5"
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
{parte}
</Link>
+24 -9
View File
@@ -1,18 +1,27 @@
import { Outfit, Inter } from "next/font/google";
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
import "./globals.css";
import Providers from "@/components/Providers";
import { SITE, jsonLd } from "@/lib/seo";
const outfit = Outfit({
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-outfit",
variable: "--font-space-grotesk",
display: "swap",
weight: ["500", "600", "700"],
});
const inter = Inter({
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-inter",
variable: "--font-manrope",
display: "swap",
weight: ["400", "500", "600", "700"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains",
display: "swap",
weight: ["400", "500", "600"],
});
export const metadata = {
@@ -93,14 +102,14 @@ export const metadata = {
export const viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f8fafc" },
{ media: "(prefers-color-scheme: dark)", color: "#020617" },
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
],
};
export default function RootLayout({ children }) {
return (
<html lang="pt-BR" className={`${outfit.variable} ${inter.variable}`} suppressHydrationWarning>
<html lang="pt-BR" className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
<head>
{/* Dados estruturados do site e da organização. É o que permite ao
Google montar o box de busca interna e entender a autoria. */}
@@ -109,7 +118,13 @@ export default function RootLayout({ children }) {
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
/>
</head>
<body className="font-sans antialiased bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-50" suppressHydrationWarning>
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
<a
href="#conteudo"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
>
Pular para o conteúdo
</a>
<Providers>
{children}
</Providers>
+359
View File
@@ -0,0 +1,359 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover";
import CodeSample from "@/components/CodeSample";
import Link from "next/link";
import { jsonLd, paginaMeta, SITE } from "@/lib/seo";
import {
ArrowRight,
ArrowUpRight,
BookOpen,
Cpu,
Gauge,
Radio,
ShieldCheck,
Terminal,
Wifi,
} from "lucide-react";
export const metadata = paginaMeta({
titulo: "Microcontroladores ESP32, o livro",
descricao:
"Conheça o livro Microcontroladores ESP32: conectividade, eficiência e desempenho. 12 partes e 78 capítulos em português sobre arquitetura, energia, rádio, periféricos e firmware.",
caminho: "/livro",
keywords: [
"livro ESP32",
"Microcontroladores ESP32",
"livro de ESP32 em português",
"Lucas Rayan Guerra",
"Ciência Embarcada",
],
});
const CHAPTERS = [
{
title: "O ecossistema ESP32",
desc: "Como as famílias se organizam, o que muda entre Xtensa e RISC-V e por que a escolha do chip começa pelo problema.",
topics: ["Famílias e sufixos", "Critérios de escolha", "Ferramentas de apoio"],
icon: Cpu,
},
{
title: "Arquitetura e memória",
desc: "Núcleos, caches, SRAM, PSRAM e o caminho entre o código em flash e a execução em alta velocidade.",
topics: ["Mapa de memória", "PSRAM na prática", "Caches e latência"],
icon: Gauge,
},
{
title: "Energia e autonomia",
desc: "Os modos de sono, o custo real do rádio e as decisões de hardware que definem a vida útil da bateria.",
topics: ["Deep sleep", "Domínios de energia", "Orçamento de consumo"],
icon: Gauge,
},
{
title: "Conectividade",
desc: "Wi-Fi, BLE, Thread, Zigbee e Matter: o que cada rádio resolve e onde a arquitetura começa a pesar.",
topics: ["Wi-Fi 6 e TWT", "BLE e malhas", "Matter e 802.15.4"],
icon: Radio,
},
{
title: "Periféricos e pinagem",
desc: "GPIO, ADC, I2S, USB, MIPI e as restrições que só aparecem quando o hardware já está na placa.",
topics: ["Pinos de strapping", "ADC e rádio", "USB nativo"],
icon: Wifi,
},
{
title: "Do protótipo ao produto",
desc: "Firmware, partições, OTA, segurança e os cuidados de engenharia que sobrevivem ao primeiro deploy.",
topics: ["ESP-IDF e Arduino", "OTA e rollback", "Secure boot"],
icon: ShieldCheck,
},
];
const ESP_IDF = `#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define LED GPIO_NUM_2
void app_main(void)
{
gpio_reset_pin(LED);
gpio_set_direction(LED, GPIO_MODE_OUTPUT);
while (1) {
gpio_set_level(LED, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED, 0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}`;
const ARDUINO = `#include <WiFi.h>
const char *ssid = "SUA_REDE";
const char *senha = "SUA_SENHA";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, senha);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
}
void loop() {}`;
const DEEP_SLEEP = `#include "esp_sleep.h"
#include "esp_log.h"
void app_main(void)
{
uint64_t intervalo_us = 30ULL * 1000000ULL;
esp_sleep_enable_timer_wakeup(intervalo_us);
ESP_LOGI("energia", "dormindo por 30 segundos");
esp_deep_sleep_start();
}`;
const FACTS = [
["Idioma", "Português"],
["Partes", "12"],
["Capítulos", "78"],
["Digital", "Amazon Kindle"],
["Impresso", "UICLAP"],
["ISBN", SITE.livroIsbn],
];
export default function LivroPage() {
return (
<div className="site-canvas min-h-screen">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.livro()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Livro", caminho: "/livro" },
])
),
}}
/>
<Header />
<main id="conteudo">
<section className="shell pt-8 sm:pt-12">
<nav aria-label="Trilha de navegação" className="flex items-center gap-2 text-sm text-muted">
<Link href="/" className="transition-colors hover:text-accent">
Início
</Link>
<span aria-hidden="true" className="text-line-strong">/</span>
<span aria-current="page" className="text-ink">Livro</span>
</nav>
<div className="mt-7 grid items-center gap-10 lg:grid-cols-[0.84fr_1.16fr] lg:gap-16">
<div className="order-1 lg:order-2">
<span className="chip chip-accent">
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
Livro novo · 1ª edição
</span>
<h1 className="page-title mt-5 text-[clamp(1.85rem,8vw,2.5rem)] sm:text-[clamp(2.5rem,5.4vw,4.4rem)]">
Microcontroladores ESP32
</h1>
<p className="mt-3 text-lg font-medium text-copper">{SITE.livroSubtitulo}</p>
<p className="lede mt-6 max-w-2xl">
Um percurso completo pelo ecossistema ESP32 — da arquitetura do chip à implantação
de produtos conectados. São 12 partes e 78 capítulos que cobrem Arduino e ESP-IDF,
FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo e segurança.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<a
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary w-full sm:w-auto"
>
<BookOpen className="h-4 w-4" aria-hidden="true" />
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost w-full sm:w-auto"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<Link href="/series" className="inline-flex min-h-11 items-center justify-center gap-1.5 px-2 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
Explorar a documentação
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<dl className="mt-9 grid grid-cols-2 gap-2.5 sm:grid-cols-3">
{FACTS.map(([label, value]) => (
<div key={label} className="rounded-2xl bg-surface-2 px-4 py-3">
<dt className="text-[0.6875rem] font-semibold text-muted">{label}</dt>
<dd className="mt-0.5 text-sm font-semibold text-ink">{value}</dd>
</div>
))}
</dl>
</div>
<div className="relative order-2 overflow-hidden rounded-[2rem] bg-[#0b0d10] p-8 ring-1 ring-white/10 sm:rounded-[2.5rem] sm:p-12 lg:order-1">
<div
className="pointer-events-none absolute left-1/2 top-1/2 -z-0 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
aria-hidden="true"
/>
<div className="relative mx-auto w-[min(16rem,76%)]">
<BookCover />
</div>
<div className="relative mt-9 flex items-end justify-between gap-4 border-t border-white/10 pt-4 text-xs text-[#a6afbd]">
<span>Lucas Rayan Guerra</span>
<span>Ciência Embarcada</span>
</div>
</div>
</div>
</section>
<section className="shell section" aria-labelledby="sumario-titulo">
<div className="grid gap-8 lg:grid-cols-[0.72fr_1.28fr] lg:gap-16">
<div>
<p className="eyebrow">Sumário</p>
<h2 id="sumario-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
Uma linha de raciocínio, do chip ao produto.
</h2>
</div>
<div className="lg:pt-2">
<p className="lede">
A ordem acompanha o projeto real: entender o chip, medir energia, conectar, expor
periféricos e sustentar o produto. Os seis eixos abaixo mostram como as 12 partes
se organizam.
</p>
<p className="mt-4 text-sm text-muted">
78 capítulos, com exercícios e projetos ao longo do percurso.
</p>
</div>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{CHAPTERS.map((chapter) => {
const Icon = chapter.icon;
return (
<article key={chapter.title} className="card flex flex-col p-5 sm:p-6">
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<h3 className="mt-5 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{chapter.title}</h3>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{chapter.desc}</p>
<ul className="mt-5 flex flex-wrap gap-1.5">
{chapter.topics.map((topic) => (
<li key={topic} className="chip">
{topic}
</li>
))}
</ul>
</article>
);
})}
</div>
</section>
<section className="border-y border-line bg-surface/70" aria-labelledby="codigo-titulo">
<div className="shell section">
<div className="grid gap-10 lg:grid-cols-[0.7fr_1.3fr] lg:gap-16">
<div className="min-w-0 lg:sticky lg:top-28 lg:self-start">
<p className="eyebrow">Código</p>
<h2 id="codigo-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
Exemplos curtos, prontos para a bancada.
</h2>
<p className="lede mt-4">
Os trechos abaixo são originais e servem como ponto de partida. Ajuste o pino, a rede
e a frequência ao seu hardware antes de gravar.
</p>
<div className="mt-6 rounded-2xl border border-line bg-surface p-4">
<p className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
Sem conteúdo protegido
</p>
<p className="mt-2 text-xs leading-relaxed text-muted">
Esta página apresenta apenas a ficha do livro e exemplos autorais. O texto integral,
PDF, EPUB e arquivos intermediários não são distribuídos pelo ESPDocs.
</p>
</div>
</div>
<div className="min-w-0 space-y-7">
<CodeSample
label="Ponto de partida · ESP-IDF"
language="C"
code={ESP_IDF}
note="Pisca o GPIO 2 usando a API atual do ESP-IDF e uma task do FreeRTOS."
/>
<CodeSample
label="Conectividade · Arduino"
language="C++"
code={ARDUINO}
note="Conexão Wi-Fi mínima, com espera ativa e impressão do IP obtido."
/>
<CodeSample
label="Energia · deep sleep"
language="C · ESP-IDF"
code={DEEP_SLEEP}
note="Timer de 30 segundos e entrada no modo de menor consumo disponível."
/>
</div>
</div>
</div>
</section>
<section className="shell section pb-20 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">Para quem é</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
Para quem já cansou de juntar pedaços de datasheet.
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
Estudantes, makers e engenheiros de firmware que querem uma visão coerente do
ecossistema — sem abandonar a fonte oficial.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary w-full sm:w-auto"
>
<BookOpen className="h-4 w-4" aria-hidden="true" />
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost w-full sm:w-auto"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
import { ArrowRight, BookOpen, Compass, Scale, SlidersHorizontal } from "lucide-react";
export const metadata = {
title: "Página não encontrada",
robots: { index: false, follow: false },
};
const CAMINHOS = [
{ href: "/series", icon: Compass, titulo: "Séries ESP32", texto: "As 12 famílias, com pinagem e especificações." },
{ href: "/seletor", icon: SlidersHorizontal, titulo: "Seletor de chip", texto: "Responda a oito perguntas e receba os modelos indicados." },
{ href: "/comparacao", icon: Scale, titulo: "Comparador", texto: "Até quatro séries lado a lado." },
{ href: "/glossario", icon: BookOpen, titulo: "Glossário", texto: "Termos de hardware explicados em português." },
];
export default function NotFound() {
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="shell section">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow justify-center">Erro 404</p>
<h1 className="page-title mt-4">Essa página não existe.</h1>
<p className="lede mx-auto mt-5 max-w-xl">
O endereço pode ter mudado ou estar digitado errado. Escolha um destes caminhos para continuar.
</p>
<div className="mt-8 flex justify-center">
<Link href="/" className="btn btn-primary">
Voltar ao início
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
<ul className="mx-auto mt-14 grid max-w-4xl gap-3 sm:grid-cols-2">
{CAMINHOS.map(({ href, icon: Icon, titulo, texto }) => (
<li key={href}>
<Link href={href} className="card card-interactive group flex h-full items-start gap-4 p-5">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<span>
<span className="block font-semibold text-ink">{titulo}</span>
<span className="mt-0.5 block text-sm text-muted">{texto}</span>
</span>
</Link>
</li>
))}
</ul>
</main>
<Footer />
</div>
);
}
+270 -469
View File
@@ -1,534 +1,335 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover";
import Link from "next/link";
import Image from "next/image";
import seriesData from "@/public/series.json";
import frameworksData from "@/public/frameworks.json";
import { SITE } from "@/lib/seo";
import {
SlidersHorizontal,
ShoppingBag,
Cpu,
Layers,
Scale,
BookOpen,
AlertTriangle,
Terminal,
Compass,
ArrowRight,
Sparkles,
CheckCircle2,
Zap,
ExternalLink,
Info,
Radio,
ShieldCheck,
Code,
HelpCircle,
Box
ArrowUpRight,
BookOpen,
Box,
Cpu,
Scale,
SlidersHorizontal,
Terminal,
} from "lucide-react";
export default function Home() {
// Estatísticas da Plataforma
const stats = [
{ value: "12", label: "Séries Documentadas", desc: "Do ESP32 clássico ao P4 e C61" },
{ value: "14", label: "Frameworks & SDKs", desc: "ESP-IDF, RainMaker, Rust, Matter, etc." },
{ value: "35+", label: "Componentes Curados", desc: "ESP Component Registry & idf_component.yml" },
{ value: "30+", label: "Guias de Diagnóstico", desc: "Soluções para bootloops, brownouts e WDT" },
];
const FEATURED = ["ESP32", "ESP32-S3", "ESP32-C6", "ESP32-C3", "ESP32-P4", "ESP32-H2"];
// Ferramentas da Plataforma (100% Clicáveis)
const tools = [
const TOOLS = [
{
title: "Seletor Inteligente",
badge: "Recomendado",
badgeColor: "bg-purple-100 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300",
description: "Responda algumas perguntas sobre seu projeto e descubra o chip ESP32 ideal com análise técnica e justificativa.",
href: "/seletor",
icon: <SlidersHorizontal className="w-6 h-6" />,
iconBg: "text-purple-600 bg-purple-50 dark:bg-purple-950/40 dark:text-purple-400",
ctaText: "Iniciar Quiz de Seleção",
title: "Seletor inteligente",
desc: "Responda oito perguntas sobre o projeto e receba os chips mais compatíveis, com justificativa técnica.",
icon: SlidersHorizontal,
action: "Encontrar meu chip",
},
{
title: "ESP Component Registry",
badge: "Oficial & Modular",
badgeColor: "bg-purple-100 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300",
description: "Explore o catálogo oficial de pacotes modulares da Espressif (LVGL, RainMaker, ESP-SR, BSPs) e gere manifestos idf_component.yml.",
href: "/componentes",
icon: <Box className="w-6 h-6" />,
iconBg: "text-purple-600 bg-purple-50 dark:bg-purple-950/40 dark:text-purple-400",
ctaText: "Explorar Componentes",
},
{
title: "Diagnóstico de Falhas",
badge: "30+ Soluções",
badgeColor: "bg-amber-100 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300",
description: "Encontre a causa e o passo a passo para corrigir erros frequentes como Brownout, Guru Meditation, Bootloops e falhas de Flash.",
href: "/diagnostico",
icon: <AlertTriangle className="w-6 h-6" />,
iconBg: "text-amber-600 bg-amber-50 dark:bg-amber-950/40 dark:text-amber-400",
ctaText: "Resolver Erro no Código",
},
{
title: "Comparador de Séries",
badge: "Lado a Lado",
badgeColor: "bg-indigo-100 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300",
description: "Compare especificações de até 4 séries simultaneamente: núcleos, clock, memória SRAM/PSRAM, interfaces e conexões.",
href: "/comparacao",
icon: <Scale className="w-6 h-6" />,
iconBg: "text-indigo-600 bg-indigo-50 dark:bg-indigo-950/40 dark:text-indigo-400",
ctaText: "Comparar Modelos",
title: "Comparador de séries",
desc: "Coloque até quatro modelos lado a lado e compare núcleos, rádio, memória, periféricos e segurança.",
icon: Scale,
action: "Comparar agora",
},
{
title: "Explorador de Séries & Pinouts",
badge: "12 Chips",
badgeColor: "bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300",
description: "Consulte diagramas interativos de conexões, mapas de periféricos, datasheets oficiais e restrições de pinos (strapping, ADC2).",
href: "/series",
icon: <Cpu className="w-6 h-6" />,
iconBg: "text-blue-600 bg-blue-50 dark:bg-blue-950/40 dark:text-blue-400",
ctaText: "Ver Todas as Séries",
href: "/diagnostico",
title: "Diagnóstico de falhas",
desc: "Causas e correções para brownout, bootloop, Guru Meditation, watchdog e falhas de flash.",
icon: AlertTriangle,
action: "Abrir diagnóstico",
},
{
title: "Catálogo de Placas",
badge: "Hardware Real",
badgeColor: "bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300",
description: "Filtre placas de desenvolvimento (DevKit, Xiao, Wemos, LilyGO) por série, interface USB, sensores integrados e links de compra.",
href: "/catalogo",
icon: <ShoppingBag className="w-6 h-6" />,
iconBg: "text-emerald-600 bg-emerald-50 dark:bg-emerald-950/40 dark:text-emerald-400",
ctaText: "Explorar Placas Físicas",
href: "/componentes",
title: "Component Registry",
desc: "Drivers, bibliotecas e BSPs do registro oficial da Espressif, com manifesto pronto para o projeto.",
icon: Box,
action: "Explorar componentes",
},
{
title: "Frameworks & SDKs",
badge: "14 Frameworks",
badgeColor: "bg-teal-100 text-teal-700 dark:bg-teal-950/60 dark:text-teal-300",
description: "Documentação completa de frameworks oficiais: ESP-IDF, RainMaker, Rust, Matter, ESP-SR, Arduino, MicroPython e mais.",
href: "/frameworks",
icon: <Terminal className="w-6 h-6" />,
iconBg: "text-teal-600 bg-teal-50 dark:bg-teal-950/40 dark:text-teal-400",
ctaText: "Acessar Guias de SDK",
title: "Frameworks e SDKs",
desc: "ESP-IDF, Arduino, Rust, Matter, MicroPython e as ferramentas que ficam em volta do chip.",
icon: Terminal,
action: "Ver frameworks",
},
{
title: "Glossário Técnico",
badge: "60+ Conceitos",
badgeColor: "bg-pink-100 text-pink-700 dark:bg-pink-950/60 dark:text-pink-300",
description: "Dicionário de termos técnicos essenciais: entenda RTOS, NVS, Partições OTA, eFuse, Brownout, RISC-V e protocolos industriais.",
href: "/glossario",
icon: <BookOpen className="w-6 h-6" />,
iconBg: "text-pink-600 bg-pink-50 dark:bg-pink-950/40 dark:text-pink-400",
ctaText: "Consultar Termos",
},
{
title: "Sobre a Plataforma",
badge: "Comunidade",
badgeColor: "bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
description: "Conheça a missão da ESPDocs, a autoria de Lucas Rayan Guerra e a parceria educacional com o blog Ciência Embarcada.",
href: "/sobre",
icon: <Info className="w-6 h-6" />,
iconBg: "text-slate-650 bg-slate-100 dark:bg-slate-800 dark:text-slate-350",
ctaText: "Conhecer o Projeto",
href: "/catalogo",
title: "Catálogo de placas",
desc: "DevKits e módulos reais, com interface USB, sensores integrados e link para a loja oficial.",
icon: Cpu,
action: "Ver placas",
},
];
// Chips em Destaque
const featuredChips = [
const ERRORS = [
{
name: "ESP32",
slug: "ESP32",
badge: "Padrão da Indústria",
arch: "Xtensa LX6 Dual-Core @ 240 MHz",
wireless: "Wi-Fi 4 (802.11 b/g/n) + BLE 4.2 / Clássico",
highlights: "Suporte maduro, 36 pinos GPIO, ADC duplo, DAC de 8 bits",
cor: "#007bff",
code: "Brownout detector was triggered",
cause: "Queda de tensão na inicialização do rádio.",
fix: "Capacitor de 10–100 µF junto ao chip e fonte de 5 V com corrente suficiente.",
},
{
name: "ESP32-S3",
slug: "ESP32-S3",
badge: "IA & USB Nativo",
arch: "Xtensa LX7 Dual-Core @ 240 MHz",
wireless: "Wi-Fi 4 + BLE 5.0 (Long Range)",
highlights: "Instruções vetoriais para IA, USB OTG nativo, até 45 GPIOs",
cor: "#fd7e14",
code: "Guru Meditation Error",
cause: "Ponteiro inválido, stack overflow ou uso incorreto de interrupção.",
fix: "Leia o backtrace com addr2line e dimensione a stack da task no FreeRTOS.",
},
{
name: "ESP32-C6",
slug: "ESP32-C6",
badge: "Wi-Fi 6 + Zigbee/Matter",
arch: "RISC-V 32-bit Single-Core @ 160 MHz + LP Core",
wireless: "Wi-Fi 6 (ax) + BLE 5.3 + Zigbee 3.0 & Thread (802.15.4)",
highlights: "Protocolo Matter, Target Wake Time (TWT), segurança de hardware",
cor: "#17a2b8",
code: "ADC2 indisponível com Wi-Fi",
cause: "O controlador de rádio reserva o ADC2 enquanto está ativo.",
fix: "Use os pinos do ADC1 para sensores analógicos que convivem com o Wi-Fi.",
},
{
name: "ESP32-C3",
slug: "ESP32-C3",
badge: "Custo-Benefício RISC-V",
arch: "RISC-V 32-bit Single-Core @ 160 MHz",
wireless: "Wi-Fi 4 + BLE 5.0 (Mesh & Long Range)",
highlights: "Substituto direto do ESP8266, pinout compacto, baixo consumo",
cor: "#20c997",
},
{
name: "ESP32-P4",
slug: "ESP32-P4",
badge: "Superprocessador 400MHz",
arch: "Dual-Core RISC-V HP @ 400 MHz + LP Core @ 40 MHz",
wireless: "Sem rádio interno (Focado em processamento multimídia)",
highlights: "MIPI-CSI/DSI, codificador H.264, Ethernet 100M, 50+ GPIOs",
cor: "#ffc107",
},
{
name: "ESP32-H2",
slug: "ESP32-H2",
badge: "Zigbee & Thread",
arch: "RISC-V 32-bit Single-Core @ 96 MHz",
wireless: "IEEE 802.15.4 (Zigbee 3.0, Thread) + BLE 5.3",
highlights: "Automação residencial ultrabaixo consumo, nós finais de rede",
cor: "#6c757d",
code: "Flash Read err / bootloader",
cause: "Tensão ou modo de flash incorretos, ou pino de strapping preso no boot.",
fix: "Confira 3,3 V, pull-ups de boot e a configuração de flash no esptool.",
},
];
// Problemas Comuns do Diagnóstico
const commonErrors = [
{
title: "Brownout detector was triggered",
cause: "Queda de tensão súbita na alimentação ao inicializar o Wi-Fi",
solution: "Adicionar capacitor de desacoplamento de 10µF–100µF e usar fonte externa de 5V 1A+",
},
{
title: "Guru Meditation Error: IllegalInstruction",
cause: "Acesso a ponteiro nulo, estouro de pilha (stack overflow) ou interrupção incorreta",
solution: "Inspecione o Backtrace usando o addr2line e aumente o tamanho da stack da task FreeRTOS",
},
{
title: "ADC2 leitura inválida com Wi-Fi",
cause: "O rádio Wi-Fi bloqueia o conversor analógico-digital ADC2 enquanto está ativo",
solution: "Utilize exclusivamente pinos do ADC1 (GPIOs 32 a 39 no ESP32) para sensores analógicos",
},
{
title: "Flash Read err / Bootloader falhou",
cause: "Tensão de Flash incorreta (1.8V vs 3.3V) ou pino de strapping (GPIO 0, 2, 12, 15) travado",
solution: "Verifique resistores pull-up/pull-down nos pinos de boot e confirme a frequência de Flash no esptool",
},
];
export default function Home() {
const seriesCount = Object.keys(seriesData).length;
const frameworkCount = Object.keys(frameworksData).length;
const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie));
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100">
<div className="site-canvas min-h-screen">
<Header />
<main className="px-4 sm:px-6 pt-10 md:pt-16 pb-24 max-w-7xl mx-auto">
{/* Hero Section */}
<section className="text-center mb-20 max-w-5xl mx-auto">
{/* Pill Tag */}
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-purple-50 dark:bg-purple-950/50 border border-purple-200 dark:border-purple-800/80 text-purple-700 dark:text-purple-300 text-xs md:text-sm font-bold mb-6 shadow-xs">
<Sparkles className="w-3.5 h-3.5 text-purple-600 dark:text-purple-400" />
<span>O Maior Hub Técnico de ESP32 em Português</span>
</div>
<h1 className="text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-display font-extrabold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent mb-6 tracking-tight leading-[1.1] select-none">
ESPDocs
<main id="conteudo">
<section className="shell pb-10 pt-14 sm:pt-20 lg:pb-14 lg:pt-24">
<div className="mx-auto max-w-4xl text-center">
<span className="chip chip-accent">
<Cpu className="h-3.5 w-3.5" aria-hidden="true" />
Documentação aberta do ESP32
</span>
<h1 className="display mt-6 text-[clamp(2.9rem,7.4vw,5.4rem)]">
O ESP32,
<br className="sm:hidden" /> sem ruído.
</h1>
<h2 className="text-xl sm:text-2xl md:text-3xl text-slate-850 dark:text-slate-100 font-extrabold mb-6 max-w-3xl mx-auto leading-snug">
Tudo o que você precisa para dominar o ecossistema ESP32
</h2>
<p className="text-sm sm:text-base md:text-lg text-slate-650 dark:text-slate-350 max-w-3xl mx-auto leading-relaxed mb-10">
Chega de perder tempo traduzindo datasheets dispersos ou decifrando bootloops no escuro.
Compare 12 séries de chips, diagnostique erros comuns de firmware, descubra o ESP32 ideal
para o seu projeto e consulte pinouts interativos 100% em português brasileiro.
<p className="lede mx-auto mt-6 max-w-2xl">
Especificações, pinagem, comparações e diagnóstico em uma leitura só.
Menos tradução de datasheet, mais tempo com o protótipo na bancada.
</p>
{/* Botões de Ação Hero */}
<div className="flex flex-wrap gap-3.5 justify-center items-center mb-16">
<Link
className="group inline-flex items-center gap-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-7 py-3.5 rounded-xl font-bold text-sm shadow-md hover:shadow-purple-500/25 hover:-translate-y-0.5 transition-all duration-200 cursor-pointer active:scale-95"
href="/seletor"
>
<Compass className="w-4 h-4" />
<span>Encontrar ESP32 Ideal</span>
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
</Link>
<Link
className="group inline-flex items-center gap-2.5 bg-amber-700 hover:bg-amber-800 text-white px-7 py-3.5 rounded-xl font-bold text-sm shadow-md hover:-translate-y-0.5 transition-all duration-200 cursor-pointer active:scale-95"
href="/diagnostico"
>
<AlertTriangle className="w-4 h-4" />
<span>Diagnosticar Falhas</span>
</Link>
<Link
className="group inline-flex items-center gap-2.5 bg-blue-600 hover:bg-blue-700 text-white px-7 py-3.5 rounded-xl font-bold text-sm shadow-md hover:-translate-y-0.5 transition-all duration-200 cursor-pointer active:scale-95"
href="/series"
>
<Cpu className="w-4 h-4" />
<span>Explorar 12 Séries</span>
</Link>
<Link
className="group inline-flex items-center gap-2.5 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-300 px-7 py-3.5 rounded-xl font-bold text-sm shadow-xs border border-slate-300 dark:border-slate-800 hover:border-purple-500 dark:hover:border-purple-400 hover:text-purple-600 dark:hover:text-purple-400 hover:-translate-y-0.5 transition-all duration-200 cursor-pointer active:scale-95"
href="/comparacao"
>
<Scale className="w-4 h-4" />
<span>Comparar Chips</span>
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
Explorar as {seriesCount} séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
{/* Faixa de Estatísticas */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
{stats.map((stat) => (
<p className="mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted">
<span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% em português</span>
</p>
</div>
<div className="panel mt-12 overflow-hidden sm:mt-16">
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 sm:px-7">
<div>
<h2 className="text-sm font-semibold text-ink">Séries em destaque</h2>
<p className="mt-0.5 text-xs text-muted">O essencial de cada família, antes de abrir o datasheet.</p>
</div>
<Link href="/series" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
Índice completo
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<div className="grid gap-px border-t border-line bg-line sm:grid-cols-2 lg:grid-cols-3">
{featured.map(([key, serie]) => (
<Link
key={key}
href={`/series/${key}`}
className="group flex min-w-0 flex-col bg-surface p-5 transition-colors duration-300 hover:bg-surface-2 sm:p-6"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="font-display text-xl font-semibold tracking-[-0.035em] text-ink">{key}</h3>
<p className="mt-1 truncate text-xs text-muted">{serie.arquitetura}</p>
</div>
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-accent-soft text-accent transition-transform duration-300 group-hover:scale-105">
<Cpu className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-5 line-clamp-2 text-sm leading-relaxed text-muted">{serie.descricao}</p>
<div className="mt-5 flex items-center justify-between gap-3 border-t border-line pt-3.5">
<span className="truncate font-mono text-[0.6875rem] text-ink-soft">{serie.frequencia}</span>
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent">
Ficha
<ArrowUpRight className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</div>
</Link>
))}
</div>
</div>
</section>
<section className="shell section pt-8 sm:pt-12" aria-labelledby="livro-titulo">
<div className="relative isolate overflow-hidden rounded-[2rem] bg-[#0b0d10] ring-1 ring-white/10 dark:bg-[#05070a] dark:ring-white/15 sm:rounded-[2.5rem]">
<div
key={stat.label}
className="bg-white dark:bg-slate-900/60 backdrop-blur-md p-4 sm:p-5 rounded-2xl border border-slate-300 dark:border-slate-800/80 shadow-xs text-center"
>
<div className="text-2xl sm:text-3xl font-display font-extrabold bg-gradient-to-r from-blue-600 to-purple-600 dark:from-blue-400 dark:to-purple-400 bg-clip-text text-transparent mb-1">
{stat.value}
</div>
<div className="text-xs sm:text-sm font-bold text-slate-850 dark:text-slate-100 mb-0.5">
{stat.label}
</div>
<div className="text-[11px] text-slate-500 dark:text-slate-400 leading-tight">
{stat.desc}
</div>
</div>
))}
</div>
</section>
{/* Seção 2: Central de Ferramentas & Recursos (Cards 100% Clicáveis) */}
<section className="mb-24">
<div className="text-center mb-12">
<h2 className="text-2xl sm:text-3xl md:text-4xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-3">
Central de Ferramentas e Guias
</h2>
<p className="text-sm sm:text-base text-slate-650 dark:text-slate-350 max-w-2xl mx-auto leading-relaxed">
Escolha o que você deseja fazer hoje. Cada ferramenta foi desenvolvida para acelerar seu desenvolvimento e eliminar gargalos.
</p>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
{tools.map((tool) => (
<Link
key={tool.title}
href={tool.href}
className="group flex flex-col justify-between bg-white dark:bg-slate-900/50 backdrop-blur-md p-6 rounded-2xl border border-slate-300 dark:border-slate-800/80 hover:border-purple-500 dark:hover:border-purple-400 shadow-xs hover:shadow-xl hover:shadow-purple-500/5 hover:-translate-y-1 transition-all duration-300 cursor-pointer"
>
<div>
{/* Top Bar: Icon + Badge */}
<div className="flex items-center justify-between gap-2 mb-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transform group-hover:scale-105 transition-transform duration-300 shadow-xs ${tool.iconBg}`}>
{tool.icon}
</div>
<span className={`text-[11px] font-bold px-2.5 py-1 rounded-full ${tool.badgeColor}`}>
{tool.badge}
</span>
</div>
{/* Content */}
<h3 className="text-base font-bold text-slate-850 dark:text-slate-100 mb-2 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{tool.title}
</h3>
<p className="text-xs sm:text-sm text-slate-650 dark:text-slate-350 leading-relaxed mb-6">
{tool.description}
</p>
</div>
{/* Bottom Action Link */}
<div className="pt-3 border-t border-slate-100 dark:border-slate-800/60 flex items-center justify-between text-xs font-bold text-purple-600 dark:text-purple-400">
<span>{tool.ctaText}</span>
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1 transition-transform" />
</div>
</Link>
))}
</div>
</section>
{/* Seção 3: Explore as Séries em Destaque */}
<section className="mb-24">
<div className="flex flex-col md:flex-row md:items-end justify-between mb-10 gap-4">
<div>
<div className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-purple-600 dark:text-purple-400 mb-2">
<Cpu className="w-4 h-4" />
<span>Hardware & Silício</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Séries Populares de ESP32
</h2>
<p className="text-sm text-slate-650 dark:text-slate-350 mt-1">
Conheça as principais famílias de chips da Espressif, da arquitetura aos protocolos sem fio.
</p>
</div>
<Link
href="/series"
className="inline-flex items-center gap-2 text-sm font-bold text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 shrink-0"
>
<span>Ver todas as 12 Séries</span>
<ArrowRight className="w-4 h-4" />
</Link>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{featuredChips.map((chip) => (
<Link
key={chip.slug}
href={`/series/${chip.slug}`}
prefetch={false}
className="group bg-white dark:bg-slate-900/50 backdrop-blur-md p-6 rounded-2xl border border-slate-300 dark:border-slate-800/80 hover:border-purple-500 dark:hover:border-purple-400 shadow-xs hover:shadow-lg transition-all duration-300 flex flex-col justify-between"
>
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{chip.name}
</h3>
<span className="text-[11px] font-bold px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
{chip.badge}
</span>
</div>
<div className="space-y-2 mb-5">
<div className="flex items-start gap-2 text-xs text-slate-650 dark:text-slate-350">
<Zap className="w-3.5 h-3.5 text-amber-500 shrink-0 mt-0.5" />
<span className="font-semibold">{chip.arch}</span>
</div>
<div className="flex items-start gap-2 text-xs text-slate-650 dark:text-slate-350">
<Radio className="w-3.5 h-3.5 text-blue-500 shrink-0 mt-0.5" />
<span>{chip.wireless}</span>
</div>
<div className="flex items-start gap-2 text-xs text-slate-500 dark:text-slate-400">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500 shrink-0 mt-0.5" />
<span>{chip.highlights}</span>
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-100 dark:border-slate-800/60 flex items-center justify-between text-xs font-bold text-purple-600 dark:text-purple-400">
<span>Ver Pinout e Especificações</span>
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1 transition-transform" />
</div>
</Link>
))}
</div>
</section>
{/* Seção 4: Problemas Comuns & Diagnóstico Rápido */}
<section className="mb-24">
<div className="bg-gradient-to-br from-indigo-50/70 via-purple-50/50 to-white dark:from-slate-900/60 dark:via-slate-900/40 dark:to-purple-950/20 rounded-3xl p-6 sm:p-10 border border-indigo-200/80 dark:border-slate-800 shadow-xs">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 mb-8">
<div>
<div className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 mb-2">
<AlertTriangle className="w-4 h-4" />
<span>Resolução Imediata</span>
</div>
<h2 className="text-2xl sm:text-3xl font-display font-extrabold text-slate-850 dark:text-slate-100">
Enfrentando Erros ou Travamentos?
</h2>
<p className="text-sm text-slate-650 dark:text-slate-350 mt-1 max-w-xl">
Acesse guias práticos com causas comprovadas e soluções passo a passo para as falhas mais comuns de hardware e firmware.
</p>
</div>
<Link
href="/diagnostico"
className="inline-flex items-center gap-2 bg-gradient-to-r from-amber-500 to-orange-500 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-md hover:-translate-y-0.5 transition-all duration-200 shrink-0 self-start lg:self-auto cursor-pointer"
>
<AlertTriangle className="w-4 h-4" />
<span>Abrir Guia de 30+ Erros</span>
</Link>
</div>
<div className="grid md:grid-cols-2 gap-4">
{commonErrors.map((err) => (
<Link
key={err.title}
href="/diagnostico"
prefetch={false}
className="group bg-white dark:bg-slate-900/80 p-5 rounded-2xl border border-slate-200 dark:border-slate-800 hover:border-amber-400 dark:hover:border-amber-400 transition-all duration-200 flex flex-col justify-between"
>
<div>
<div className="font-mono text-xs font-bold text-amber-600 dark:text-amber-400 mb-2 group-hover:underline">
{err.title}
</div>
<p className="text-xs text-slate-650 dark:text-slate-350 mb-3">
<strong className="text-slate-800 dark:text-slate-200">Causa provável: </strong>
{err.cause}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
<strong className="text-emerald-700 dark:text-emerald-400">Solução rápida: </strong>
{err.solution}
</p>
</div>
<div className="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/60 flex items-center justify-between text-[11px] font-bold text-amber-600 dark:text-amber-400">
<span>Ver diagnóstico completo</span>
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1 transition-transform" />
</div>
</Link>
))}
</div>
</div>
</section>
{/* Seção 5: Ciência Embarcada & Curadoria */}
<section className="mb-8">
<div className="bg-gradient-to-br from-white to-purple-50/50 dark:from-slate-900/40 dark:to-purple-950/10 rounded-3xl shadow-xs border border-slate-300 dark:border-slate-800/80 p-8 md:p-12 relative overflow-hidden">
{/* Background soft glow decoration */}
<div className="absolute right-0 bottom-0 w-80 h-80 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col items-center text-center relative z-10 max-w-3xl mx-auto">
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="group mb-6"
>
<Image
src="/logo.png"
alt="Ciência Embarcada Logo"
width={80}
height={80}
className="h-16 w-16 md:h-20 md:w-20 mb-2 transition-transform duration-300 group-hover:scale-105 object-contain"
className="pointer-events-none absolute -left-24 top-1/2 -z-10 h-80 w-80 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
aria-hidden="true"
/>
</a>
<h2 className="text-xl sm:text-2xl font-bold text-slate-850 dark:text-slate-100 mb-3">
Conteúdo e Curadoria por{' '}
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline transition-colors font-extrabold"
>
Ciência Embarcada
</a>
<div className="grid lg:grid-cols-[0.82fr_1.18fr]">
<div className="order-1 flex flex-col justify-center p-7 sm:p-10 lg:order-2 lg:p-14">
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs font-semibold text-[#75cdc1]">
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
Livro novo
</span>
<h2 id="livro-titulo" className="mt-5 font-display text-[clamp(2rem,4.2vw,3.2rem)] font-semibold leading-[1.05] tracking-[-0.04em] text-[#f4f6f8]">
{SITE.livroTitulo}
</h2>
<p className="text-sm text-slate-650 dark:text-slate-350 leading-relaxed mb-8 max-w-xl">
Acesse artigos profundos sobre programação de microcontroladores, design de hardware, Internet das Coisas (IoT), laboratórios práticos e projetos reais em nosso blog parceiro oficial.
<p className="mt-2 text-base text-[#a6afbd]">{SITE.livroSubtitulo}</p>
<p className="mt-5 max-w-xl text-sm leading-relaxed text-[#c4cbd5]">
Um percurso completo pelo hardware e pelo firmware do ESP32: da arquitetura do chip
à implantação de produtos conectados. São 12 partes e 78 capítulos sobre Arduino,
ESP-IDF, FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo
e segurança.
</p>
<p className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-[#a6afbd]">
<span>12 partes</span>
<span aria-hidden="true" className="text-white/20">·</span>
<span>78 capítulos</span>
<span aria-hidden="true" className="text-white/20">·</span>
<span>ISBN {SITE.livroIsbn}</span>
</p>
<div className="mt-7 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<a
href="https://cienciaembarcada.com.br"
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3 rounded-xl font-bold text-sm shadow-md hover:shadow-purple-500/10 hover:-translate-y-0.5 transition-all duration-200 cursor-pointer active:scale-95"
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full bg-[#2dd4bf] px-5 text-sm font-semibold text-[#06201d] transition-colors duration-300 hover:bg-[#5eead4]"
>
<span>Visitar Blog Ciência Embarcada</span>
<ExternalLink className="w-4 h-4" />
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full border border-white/20 px-5 text-sm font-semibold text-[#f4f6f8] transition-colors duration-300 hover:border-white/40 hover:bg-white/5"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<Link href="/livro" className="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium text-[#a6afbd] transition-colors duration-300 hover:text-[#f4f6f8]">
Ficha completa
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
{/* Disclaimer Oficial */}
<div className="border-t border-slate-300 dark:border-slate-800/80 pt-8 mt-10">
<div className="bg-slate-100/60 dark:bg-slate-900/60 border-l-4 border-purple-500 p-5 rounded-r-2xl">
<div className="flex gap-4">
<Info className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5 shrink-0" />
<p className="text-xs leading-relaxed text-slate-500 dark:text-slate-400">
<strong className="font-bold text-slate-700 dark:text-slate-200">Plataforma independente e não oficial:</strong> O ESPDocs é um projeto educativo que compila, filtra e expõe dados públicos e links de documentações oficiais fornecidas pela fabricante Espressif Systems. Para projetos industriais ou decisões de engenharia críticas, sempre consulte a documentação técnica nativa do fabricante.
<div className="order-2 flex items-center justify-center border-t border-white/10 p-8 sm:p-12 lg:order-1 lg:border-r lg:border-t-0 lg:p-14">
<div className="relative w-[min(15.5rem,68%)] lg:w-[min(17rem,100%)]">
<div className="absolute -inset-10 -z-10 rounded-full bg-[#2dd4bf]/10 blur-3xl" aria-hidden="true" />
<BookCover />
</div>
</div>
</div>
</div>
</section>
<section className="shell section" aria-labelledby="ferramentas-titulo">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow">Ferramentas</p>
<h2 id="ferramentas-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
Comece pelo problema.
</h2>
<p className="lede mt-4">
Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio.
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{TOOLS.map((tool) => {
const Icon = tool.icon;
return (
<Link key={tool.href} href={tool.href} className="card card-interactive group flex flex-col p-5 sm:p-6">
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<h3 className="mt-5 font-display text-lg font-semibold tracking-[-0.025em] text-ink">{tool.title}</h3>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{tool.desc}</p>
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-accent">
{tool.action}
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
);
})}
</div>
</section>
<section className="shell section pt-0" aria-labelledby="diagnostico-titulo">
<div className="grid items-start gap-10 lg:grid-cols-[0.78fr_1.22fr] lg:gap-16">
<div className="lg:sticky lg:top-28">
<p className="eyebrow">Diagnóstico</p>
<h2 id="diagnostico-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
O primeiro boot não precisa virar noite.
</h2>
<p className="lede mt-4">
Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware.
</p>
<Link href="/diagnostico" className="btn btn-accent mt-7">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
Abrir os 30+ diagnósticos
</Link>
</div>
<div className="panel-dark min-w-0 overflow-hidden rounded-[1.75rem]">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-white/10 px-5 py-4 sm:px-6">
<span className="inline-flex items-center gap-2 font-mono text-xs text-[#75cdc1]">
<Terminal className="h-4 w-4" aria-hidden="true" />
boot.log
</span>
<span className="text-xs text-[#a6afbd]">4 exemplos de 30+</span>
</div>
<div className="divide-y divide-white/10">
{ERRORS.map((item) => (
<Link key={item.code} href="/diagnostico" className="group grid gap-2 px-5 py-5 transition-colors duration-300 hover:bg-white/[0.035] sm:px-6">
<span className="break-words font-mono text-[0.75rem] font-medium leading-relaxed text-[#ffb39b]">{item.code}</span>
<span className="text-sm font-medium text-[#f4f6f8]">{item.cause}</span>
<span className="text-xs leading-relaxed text-[#a6afbd]">{item.fix}</span>
</Link>
))}
</div>
</div>
</div>
</section>
<section className="shell pb-20 pt-4 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">Curadoria independente</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
Feito para consultar no meio do projeto.
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial.
Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/glossario" className="btn btn-ghost w-full sm:w-auto">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Glossário
</Link>
<Link href="/sobre" className="btn btn-primary w-full sm:w-auto">
Sobre o projeto
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
+408 -888
View File
File diff suppressed because it is too large. Load diff
+263 -388
View File
@@ -3,57 +3,31 @@ import Link from "next/link";
import Image from "next/image";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import { notFound } from "next/navigation";
import seriesData from "@/public/series.json";
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
import SeriesTabMenu from "@/components/SeriesTabMenu";
import PinRestrictions from "@/components/PinRestrictions";
import {
Cpu,
Activity,
Database,
Wifi,
Bluetooth,
Battery,
Sparkles,
Home,
ArrowRight,
ArrowLeft,
ExternalLink,
Scale,
FileText,
Code,
BookOpen,
AlertTriangle,
Moon,
Plug,
Compass,
Sliders,
MapPin,
Volume2,
Fingerprint,
MessageSquare,
Repeat,
Share2,
Music,
Clock,
HardDrive,
Folder,
Thermometer,
ChevronRight,
ArrowLeft,
ArrowRight,
ArrowUpRight,
Battery,
BookOpen,
Code,
Cpu,
Database,
ExternalLink,
FileText,
Lightbulb,
ShieldCheck,
KeyRound,
Lock,
Hash,
Spline,
FileSignature,
Dices,
Waves,
Bug,
Box,
Package,
Terminal
Scale,
ShieldCheck,
Sliders,
Thermometer,
Wifi,
} from "lucide-react";
export default async function SerieDetail({ params }) {
@@ -77,75 +51,75 @@ export default async function SerieDetail({ params }) {
const nextKey = currentIndex < seriesKeys.length - 1 ? seriesKeys[currentIndex + 1] : null;
const processadorSpecs = [
{ label: "Arquitetura", value: serie.arquitetura, icon: <Cpu className="w-5 h-5 text-blue-500" /> },
{ label: "Núcleos", value: serie.nucleos, icon: <Sliders className="w-5 h-5 text-blue-500" /> },
{ label: "Frequência", value: serie.frequencia, icon: <Activity className="w-5 h-5 text-blue-500" /> },
{ label: "Coprocessador ULP", value: serie.coprocessador_ulp || "Não", icon: <Moon className="w-5 h-5 text-blue-500" /> },
{ label: "Aceleradores IA", value: serie.aceleradores_ia || "Não", icon: <Sparkles className="w-5 h-5 text-purple-500" /> },
{ label: "Arquitetura", value: serie.arquitetura },
{ label: "Núcleos", value: serie.nucleos },
{ label: "Frequência", value: serie.frequencia },
{ label: "Coprocessador ULP", value: serie.coprocessador_ulp || "Não" },
{ label: "Aceleradores IA", value: serie.aceleradores_ia || "Não" },
];
const seg = serie.seguranca || {};
const segurancaSpecs = [
{ label: "AES", value: seg.aes || "Não", icon: <Lock className="w-5 h-5 text-teal-500" /> },
{ label: "SHA", value: seg.sha || "Não", icon: <Hash className="w-5 h-5 text-teal-500" /> },
{ label: "RSA / MPI", value: seg.rsa || "Não", icon: <KeyRound className="w-5 h-5 text-teal-500" /> },
{ label: "ECC (curvas elípticas)", value: seg.ecc || "Não", icon: <Spline className="w-5 h-5 text-teal-500" /> },
{ label: "ECDSA em hardware", value: seg.ecdsa || "Não", icon: <FileSignature className="w-5 h-5 text-teal-500" /> },
{ label: "HMAC", value: seg.hmac || "Não", icon: <Fingerprint className="w-5 h-5 text-teal-500" /> },
{ label: "Assinatura Digital (DS)", value: seg.assinatura_digital || "Não", icon: <FileSignature className="w-5 h-5 text-teal-500" /> },
{ label: "Key Manager", value: seg.key_manager || "Não", icon: <KeyRound className="w-5 h-5 text-teal-500" /> },
{ label: "Gerador aleatório (RNG)", value: seg.rng || "Não", icon: <Dices className="w-5 h-5 text-teal-500" /> },
{ label: "Criptografia de flash", value: seg.criptografia_flash || "Não", icon: <HardDrive className="w-5 h-5 text-teal-500" /> },
{ label: "Secure Boot", value: seg.secure_boot || "Não", icon: <ShieldCheck className="w-5 h-5 text-teal-500" /> },
{ label: "Proteção contra DPA", value: seg.protecao_dpa || "Não", icon: <Waves className="w-5 h-5 text-teal-500" /> },
{ label: "AES", value: seg.aes || "Não" },
{ label: "SHA", value: seg.sha || "Não" },
{ label: "RSA / MPI", value: seg.rsa || "Não" },
{ label: "ECC (curvas elípticas)", value: seg.ecc || "Não" },
{ label: "ECDSA em hardware", value: seg.ecdsa || "Não" },
{ label: "HMAC", value: seg.hmac || "Não" },
{ label: "Assinatura Digital (DS)", value: seg.assinatura_digital || "Não" },
{ label: "Key Manager", value: seg.key_manager || "Não" },
{ label: "Gerador aleatório (RNG)", value: seg.rng || "Não" },
{ label: "Criptografia de flash", value: seg.criptografia_flash || "Não" },
{ label: "Secure Boot", value: seg.secure_boot || "Não" },
{ label: "Proteção contra DPA", value: seg.protecao_dpa || "Não" },
];
const ferramentasSpecs = [
{ label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "Não informado", icon: <Code className="w-5 h-5 text-amber-500" /> },
{ label: "Core Arduino", value: serie.arduino_core || "Não informado", icon: <Terminal className="w-5 h-5 text-amber-500" /> },
{ label: "Depuração", value: serie.depuracao || "Não informado", icon: <Bug className="w-5 h-5 text-amber-500" /> },
{ label: "Encapsulamento", value: serie.encapsulamento || "Não informado", icon: <Box className="w-5 h-5 text-amber-500" /> },
{ label: "Módulos disponíveis", value: serie.modulos || "Não informado", icon: <Package className="w-5 h-5 text-amber-500" /> },
{ label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "Não informado" },
{ label: "Core Arduino", value: serie.arduino_core || "Não informado" },
{ label: "Depuração", value: serie.depuracao || "Não informado" },
{ label: "Encapsulamento", value: serie.encapsulamento || "Não informado" },
{ label: "Módulos disponíveis", value: serie.modulos || "Não informado" },
];
const conectividadeSpecs = [
{ label: "Wi-Fi", value: serie.wifi || "Não", icon: <Wifi className="w-5 h-5 text-indigo-500" /> },
{ label: "Bluetooth", value: serie.bluetooth || "Não", icon: <Bluetooth className="w-5 h-5 text-indigo-500" /> },
{ label: "Zigbee/Thread", value: serie.zigbee_thread || "Não", icon: <Compass className="w-5 h-5 text-indigo-500" /> },
{ label: "Matter", value: serie.matter || "Não", icon: <Home className="w-5 h-5 text-indigo-500" /> },
{ label: "Ethernet", value: serie.ethernet || serie.ethernet_mac || "Não", icon: <Plug className="w-5 h-5 text-indigo-500" /> },
{ label: "CAN", value: serie.can || "Não", icon: <Activity className="w-5 h-5 text-indigo-500" /> },
{ label: "Wi-Fi", value: serie.wifi || "Não" },
{ label: "Bluetooth", value: serie.bluetooth || "Não" },
{ label: "Zigbee/Thread", value: serie.zigbee_thread || "Não" },
{ label: "Matter", value: serie.matter || "Não" },
{ label: "Ethernet", value: serie.ethernet || serie.ethernet_mac || "Não" },
{ label: "CAN", value: serie.can || "Não" },
];
const memoriaSpecs = [
{ label: "SRAM", value: serie.memoria_sram, icon: <Database className="w-5 h-5 text-pink-500" /> },
{ label: "SRAM RTC", value: serie.memoria_sram_rtc, icon: <Clock className="w-5 h-5 text-pink-500" /> },
{ label: "ROM", value: serie.memoria_rom, icon: <Folder className="w-5 h-5 text-pink-500" /> },
{ label: "Flash Externa", value: serie.flash_externa, icon: <HardDrive className="w-5 h-5 text-pink-500" /> },
{ label: "PSRAM Externa", value: serie.psram_externa || "Não", icon: <Sliders className="w-5 h-5 text-pink-500" /> },
{ label: "SRAM", value: serie.memoria_sram },
{ label: "SRAM RTC", value: serie.memoria_sram_rtc },
{ label: "ROM", value: serie.memoria_rom },
{ label: "Flash Externa", value: serie.flash_externa },
{ label: "PSRAM Externa", value: serie.psram_externa || "Não" },
];
const perifericos = [
{ label: "GPIO", value: serie.gpio, icon: <MapPin className="w-5 h-5 text-emerald-500" /> },
{ label: "ADC", value: serie.adc, icon: <Activity className="w-5 h-5 text-emerald-500" /> },
{ label: "DAC", value: serie.dac || "Não", icon: <Volume2 className="w-5 h-5 text-emerald-500" /> },
{ label: "Touch", value: serie.touch || "Não", icon: <Fingerprint className="w-5 h-5 text-emerald-500" /> },
{ label: "UART", value: serie.uart, icon: <MessageSquare className="w-5 h-5 text-emerald-500" /> },
{ label: "SPI", value: serie.spi, icon: <Repeat className="w-5 h-5 text-emerald-500" /> },
{ label: "I2C", value: serie.i2c, icon: <Share2 className="w-5 h-5 text-emerald-500" /> },
{ label: "I2S", value: serie.i2s, icon: <Music className="w-5 h-5 text-emerald-500" /> },
{ label: "PWM", value: serie.pwm, icon: <Sliders className="w-5 h-5 text-emerald-500" /> },
{ label: "USB", value: serie.usb || "Não", icon: <Plug className="w-5 h-5 text-emerald-500" /> },
{ label: "GPIO", value: serie.gpio },
{ label: "ADC", value: serie.adc },
{ label: "DAC", value: serie.dac || "Não" },
{ label: "Touch", value: serie.touch || "Não" },
{ label: "UART", value: serie.uart },
{ label: "SPI", value: serie.spi },
{ label: "I2C", value: serie.i2c },
{ label: "I2S", value: serie.i2s },
{ label: "PWM", value: serie.pwm },
{ label: "USB", value: serie.usb || "Não" },
];
const interfacesEspeciais = [];
if (serie.lcd) interfacesEspeciais.push({ label: "LCD", value: serie.lcd, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.camera) interfacesEspeciais.push({ label: "Câmera", value: serie.camera, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.mipi_csi) interfacesEspeciais.push({ label: "MIPI CSI", value: serie.mipi_csi, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.mipi_dsi) interfacesEspeciais.push({ label: "MIPI DSI", value: serie.mipi_dsi, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.sdio) interfacesEspeciais.push({ label: "SDIO", value: serie.sdio, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.sensor_hall) interfacesEspeciais.push({ label: "Sensor Hall", value: serie.sensor_hall, icon: <Sliders className="w-5 h-5 text-cyan-500" /> });
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: "Sensor Temperatura", value: serie.sensor_temperatura, icon: <Thermometer className="w-5 h-5 text-cyan-500" /> });
if (serie.lcd) interfacesEspeciais.push({ label: "LCD", value: serie.lcd });
if (serie.camera) interfacesEspeciais.push({ label: "Câmera", value: serie.camera });
if (serie.mipi_csi) interfacesEspeciais.push({ label: "MIPI CSI", value: serie.mipi_csi });
if (serie.mipi_dsi) interfacesEspeciais.push({ label: "MIPI DSI", value: serie.mipi_dsi });
if (serie.sdio) interfacesEspeciais.push({ label: "SDIO", value: serie.sdio });
if (serie.sensor_hall) interfacesEspeciais.push({ label: "Sensor Hall", value: serie.sensor_hall });
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: "Sensor Temperatura", value: serie.sensor_temperatura });
// Consumo de Energia
const consumoEnergia = [];
@@ -168,216 +142,119 @@ export default async function SerieDetail({ params }) {
consumoEnergia.push({
label: modoLabels[modo] || modo.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
value: consumo,
icon: <Battery className="w-5 h-5 text-teal-500" />
value: consumo
});
});
}
const tabs = [
{
id: 'devboard',
label: 'Placa de Desenvolvimento',
id: "devboard",
label: "Placa de desenvolvimento",
available: serie.placa,
content: (
<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">
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-100 mb-6 pb-3 border-b border-slate-300 dark:border-slate-800/65 flex items-center gap-3">
<Sliders className="w-5 h-5" style={{ color: serie.cor }} />
<span>Kit de Desenvolvimento Oficial</span>
</h2>
<div className="flex justify-center">
<div className="relative w-full max-w-3xl bg-white dark:bg-white overflow-hidden rounded-2xl border border-slate-300 dark:border-slate-850/50 shadow-lg p-4">
<div className="card p-5 sm:p-7">
<h2 className="text-lg font-semibold text-ink">Kit de desenvolvimento oficial</h2>
<div className="mt-5 overflow-hidden rounded-xl border border-line bg-surface-2 p-4">
<Image
src={serie.placa}
alt={`${key} DevKit - Placa de Desenvolvimento`}
alt={`Placa de desenvolvimento ${key}`}
width={1200}
height={800}
className="rounded-xl object-contain w-full h-auto max-h-[450px]"
className="mx-auto h-auto max-h-[28rem] w-full rounded-lg object-contain"
priority
/>
<p className="text-center text-xs text-slate-500 dark:text-slate-400 mt-4 select-none">
Placa de desenvolvimento padrão da Espressif Systems para a série {key}
</p>
</div>
<p className="mt-3 text-sm text-muted">Placa padrão da Espressif para a série {key}.</p>
</div>
</div>
)
),
},
{
id: 'connections',
label: 'Diagrama de Conexões',
id: "connections",
label: "Diagrama de conexões",
available: conexoes.length > 0,
content: (
<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">
<div className="bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 p-5 mb-8 rounded-2xl">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<div className="card p-5 sm:p-7">
<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>
<h4 className="font-bold text-sm text-amber-850 dark:text-amber-300 mb-1 select-none">Diagrama do SoC (System on Chip)</h4>
<p className="text-xs text-amber-800 dark:text-amber-400 leading-relaxed">
Este mapa refere-se exclusivamente às portas e conexões do silício bruto do microcontrolador <strong>{key}</strong>. As placas de desenvolvimento podem expor apenas uma parcela destas conexões, ou roteá-las de formas diferentes dependendo do circuito impresso.
<p className="font-semibold text-ink">Este é o diagrama do chip, não da placa</p>
<p className="mt-1">
O mapa mostra as conexões do silício do <strong>{key}</strong>. Cada placa de desenvolvimento
expõe só parte delas, ou as roteia de outra forma.
</p>
</div>
</div>
<div className="mt-6">
<ConnectionsDiagram connections={conexoes} serie={serie} />
</div>
<ConnectionsDiagram connections={conexoes} serie={serie} />
<div className="bg-slate-100/50 dark:bg-slate-950/40 rounded-2xl p-6 border border-slate-300 dark:border-slate-800/80 mt-8">
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-100 mb-3 flex items-center gap-2 select-none">
<Lightbulb className="w-4 h-4 text-purple-500" />
<span>Flexibilidade das Portas GPIO</span>
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
O {key} integra uma matriz IO multiplexada e de baixa latência (IO MUX) que permite reconfigurar a grande maioria dos pinos digitais para assumir funções de SPI, I2C, UART, LEDC PWM ou barramentos de câmera e display.
Para mais detalhes sobre as restrições físicas de GPIOs strapping ou analógicas, consulte o&nbsp;
<div className="callout mt-8">
<p className="flex items-center gap-2 font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
Quase todo pino é reconfigurável
</p>
<p className="mt-1">
A matriz de IO do {key} permite mover SPI, I2C, UART, PWM e barramentos de câmera ou display
para a maioria dos pinos digitais. Para as restrições de strapping e dos pinos analógicos,
consulte o{" "}
<a
href={`https://docs.espressif.com/projects/esp-idf/en/latest/${String(key).toLowerCase().replace("-", "")}/api-reference/peripherals/gpio.html`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 dark:text-purple-400 hover:underline inline-flex items-center gap-0.5"
className="inline-flex items-center gap-1 font-medium text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
<span>Guia Oficial de GPIO</span>
<ExternalLink className="w-2.5 h-2.5" />
</a>.
guia oficial de GPIO
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
.
</p>
</div>
</div>
)
}
),
},
];
const secoes = [
{ id: "processador", titulo: "Processador", icon: Cpu, specs: processadorSpecs },
{ id: "conectividade", titulo: "Conectividade", icon: Wifi, specs: conectividadeSpecs },
{ id: "ferramentas", titulo: "Ferramentas e disponibilidade", icon: Package, specs: ferramentasSpecs },
{ id: "seguranca", titulo: "Segurança", icon: ShieldCheck, specs: segurancaSpecs },
{ id: "memoria", titulo: "Memória", icon: Database, specs: memoriaSpecs },
...(consumoEnergia.length > 0
? [{
id: "energia",
titulo: "Consumo de energia",
icon: Battery,
specs: consumoEnergia,
nota: "Valores típicos a 3,3 V e 25 °C. O consumo real depende do ciclo de trabalho do rádio e da frequência de clock configurada.",
}]
: []),
{ id: "perifericos", titulo: "Periféricos", icon: Sliders, specs: perifericos },
...(interfacesEspeciais.length > 0
? [{ id: "interfaces", titulo: "Interfaces especiais", icon: Sliders, specs: interfacesEspeciais }]
: []),
{
id: "temperatura",
titulo: "Limites de temperatura",
icon: Thermometer,
specs: [
{ label: "Operação", value: serie.temperatura_operacao },
{ label: "Armazenamento", value: serie.temperatura_armazenamento },
],
},
];
const fatos = [
{ label: "Processador", value: String(serie.nucleos).includes("2") ? "Dual-core" : "Single-core", detail: serie.frequencia },
{ label: "SRAM", value: serie.memoria_sram, detail: `+ ${serie.memoria_sram_rtc} de SRAM RTC` },
{ label: "GPIO", value: serie.gpio, detail: "pinos programáveis" },
{ label: "Arquitetura", value: String(serie.arquitetura).includes("RISC-V") ? "RISC-V" : "Xtensa", detail: serie.arquitetura },
];
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<Header />
<main className="px-6 pt-16 pb-24 max-w-7xl mx-auto">
{/* Breadcrumb Navigation */}
<div className="mb-8">
<Link
href="/series"
className="inline-flex items-center gap-2 text-xs font-bold text-slate-500 dark:text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors uppercase tracking-wider mb-6"
>
<ArrowLeft className="w-3.5 h-3.5" />
<span>Voltar para Séries</span>
</Link>
{/* Top Info Banner Card */}
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 shadow-2xl border relative overflow-hidden"
style={{ borderColor: `${serie.cor}40` }}
>
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-8">
<div className="flex items-center gap-5">
<span className="text-6xl select-none filter drop-shadow-md">{serie.icone}</span>
<div>
<h1 className="text-4xl md:text-5xl font-display font-extrabold text-slate-850 dark:text-slate-100 tracking-tight leading-none mb-3">
{key}
</h1>
<p className="text-sm font-semibold text-slate-500 dark:text-slate-400 mb-4">{serie.nome_completo}</p>
<span
className="inline-block px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest"
style={{ backgroundColor: `${serie.cor}18`, color: serie.cor, border: `1px solid ${serie.cor}30` }}
>
{serie.arquitetura}
</span>
</div>
</div>
{/* Links Block */}
<div className="grid sm:grid-cols-2 lg:grid-cols-1 gap-3 w-full lg:w-auto">
{serie.datasheet ? (
<a
href={serie.datasheet}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 transition-opacity shadow-lg"
>
<FileText className="w-4 h-4" />
<span>Datasheet Oficial</span>
</a>
) : (
<span className="inline-flex items-center justify-center gap-2 bg-slate-200/60 dark:bg-slate-800/60 text-slate-500 dark:text-slate-400 border border-slate-300/60 dark:border-slate-700/60 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider select-none cursor-not-allowed">
<FileText className="w-4 h-4" />
<span>Datasheet ainda não publicado</span>
</span>
)}
<a
href={serie.guia_de_programacao}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:bg-emerald-500/20 transition-all shadow-xs"
>
<Code className="w-4 h-4" />
<span>Guia de Programação</span>
</a>
{serie.manual_tecnico && (
<a
href={serie.manual_tecnico}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/20 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:bg-purple-500/20 transition-all shadow-xs"
>
<BookOpen className="w-4 h-4" />
<span>Manual Técnico</span>
</a>
)}
</div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-6 leading-relaxed max-w-4xl">
{serie.descricao}
</p>
{serie.status_documentacao && (
<div className="mt-6 max-w-4xl bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-500/20 p-5 rounded-2xl">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<div>
<p className="text-xs font-bold uppercase tracking-wider mb-1">Documentação preliminar</p>
<p className="text-xs leading-relaxed">{serie.status_documentacao}</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Primary Features Highlight Grid */}
<div className="grid md:grid-cols-3 gap-6 mb-8">
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: serie.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5 select-none">PROCESSADOR</h3>
<p className="text-2xl font-extrabold text-slate-800 dark:text-slate-100">{serie.nucleos.includes("2") ? "Dual-Core" : "Single-Core"}</p>
<p className="text-xs text-slate-400 mt-1">{serie.frequencia}</p>
</div>
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: serie.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5 select-none">MEMÓRIA SRAM</h3>
<p className="text-2xl font-extrabold text-slate-800 dark:text-slate-100">{serie.memoria_sram}</p>
<p className="text-xs text-slate-400 mt-1">+ {serie.memoria_sram_rtc} RTC SRAM</p>
</div>
<div
className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg p-6 border-l-4"
style={{ borderColor: serie.cor }}
>
<h3 className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1.5 select-none">GPIO DISPONÍVEIS</h3>
<p className="text-2xl font-extrabold text-slate-800 dark:text-slate-100">{serie.gpio}</p>
<p className="text-xs text-slate-400 mt-1">Pinos I/O programáveis</p>
</div>
</div>
{/* DevBoard and Connections Tabs */}
{/* Ficha do produto e trilha de navegação para o Google. */}
<div className="site-canvas min-h-screen">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
@@ -393,136 +270,133 @@ export default async function SerieDetail({ params }) {
}}
/>
<SeriesTabMenu tabs={tabs} color={serie.cor} />
<Header />
<PinRestrictions conexoes={conexoes} serieKey={key} cor={serie.cor} />
<main id="conteudo">
<PageHero
trail={[{ label: "Séries", href: "/series" }, { label: key }]}
icon={Cpu}
label={serie.arquitetura}
title={key}
lede={serie.descricao}
>
{serie.datasheet ? (
<a href={serie.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<FileText className="h-4 w-4" aria-hidden="true" />
Datasheet oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
) : (
<span className="btn btn-ghost cursor-not-allowed opacity-70" aria-disabled="true">
<FileText className="h-4 w-4" aria-hidden="true" />
Datasheet ainda não publicado
</span>
)}
{serie.guia_de_programacao && (
<a href={serie.guia_de_programacao} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
Guia de programação
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{serie.manual_tecnico && (
<a href={serie.manual_tecnico} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Manual técnico
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
{/* Specs List Cards */}
<div className="space-y-6 mt-12">
<SpecSection title="Processador" specs={processadorSpecs} cor={serie.cor} icon={<Cpu className="w-5 h-5" style={{ color: serie.cor }} />} />
<SpecSection title="Conectividade RF" specs={conectividadeSpecs} cor={serie.cor} icon={<Wifi className="w-5 h-5" style={{ color: serie.cor }} />} />
<SpecSection title="Disponibilidade e Ferramentas" specs={ferramentasSpecs} cor={serie.cor} icon={<Package className="w-5 h-5" style={{ color: serie.cor }} />} />
<SpecSection title="Segurança e Criptografia" specs={segurancaSpecs} cor={serie.cor} icon={<ShieldCheck className="w-5 h-5" style={{ color: serie.cor }} />} />
<SpecSection title="Estrutura de Memória" specs={memoriaSpecs} cor={serie.cor} icon={<Database className="w-5 h-5" style={{ color: serie.cor }} />} />
<section className="shell mt-10" aria-label="Resumo">
<dl className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{fatos.map((fato) => (
<div key={fato.label} className="tile">
<dt className="text-xs font-medium text-muted">{fato.label}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{fato.value}</dd>
<dd className="mt-0.5 text-xs text-muted">{fato.detail}</dd>
</div>
))}
</dl>
{/* Power Consumption section */}
{consumoEnergia.length > 0 && (
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl">
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-100 mb-6 pb-3 border-b border-slate-300 dark:border-slate-800/65 flex items-center gap-3 select-none">
<Battery className="w-5 h-5" style={{ color: serie.cor }} />
<span>Consumo de Energia</span>
</h2>
<div className="bg-blue-500/10 border border-blue-500/20 p-4 mb-6 rounded-xl">
<div className="flex items-start gap-3 text-blue-650 dark:text-blue-400">
<Lightbulb className="w-5 h-5 shrink-0 mt-0.5" />
<p className="text-xs leading-relaxed">
<strong>Especificações típicas medidas sob 3.3V com 25°C de temperatura ambiente.</strong> O consumo absoluto depende ativamente do ciclo de trabalho (duty cycle) das interfaces Wi-Fi/Bluetooth e da frequência de clock configurada via software.
</p>
{serie.status_documentacao && (
<div className="callout callout-warn mt-4 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">Documentação preliminar</p>
<p className="mt-1">{serie.status_documentacao}</p>
</div>
</div>
)}
</section>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{consumoEnergia.map((spec, index) => (
<div key={index} className="flex items-center gap-3.5 p-3.5 bg-slate-50/50 dark:bg-slate-950/20 rounded-xl hover:bg-purple-500/5 dark:hover:bg-purple-400/5 border border-slate-150/40 dark:border-slate-850/40 transition-colors">
<div className="p-2 bg-white dark:bg-slate-900 rounded-lg shadow-xs border border-slate-300 dark:border-slate-850/30 shrink-0">
{spec.icon}
</div>
<div className="min-w-0">
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">{spec.label}</p>
<p className="text-sm font-extrabold text-slate-800 dark:text-slate-200 mt-0.5">{spec.value}</p>
</div>
<div className="shell mt-10">
<SeriesTabMenu tabs={tabs} />
<PinRestrictions conexoes={conexoes} serieKey={key} />
</div>
<div className="shell mt-12">
<nav
aria-label="Seções da ficha"
className="nav-pill no-scrollbar sticky top-20 z-30 flex gap-1 overflow-x-auto rounded-full p-1.5"
>
{secoes.map((secao) => (
<a key={secao.id} href={`#${secao.id}`} className="nav-link shrink-0">
{secao.titulo}
</a>
))}
</nav>
<div className="mt-8 space-y-5">
{secoes.map((secao) => (
<SpecSection key={secao.id} {...secao} />
))}
</div>
</div>
)}
<SpecSection title="Periféricos Disponíveis" specs={perifericos} cor={serie.cor} icon={<Sliders className="w-5 h-5" style={{ color: serie.cor }} />} />
{interfacesEspeciais.length > 0 && (
<SpecSection title="Interfaces de Hardware Especiais" specs={interfacesEspeciais} cor={serie.cor} icon={<Sparkles className="w-5 h-5" style={{ color: serie.cor }} />} />
)}
<SpecSection
title="Limites Operacionais de Temperatura"
icon={<Thermometer className="w-5 h-5" style={{ color: serie.cor }} />}
specs={[
{ label: "Operação", value: serie.temperatura_operacao, icon: <Thermometer className="w-5 h-5 text-red-500" /> },
{ label: "Armazenamento", value: serie.temperatura_armazenamento, icon: <Folder className="w-5 h-5 text-red-500" /> },
]}
cor={serie.cor}
/>
</div>
{/* DevBoard CTA Panel */}
<div className="my-20 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-slate-950 dark:to-purple-950/20 border border-indigo-200 dark:border-slate-800/80 rounded-3xl p-8 md:p-12 shadow-2xl relative overflow-hidden">
<div className="absolute right-0 bottom-0 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
<div className="flex-1 max-w-2xl text-center md:text-left">
<div className="inline-flex items-center gap-2 bg-indigo-500/10 text-indigo-700 dark:bg-white/10 dark:text-slate-350 px-3 py-1 rounded-full mb-4 text-xs font-semibold select-none">
<Home className="w-3.5 h-3.5" />
<span>Hardware Fisico</span>
</div>
<h3 className="text-2xl md:text-3xl font-display font-extrabold text-slate-900 dark:text-white mb-4">
Pronto para começar a prototipar?
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Explore toda a nossa grade de placas de desenvolvimento de parceiros oficiais para a série {key}. Compare dimensões, conversores USB-serial e pinagens prontas para protoboards.
<section className="shell section">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Pronto para prototipar o {key}?</h2>
<p className="mt-3 text-base text-muted">
Veja as placas de desenvolvimento disponíveis para a série, com USB, sensores integrados e link para a loja oficial.
</p>
</div>
<Link
href="/catalogo"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all duration-300 shrink-0 shadow-md active:scale-95"
>
<span>Ver Placas Disponíveis</span>
<ArrowRight className="w-4 h-4" />
<Link href="/catalogo" className="btn btn-primary shrink-0">
Ver placas
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
{/* Previous / Next Nav bars */}
<div className="grid sm:flex sm:justify-between items-center gap-4 mt-12 px-2 select-none">
<nav aria-label="Outras séries" className="mt-6 grid gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
{previousKey ? (
<Link
href={`/series/${previousKey}`}
className="flex items-center gap-3 bg-white dark:bg-slate-900/40 backdrop-blur-xl px-5 py-3.5 rounded-2xl shadow-lg border border-slate-300 dark:border-slate-800/80 hover:border-purple-500/55 dark:hover:border-purple-400/55 transition-all group shrink-0"
>
<ArrowLeft className="w-4 h-4 text-slate-400 group-hover:-translate-x-1.5 transition-transform" />
<div className="text-left">
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">Anterior</p>
<p className="text-sm font-extrabold text-slate-800 dark:text-slate-200">{previousKey}</p>
</div>
<Link href={`/series/${previousKey}`} className="card card-interactive group flex items-center gap-3 px-5 py-4">
<ArrowLeft className="h-4 w-4 text-muted transition-transform group-hover:-translate-x-1" aria-hidden="true" />
<span>
<span className="block text-xs text-muted">Série anterior</span>
<span className="block font-display text-lg font-semibold text-ink">{previousKey}</span>
</span>
</Link>
) : (
<div className="hidden sm:block w-36" />
<span className="hidden sm:block" />
)}
<Link
href="/comparacao"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 transition-opacity shadow-md"
>
<Scale className="w-4 h-4" />
<span>Comparar Séries</span>
<Link href="/comparacao" className="btn btn-ghost justify-self-center">
<Scale className="h-4 w-4" aria-hidden="true" />
Comparar séries
</Link>
{nextKey ? (
<Link
href={`/series/${nextKey}`}
className="flex items-center gap-3 bg-white dark:bg-slate-900/40 backdrop-blur-xl px-5 py-3.5 rounded-2xl shadow-lg border border-slate-300 dark:border-slate-800/80 hover:border-purple-500/55 dark:hover:border-purple-400/55 transition-all group shrink-0"
>
<div className="text-right">
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">Próximo</p>
<p className="text-sm font-extrabold text-slate-800 dark:text-slate-200">{nextKey}</p>
</div>
<ArrowRight className="w-4 h-4 text-slate-400 group-hover:translate-x-1.5 transition-transform" />
<Link href={`/series/${nextKey}`} className="card card-interactive group flex items-center justify-end gap-3 px-5 py-4 text-right">
<span>
<span className="block text-xs text-muted">Próxima série</span>
<span className="block font-display text-lg font-semibold text-ink">{nextKey}</span>
</span>
<ArrowRight className="h-4 w-4 text-muted transition-transform group-hover:translate-x-1" aria-hidden="true" />
</Link>
) : (
<div className="hidden sm:block w-36" />
<span className="hidden sm:block" />
)}
</div>
</nav>
</section>
</main>
<Footer />
@@ -530,28 +404,29 @@ export default async function SerieDetail({ params }) {
);
}
function SpecSection({ title, specs, cor, icon }) {
function SpecSection({ id, titulo, icon: Icon, specs, nota }) {
return (
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl">
<h2 className="text-xl font-bold text-slate-800 dark:text-slate-100 mb-6 pb-3 border-b border-slate-300 dark:border-slate-800/65 flex items-center gap-3 select-none">
{icon}
<span>{title}</span>
<section id={id} aria-labelledby={`${id}-titulo`} className="card scroll-mt-36 p-5 sm:p-7">
<h2 id={`${id}-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-accent-soft text-accent">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
{titulo}
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{specs.map((spec, index) => (
<div key={index} className="flex items-center gap-3.5 p-3.5 bg-slate-50/50 dark:bg-slate-950/20 rounded-xl hover:bg-purple-500/5 dark:hover:bg-purple-400/5 border border-slate-150/40 dark:border-slate-850/40 transition-colors">
<div className="p-2 bg-white dark:bg-slate-900 rounded-lg shadow-xs border border-slate-300 dark:border-slate-850/30 shrink-0">
{spec.icon}
</div>
<div className="min-w-0">
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">{spec.label}</p>
<p className="text-sm font-extrabold text-slate-800 dark:text-slate-200 mt-0.5">{spec.value}</p>
</div>
</div>
))}
</div>
{nota && <p className="mt-3 text-sm text-muted">{nota}</p>}
<dl className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-3">
{specs.map((spec) => {
const negativo = /^n[ãa]o\b/i.test(String(spec.value));
return (
<div key={spec.label} className="tile">
<dt className="text-xs font-medium text-muted">{spec.label}</dt>
<dd className={`mt-1 text-sm font-semibold leading-snug ${negativo ? "text-muted" : "text-ink"}`}>{spec.value}</dd>
</div>
);
})}
</dl>
</section>
);
}
export async function generateMetadata({ params }) {
+108 -202
View File
@@ -1,245 +1,151 @@
"use client";
import { useMemo, useState } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import seriesData from "@/public/series.json";
import {
Cpu,
Activity,
Database,
Wifi,
Bluetooth,
Battery,
Sparkles,
Home,
AlertTriangle,
ArrowRight,
Scale,
ArrowUpRight,
Compass,
Home,
Scale,
ShieldCheck,
AlertTriangle
Sparkles,
} from "lucide-react";
const FILTROS = [
{ id: "todas", rotulo: "Todas", testa: () => true },
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Sim" },
{ id: "ia", rotulo: "Aceleração de IA", testa: (s) => Boolean(s.aceleradores_ia) },
];
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
function nivelSeguranca(serie) {
const seg = serie.seguranca || {};
if (seg.key_manager === "Sim") return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim")) return "ECDSA em hardware";
if (String(seg.ecc).startsWith("Sim")) return "ECC em hardware";
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
return "Só SHA";
}
const sim = (v) => !String(v).includes("Não");
export default function Series() {
const series = Object.entries(seriesData);
const [filtro, setFiltro] = useState("todas");
const todas = Object.entries(seriesData);
const ativo = FILTROS.find((f) => f.id === filtro);
const series = useMemo(() => todas.filter(([, s]) => ativo.testa(s)), [todas, ativo]);
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="px-6 pt-16 pb-24 max-w-7xl mx-auto">
<main id="conteudo">
<PageHero
trail={[{ label: "Séries" }]}
icon={Compass}
label={`${todas.length} famílias da Espressif`}
title="Séries ESP32"
lede="Cada série foi desenhada para um tipo de projeto, do sensor a bateria ao acelerador de IA. Abra a ficha para ver pinagem, periféricos e segurança."
/>
{/* Top Section */}
<section className="text-center mb-20">
<div className="inline-flex items-center gap-2 bg-purple-500/10 dark:bg-purple-400/5 px-4 py-2 rounded-full mb-6 border border-purple-500/20 shadow-xs select-none">
<Compass className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span className="text-xs font-bold text-purple-600 dark:text-purple-400 uppercase tracking-widest">Explore o Ecossistema ESP32</span>
<section className="shell pt-10 sm:pt-12" aria-label="Lista de séries">
<div role="group" aria-label="Filtrar séries" className="flex flex-wrap gap-2">
{FILTROS.map((f) => (
<button key={f.id} type="button" aria-pressed={filtro === f.id} onClick={() => setFiltro(f.id)} className="chip-btn">
{f.rotulo}
</button>
))}
</div>
<h1 className="text-5xl md:text-7xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 tracking-tight leading-tight">
Séries ESP32
</h1>
<p className="text-base md:text-lg text-slate-500 dark:text-slate-400 max-w-3xl mx-auto leading-relaxed">
Conheça toda a família de microcontroladores da Espressif. Cada série foi projetada com foco em engenharia especializada, desde nós de sensores autônomos de baixo consumo até poderosos aceleradores de inteligência artificial.
<p role="status" aria-live="polite" className="mt-4 text-sm text-muted">
{series.length} {series.length === 1 ? "série" : "séries"}
</p>
</section>
{/* Series Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{series.map(([key, serie]) => {
const isRiscV = serie.arquitetura.includes("RISC-V");
const riscv = serie.arquitetura.includes("RISC-V");
const fatos = [
["Núcleos", serie.nucleos],
["Frequência", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", sim(serie.wifi) ? "Sim" : "Não"],
["Bluetooth", serie.bluetooth === "Não" ? "Não" : "Sim"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean);
return (
<Link
key={key}
href={`/series/${key}`}
className="group block"
>
{/* Glassmorphic card container with dynamic color borders and shadows */}
<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 hover:shadow-2xl transition-all duration-500 h-full flex flex-col justify-between"
style={{
transition: 'border-color 0.3s, box-shadow 0.3s, transform 0.3s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = serie.cor;
e.currentTarget.style.boxShadow = `0 15px 35px -12px ${serie.cor}35`;
e.currentTarget.style.transform = 'translateY(-6px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '';
e.currentTarget.style.boxShadow = '';
e.currentTarget.style.transform = '';
}}
>
<div>
{/* Header block with Icon & Architecture */}
<div className="flex items-center justify-between mb-6">
<span className="text-4xl select-none filter drop-shadow-md transform group-hover:scale-110 transition-transform duration-300">{serie.icone}</span>
<span
className={`px-3 py-1 rounded-full text-[10px] font-bold tracking-widest uppercase select-none`}
style={{ backgroundColor: `${serie.cor}18`, color: serie.cor, border: `1px solid ${serie.cor}30` }}
>
{isRiscV ? "RISC-V" : "Xtensa"}
</span>
<Link key={key} href={`/series/${key}`} className="card card-interactive group flex flex-col p-5 sm:p-6">
<div className="flex items-start justify-between gap-3">
<h2 className="font-display text-2xl font-semibold tracking-[-0.035em] text-ink">{key}</h2>
<span className="chip">{riscv ? "RISC-V" : "Xtensa"}</span>
</div>
<p className="mt-2 line-clamp-2 min-h-[2.9rem] text-sm leading-relaxed text-muted">{serie.descricao}</p>
{/* Title and description */}
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-3 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
{key}
</h2>
<dl className="mt-5 divide-y divide-line border-y border-line text-sm">
{fatos.map(([rotulo, valor]) => (
<div key={rotulo} className="flex items-baseline justify-between gap-4 py-2">
<dt className="text-muted">{rotulo}</dt>
<dd className={`text-right font-medium ${valor === "Não" ? "text-muted" : "text-ink"}`}>{valor}</dd>
</div>
))}
</dl>
<p className="text-xs text-slate-500 dark:text-slate-450 line-clamp-2 leading-relaxed mb-6">
{serie.descricao}
</p>
{/* Detailed specs micro-grid */}
<div className="space-y-3.5 border-t border-slate-200 dark:border-slate-800/60 pt-6">
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Cpu className="w-3.5 h-3.5 text-blue-500/80" />
<span>Núcleos:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">{serie.nucleos}</span>
</div>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Activity className="w-3.5 h-3.5 text-amber-500/80" />
<span>Frequência:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">{serie.frequencia}</span>
</div>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Database className="w-3.5 h-3.5 text-pink-500/80" />
<span>SRAM:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">{serie.memoria_sram}</span>
</div>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Wifi className="w-3.5 h-3.5 text-emerald-500/80" />
<span>Wi-Fi:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">
{String(serie.wifi).includes("Não") ? (
<span className="text-slate-500 dark:text-slate-400">Não</span>
) : (
<span className="text-emerald-500 dark:text-emerald-400 font-extrabold">Sim</span>
)}
</span>
</div>
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Bluetooth className="w-3.5 h-3.5 text-indigo-500/80" />
<span>Bluetooth:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">
{serie.bluetooth === "Não" ? (
<span className="text-slate-500 dark:text-slate-400">Não</span>
) : (
<span className="text-indigo-500 dark:text-indigo-400 font-extrabold">Sim</span>
)}
</span>
</div>
{serie.consumo_energia?.deep_sleep && (
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 font-semibold select-none">
<Battery className="w-3.5 h-3.5 text-green-500/80" />
<span>Deep Sleep:</span>
</div>
<span className="font-bold text-slate-800 dark:text-slate-200">{serie.consumo_energia.deep_sleep}</span>
</div>
)}
</div>
</div>
{/* IA / Cripto / Matter Badges & Action Footer */}
<div className="mt-8">
<div className="flex flex-wrap gap-2 mb-6">
<ul className="mt-4 flex flex-1 flex-wrap content-start gap-1.5">
{serie.aceleradores_ia && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-500/10 text-purple-600 dark:text-purple-400 text-[10px] font-bold uppercase rounded-lg border border-purple-500/20 shadow-xs select-none">
<Sparkles className="w-3 h-3" />
<span>Aceleradores IA</span>
</span>
<li className="chip chip-accent">
<Sparkles className="h-3 w-3" aria-hidden="true" />
Aceleradores de IA
</li>
)}
{serie.status_documentacao && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] font-bold uppercase rounded-lg border border-amber-500/20 shadow-xs select-none">
<AlertTriangle className="w-3 h-3" />
<span>Doc. Preliminar</span>
</span>
)}
{/* Marcador de segurança: mostra o recurso que DIFERENCIA a série,
não o que todas têm. Ordem = do mais raro ao mais comum. */}
{(() => {
const seg = serie.seguranca || {};
const nivel =
seg.key_manager === "Sim" ? "Key Manager"
: String(seg.ecdsa).startsWith("Sim") ? "ECDSA em hardware"
: String(seg.ecc).startsWith("Sim") ? "ECC em hardware"
: String(seg.aes).startsWith("AES") ? "AES + SHA"
: "Só SHA";
return (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-teal-500/10 text-teal-600 dark:text-teal-400 text-[10px] font-bold uppercase rounded-lg border border-teal-500/20 select-none">
<ShieldCheck className="w-3 h-3" />
<span>{nivel}</span>
</span>
);
})()}
<li className="chip">
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
{nivelSeguranca(serie)}
</li>
{serie.matter === "Sim" && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold uppercase rounded-lg border border-emerald-500/20 shadow-xs select-none">
<Home className="w-3 h-3" />
<span>Suporte Matter</span>
</span>
<li className="chip">
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
Matter
</li>
)}
</div>
{serie.status_documentacao && (
<li className="chip">
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
Documentação preliminar
</li>
)}
</ul>
<div className="flex items-center justify-between text-xs font-bold text-slate-500 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors uppercase tracking-wider select-none border-t border-slate-200 dark:border-slate-800/60 pt-4">
<span>Ver Detalhes</span>
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-2 transition-transform duration-300" />
</div>
</div>
</div>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
Abrir ficha
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
);
})}
</div>
</section>
{/* Quick comparison CTA Box */}
<div className="mt-20 bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-slate-950 dark:to-purple-950/20 rounded-3xl p-8 md:p-12 shadow-2xl relative overflow-hidden border border-indigo-200 dark:border-slate-800/80">
<div className="absolute right-0 bottom-0 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex flex-col md:flex-row items-center justify-between gap-8 relative z-10">
<div className="flex-1 max-w-2xl text-center md:text-left">
<div className="inline-flex items-center gap-2 bg-indigo-500/10 text-indigo-700 dark:bg-white/10 dark:text-slate-350 px-3 py-1 rounded-full mb-4 text-xs font-semibold select-none animate-pulse">
<Scale className="w-3.5 h-3.5" />
<span>Decisão Técnica</span>
</div>
<h3 className="text-2xl md:text-3xl font-display font-extrabold text-slate-900 dark:text-white mb-4">
Ficou em dúvida de qual escolher?
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
Use nosso comparador técnico dinâmico para posicionar as especificações de conectividade RF, potência de rádio e suporte a barramentos de periféricos lado a lado.
<section className="shell section">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Ficou em dúvida entre duas séries?</h2>
<p className="mt-3 text-base text-muted">
O comparador coloca até quatro modelos lado a lado: núcleos, rádio, memória, periféricos e segurança.
</p>
</div>
<Link
href="/comparacao"
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-3.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all duration-300 shrink-0 shadow-md active:scale-95"
>
<span>Comparar Séries</span>
<ArrowRight className="w-4 h-4" />
<Link href="/comparacao" className="btn btn-primary shrink-0">
<Scale className="h-4 w-4" aria-hidden="true" />
Comparar séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
</main>
<Footer />
+1
View File
@@ -22,6 +22,7 @@ export default function sitemap() {
{ caminho: "/seletor", prioridade: 0.8, frequencia: "monthly", lastModified: dataRevisao },
{ caminho: "/frameworks", prioridade: 0.8, frequencia: "monthly", lastModified: dataRevisao },
{ caminho: "/componentes", prioridade: 0.8, frequencia: "weekly", lastModified: dataRevisao },
{ caminho: "/livro", prioridade: 0.7, frequencia: "monthly", lastModified: dataRevisao },
{ caminho: "/glossario", prioridade: 0.7, frequencia: "monthly", lastModified: dataRevisao },
{ caminho: "/catalogo", prioridade: 0.6, frequencia: "weekly", lastModified: dataRevisao },
{ caminho: "/sobre", prioridade: 0.4, frequencia: "yearly", lastModified: dataRevisao },
+119 -235
View File
@@ -1,24 +1,19 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import Image from "next/image";
import {
Languages,
GitCompare,
Globe,
Code,
Target,
HelpCircle,
Users,
Accessibility,
ArrowLeft,
Award,
BookOpen,
Compass,
Terminal,
Layers
} from "lucide-react";
import { ArrowUpRight, Code, Globe, GitCompare, Info, Languages } from "lucide-react";
const GITHUB = "https://github.com/lucasrguerra/ESPDocs";
function GithubMark({ className }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" 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" />
</svg>
);
}
export const metadata = paginaMeta({
@@ -30,14 +25,14 @@ export const metadata = paginaMeta({
export default function Sobre() {
const features = [
{ icon: <Languages className="w-6 h-6 text-purple-500" />, title: "Interface em Português", desc: "Organização e apresentação de dados técnicos totalmente em português." },
{ icon: <GitCompare className="w-6 h-6 text-blue-500" />, title: "Comparações Visuais", desc: "Tabelas comparativas e diagramas para facilitar a escolha da série ideal." },
{ icon: <Globe className="w-6 h-6 text-emerald-500" />, title: "Acesso Centralizado", desc: "Links diretos para documentação oficial, repositórios e exemplos." },
{ icon: <Code className="w-6 h-6 text-amber-500" />, title: "Código Aberto", desc: "Projeto open-source com contribuições bem-vindas no GitHub." },
{ icon: Languages, title: "Em português", desc: "Dados técnicos organizados e apresentados em português do Brasil." },
{ icon: GitCompare, title: "Comparações visuais", desc: "Tabelas e diagramas para escolher a série certa sem abrir dez datasheets." },
{ icon: Globe, title: "Acesso centralizado", desc: "Links diretos para a documentação oficial, repositórios e exemplos." },
{ icon: Code, title: "Código aberto", desc: "Contribuições são bem-vindas no GitHub." },
];
return (
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
<div className="site-canvas min-h-screen">
{/* Metadados E-E-A-T de autoria e organização para o Google */}
<script
type="application/ld+json"
@@ -55,231 +50,120 @@ export default function Sobre() {
}}
/>
<a href="#conteudo" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:bg-white focus:px-4 focus:py-2 focus:rounded-md focus:shadow focus:ring-2 focus:ring-purple-500">
Ir para o conteúdo
</a>
<Header />
<main id="conteudo" className="px-6 pt-16 pb-24 max-w-7xl mx-auto">
{/* Top Hero Heading Block */}
<header className="relative overflow-hidden rounded-3xl p-10 md:p-16 bg-gradient-to-br from-blue-600 via-purple-600 to-pink-600 text-white shadow-2xl mb-12 select-none">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10 max-w-4xl mx-auto">
<h1 className="text-4xl md:text-6xl font-display font-extrabold leading-tight mb-6">
Sobre o ESPDocs
</h1>
<p className="text-sm md:text-base opacity-95 max-w-3xl mb-8 leading-relaxed font-semibold">
Uma plataforma não oficial com interface em português que organiza
informações técnicas do ESP32 e facilita o acesso à documentação oficial
da Espressif. Desenvolvida para a comunidade de hardware livre.
</p>
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<Link href="/" className="inline-flex items-center justify-center gap-2 bg-white text-blue-700 px-6 py-3 rounded-xl font-bold shadow-lg hover:shadow-xl active:scale-98 transition-all" aria-label="Voltar para a página inicial">
<ArrowLeft className="w-5 h-5" />
<span>Início</span>
</Link>
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 bg-white/10 backdrop-blur-xs border border-white/30 text-white px-6 py-3 rounded-xl font-bold hover:bg-white/20 active:scale-98 transition-all"
aria-label="Abrir repositório do projeto no GitHub (abre em nova aba)"
<main id="conteudo">
<PageHero
trail={[{ label: "Sobre" }]}
icon={Info}
label="Projeto independente"
title="Sobre o ESPDocs"
lede="Uma plataforma não oficial, em português, que organiza as informações técnicas do ESP32 e leva você à documentação oficial da Espressif. Feita para a comunidade de hardware livre."
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" 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" />
</svg>
<span>GitHub</span>
<a href={GITHUB} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<GithubMark className="h-4 w-4" />
Ver o repositório
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
</div>
</header>
</PageHero>
{/* Two columns details grid */}
<div className="mt-12 grid grid-cols-1 lg:grid-cols-3 gap-8">
<section className="lg:col-span-2 space-y-8">
{/* Missao article */}
<article className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-8 shadow-xl border border-slate-300 dark:border-slate-800/80 hover:border-purple-500/20 dark:hover:border-purple-400/20 transition-all">
<div className="flex items-center gap-3.5 mb-5 select-none">
<div className="w-12 h-12 bg-purple-500/10 border border-purple-500/20 rounded-2xl flex items-center justify-center text-purple-500">
<Target className="w-6 h-6" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">Missão</h2>
</div>
<p className="text-slate-650 dark:text-slate-350 leading-relaxed font-semibold text-sm">
Facilitar o acesso e a compreensão do ecossistema ESP32 através de uma
interface em português que organiza especificações técnicas, compara séries,
apresenta frameworks e centraliza links para a documentação oficial da
Espressif. O objetivo principal é otimizar o tempo no desenvolvimento de firmware e IoT.
<div className="shell section grid grid-cols-1 gap-10 pt-12 sm:pt-16 lg:grid-cols-[1fr_22rem] lg:gap-14">
<div className="min-w-0 max-w-3xl space-y-12">
<section aria-labelledby="missao">
<h2 id="missao" className="section-title">Missão</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
Facilitar o acesso e a compreensão do ecossistema ESP32 com uma interface em português que organiza
especificações, compara séries, apresenta frameworks e centraliza os links da documentação oficial.
O objetivo é economizar o seu tempo no desenvolvimento de firmware e IoT.
</p>
</article>
{/* Por que o ESPDocs article */}
<article className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-8 shadow-xl border border-slate-300 dark:border-slate-800/80">
<div className="flex items-center gap-3.5 mb-5 select-none">
<div className="w-12 h-12 bg-blue-500/10 border border-blue-500/20 rounded-2xl flex items-center justify-center text-blue-500">
<HelpCircle className="w-6 h-6" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">Por que o ESPDocs?</h2>
</div>
<p className="text-slate-650 dark:text-slate-350 leading-relaxed mb-6 font-semibold text-sm">
As informações sobre os microcontroladores ESP32 muitas vezes estão dispersas em datasheets densos,
manuais de referência (TRM), repositórios secundários e documentação técnica, tudo inteiramente em inglês.
O ESPDocs condensa e estrutura esses dados, auxiliando na triagem rápida antes de aprofundar na engenharia oficial.
</p>
<div className="grid sm:grid-cols-2 gap-4">
{features.map((f) => (
<div key={f.title} className="flex gap-4 items-start bg-slate-50/50 dark:bg-slate-950/20 p-5 rounded-2xl border border-slate-150/40 dark:border-slate-850/40 hover:border-purple-500/10 dark:hover:border-purple-400/10 transition-colors">
<div className="shrink-0 p-1 bg-white dark:bg-slate-900 rounded-xl shadow-xs" aria-hidden="true">
{f.icon}
</div>
<div className="flex-1">
<h3 className="font-bold text-slate-800 dark:text-slate-200 mb-1 text-sm">{f.title}</h3>
<p className="text-xs text-slate-500 dark:text-slate-450 leading-relaxed font-semibold">{f.desc}</p>
</div>
</div>
))}
</div>
</article>
{/* Contribuir article */}
<article className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-8 shadow-xl border border-slate-300 dark:border-slate-800/80">
<div className="flex items-center gap-3.5 mb-5 select-none">
<div className="w-12 h-12 bg-emerald-500/10 border border-emerald-500/20 rounded-2xl flex items-center justify-center text-emerald-500">
<Users className="w-6 h-6" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">Como Contribuir</h2>
</div>
<p className="text-slate-650 dark:text-slate-350 leading-relaxed mb-6 font-semibold text-sm">
Correções de tradução, refinamento de tabelas e novos trechos funcionais de exemplos são muito bem-vindos!
A plataforma cresce por meio da comunidade.
</p>
<div className="bg-slate-50/50 dark:bg-slate-950/20 rounded-2xl p-5 border border-slate-150/40 dark:border-slate-850/40">
<ol className="list-decimal list-inside text-slate-700 dark:text-slate-300 space-y-2 text-xs font-semibold">
<li>Faça o fork do repositório no GitHub.</li>
<li>Implemente melhorias e revise referências técnicas oficiais.</li>
<li>Abra o seu Pull Request (PR) detalhando os ajustes efetuados.</li>
</ol>
</div>
</article>
{/* Accessibility note */}
<article className="bg-gradient-to-br from-purple-500/5 to-pink-500/5 dark:from-purple-500/10 dark:to-pink-500/10 rounded-3xl p-8 shadow-lg border border-purple-500/20 dark:border-pink-500/20">
<div className="flex items-center gap-3.5 mb-5 select-none">
<div className="w-12 h-12 bg-pink-500/10 border border-pink-500/20 rounded-2xl flex items-center justify-center text-pink-500">
<Accessibility className="w-6 h-6" />
</div>
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">Acessibilidade</h2>
</div>
<p className="text-slate-650 dark:text-slate-350 leading-relaxed font-semibold text-sm">
O ESPDocs preza pela legibilidade, alto contraste cromático e compatibilidade de navegação por teclado.
Caso encontre alguma barreira técnica de leitura ou navegação, sinta-se encorajado a relatar via Issue.
</p>
</article>
</section>
{/* Sidebar area */}
<aside className="space-y-6">
{/* Author Card */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-8 shadow-xl">
<div className="flex flex-col items-center mb-6 select-none">
<div className="w-20 h-20 bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-2xl flex items-center justify-center mb-4 border border-purple-500/20 shadow-inner">
<Image
src="/logo.png"
alt="Ciência Embarcada Logo"
width={64}
height={64}
className="h-16 w-16 object-contain"
/>
</div>
<h3 className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 text-center">Sobre o Autor</h3>
</div>
<div className="space-y-4 text-xs font-semibold text-slate-650 dark:text-slate-350 text-justify leading-relaxed">
<p>
<strong>Lucas Rayan Guerra</strong>, criador da iniciativa <a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-300 font-bold"
>
Ciência Embarcada
</a>, é Técnico em Desenvolvimento de Sistemas, estudante de Ciência da Computação na UFRPE e estuda Blockchain e Criptografia.
</p>
<p>
Diretor de Software na Semine AgriTech e palestrante em feiras e conferências (como o REC'n'Play), compartilha ativamente material sobre hardware, internet das coisas e cibersegurança.
<section aria-labelledby="porque">
<h2 id="porque" className="section-title">Por que o ESPDocs existe</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
As informações sobre os ESP32 costumam estar espalhadas entre datasheets densos, manuais de referência
(TRM) e repositórios secundários, tudo em inglês. O ESPDocs condensa e estrutura esses dados para a
triagem rápida, antes de você aprofundar na engenharia oficial.
</p>
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
{features.map(({ icon: Icon, title, desc }) => (
<li key={title} className="card flex items-start gap-4 p-5">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h3 className="font-semibold text-ink">{title}</h3>
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
</div>
<div className="bg-purple-500/5 dark:bg-purple-400/5 border border-purple-500/10 dark:border-purple-400/10 rounded-2xl p-4 my-5 text-justify">
<p className="text-[10px] text-slate-650 dark:text-slate-400 italic font-semibold leading-relaxed">
"O Ciência Embarcada nasceu para criar um espaço de curadoria técnica livre, promovendo a popularização científica e do hardware embarcado."
</p>
</div>
<a
href="https://cienciaembarcada.com.br"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-1.5 w-full px-4 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 text-white font-bold text-xs uppercase tracking-wider shadow-md hover:shadow-lg active:scale-98 transition-all"
>
<span>Visitar Ciência Embarcada</span>
</a>
</div>
{/* Repository / Contributions card */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-6 shadow-xl">
<div className="flex items-center justify-between mb-4 select-none">
<h3 className="text-sm font-display font-extrabold text-slate-800 dark:text-slate-200">Contribua</h3>
<span className="text-[9px] bg-emerald-500/10 text-emerald-600 dark:text-emerald-450 px-2.5 py-0.5 rounded-full font-bold uppercase tracking-wider border border-emerald-500/20">Open Source</span>
</div>
<p className="text-xs text-slate-500 dark:text-slate-450 leading-relaxed mb-4 font-semibold">
Ajude a expandir a cobertura do ESPDocs submetendo exemplos de códigos ou aprimoramentos técnicos.
</p>
<a
href="https://github.com/lucasrguerra/ESPDocs"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 w-full px-4 py-3 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-bold text-xs uppercase tracking-wider shadow-md hover:opacity-90 active:scale-98 transition-all"
aria-label="Abrir repositório no GitHub"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" 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" />
</svg>
<span>Ver Repositório</span>
</a>
<div className="mt-4 text-[10px] text-slate-500 dark:text-slate-450 leading-relaxed font-semibold border-t border-slate-200 dark:border-slate-850/45 pt-4">
<strong>Aviso Legal:</strong> O ESPDocs é um agregador e organizador. Manuais técnicos detalhados e repositórios oficiais referenciados são de propriedade exclusiva da Espressif Systems. Para projetos críticos comerciais, consulte sempre a documentação nativa oficial.
</div>
</div>
{/* Summary sidebar card */}
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl p-6 shadow-xl select-none">
<h4 className="text-xs font-display font-extrabold text-slate-800 dark:text-slate-200 mb-3 uppercase tracking-wider">Conteúdo Técnico</h4>
<ul className="text-xs text-slate-600 dark:text-slate-400 space-y-2.5 font-bold">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-500" />
<span>Focado em clareza técnica</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
<span>Exemplos práticos de códigos</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
<span>Links oficiais mapeados</span>
</li>
))}
</ul>
</section>
<section aria-labelledby="contribuir">
<h2 id="contribuir" className="section-title">Como contribuir</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
Correções de tradução, ajustes em tabelas e novos exemplos de código são muito bem-vindos. O projeto
cresce com a comunidade.
</p>
<ol className="mt-6 space-y-3">
{[
"Faça o fork do repositório no GitHub.",
"Implemente as melhorias e confira as referências técnicas oficiais.",
"Abra um Pull Request descrevendo os ajustes feitos.",
].map((passo, i) => (
<li key={passo} className="card flex items-center gap-4 px-5 py-4">
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-ink text-sm font-semibold text-canvas" aria-hidden="true">
{i + 1}
</span>
<span className="text-sm text-ink-soft sm:text-base">{passo}</span>
</li>
))}
</ol>
</section>
<section aria-labelledby="acessibilidade">
<h2 id="acessibilidade" className="section-title">Acessibilidade</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
O ESPDocs prioriza a legibilidade, o contraste alto (nível AAA da WCAG) e a navegação completa por
teclado. Se encontrar alguma barreira de leitura ou navegação, abra uma issue no repositório.
</p>
</section>
</div>
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label="Autor e aviso legal">
<div className="panel p-6 sm:p-7">
<div className="flex items-center gap-4">
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-2xl border border-line bg-white">
<Image src="/logo.png" alt="" aria-hidden="true" width={48} height={48} className="h-12 w-12 object-contain" />
</span>
<div>
<h2 className="font-display text-lg font-semibold text-ink">Lucas Rayan Guerra</h2>
<p className="text-sm text-muted">Criador do Ciência Embarcada</p>
</div>
</div>
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
<p>
Técnico em Desenvolvimento de Sistemas, estudante de Ciência da Computação na UFRPE e pesquisador de
Blockchain e Criptografia.
</p>
<p>
Diretor de Software na Semine AgriTech e palestrante em eventos como o REC&apos;n&apos;Play, compartilha
conteúdo sobre hardware, IoT e cibersegurança.
</p>
</div>
<a href="https://cienciaembarcada.com.br" target="_blank" rel="noopener noreferrer" className="btn btn-ghost mt-6 w-full">
Visitar o Ciência Embarcada
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
<div className="callout callout-warn">
<p className="font-semibold text-ink">Aviso legal</p>
<p className="mt-1">
O ESPDocs é um agregador e organizador. Os manuais e repositórios referenciados pertencem à Espressif
Systems. Em projetos críticos ou comerciais, consulte sempre a documentação oficial.
</p>
</div>
</aside>
</div>
+35 -67
View File
@@ -1,99 +1,67 @@
'use client';
"use client";
import { Award, CheckCircle, HelpCircle, ExternalLink } from 'lucide-react';
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
export default function BoardCard({ board }) {
const mainFields = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
const technicalFields = Object.keys(board).filter(
key => !mainFields.includes(key) && board[key]
);
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
const hasFeature = (value) => {
const yesValues = ['sim', 'opcional'];
return yesValues.includes(value?.toString().toLowerCase().trim());
};
// 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"
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>
<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>
</div>
)}
{/* 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 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);
{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 (
<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" />
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
{opcional ? (
<CircleHelp className="h-3 w-3 text-gold" aria-hidden="true" />
) : (
<CheckCircle className="w-2.5 h-2.5 shrink-0" />
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
)}
<span>{field}</span>
</div>
{campo}
{opcional && <span className="sr-only"> (opcional)</span>}
</li>
);
})}
</div>
</ul>
</div>
)}
</div>
{/* Link de Compra */}
{board.Link && (
<a
href={board.Link}
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-center px-4 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 active:scale-98 transition-all shadow-md mt-auto flex items-center justify-center gap-1.5"
>
<span>Adquirir no AliExpress</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
Ver no AliExpress
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}
</div>
</div>
</article>
);
}
+16
View File
@@ -0,0 +1,16 @@
export default function BookCover({ className = "" }) {
return (
<div
className={`book-cover dark:opacity-100 ${className}`}
role="img"
aria-label="Capa do livro Microcontroladores ESP32: conectividade, eficiência e desempenho, de Lucas Rayan Guerra"
>
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: "url('/livro/capa.jpg')" }}
aria-hidden="true"
/>
<div className="book-spine" aria-hidden="true" />
</div>
);
}
+38 -99
View File
@@ -1,19 +1,18 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { Code, Copy, CheckCircle, ExternalLink, HelpCircle } from 'lucide-react'
import { useEffect, useState } from "react";
import { Check, Copy, ExternalLink } from "lucide-react";
export default function CodeExample({ framework }) {
const examplesUrl = framework?.exemplos;
const color = framework?.cor || '#6B7280';
const [sample, setSample] = useState('');
const [sample, setSample] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
setSample('');
setSample("");
setError(null);
setLoading(false);
@@ -21,12 +20,11 @@ export default function CodeExample({ framework }) {
const exemploPath = framework.exemplo;
if (!exemploPath) {
const msg = `// Exemplos completos: ${examplesUrl || 'Ver repositório'}\n// Abra o link acima para ver exemplos e código completo.\n`;
setSample(msg);
setSample(`// Exemplos completos: ${examplesUrl || "ver repositório"}\n`);
return;
}
const url = exemploPath.startsWith('/') ? exemploPath : `/${exemploPath}`;
const url = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
let cancelled = false;
setLoading(true);
@@ -41,14 +39,14 @@ export default function CodeExample({ framework }) {
if (text && text.trim().length > 0) {
setSample(text);
} else {
setSample(`// Arquivo de exemplo está vazio. Consulte o repositório oficial.\n`);
setError('Arquivo vazio');
setSample("// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n");
setError("arquivo vazio");
}
})
.catch((err) => {
if (cancelled) return;
setSample(`// Não foi possível carregar o exemplo localmente.\n// Consulte o link do GitHub acima para exemplos funcionais.\n`);
setError(err.message || 'Falha ao carregar exemplo');
setSample("// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n");
setError(err.message || "falha ao carregar");
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -59,7 +57,7 @@ export default function CodeExample({ framework }) {
};
}, [framework?.exemplo, framework?.exemplos]);
const lines = (sample || '').split('\n');
const lines = (sample || "").replace(/\n$/, "").split("\n");
async function handleCopy() {
try {
@@ -70,109 +68,50 @@ export default function CodeExample({ framework }) {
}
return (
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl mb-8 animate-fadeIn">
<div className="md:flex items-center justify-between gap-6 mb-6">
<div className="select-none">
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 flex items-center gap-2">
<Code className="w-5 h-5" style={{ color }} />
<span>Demonstração de Código</span>
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 font-semibold">Trecho de inicialização rápida. Copie e personalize em seu projeto.</p>
<div className="card p-5 sm:p-7">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Exemplo de código</h2>
<p className="mt-1 text-sm text-muted">Trecho de partida em {framework?.linguagem}. Ajuste pinos e configuração à sua placa.</p>
</div>
<div className="flex flex-wrap items-center gap-2.5 mt-4 md:mt-0 select-none">
<span
className="inline-flex items-center text-[10px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider"
style={{
backgroundColor: `${color}18`,
color,
border: `1px solid ${color}25`
}}
>
{framework?.linguagem || 'Código'}
</span>
<button
onClick={handleCopy}
className="inline-flex items-center gap-1.5 bg-slate-50 dark:bg-slate-850 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-250 px-3.5 py-2 rounded-xl font-bold text-xs shadow-xs border border-slate-300 dark:border-slate-800/80 cursor-pointer transition active:scale-95"
title="Copiar código para a área de transferência"
>
{copied ? (
<>
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-600 dark:text-emerald-450">Copiado!</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copiar</span>
</>
)}
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={handleCopy} className="btn btn-ghost btn-sm">
{copied ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
<span role="status">{copied ? "Copiado" : "Copiar"}</span>
</button>
{examplesUrl && (
<a
href={examplesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white shadow-lg active:scale-95 transition hover:opacity-95"
style={{
background: `linear-gradient(90deg, ${color} 0%, rgba(147,51,234,1) 100%)`,
boxShadow: `0 8px 20px -8px ${color}`
}}
title="Abrir exemplos no GitHub"
>
<span>Mais Exemplos</span>
<ExternalLink className="w-3.5 h-3.5" />
<a href={examplesUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
Mais exemplos
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
)}
</div>
</div>
<div className="rounded-2xl overflow-hidden border border-slate-300 dark:border-slate-800/80 shadow-md">
<div className="select-none">
<div className="flex items-center justify-between bg-slate-100/80 dark:bg-slate-950/70 border-b border-slate-300 dark:border-slate-850/60 px-4 py-3.5">
<div className="text-xs font-bold text-slate-750 dark:text-slate-250 flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
<span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
<span className="ml-2 font-mono text-[10px] text-slate-500 dark:text-slate-400">{framework?.nome} example</span>
<div 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="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>
<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>
))}
</code>
</pre>
</div>
</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 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 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>
</div>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
export default function CodeSample({ label, language, code, note }) {
const lines = code.replace(/\n$/, "").split("\n");
return (
<figure className="min-w-0">
<figcaption className="mb-2.5 flex flex-wrap items-center justify-between gap-2 px-1">
<span className="text-xs font-semibold text-ink">{label}</span>
<span className="font-mono text-[0.6875rem] text-muted">{language}</span>
</figcaption>
<div className="code-panel rounded-2xl shadow-[0_18px_40px_-24px_rgba(0,0,0,0.5)]">
<pre>
<code>
{lines.map((line, index) => (
<span key={`${index}-${line}`} className="flex">
<span className="mr-4 w-6 shrink-0 select-none text-right text-[#7d8794]">{index + 1}</span>
<span className="min-w-0 whitespace-pre text-[#e9eef4]">{line || " "}</span>
</span>
))}
</code>
</pre>
</div>
{note && <p className="mt-2.5 px-1 text-xs leading-relaxed text-muted">{note}</p>}
</figure>
);
}
+205 -525
View File
@@ -1,22 +1,14 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import {
Search,
X,
Grid,
List,
Filter,
AlertTriangle,
Info,
Tag,
Zap,
Sliders,
CheckCircle,
ExternalLink
} from "lucide-react";
import { AlertTriangle, CheckCircle, List, Grid, Search, Sliders, X, Zap } from "lucide-react";
/*
* As cores abaixo são dado de visualização: identificam o tipo de função do
* pino no diagrama. O texto nunca fica sobre elas; elas aparecem só como borda
* ou ponto, e o rótulo usa as cores do tema (contraste AAA).
*/
const FILTER_CATEGORIES = [
{ id: "All", name: "Todos", color: "#8b5cf6" }, // purple
{ id: "GND", name: "Terra (GND)", color: "#64748b" }, // slate
@@ -52,11 +44,11 @@ const type_colors = {
export default function ConnectionsDiagram({ connections, serie }) {
const [activeFilter, setActiveFilter] = useState("All");
const [selectedConnection, setSelectedConnection] = useState(null);
const [hoveredConnection, setHoveredConnection] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState("grid");
const [showTooltip, setShowTooltip] = useState(null);
const [mounted, setMounted] = useState(false);
const dialogRef = useRef(null);
const gatilhoRef = useRef(null);
useEffect(() => {
setMounted(true);
@@ -64,7 +56,8 @@ export default function ConnectionsDiagram({ connections, serie }) {
const isConnectionVisible = (connection) => {
const matchesFilter = activeFilter === "All" || connection.categorias.includes(activeFilter);
const matchesSearch = searchQuery === "" ||
const matchesSearch =
searchQuery === "" ||
connection.nome.toLowerCase().includes(searchQuery.toLowerCase()) ||
connection.numero.toString().toLowerCase().includes(searchQuery.toLowerCase());
return matchesFilter && matchesSearch;
@@ -75,418 +68,211 @@ 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]);
useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape") {
const abrir = (connection, evento) => {
gatilhoRef.current = evento.currentTarget;
setSelectedConnection(connection);
};
const fechar = () => {
setSelectedConnection(null);
setShowTooltip(null);
}
gatilhoRef.current?.focus?.();
};
if (selectedConnection) {
document.addEventListener("keydown", handleEscape);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "unset";
};
}, [selectedConnection]);
// Diálogo: Esc fecha, o foco entra nele, fica preso enquanto aberto e volta ao pino de origem.
useEffect(() => {
const handleKeyDown = (e) => {
if (selectedConnection) return;
if (!selectedConnection) return;
const dialogo = dialogRef.current;
const focaveis = () => dialogo?.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])') ?? [];
focaveis()[0]?.focus();
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();
const aoTeclar = (e) => {
if (e.key === "Escape") {
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();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
document.addEventListener("keydown", aoTeclar);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", aoTeclar);
document.body.style.overflow = "";
};
}, [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>
);
}
return (
<div className="space-y-8 pb-8">
const numeroCurto = (c) => (typeof c.numero === "number" ? c.numero : String(c.numero).substring(0, 4));
{/* 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.
return (
<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" />
<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
type="text"
placeholder="Buscar por nome ou número da conexão (Ex: GPIO1, TXD, XTAL)..."
id="busca-pino"
type="search"
placeholder="Buscar pino (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"
autoComplete="off"
className="field pl-11 pr-4 text-sm"
/>
{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>
<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
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 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>
<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>
{/* 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 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}`;
<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 (
<div key={index} className="relative">
<li key={`${connection.numero}-${connection.nome}`} 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,
}}
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"
>
{/* 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}` }}
<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 }}
>
{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">
{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="w-1.5 h-1.5 rounded-full shrink-0"
style={{ backgroundColor: getCategoryColor(cat) }}
title={cat === "Strapping" ? "Inicialização" : cat}
/>
<span key={cat} className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: getCategoryColor(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}
</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}
{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>
))}
</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>
</li>
);
})}
</div>
</div>
</ul>
) : (
/* 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">
<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="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 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-slate-100 dark:divide-slate-850/30">
{visibleConnections.map((connection, index) => {
<tbody className="divide-y divide-line">
{visibleConnections.map((connection) => {
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"
<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 }}
>
<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>
{numeroCurto(connection)}
</button>
</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 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="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">
<td className="whitespace-nowrap px-4 py-2.5">
{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 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 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 className="inline-flex items-center gap-1.5 text-ok">
<CheckCircle className="h-4 w-4" aria-hidden="true" />
Livre
</span>
)}
</td>
@@ -496,220 +282,114 @@ export default function ConnectionsDiagram({ connections, serie }) {
</tbody>
</table>
</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>
{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>
</div>
)}
{/* Interactive Overlay Dialog Details Sheet */}
{selectedConnection && mounted && createPortal(
<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="fixed inset-0 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fadeIn"
onClick={() => setSelectedConnection(null)}
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<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"
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" }}
<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" }}
>
{typeof selectedConnection.numero === "number"
? selectedConnection.numero
: selectedConnection.numero.substring(0, 4)}
</div>
{numeroCurto(selectedConnection)}
</span>
<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">
<h3 id="modal-title" className="break-words font-display text-2xl font-semibold tracking-[-0.03em] text-ink">
{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>
<p className="text-sm text-muted">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"
>
<X className="w-5 h-5" />
<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>
</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>
<li key={cat} className="chip">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} aria-hidden="true" />
{nomeCategoria(cat)}
</li>
))}
</div>
</div>
</ul>
</section>
)}
{/* 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>
<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>
<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 className="tile mt-3 space-y-2">
{selectedConnection.alimentacao.map((source) => (
<li key={source} className="text-sm text-ink-soft">{source}</li>
))}
</ul>
</div>
</div>
</section>
)}
{/* 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>
<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>
</div>
</div>
</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>
<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>
))}
</div>
</div>
</div>
</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>
);
}
+124 -122
View File
@@ -1,161 +1,163 @@
"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]"
<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"
>
<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>
{/* 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"
<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"
>
<span>ESP Component Registry</span>
{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>
<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>
))}
{/* 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.
<br />
<span className="text-xs text-faint">Ciência Embarcada</span>
</p>
<div className="flex items-center gap-3 pt-2">
<div className="mt-4 flex items-center gap-2">
<a
href="https://github.com/lucasrguerra/ESPDocs"
href={SITE.repositorio}
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"
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="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 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="https://linkedin.com/in/lucasrguerra"
href={SITE.autorLinkedin}
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"
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="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 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>
+217 -607
View File
@@ -1,689 +1,301 @@
"use client";
import { usePathname } from "next/navigation";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import {
Home,
Info,
BookOpen,
Cpu,
SlidersHorizontal,
Terminal,
Scale,
ShoppingBag,
AlertTriangle,
Menu,
X,
Sun,
Moon,
ChevronDown,
Sparkles,
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);
function onPointerDown(event) {
if (navRef.current && !navRef.current.contains(event.target)) setOpenMenu(null);
}
if (toolsRef.current && !toolsRef.current.contains(e.target)) {
setToolsDropdown(false);
}
if (docsRef.current && !docsRef.current.contains(e.target)) {
setDocsDropdown(false);
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">
<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="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"
alt=""
aria-hidden="true"
width={30}
height={30}
className="h-[30px] w-[30px]"
/>
<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>
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
</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>
</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
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}
type="button"
className={`nav-link ${active ? "is-active" : ""}`}
aria-expanded={expanded}
aria-controls={`menu-${menu.id}`}
onClick={() => setOpenMenu(expanded ? null : menu.id)}
>
<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.label}
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${expanded ? "rotate-180" : ""}`} 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"
{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)]"
>
<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>
{menu.items.map((item) => {
const Icon = item.icon;
return (
<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"
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}
>
<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 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="text-[10px] text-slate-500 dark:text-slate-400 block truncate">
{chip.desc}
<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>
);
})}
</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 href="/livro" className={`nav-link ${isActivePath(pathname, "/livro") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro") ? "page" : undefined}>
Livro
</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 href="/sobre" className={`nav-link ${isActivePath(pathname, "/sobre") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre") ? "page" : undefined}>
Sobre
</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>
<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>
<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"
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)}
>
<Menu className="w-5 h-5" />
<span className="text-xs font-bold hidden xl:inline">Menu</span>
<Menu className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
{/* Full Screen / Side Drawer Portal */}
{mounted && createPortal(
{mounted &&
createPortal(
<>
{drawerOpen && (
<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"
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"
}`}
>
{/* 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 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>
</div>
<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="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"
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="w-5 h-5" />
<X className="h-5 w-5" aria-hidden="true" />
</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">
<div className="px-4 pb-2">
<Link
href="/seletor"
href="/livro"
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"
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"
>
<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
{/* 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>
</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>
<ArrowRight className="h-4 w-4 shrink-0 text-[#75cdc1] transition-transform duration-300 group-hover:translate-x-1" aria-hidden="true" />
</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) => (
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel">
{MOBILE_LINKS.map((link) => (
<Link
key={chip.name}
href={chip.href}
key={link.href}
href={link.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"
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}
>
<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.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>
))}
</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 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>
</div>
</>
, document.body
</>,
document.body
)}
</header>
);
}
/* ThemeToggle Component */
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
@@ -691,22 +303,20 @@ function ThemeToggle() {
useEffect(() => setMounted(true), []);
if (!mounted) {
return (
<div className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 animate-pulse border border-transparent" />
);
return <span className="h-11 w-11 rounded-full border border-line bg-surface-2" aria-hidden="true" />;
}
const dark = resolvedTheme === "dark";
return (
<button
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-800/80 hover:text-purple-600 dark:hover:text-purple-400 border border-slate-200 dark:border-slate-800 shadow-xs flex items-center justify-center transition-all duration-200 cursor-pointer active:scale-95 shrink-0"
aria-label="Alternar tema claro/escuro"
type="button"
onClick={() => setTheme(dark ? "light" : "dark")}
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
aria-label={dark ? "Ativar tema claro" : "Ativar tema escuro"}
title={dark ? "Tema claro" : "Tema escuro"}
>
{resolvedTheme === "dark" ? (
<Sun className="w-4 h-4 md:w-5 md:h-5 text-yellow-500" />
) : (
<Moon className="w-4 h-4 md:w-5 md:h-5 text-indigo-600" />
)}
{dark ? <Sun className="h-4 w-4" aria-hidden="true" /> : <Moon className="h-4 w-4" aria-hidden="true" />}
</button>
);
}
+47
View File
@@ -0,0 +1,47 @@
import Link from "next/link";
/**
* Abertura padrão das páginas internas: trilha, rótulo, título, frase de apoio
* e ações. Mesma voz da /livro e da home, para que nenhuma página pareça
* de outro produto.
*/
export default function PageHero({ trail = [], icon: Icon, label, title, lede, align = "left", children }) {
const center = align === "center";
return (
<section className="shell pt-8 sm:pt-12">
{trail.length > 0 && (
<nav aria-label="Trilha de navegação" className="flex flex-wrap items-center gap-2 text-sm text-muted">
<Link href="/" className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
Início
</Link>
{trail.map((item, i) => (
<span key={item.label} className="flex min-h-11 items-center gap-2">
<span aria-hidden="true" className="text-line-strong">/</span>
{item.href && i < trail.length - 1 ? (
<Link href={item.href} className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
{item.label}
</Link>
) : (
<span aria-current="page" className="text-ink">
{item.label}
</span>
)}
</span>
))}
</nav>
)}
<div className={`${trail.length ? "mt-8 sm:mt-10" : ""} max-w-3xl ${center ? "mx-auto text-center" : ""}`}>
{label && (
<span className="chip chip-accent">
{Icon && <Icon className="h-3.5 w-3.5" aria-hidden="true" />}
{label}
</span>
)}
<h1 className={`page-title ${label ? "mt-5" : ""}`}>{title}</h1>
{lede && <p className={`lede mt-5 max-w-2xl ${center ? "mx-auto" : ""}`}>{lede}</p>}
{children && <div className={`mt-7 flex flex-wrap gap-3 ${center ? "justify-center" : ""}`}>{children}</div>}
</div>
</section>
);
}
+32 -47
View File
@@ -23,7 +23,7 @@ const GRUPOS = [
id: "entrada",
titulo: "Somente entrada",
icone: ArrowDownToLine,
cor: "text-sky-500",
cor: "text-accent",
explicacao:
"Aceitam apenas leitura. Não podem ser configurados como saída e não possuem resistores de pull-up ou pull-down internos. Se precisar de um, coloque no circuito.",
combina: (avisos) => /apenas de entrada|input only/.test(avisos),
@@ -32,7 +32,7 @@ const GRUPOS = [
id: "flash",
titulo: "Ligados à memória flash/PSRAM",
icone: HardDrive,
cor: "text-rose-500",
cor: "text-copper",
explicacao:
"Já estão conectados à memória de onde o firmware é lido. Usá-los como GPIO comum normalmente trava o chip ou corrompe a execução. Em módulos com flash interna eles nem chegam ao encapsulamento.",
combina: (avisos) =>
@@ -43,7 +43,7 @@ const GRUPOS = [
id: "strapping",
titulo: "Strapping (modo de boot)",
icone: Power,
cor: "text-amber-500",
cor: "text-gold",
explicacao:
"O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Dá para usá-los depois do boot, mas o circuito externo não pode forçar o nível errado durante o reset, senão a placa não inicia ou entra em modo de gravação sozinha.",
combina: (avisos, cats) =>
@@ -54,7 +54,7 @@ const GRUPOS = [
id: "debug",
titulo: "Gravação e depuração",
icone: Bug,
cor: "text-violet-500",
cor: "text-accent",
explicacao:
"São a UART de gravação e as linhas de JTAG. Continuam utilizáveis como GPIO, mas ao reaproveitá-los você perde o caminho de gravar e depurar a placa sem desconectar o que estiver ali.",
combina: (avisos, cats) => /debugging/.test(avisos) || cats.includes("JTAG"),
@@ -63,7 +63,7 @@ const GRUPOS = [
id: "usb",
titulo: "USB nativo",
icone: Usb,
cor: "text-teal-500",
cor: "text-accent",
explicacao:
"Saem de fábrica ligados ao USB Serial/JTAG interno. Para usá-los como GPIO é preciso desativar essa função, o que também remove a gravação por USB direto.",
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/.test(avisos),
@@ -72,7 +72,7 @@ const GRUPOS = [
id: "eletrico",
titulo: "Não podem ficar flutuando",
icone: Zap,
cor: "text-orange-500",
cor: "text-gold",
explicacao:
"Precisam de nível definido por hardware: pull-up externo ou circuito RC de atraso. Deixá-los soltos causa reset aleatório ou inicialização instável.",
combina: (avisos) => /flutuando/.test(avisos),
@@ -101,7 +101,7 @@ function derivarGrupos(conexoes) {
}).filter((g) => g.pinos.length > 0);
}
export default function PinRestrictions({ conexoes, serieKey, cor }) {
export default function PinRestrictions({ conexoes, serieKey }) {
if (!conexoes?.length) return null;
const grupos = derivarGrupos(conexoes);
@@ -110,69 +110,54 @@ export default function PinRestrictions({ conexoes, serieKey, cor }) {
const totalPinos = new Set(grupos.flatMap((g) => g.pinos)).size;
return (
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl mb-12">
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-3 pb-3 border-b border-slate-200 dark:border-slate-800/65 flex items-center gap-3">
<AlertTriangle className="w-5 h-5" style={{ color: cor }} />
<span>Pinos com Restrição de Uso</span>
<section id="restricoes" aria-labelledby="restricoes-titulo" className="card mt-5 scroll-mt-28 p-5 sm:p-7">
<h2 id="restricoes-titulo" className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-gold-soft text-gold">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
</span>
Pinos com restrição de uso
</h2>
<p className="text-sm text-slate-650 dark:text-slate-350 leading-relaxed mb-8">
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
Nem todo pino do {serieKey} está livre para uso geral. {totalPinos} deles têm alguma
condição: já estão ocupados por memória, são lidos durante o boot ou só funcionam
como entrada. Vale conferir antes de fechar o esquemático.
</p>
<div className="grid md:grid-cols-2 gap-5">
<div className="mt-6 grid gap-4 md:grid-cols-2">
{grupos.map(({ id, titulo, icone: Icone, cor: corIcone, explicacao, pinos }) => (
<div
key={id}
className="bg-slate-50 dark:bg-slate-950/40 rounded-2xl p-5 border border-slate-200 dark:border-slate-800/60"
>
<div className="flex items-center gap-2.5 mb-3">
<Icone className={`w-4 h-4 shrink-0 ${corIcone}`} />
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-200">{titulo}</h3>
<span className="ml-auto text-[10px] font-bold text-slate-500 dark:text-slate-400 tabular">
<div key={id} className="tile">
<div className="flex items-center gap-2.5">
<Icone className={`h-4 w-4 shrink-0 ${corIcone}`} aria-hidden="true" />
<h3 className="text-sm font-semibold text-ink">{titulo}</h3>
<span className="ml-auto text-xs text-muted tabular-nums">
{pinos.length} {pinos.length === 1 ? "pino" : "pinos"}
</span>
</div>
<div className="flex flex-wrap gap-1.5 mb-3.5">
<ul className="mt-3 flex flex-wrap gap-1.5">
{pinos.map((p) => (
<span
key={p}
className="px-2 py-1 rounded-lg text-[10px] font-bold font-mono"
style={{
backgroundColor: `${cor}18`,
color: cor,
border: `1px solid ${cor}30`,
}}
>
<li key={p} className="rounded-md border border-line bg-surface px-2 py-1 font-mono text-xs text-ink-soft">
{p}
</span>
</li>
))}
</div>
</ul>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
{explicacao}
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{explicacao}</p>
</div>
))}
</div>
<div className="mt-7 pt-6 border-t border-slate-200 dark:border-slate-800/65 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed max-w-xl">
<div className="mt-6 flex flex-col justify-between gap-4 border-t border-line pt-5 sm:flex-row sm:items-center">
<p className="max-w-xl text-sm leading-relaxed text-muted">
Estes grupos vêm dos mesmos dados do diagrama de conexões. Lá você vê pino a pino
todas as funções, o domínio de alimentação e os avisos completos de cada um.
todas as funções, o domínio de alimentação e os avisos completos.
</p>
<Link
href="#connections"
className="inline-flex items-center justify-center gap-2 shrink-0 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider text-white transition-opacity hover:opacity-90"
style={{ backgroundColor: cor }}
>
<span>Abrir diagrama de pinos</span>
<ArrowRight className="w-4 h-4" />
<Link href="#connections" className="btn btn-ghost shrink-0">
Abrir diagrama de pinos
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
);
}
+15
View File
@@ -0,0 +1,15 @@
/** Título de seção: rótulo curto + título + frase de apoio, alinhado à esquerda. */
export default function SectionHead({ id, eyebrow, title, lede, children }) {
return (
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-2xl">
{eyebrow && <p className="eyebrow">{eyebrow}</p>}
<h2 id={id} className={`section-title ${eyebrow ? "mt-3" : ""}`}>
{title}
</h2>
{lede && <p className="mt-3 text-base leading-relaxed text-muted">{lede}</p>}
</div>
{children && <div className="shrink-0">{children}</div>}
</div>
);
}
+48 -74
View File
@@ -1,11 +1,11 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { ChevronDown } from 'lucide-react';
import { useEffect, useRef, useState } from "react";
export default function SeriesTabMenu({ tabs, color }) {
const availableTabs = tabs.filter(tab => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'specs');
export default function SeriesTabMenu({ tabs }) {
const availableTabs = tabs.filter((tab) => tab.available);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id);
const refs = useRef({});
// Troca manual limpa o hash. Sem isso, depois de abrir o diagrama pelo link e
// mudar de aba na mão, o hash continuaria em #connections e um novo clique no
@@ -13,7 +13,7 @@ export default function SeriesTabMenu({ tabs, color }) {
const trocarAba = (id) => {
setActiveTab(id);
if (window.location.hash) {
history.replaceState(null, '', window.location.pathname + window.location.search);
history.replaceState(null, "", window.location.pathname + window.location.search);
}
};
@@ -21,92 +21,66 @@ 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 (
<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
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)}
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}` }
: {}
}
onKeyDown={(e) => aoTeclar(e, indice)}
className="chip-btn shrink-0"
>
<span>{tab.label}</span>
{tab.label}
</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">
<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 }}
>
<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" />
</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>
);
+60
View File
@@ -15,6 +15,12 @@ export const SITE = {
autorEmail: "contato@lucasrguerra.dev.br",
repositorio: "https://github.com/lucasrguerra/ESPDocs",
blog: "https://cienciaembarcada.com.br",
livroTitulo: "Microcontroladores ESP32",
livroSubtitulo: "Conectividade, Eficiência e Desempenho",
livroIsbn: "978-65-02-43432-1",
livroCapa: "/livro/capa.jpg",
livroAmazon: "https://a.co/d/0fdCvzlg",
livroUiclap: "https://loja.uiclap.com/titulo/ua213708",
};
/**
@@ -280,6 +286,60 @@ export const jsonLd = {
};
},
/** Livro do autor: dados de obra para mecanismos de busca. */
livro() {
return {
"@context": "https://schema.org",
"@type": "Book",
"@id": `${SITE.url}/livro#livro`,
name: SITE.livroTitulo,
alternateName: `${SITE.livroTitulo}: ${SITE.livroSubtitulo}`,
description:
"Livro em português sobre o hardware e o firmware do ESP32: arquitetura, energia, rádio, periféricos e projeto de produto, em 12 partes e 78 capítulos.",
inLanguage: "pt-BR",
url: `${SITE.url}/livro`,
isbn: SITE.livroIsbn,
image: `${SITE.url}${SITE.livroCapa}`,
author: {
"@type": "Person",
name: SITE.autor,
url: SITE.autorUrl,
},
publisher: {
"@type": "Organization",
name: "Ciência Embarcada",
url: SITE.blog,
},
about: [
{ "@type": "Thing", name: "ESP32" },
{ "@type": "Thing", name: "Sistemas embarcados" },
{ "@type": "Thing", name: "Internet das Coisas" },
],
workExample: [
{
"@type": "Book",
"@id": `${SITE.url}/livro#digital`,
bookFormat: "https://schema.org/EBook",
url: SITE.livroAmazon,
potentialAction: {
"@type": "BuyAction",
target: SITE.livroAmazon,
},
},
{
"@type": "Book",
"@id": `${SITE.url}/livro#fisico`,
bookFormat: "https://schema.org/Paperback",
url: SITE.livroUiclap,
potentialAction: {
"@type": "BuyAction",
target: SITE.livroUiclap,
},
},
],
};
},
/** Página Sobre com metadados E-E-A-T de autoria e reputação. */
paginaSobre() {
return {
Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

+3 -3
View File
@@ -2,9 +2,9 @@
<title>ESPDocs</title>
<defs>
<linearGradient id="gl" x1="6" y1="58" x2="250" y2="6" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3b82f6"/>
<stop offset="0.55" stop-color="#8b5cf6"/>
<stop offset="1" stop-color="#ec4899"/>
<stop offset="0" stop-color="#24645f"/>
<stop offset="0.55" stop-color="#8a3410"/>
<stop offset="1" stop-color="#c4744f"/>
</linearGradient>
</defs>
<g stroke="url(#gl)" stroke-width="3.4" stroke-linecap="round">

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 954 B

+3 -3
View File
@@ -2,9 +2,9 @@
<title>ESPDocs</title>
<defs>
<linearGradient id="g" x1="6" y1="58" x2="58" y2="6" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3b82f6"/>
<stop offset="0.55" stop-color="#8b5cf6"/>
<stop offset="1" stop-color="#ec4899"/>
<stop offset="0" stop-color="#24645f"/>
<stop offset="0.55" stop-color="#8a3410"/>
<stop offset="1" stop-color="#c4744f"/>
</linearGradient>
</defs>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+94 -193
View File
@@ -1,218 +1,119 @@
#!/usr/bin/env node
/**
* Auditor de consistência entre os temas claro e escuro.
* Auditor do sistema de design.
*
* Verifica quatro coisas nos arquivos .jsx do projeto:
* Regra-mãe: nenhuma tela inventa cor, tamanho de texto ou rótulo. Tudo vem
* dos tokens e das classes de app/globals.css, que já trocam sozinhos entre
* claro e escuro. O auditor falha (código 1) quando encontra:
*
* 1. TOM INEXISTENTE: classe usando um valor fora da escala declarada.
* Não gera CSS: o elemento herda a cor do pai.
* 2. PAR INVERTIDO: no tema escuro o texto deveria clarear (número menor)
* e o fundo/borda deveria escurecer (número maior).
* 3. CONTRASTE BAIXO: par abaixo de 4.5:1 (AA para texto).
* 4. SEM TEMA ESCURO: arquivo que pinta cor mas não tem nenhuma variante
* dark:. Foi assim que o glossário passou meses só com
* tema claro sem ninguém notar.
* 5. FAMÍLIA FORA DO PADRÃO: gray/zinc/neutral/stone no lugar de slate. Além
* de destoar, escapa das checagens acima, que olham slate.
* 6. COR FIXA INLINE: style={{ borderColor: 'rgba(...)' }} com valor literal.
* Style inline vence classe, então anula a cor do tema
* (inclusive as variantes dark: e hover:). Cor vinda do
* dado (serie.cor) é permitida, ver DESIGN.md seção 5.
* 1. PALETA CRUA: bg-slate-100, text-purple-600, border-gray-300...
* Use bg-surface-2, text-accent, border-line.
* 2. PAR dark: dark:bg-..., dark:text-... Os tokens já adaptam ao tema,
* então o par é redundante e foi como o tema escuro ficou
* inconsistente no passado. Exceção: painel que é escuro
* nos dois temas pode afinar com dark:...-[#hex] ou white/NN.
* 3. TEXTO MINÚSCULO: text-[9px] a text-[11px]. O menor tamanho é text-xs.
* 4. CAIXA ALTA COM TRACKING: uppercase / tracking-wide(r|st). Os rótulos
* do site são frases curtas em caixa normal.
* 5. EMOJI NA INTERFACE: ícones vêm do lucide-react.
* 6. COR FIXA INLINE: style={{ color: '#fff' }} anula o tema. Cor vinda de
* variável (dado de visualização) é permitida.
* 7. CONTRASTE DOS TOKENS: todo par de texto de globals.css em 7:1 (AAA),
* nos temas claro e escuro.
*
* Uso: npm run auditar-temas
* Sai com código 1 se encontrar problema. Ver DESIGN.md.
*/
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
// Escala slate: oficial do Tailwind + os meios-tons declarados em globals.css
const ESCALA = {
"50": "#f8fafc", "100": "#f1f5f9", "150": "#eaeef4", "200": "#e2e8f0",
"250": "#d6dee8", "300": "#cbd5e1", "350": "#b0bccc", "400": "#94a3b8",
"450": "#7c8ca2", "500": "#64748b", "550": "#56647a", "600": "#475569",
"650": "#3d4b5f", "700": "#334155", "750": "#283548", "800": "#1e293b",
"850": "#162032", "900": "#0f172a", "950": "#020617",
};
const RAIZ = new URL("..", import.meta.url).pathname;
const PASTAS = ["app", "components"];
const FUNDO_CLARO = "#ffffff";
const FUNDO_ESCURO = "#0f172a"; // slate-900, superfície de card no tema escuro
const AA = 4.5;
const FAMILIAS = "slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose";
const REGRAS = [
{ id: "paleta crua", re: new RegExp(`(?<![\\w-])(?:[a-z-]+:)*(?:bg|text|border|ring|divide|from|via|to|fill|stroke|outline|decoration|placeholder|shadow)-(?:${FAMILIAS})-\\d{2,3}(?:/\\d+)?`, "g"), dica: "use um token (surface, ink, muted, line, accent, ok, danger, gold, copper)" },
{ id: "par dark:", re: /(?<![\w-])dark:(?:[a-z-]+:)*(?:bg|text|border|ring|divide|from|via|to|fill|stroke|outline|placeholder)-(?!\[#|white\/)[\w/[\]#.-]+/g, dica: "o token já muda com o tema; remova o par" },
{ id: "texto minúsculo", re: /(?<![\w-])text-\[(?:[0-9]|10|11)(?:\.\d+)?px\]/g, dica: "o menor tamanho permitido é text-xs" },
{ id: "caixa alta/tracking", re: /(?<![\w-])(?:uppercase|tracking-(?:wide|wider|widest))(?![\w-])/g, dica: "rótulo em caixa normal, sem tracking" },
{ id: "cor fixa inline", re: /style=\{\{[^}]*(?:color|background(?:Color)?|borderColor)\s*:\s*["'`](?:#|rgb|hsl)/g, dica: "style inline vence o tema; use classe de token" },
];
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{2300}-\u{23FF}]/u;
// Emoji que aparecem só dentro de comentários ou strings de teste não contam.
const SEM_COMENTARIO = (txt) => txt.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
function luminancia(hexa) {
const limpo = hexa.replace("#", "");
const canal = (c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
const [r, g, b] = [0, 2, 4].map((i) => canal(parseInt(limpo.slice(i, i + 2), 16)));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contraste(a, b) {
const [maior, menor] = [luminancia(a), luminancia(b)].sort((x, y) => y - x);
return (maior + 0.05) / (menor + 0.05);
}
/** Caminha app/ e components/ recursivamente atrás de .jsx. */
function arquivos() {
const achados = [];
const anda = (dir) => {
let entradas;
try {
entradas = readdirSync(dir);
} catch {
return;
}
for (const nome of entradas) {
function arquivos(dir) {
return readdirSync(dir).flatMap((nome) => {
const caminho = join(dir, nome);
if (statSync(caminho).isDirectory()) anda(caminho);
else if (nome.endsWith(".jsx")) achados.push(caminho);
}
};
anda("app");
anda("components");
return achados.sort();
}
const RX_QUALQUER = /\b(?:text|bg|border|divide|ring|from|via|to)-slate-(\d+)\b/g;
const RX_TEXTO = /\btext-slate-(\d+) dark:text-slate-(\d+)\b/g;
const RX_SUPERFICIE = /\b(bg|border)-slate-(\d+)(?:\/\d+)? dark:\1-slate-(\d+)(?:\/\d+)?\b/g;
// texto que acompanha um componente invertido: claro -> escuro (sem /g: usado com .test)
const RX_TEXTO_INVERTIDO = /\btext-white dark:text-slate-(?:[5-9]\d0|950)\b/;
// Blocos style={{ ... }}: só dentro deles uma cor literal é problema.
// Fora disso, `color: "#6366f1"` costuma ser tabela de dados (categorias de pino).
const RX_BLOCO_STYLE = /style=\{\{([\s\S]*?)\}\}/g;
// Cor literal: aspas seguidas de #, rgb(), hsl() ou nome de cor.
const RX_COR_FIXA = /\b(borderColor|backgroundColor|color)\s*:\s*['"](?:#|rgb|hsl|transparent|white|black)/g;
// Qualquer classe que pinte cor, para medir se o arquivo tem tema escuro.
const RX_PINTA = /\b(?:text|bg|border|divide|from|via|to)-(?:slate|gray|zinc|neutral|stone|white)(?:-\d+)?(?:\/\d+)?\b/g;
// Famílias de cinza fora do padrão do projeto (que é slate).
const RX_FAMILIA_ERRADA = /\b(?:text|bg|border|divide|from|via|to)-(gray|zinc|neutral|stone)-\d+/g;
// Variante dark: DE VERDADE, seguida de um utilitário. Procurar só por "dark:"
// daria falso negativo: a palavra aparece em comentário e em texto visível.
const RX_VARIANTE_DARK = /\bdark:(?:hover:|focus:|group-hover:|active:)?[a-z]+-/;
/** Cor literal dentro de style={{}}: vence a classe e mata a variante dark:. */
function coresFixasInline(arq) {
const texto = readFileSync(arq, "utf8");
const achados = [];
for (const bloco of texto.matchAll(RX_BLOCO_STYLE)) {
for (const m of bloco[1].matchAll(RX_COR_FIXA)) {
const posicao = bloco.index + m.index;
const linha = texto.slice(0, posicao).split("\n").length;
achados.push([arq, linha, m[1], "cor literal inline anula a classe de tema"]);
}
}
return achados;
}
function main() {
const inexistentes = new Map();
const invertidos = [];
const fracos = [];
const fixas = [];
const semTemaEscuro = [];
const familiaErrada = new Map();
for (const arq of arquivos()) {
fixas.push(...coresFixasInline(arq));
const bruto = readFileSync(arq, "utf8");
// Um arquivo que pinta muita cor e não tem nenhuma variante dark: quase
// certamente não acompanha o tema.
const pinta = (bruto.match(RX_PINTA) || []).length;
const temDark = RX_VARIANTE_DARK.test(bruto);
if (pinta >= 8 && !temDark) {
semTemaEscuro.push([arq, `${pinta} classes de cor, nenhuma variante dark:`]);
}
for (const m of bruto.matchAll(RX_FAMILIA_ERRADA)) {
const chave = `${arq} ${m[1]}-*`;
familiaErrada.set(chave, (familiaErrada.get(chave) ?? 0) + 1);
}
const linhas = bruto.split("\n");
linhas.forEach((linha, i) => {
const n = i + 1;
for (const m of linha.matchAll(RX_QUALQUER)) {
if (!(m[1] in ESCALA)) {
const chave = `${arq}:${n} ${m[0]}`;
inexistentes.set(chave, (inexistentes.get(chave) ?? 0) + 1);
}
}
for (const m of linha.matchAll(RX_TEXTO)) {
const [, claro, escuro] = m;
if (!(claro in ESCALA) || !(escuro in ESCALA)) continue;
if (Number(escuro) > Number(claro)) {
invertidos.push([arq, n, m[0], "texto escurece no tema escuro"]);
}
const c1 = contraste(ESCALA[claro], FUNDO_CLARO);
const c2 = contraste(ESCALA[escuro], FUNDO_ESCURO);
if (c1 < AA || c2 < AA) {
fracos.push([arq, n, m[0], +c1.toFixed(2), +c2.toFixed(2)]);
}
}
for (const m of linha.matchAll(RX_SUPERFICIE)) {
const [, , claro, escuro] = m;
if (!(claro in ESCALA) || !(escuro in ESCALA)) continue;
if (Number(escuro) > Number(claro)) continue;
// Inversão deliberada (DESIGN.md seção 4): superfície escura nos DOIS
// temas: tooltip, bloco de código, botão escuro secundário.
if (Number(claro) >= 800 && Number(escuro) >= 800) continue;
// Botão que inverte preto<->branco NÃO é exceção: é bug.
// Ver DESIGN.md 4.1: botão carrega a própria cor.
const textoInverte =
RX_TEXTO_INVERTIDO.test(linha) ||
[...linha.matchAll(RX_TEXTO)].some((t) => Number(t[2]) > Number(t[1]));
if (textoInverte) {
invertidos.push([arq, n, m[0],
"botão invertendo preto/branco, use cor própria (DESIGN.md 4.1)"]);
continue;
}
invertidos.push([arq, n, m[0], "fundo/borda não escurece no tema escuro"]);
}
if (statSync(caminho).isDirectory()) return arquivos(caminho);
return /\.(jsx|js)$/.test(nome) ? [caminho] : [];
});
}
const secao = (titulo, itens) => {
console.log(`\n${titulo}: ${itens.length}`);
for (const i of itens) console.log(" ", ...i);
let problemas = 0;
const relatar = (arq, linha, regra, trecho, dica) => {
problemas++;
console.log(` \x1b[31m✖\x1b[0m ${arq.replace(RAIZ, "")}:${linha} [${regra}] ${trecho} → ${dica}`);
};
if (inexistentes.size) {
const total = [...inexistentes.values()].reduce((a, b) => a + b, 0);
console.log(`\nTONS FORA DA ESCALA: ${total}`);
console.log(" (não geram CSS: declare em globals.css ou troque por um tom válido)");
for (const [k, v] of [...inexistentes].sort((a, b) => b[1] - a[1])) {
console.log(` ${v}x ${k}`);
console.log("\nAuditoria do sistema de design\n");
for (const pasta of PASTAS) {
for (const arq of arquivos(join(RAIZ, pasta))) {
if (/(^|\/)api\//.test(arq.replace(RAIZ, ""))) continue;
const bruto = readFileSync(arq, "utf8");
const texto = SEM_COMENTARIO(bruto);
const linhas = texto.split("\n");
linhas.forEach((linha, i) => {
for (const regra of REGRAS) {
for (const m of linha.matchAll(regra.re)) relatar(arq, i + 1, regra.id, m[0], regra.dica);
}
// Emoji só interessa dentro de JSX/strings visíveis, não em dados .js
if (arq.endsWith(".jsx") && EMOJI.test(linha)) relatar(arq, i + 1, "emoji", linha.trim().slice(0, 60), "use um ícone do lucide-react");
});
}
}
secao("ARQUIVOS SEM TEMA ESCURO", semTemaEscuro);
if (familiaErrada.size) {
const total = [...familiaErrada.values()].reduce((a, b) => a + b, 0);
console.log(`\nFAMÍLIA DE CINZA FORA DO PADRÃO: ${total}`);
console.log(" (o projeto usa slate; gray/zinc/neutral/stone destoam e escapam da auditoria)");
for (const [k, v] of [...familiaErrada].sort((a, b) => b[1] - a[1])) {
console.log(` ${v}x ${k}`);
// 7. Contraste dos tokens
const css = readFileSync(join(RAIZ, "app/globals.css"), "utf8");
const bloco = (sel) => {
const m = css.match(new RegExp(`\\n${sel.replace(".", "\\.")} \\{([\\s\\S]*?)\\n\\}`));
const o = {};
for (const [, k, v] of m[1].matchAll(/--([\w-]+):\s*(#[0-9a-fA-F]{6})/g)) o[k] = v;
return o;
};
const lum = (h) => {
const c = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
};
const razao = (a, b) => {
const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
return (x + 0.05) / (y + 0.05);
};
const PARES = [
["ink", "canvas"], ["ink", "surface"], ["ink-soft", "surface"], ["ink-soft", "surface-2"],
["muted", "canvas"], ["muted", "surface"], ["muted", "surface-2"], ["faint", "surface-2"],
["accent", "canvas"], ["accent", "surface"], ["accent", "surface-2"], ["accent", "accent-soft"],
["accent-strong", "surface"], ["accent-contrast", "accent"],
["ok", "surface"], ["ok", "surface-2"], ["ok", "ok-soft"],
["danger", "surface"], ["danger", "surface-2"], ["danger", "danger-soft"],
["gold", "surface"], ["gold", "surface-2"], ["gold", "gold-soft"],
["copper", "surface"], ["copper", "surface-2"], ["copper", "copper-soft"],
];
const claro = bloco(":root");
const temas = { claro, escuro: { ...claro, ...bloco(".dark") } };
for (const [nome, t] of Object.entries(temas)) {
for (const [a, b] of PARES) {
if (!t[a] || !t[b]) continue;
const r = razao(t[a], t[b]);
if (r < 7) {
problemas++;
console.log(` \x1b[31m✖\x1b[0m contraste [${nome}] ${a} sobre ${b} = ${r.toFixed(2)}:1 (mínimo 7:1)`);
}
}
}
secao("PARES INVERTIDOS", invertidos);
secao(`PARES ABAIXO DE AA (${AA}:1)`, fracos);
secao("COR FIXA EM STYLE INLINE", fixas);
const total =
[...inexistentes.values()].reduce((a, b) => a + b, 0) +
[...familiaErrada.values()].reduce((a, b) => a + b, 0) +
invertidos.length + fracos.length + fixas.length + semTemaEscuro.length;
console.log(`\n${total === 0 ? "OK, nenhum problema." : `${total} problema(s).`}`);
console.log("Exceção legítima: painel escuro nos DOIS temas (tooltip, bloco de código).");
console.log("Nesses casos não use variante dark:. Ver DESIGN.md seção 4.");
return total ? 1 : 0;
if (problemas === 0) {
console.log(" \x1b[32m✔\x1b[0m nenhuma violação: tokens, tamanhos, rótulos e contraste AAA em dia.\n");
} else {
console.log(`\n${problemas} violação(ões).\n`);
process.exit(1);
}
process.exitCode = main();