Redesign das páginas internas no novo design system (AAA) (#2)
* feat(design): redesign das páginas internas no novo design system Padroniza todas as páginas e componentes internos nos tokens e primitivos de app/globals.css, com contraste AAA (7:1) nos dois temas e alvos de toque de 44px. - Páginas reescritas: glossário, séries, série, seletor, comparador, diagnóstico, catálogo, component registry, frameworks, framework, sobre e a nova 404. - Componentes: PageHero e SectionHead novos; BoardCard, CodeExample, PinRestrictions, SeriesTabMenu e ConnectionsDiagram refeitos, com abas e diálogo acessíveis. - Remove emojis, cores avulsas por série, caixa alta com tracking, texto menor que 12px e os pares slate-*/dark:*. - Comparador: "mostrar só as diferenças" e leitura de ?chips=. - Corrige overflow em 320/390px e ajusta accent/copper do tema claro para fechar 7:1. - scripts/auditar-temas.mjs passa a travar paleta crua, dark:, texto pequeno, emoji e contraste dos tokens. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc * ci: roda lint, auditorias de design e build em cada PR Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016W1xmizFcmXLrmabSvfSuc --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f27f5858b
commit
bc9ae5c036
34 files changed
+4911
-6874
No files matched your search
@@ -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
|
||||
+147
-214
@@ -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>
|
||||
</div>
|
||||
<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,227 +176,150 @@ 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" />
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filterableColumns = headers.filter(h => !excludedColumns.includes(h));
|
||||
const filtros = (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="busca-placa" className="text-sm font-semibold text-ink">
|
||||
Buscar placa
|
||||
</label>
|
||||
<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
|
||||
id="busca-placa"
|
||||
type="search"
|
||||
placeholder="Nome ou descrição"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="field pl-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterableColumns.map((column) => {
|
||||
const filterData = filterOptions[column];
|
||||
if (!filterData) return null;
|
||||
|
||||
if (filterData.isBinary) {
|
||||
return (
|
||||
<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}
|
||||
<input
|
||||
type="checkbox"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 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="h-4 w-4 shrink-0 cursor-pointer accent-[var(--accent)]"
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
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="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>
|
||||
<main id="conteudo">
|
||||
{hero}
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<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
|
||||
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"
|
||||
type="button"
|
||||
onClick={(e) => { e.preventDefault(); clearFilters(); }}
|
||||
className="text-sm font-medium text-accent hover:text-accent-strong"
|
||||
>
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div className="border-t border-line p-5 lg:max-h-[calc(100vh-11rem)] lg:overflow-y-auto">{filtros}</div>
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
{/* 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>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Digite o nome da placa..."
|
||||
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"
|
||||
/>
|
||||
<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;
|
||||
|
||||
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">
|
||||
{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"
|
||||
/>
|
||||
</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">
|
||||
{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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</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)
|
||||
<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>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredBoards.map((board, index) => (
|
||||
<BoardCard key={index} board={board} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{filteredBoards.length === 0 ? (
|
||||
<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="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredBoards.map((board, index) => (
|
||||
<BoardCard key={index} board={board} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
+148
-240
@@ -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) {
|
||||
setSelectedSeries([...selectedSeries, seriesKey]);
|
||||
}
|
||||
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">
|
||||
|
||||
{/* 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>
|
||||
<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."
|
||||
/>
|
||||
|
||||
{/* 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,160 +187,120 @@ 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">
|
||||
<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">
|
||||
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>
|
||||
<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="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) => (
|
||||
<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 visiveis = soDiferencas ? fields.filter(({ key }) => diferem(key)) : fields;
|
||||
if (soDiferencas && visiveis.length === 0) return null;
|
||||
const isCollapsed = collapsedCategories[category] && !soDiferencas;
|
||||
return (
|
||||
<Fragment key={category}>
|
||||
<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)}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
||||
{!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>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{compareFields.map(({ category, fields }) => {
|
||||
const isCollapsed = collapsedCategories[category];
|
||||
return (
|
||||
<Fragment key={category}>
|
||||
{/* Collapsible Category Accordion Header */}
|
||||
<tr
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</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">
|
||||
{label}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
{/* 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">
|
||||
{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>
|
||||
|
||||
<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}
|
||||
</Link>
|
||||
{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>
|
||||
|
||||
<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 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+249
-474
@@ -3,41 +3,46 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import Link from "next/link";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionHead from "@/components/SectionHead";
|
||||
import {
|
||||
CATEGORIAS_COMPONENTES,
|
||||
COMPONENTES_REGISTRY,
|
||||
GUIA_COMPONENT_MANAGER
|
||||
} from "@/lib/componentRegistryData";
|
||||
import {
|
||||
Layers,
|
||||
Search,
|
||||
Copy,
|
||||
Check,
|
||||
ExternalLink,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
Code2,
|
||||
Plus,
|
||||
Trash2,
|
||||
Download,
|
||||
Cpu,
|
||||
ArrowUpRight,
|
||||
Box,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
HelpCircle,
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
ShieldCheck,
|
||||
Zap,
|
||||
Monitor,
|
||||
Cloud,
|
||||
Brain,
|
||||
Home,
|
||||
Check,
|
||||
CircuitBoard,
|
||||
Wrench
|
||||
Cloud,
|
||||
Copy,
|
||||
Cpu,
|
||||
Download,
|
||||
HelpCircle,
|
||||
Home,
|
||||
Layers,
|
||||
Monitor,
|
||||
Plus,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
const ICONES_CATEGORIA = {
|
||||
ui_display: Monitor,
|
||||
cloud_conectividade: Cloud,
|
||||
audio_voz_ia: Brain,
|
||||
smart_home_mesh: Home,
|
||||
drivers_perifericos: Cpu,
|
||||
bsp_placas: CircuitBoard,
|
||||
sistema_utilitarios: Wrench,
|
||||
};
|
||||
|
||||
export default function ComponentRegistryPage() {
|
||||
const [busca, setBusca] = useState("");
|
||||
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
|
||||
@@ -119,520 +124,290 @@ export default function ComponentRegistryPage() {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Mapeamento de ícones de categoria
|
||||
const renderIconeCategoria = (id) => {
|
||||
switch (id) {
|
||||
case "ui_display":
|
||||
return <Monitor className="w-4 h-4" />;
|
||||
case "cloud_conectividade":
|
||||
return <Cloud className="w-4 h-4" />;
|
||||
case "audio_voz_ia":
|
||||
return <Brain className="w-4 h-4" />;
|
||||
case "smart_home_mesh":
|
||||
return <Home className="w-4 h-4" />;
|
||||
case "drivers_perifericos":
|
||||
return <Cpu className="w-4 h-4" />;
|
||||
case "bsp_placas":
|
||||
return <CircuitBoard className="w-4 h-4" />;
|
||||
case "sistema_utilitarios":
|
||||
return <Wrench className="w-4 h-4" />;
|
||||
default:
|
||||
return <Layers className="w-4 h-4" />;
|
||||
}
|
||||
const limparFiltros = () => {
|
||||
setBusca("");
|
||||
setCategoriaAtiva("todos");
|
||||
setChipFiltro("todos");
|
||||
};
|
||||
|
||||
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/40 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo" className="px-4 md:px-6 pt-12 md:pt-16 pb-24 max-w-7xl mx-auto space-y-12">
|
||||
{/* Top Hero Section */}
|
||||
<section className="text-center select-none relative">
|
||||
<div className="inline-flex items-center gap-2 bg-white/80 dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-6 border border-slate-300 dark:border-slate-800/80 shadow-xs">
|
||||
<Box className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
|
||||
ESP-IDF Component Manager
|
||||
</span>
|
||||
<span className="bg-purple-100 dark:bg-purple-950/80 text-purple-700 dark:text-purple-300 text-[10px] font-extrabold px-2 py-0.5 rounded-full">
|
||||
Oficial
|
||||
</span>
|
||||
</div>
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Component Registry" }]}
|
||||
icon={Box}
|
||||
label="ESP-IDF Component Manager"
|
||||
title="ESP Component Registry"
|
||||
lede="O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
|
||||
>
|
||||
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
Abrir o registro oficial
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
<a href="#manifesto-builder" className="btn btn-ghost">
|
||||
Gerar idf_component.yml
|
||||
</a>
|
||||
</PageHero>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-6 pb-2 pt-1 leading-tight md:leading-[1.18] tracking-tight">
|
||||
ESP Component Registry
|
||||
</h1>
|
||||
|
||||
<p className="text-sm md:text-base text-slate-600 dark:text-slate-350 max-w-3xl mx-auto leading-relaxed font-semibold">
|
||||
O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF.
|
||||
Adicione drivers de displays, reconhecimento de voz, pilhas industriais e bibliotecas
|
||||
com comandos de um clique e geração de manifesto declarativo.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 mt-6">
|
||||
<a
|
||||
href="https://components.espressif.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white text-xs font-bold px-4 py-2.5 rounded-xl shadow-md transition-all active:scale-95"
|
||||
>
|
||||
<span>Acessar Registry Oficial</span>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<a
|
||||
href="#manifesto-builder"
|
||||
className="inline-flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 text-xs font-bold px-4 py-2.5 rounded-xl shadow-xs transition-all active:scale-95"
|
||||
>
|
||||
<Code2 className="w-3.5 h-3.5 text-purple-500" />
|
||||
<span>Gerador de idf_component.yml</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Interactive Search & Filter Bar */}
|
||||
<section className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 border border-slate-300 dark:border-slate-800/80 shadow-xl space-y-5">
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||
{/* Search Input */}
|
||||
<div className="relative w-full md:flex-1">
|
||||
<Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
|
||||
<div className="panel p-5 sm:p-6">
|
||||
<div className="relative">
|
||||
<label htmlFor="busca-componente" className="sr-only">Buscar por nome, namespace ou tag</label>
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||||
<input
|
||||
type="text"
|
||||
id="busca-componente"
|
||||
type="search"
|
||||
value={busca}
|
||||
onChange={(e) => setBusca(e.target.value)}
|
||||
placeholder="Buscar componentes por nome, namespace, funcionalidade ou tags (ex: lvgl, rainmaker, ws2812)..."
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-slate-50 dark:bg-slate-950/70 border border-slate-250 dark:border-slate-800 rounded-2xl text-xs md:text-sm font-semibold text-slate-800 dark:text-slate-100 placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-purple-500 transition-all"
|
||||
placeholder="Buscar por nome, namespace ou tag"
|
||||
autoComplete="off"
|
||||
className="field pl-12 pr-4 text-base"
|
||||
/>
|
||||
{busca && (
|
||||
<button
|
||||
onClick={() => setBusca("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
|
||||
>
|
||||
Limpar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chip Filter */}
|
||||
<div className="flex items-center gap-1.5 w-full md:w-auto overflow-x-auto pb-1 md:pb-0">
|
||||
<span className="text-[11px] font-bold text-slate-500 uppercase tracking-wider shrink-0 mr-1">
|
||||
SoC:
|
||||
</span>
|
||||
{chipsDisponiveis.map((chip) => (
|
||||
<button
|
||||
key={chip}
|
||||
onClick={() => setChipFiltro(chip)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
|
||||
chipFiltro === chip
|
||||
? "bg-purple-600 text-white shadow-xs"
|
||||
: "bg-slate-100 dark:bg-slate-800/80 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
{chip === "todos" ? "Todos SoCs" : chip}
|
||||
</button>
|
||||
))}
|
||||
<div className="mt-5">
|
||||
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{CATEGORIAS_COMPONENTES.map((cat) => {
|
||||
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
|
||||
const contagem = cat.id === "todos" ? COMPONENTES_REGISTRY.length : COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
|
||||
return (
|
||||
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
|
||||
<Icone className="h-4 w-4" aria-hidden="true" />
|
||||
{cat.id === "todos" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
|
||||
<span className="tabular-nums opacity-80">{contagem}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-2 border-t border-slate-200 dark:border-slate-800/60 pt-4">
|
||||
{CATEGORIAS_COMPONENTES.map((cat) => {
|
||||
const ativa = categoriaAtiva === cat.id;
|
||||
const contagem =
|
||||
cat.id === "todos"
|
||||
? COMPONENTES_REGISTRY.length
|
||||
: COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setCategoriaAtiva(cat.id)}
|
||||
className={`inline-flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold shrink-0 transition-all cursor-pointer ${
|
||||
ativa
|
||||
? "bg-purple-50 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/60 shadow-xs"
|
||||
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
{renderIconeCategoria(cat.id)}
|
||||
<span>{cat.nome}</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.2 rounded-full ${
|
||||
ativa
|
||||
? "bg-purple-200 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
|
||||
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{contagem}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="mt-5 border-t border-line pt-5">
|
||||
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{chipsDisponiveis.map((chip) => (
|
||||
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
|
||||
{chip === "todos" ? "Todos" : chip}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Components Grid */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-purple-500" />
|
||||
<span>Componentes Disponíveis ({componentesFiltrados.length})</span>
|
||||
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
|
||||
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? "componente" : "componentes"}
|
||||
</h2>
|
||||
<span className="text-xs text-slate-500 font-semibold">
|
||||
Clique em um comando para copiar para o terminal
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
|
||||
{totalFiltros > 0 && (
|
||||
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
Limpar filtros
|
||||
</button>
|
||||
)}
|
||||
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{componentesFiltrados.length === 0 ? (
|
||||
<div className="text-center py-16 bg-white dark:bg-slate-900/40 rounded-3xl border border-slate-300 dark:border-slate-800 p-8">
|
||||
<Box className="w-12 h-12 text-slate-400 mx-auto mb-3 opacity-60" />
|
||||
<p className="text-base font-bold text-slate-750 dark:text-slate-200">
|
||||
Nenhum componente encontrado para esses filtros.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Tente buscar por termos mais genéricos ou resetar a categoria.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBusca("");
|
||||
setCategoriaAtiva("todos");
|
||||
setChipFiltro("todos");
|
||||
}}
|
||||
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold"
|
||||
>
|
||||
Limpar Todos os Filtros
|
||||
</button>
|
||||
<div className="card mt-6 px-6 py-14 text-center">
|
||||
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum componente encontrado</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
|
||||
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{componentesFiltrados.map((comp) => {
|
||||
const selecionado = componentesSelecionados.includes(comp.id);
|
||||
const cliCmd = `idf.py add-dependency "${comp.namespace}/${comp.name}${comp.versao}"`;
|
||||
const foiCopiado = copiadoId === comp.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className={`bg-white dark:bg-slate-900/45 backdrop-blur-xl border rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-300 flex flex-col justify-between overflow-hidden relative group ${
|
||||
selecionado
|
||||
? "border-purple-500/80 ring-2 ring-purple-500/20"
|
||||
: "border-slate-300 dark:border-slate-800/80 hover:-translate-y-1"
|
||||
}`}
|
||||
>
|
||||
{/* Top Header Card */}
|
||||
<div className="p-6 pb-4 border-b border-slate-150 dark:border-slate-850/40">
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-[10px] font-extrabold text-slate-400 uppercase tracking-wider">
|
||||
{comp.namespace}/
|
||||
</span>
|
||||
<article key={comp.id} className={`card flex flex-col overflow-hidden ${selecionado ? "border-accent ring-2 ring-accent/20" : ""}`}>
|
||||
<div className="flex-1 p-5 sm:p-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
{comp.namespace}/
|
||||
{comp.oficial && (
|
||||
<span className="inline-flex items-center gap-1 bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-300 text-[9px] font-extrabold px-2 py-0.2 rounded-full border border-emerald-300 dark:border-emerald-800/60">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span className="chip chip-accent px-2 py-0">
|
||||
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
|
||||
Oficial
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
|
||||
{comp.name}
|
||||
</h3>
|
||||
<p className="text-xs font-semibold text-purple-600 dark:text-purple-400">
|
||||
{comp.titulo}
|
||||
</p>
|
||||
<h3 className="mt-1 break-words font-display text-xl font-semibold tracking-[-0.03em] text-ink">{comp.name}</h3>
|
||||
<p className="mt-0.5 text-sm font-medium text-accent">{comp.titulo}</p>
|
||||
</div>
|
||||
|
||||
<span className="px-2.5 py-1 rounded-lg text-[10px] font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 shrink-0">
|
||||
{comp.versao}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-md border border-line bg-surface-2 px-2 py-1 font-mono text-xs text-ink-soft">{comp.versao}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-slate-350 leading-relaxed mt-2.5 line-clamp-3">
|
||||
{comp.descricao}
|
||||
</p>
|
||||
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1.5 mt-3.5">
|
||||
<ul className="mt-4 flex flex-wrap gap-1.5">
|
||||
{comp.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-[9px] font-bold bg-slate-100 dark:bg-slate-950/60 text-slate-600 dark:text-slate-400 px-2 py-0.5 rounded-md border border-slate-250 dark:border-slate-800"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
<li key={tag} className="chip">{tag}</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Interactive CLI Action Box */}
|
||||
<div className="p-6 py-4 bg-slate-50/50 dark:bg-slate-950/40 space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<Terminal className="w-3 h-3 text-purple-500" />
|
||||
Comando CLI
|
||||
</span>
|
||||
{foiCopiado && (
|
||||
<span className="text-emerald-500 font-bold flex items-center gap-0.5">
|
||||
<Check className="w-3 h-3" /> Copiado!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copiarComando(comp.id, cliCmd)}
|
||||
className="w-full text-left bg-slate-900 text-slate-100 p-2.5 rounded-xl font-mono text-[11px] hover:bg-slate-800 border border-slate-800 flex items-center justify-between group/cmd transition-colors cursor-pointer"
|
||||
title="Clique para copiar comando"
|
||||
>
|
||||
<span className="truncate pr-2">{cliCmd}</span>
|
||||
<Copy className="w-3.5 h-3.5 text-slate-400 group-hover/cmd:text-white shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Compatible Chips */}
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-1">
|
||||
SoCs Compatíveis:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{comp.chips.map((chip) => (
|
||||
<span
|
||||
key={chip}
|
||||
className="text-[9px] font-semibold text-slate-650 dark:text-slate-350 bg-slate-200/60 dark:bg-slate-800 px-1.5 py-0.2 rounded"
|
||||
>
|
||||
{chip}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="p-4 px-6 border-t border-slate-150 dark:border-slate-850/40 flex items-center justify-between text-xs font-bold">
|
||||
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
|
||||
<button
|
||||
onClick={() => toggleSelecaoManifesto(comp.id)}
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
selecionado
|
||||
? "bg-purple-600 text-white shadow-xs"
|
||||
: "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-800/40 hover:bg-purple-100"
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => copiarComando(comp.id, cliCmd)}
|
||||
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
|
||||
aria-label={`Copiar comando para adicionar ${comp.name}`}
|
||||
>
|
||||
{selecionado ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<span>No Manifesto</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Adicionar ao Manifesto</span>
|
||||
</>
|
||||
)}
|
||||
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
|
||||
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
|
||||
</button>
|
||||
<p role="status" className="sr-only">{foiCopiado ? "Comando copiado" : ""}</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={comp.urlRegistry}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors p-1"
|
||||
title="Ver página oficial no ESP Component Registry"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-muted">
|
||||
<span className="font-medium text-ink-soft">Chips: </span>
|
||||
{comp.chips.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 border-t border-line p-4 sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSelecaoManifesto(comp.id)}
|
||||
aria-pressed={selecionado}
|
||||
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
|
||||
>
|
||||
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
|
||||
{selecionado ? "No manifesto" : "Adicionar ao manifesto"}
|
||||
</button>
|
||||
<a
|
||||
href={comp.urlRegistry}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
|
||||
aria-label={`Abrir ${comp.name} no registro oficial (nova aba)`}
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Interactive Manifest Builder Section */}
|
||||
<section
|
||||
id="manifesto-builder"
|
||||
className="bg-white dark:bg-slate-900/60 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-purple-300 dark:border-purple-800/60 shadow-2xl space-y-6"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pb-4 border-b border-slate-250 dark:border-slate-800">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 text-xs font-bold rounded-full mb-2">
|
||||
<Code2 className="w-3.5 h-3.5" />
|
||||
<span>Gerador de Manifesto Declarativo</span>
|
||||
<section id="manifesto-builder" className="shell scroll-mt-28 pb-16" aria-labelledby="manifesto-titulo">
|
||||
<div className="panel p-6 sm:p-9">
|
||||
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
|
||||
<div className="max-w-2xl">
|
||||
<p className="eyebrow">Gerador de manifesto</p>
|
||||
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted">
|
||||
Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o
|
||||
ESP-IDF baixa todas as bibliotecas na compilação.
|
||||
</p>
|
||||
</div>
|
||||
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
|
||||
Construtor de `idf_component.yml`
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1 font-semibold">
|
||||
Adicione este arquivo na pasta <code>main/</code> do seu projeto ESP-IDF para
|
||||
instalar todas as bibliotecas automaticamente na compilação.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 w-full md:w-auto">
|
||||
<button
|
||||
onClick={copiarManifesto}
|
||||
className="flex-1 md:flex-none inline-flex items-center justify-center gap-1.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-2.5 rounded-xl text-xs font-bold hover:opacity-90 shadow-md transition-all active:scale-95 cursor-pointer"
|
||||
>
|
||||
{manifestoCopiado ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
<span>Manifesto Copiado!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4" />
|
||||
<span>Copiar YAML</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={baixarManifesto}
|
||||
className="inline-flex items-center justify-center gap-1.5 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95 cursor-pointer"
|
||||
title="Baixar arquivo idf_component.yml pronto"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Baixar .yml</span>
|
||||
</button>
|
||||
|
||||
{componentesSelecionados.length > 0 && (
|
||||
<button
|
||||
onClick={() => setComponentesSelecionados([])}
|
||||
className="p-2.5 text-slate-400 hover:text-rose-500 rounded-xl hover:bg-rose-50 dark:hover:bg-rose-950/30 transition-colors"
|
||||
title="Limpar seleção"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
|
||||
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
<span role="status">{manifestoCopiado ? "Copiado" : "Copiar YAML"}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* YAML Output Terminal */}
|
||||
<div className="relative">
|
||||
<div className="bg-slate-950 text-slate-200 rounded-2xl p-4 md:p-6 font-mono text-xs md:text-sm overflow-x-auto border border-slate-800 shadow-inner">
|
||||
<pre>{manifestoYaml}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected components badges */}
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider mr-2">
|
||||
Componentes Inclusos ({componentesSelecionados.length}):
|
||||
</span>
|
||||
{componentesSelecionados.map((id) => {
|
||||
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
|
||||
if (!c) return null;
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center gap-1.5 bg-purple-50 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-800 px-3 py-1 rounded-xl text-xs font-bold"
|
||||
>
|
||||
<span>{c.name}</span>
|
||||
<button
|
||||
onClick={() => toggleSelecaoManifesto(id)}
|
||||
className="hover:text-rose-500 ml-1 cursor-pointer"
|
||||
title="Remover"
|
||||
>
|
||||
×
|
||||
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
|
||||
<Download className="h-4 w-4" aria-hidden="true" />
|
||||
Baixar .yml
|
||||
</button>
|
||||
{componentesSelecionados.length > 0 && (
|
||||
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
|
||||
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||
Limpar
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="code-panel mt-6">
|
||||
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
|
||||
</div>
|
||||
|
||||
{componentesSelecionados.length > 0 && (
|
||||
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
|
||||
{componentesSelecionados.map((id) => {
|
||||
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
|
||||
if (!c) return null;
|
||||
return (
|
||||
<li key={id} className="chip chip-accent gap-1 pr-1">
|
||||
{c.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSelecaoManifesto(id)}
|
||||
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
|
||||
aria-label={`Remover ${c.name} do manifesto`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Educational Guide: O que é o Component Manager */}
|
||||
<section className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800 shadow-xl space-y-8 select-none">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400">
|
||||
<BookOpen className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100">
|
||||
{GUIA_COMPONENT_MANAGER.titulo}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs md:text-sm text-slate-600 dark:text-slate-350 leading-relaxed font-semibold max-w-4xl">
|
||||
{GUIA_COMPONENT_MANAGER.resumo}
|
||||
</p>
|
||||
</div>
|
||||
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
|
||||
<SectionHead id="guia-titulo" eyebrow="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
|
||||
|
||||
{/* Vantagens Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{GUIA_COMPONENT_MANAGER.vantagens.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-slate-50/70 dark:bg-slate-950/40 p-5 rounded-2xl border border-slate-200 dark:border-slate-800/80 space-y-2"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-100 dark:bg-purple-900/40 text-purple-600 dark:text-purple-400 flex items-center justify-center text-xs font-extrabold">
|
||||
0{i + 1}
|
||||
</div>
|
||||
<h3 className="text-sm font-bold text-slate-850 dark:text-slate-100">{v.titulo}</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{v.descricao}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
|
||||
<li key={v.titulo} className="card p-5">
|
||||
<h3 className="font-semibold text-ink">{v.titulo}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted">{v.descricao}</p>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
{/* Comandos Essenciais da CLI */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-purple-500" />
|
||||
<span>Comandos Essenciais no Terminal</span>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-slate-900 text-slate-100 p-4 rounded-2xl border border-slate-800 flex flex-col md:flex-row md:items-center justify-between gap-3 font-mono text-xs"
|
||||
>
|
||||
<div>
|
||||
<span className="text-purple-400 font-bold">$ {cmd.exemplo}</span>
|
||||
<p className="text-[11px] text-slate-400 font-sans mt-1">
|
||||
{cmd.explicacao}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
|
||||
className="self-start md:self-auto inline-flex items-center gap-1 bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-lg text-[11px] font-bold text-slate-200 transition-colors shrink-0"
|
||||
>
|
||||
{copiadoId === `cmd-${i}` ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Copiado</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Copiar</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
|
||||
Comandos essenciais
|
||||
</h3>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
|
||||
<li key={cmd.exemplo} className="code-panel flex flex-col justify-between gap-3 p-4 md:flex-row md:items-center">
|
||||
<div className="min-w-0">
|
||||
<code className="block break-all font-mono text-sm text-[#5eead4]">$ {cmd.exemplo}</code>
|
||||
<p className="mt-1 text-sm text-[#a6afbd]">{cmd.explicacao}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
|
||||
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
|
||||
>
|
||||
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
{copiadoId === `cmd-${i}` ? "Copiado" : "Copiar"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Versioning Semantics Callout */}
|
||||
<div className="bg-purple-50 dark:bg-purple-950/30 rounded-2xl p-5 border border-purple-200 dark:border-purple-800/40 text-xs space-y-2">
|
||||
<h4 className="font-bold text-purple-900 dark:text-purple-300 flex items-center gap-1.5">
|
||||
<HelpCircle className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span>Regras de Versionamento Semântico no ESP-IDF</span>
|
||||
</h4>
|
||||
<ul className="list-disc list-inside space-y-1 text-slate-750 dark:text-slate-350">
|
||||
<li>
|
||||
<code>^1.2.0</code>: Permite atualizações menores e correções (ex: 1.2.1, 1.3.0),
|
||||
mas bloqueia grandes mudanças de API (2.0.0). <strong>(Recomendado)</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>~1.2.0</code>: Permite apenas correções de bugs (ex: 1.2.1, 1.2.5), travando
|
||||
na mesma versão menor.
|
||||
</li>
|
||||
<li>
|
||||
<code>1.2.0</code>: Trava na versão exata indicada, sem receber atualizações.
|
||||
</li>
|
||||
<li>
|
||||
<code>*</code>: Aceita qualquer versão disponível (não recomendado para produção).
|
||||
</li>
|
||||
<div className="callout mt-8">
|
||||
<h3 className="flex items-center gap-2 font-semibold text-ink">
|
||||
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Versionamento semântico no ESP-IDF
|
||||
</h3>
|
||||
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
||||
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
|
||||
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
|
||||
<li><code>1.2.0</code>: trava na versão exata.</li>
|
||||
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+325
-691
File diff suppressed because it is too large.
Load diff
+85
-244
@@ -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"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<span>Voltar para Frameworks</span>
|
||||
</Link>
|
||||
<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}
|
||||
>
|
||||
{framework.documentacao && (
|
||||
<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="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>
|
||||
)}
|
||||
</PageHero>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<p className="text-sm text-slate-600 dark:text-slate-200 mt-6 leading-relaxed max-w-4xl font-semibold">
|
||||
{framework.descricao}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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 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>
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
|
||||
|
||||
+107
-283
@@ -1,312 +1,136 @@
|
||||
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
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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" />
|
||||
<Link href="/componentes" className="btn btn-primary shrink-0">
|
||||
Explorar componentes
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</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
|
||||
<strong className="text-slate-750 dark:text-slate-300">Arduino Core</strong> ou
|
||||
<strong className="text-slate-750 dark:text-slate-300">MicroPython</strong>
|
||||
são ideais para testar sensores e APIs. Para produtos comerciais, adote o
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-IDF</strong> ou
|
||||
<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>
|
||||
<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>
|
||||
<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
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-Matter</strong>, implemente produtos de nuvem rápida com
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-RainMaker</strong> ou redes locais com
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-NOW</strong> e
|
||||
<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
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-ADF</strong>, reconheça fala offline com o
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-SR</strong>, processe vídeo com o
|
||||
<strong className="text-slate-750 dark:text-slate-300">ESP-WHO</strong> e acelere TinyML com o
|
||||
<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 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+729
-42
@@ -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;
|
||||
|
||||
/* Premium dark mode palette accents */
|
||||
--color-brand-purple: #8b5cf6;
|
||||
--color-brand-blue: #3b82f6;
|
||||
--color-brand-pink: #ec4899;
|
||||
--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;
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Meios-tons de slate.
|
||||
--radius-md: 0.625rem;
|
||||
--radius-lg: 0.875rem;
|
||||
--radius-xl: 1.25rem;
|
||||
--radius-2xl: 1.75rem;
|
||||
--radius-3xl: 2.25rem;
|
||||
|
||||
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.
|
||||
/* 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;
|
||||
|
||||
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-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;
|
||||
|
||||
--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;
|
||||
}
|
||||
}
|
||||
+189
-432
@@ -3,31 +3,37 @@
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import Link from "next/link";
|
||||
import { categoriasGlossario } from "@/lib/glossarioData";
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Search,
|
||||
Check,
|
||||
Code2,
|
||||
Cpu,
|
||||
Database,
|
||||
Wifi,
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
Lightbulb,
|
||||
Link2,
|
||||
BatteryCharging,
|
||||
RotateCcw,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Sliders,
|
||||
Sparkles,
|
||||
BatteryCharging,
|
||||
Code2,
|
||||
Link2,
|
||||
Check,
|
||||
ArrowRight,
|
||||
Lightbulb,
|
||||
HelpCircle,
|
||||
Wifi,
|
||||
X,
|
||||
SlidersHorizontal,
|
||||
Filter,
|
||||
RotateCcw,
|
||||
Tag,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
const BUSCAS_POPULARES = ["GPIO", "ULP", "PSRAM", "Strapping Pins", "Deep Sleep", "OTA", "Matter", "Secure Boot", "Wi-Fi 6", "MIPI"];
|
||||
|
||||
const ICONES = { Cpu, Database, Wifi, Sliders, Sparkles, BatteryCharging, Code2, ShieldCheck };
|
||||
|
||||
/** "Processamento, CPU & Silício" vira "Processamento": cabe no filtro sem truncar. */
|
||||
const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
|
||||
|
||||
export default function GlossarioPage() {
|
||||
const [busca, setBusca] = useState("");
|
||||
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
|
||||
@@ -35,67 +41,30 @@ export default function GlossarioPage() {
|
||||
const [copiadoSlug, setCopiadoSlug] = useState(null);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
// Sugestões populares para busca rápida
|
||||
const buscasPopulares = [
|
||||
{ rotulo: "GPIO", emoji: "🔌" },
|
||||
{ rotulo: "ULP", emoji: "⚡" },
|
||||
{ rotulo: "PSRAM", emoji: "💾" },
|
||||
{ rotulo: "Strapping Pins", emoji: "🚀" },
|
||||
{ rotulo: "Deep Sleep", emoji: "🔋" },
|
||||
{ rotulo: "OTA", emoji: "📡" },
|
||||
{ rotulo: "Matter", emoji: "🌐" },
|
||||
{ rotulo: "Secure Boot", emoji: "🔒" },
|
||||
{ rotulo: "Wi-Fi 6", emoji: "📶" },
|
||||
{ rotulo: "MIPI", emoji: "✨" },
|
||||
];
|
||||
const todosOsTermos = useMemo(
|
||||
() =>
|
||||
categoriasGlossario.flatMap((cat) =>
|
||||
cat.termos.map((t) => ({
|
||||
...t,
|
||||
categoriaId: cat.id,
|
||||
categoriaNome: cat.nome,
|
||||
categoriaIcone: cat.icone,
|
||||
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
|
||||
primeiraLetra: t.termo.charAt(0).toUpperCase(),
|
||||
}))
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
// Mapeamento de ícones por nome
|
||||
const iconMap = {
|
||||
Cpu: <Cpu className="w-4 h-4" />,
|
||||
Database: <Database className="w-4 h-4" />,
|
||||
Wifi: <Wifi className="w-4 h-4" />,
|
||||
Sliders: <Sliders className="w-4 h-4" />,
|
||||
Sparkles: <Sparkles className="w-4 h-4" />,
|
||||
BatteryCharging: <BatteryCharging className="w-4 h-4" />,
|
||||
Code2: <Code2 className="w-4 h-4" />,
|
||||
ShieldCheck: <ShieldCheck className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
// Lista de todos os termos com metadados de categoria
|
||||
const todosOsTermos = useMemo(() => {
|
||||
return categoriasGlossario.flatMap((cat) =>
|
||||
cat.termos.map((t) => ({
|
||||
...t,
|
||||
categoriaId: cat.id,
|
||||
categoriaNome: cat.nome,
|
||||
categoriaCor: cat.cor,
|
||||
categoriaIcone: cat.icone,
|
||||
categoriaEmoji: cat.emoji,
|
||||
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
|
||||
primeiraLetra: t.termo.charAt(0).toUpperCase(),
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Todas as letras do alfabeto de A a Z com indicador de existência de termos
|
||||
const alfabeto = useMemo(() => {
|
||||
const letras = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
const setDisponivel = new Set(
|
||||
todosOsTermos.map((t) => t.termo.charAt(0).toUpperCase())
|
||||
);
|
||||
return letras.map((letra) => ({
|
||||
letra,
|
||||
temTermos: setDisponivel.has(letra),
|
||||
}));
|
||||
const disponiveis = new Set(todosOsTermos.map((t) => t.primeiraLetra));
|
||||
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letra) => ({ letra, temTermos: disponiveis.has(letra) }));
|
||||
}, [todosOsTermos]);
|
||||
|
||||
// Filtragem dinâmica
|
||||
const termosFiltrados = useMemo(() => {
|
||||
const termoBusca = busca.toLowerCase().trim();
|
||||
return todosOsTermos.filter((item) => {
|
||||
const matchCategoria =
|
||||
categoriaSelecionada === "todas" ||
|
||||
item.categoriaId === categoriaSelecionada;
|
||||
const matchCategoria = categoriaSelecionada === "todas" || item.categoriaId === categoriaSelecionada;
|
||||
const matchLetra = !letraFiltro || item.primeiraLetra === letraFiltro;
|
||||
const matchBusca =
|
||||
!termoBusca ||
|
||||
@@ -103,15 +72,11 @@ export default function GlossarioPage() {
|
||||
item.definicao.toLowerCase().includes(termoBusca) ||
|
||||
item.exemplo.toLowerCase().includes(termoBusca) ||
|
||||
item.categoriaNome.toLowerCase().includes(termoBusca);
|
||||
|
||||
return matchCategoria && matchLetra && matchBusca;
|
||||
});
|
||||
}, [todosOsTermos, categoriaSelecionada, letraFiltro, busca]);
|
||||
|
||||
const temFiltroAtivo =
|
||||
busca.trim() !== "" ||
|
||||
categoriaSelecionada !== "todas" ||
|
||||
letraFiltro !== null;
|
||||
const temFiltroAtivo = busca.trim() !== "" || categoriaSelecionada !== "todas" || letraFiltro !== null;
|
||||
|
||||
const limparTodosFiltros = () => {
|
||||
setBusca("");
|
||||
@@ -121,365 +86,178 @@ export default function GlossarioPage() {
|
||||
};
|
||||
|
||||
const copiarLinkTermo = (slug) => {
|
||||
const url = `${window.location.origin}/glossario#${slug}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
navigator.clipboard.writeText(`${window.location.origin}/glossario#${slug}`);
|
||||
setCopiadoSlug(slug);
|
||||
setTimeout(() => setCopiadoSlug(null), 2000);
|
||||
};
|
||||
|
||||
const catAtivaInfo = categoriasGlossario.find(
|
||||
(c) => c.id === categoriaSelecionada
|
||||
);
|
||||
const total = termosFiltrados.length;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-slate-100 via-slate-50 to-purple-100/30 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 min-h-screen text-slate-900 dark:text-slate-100 transition-colors duration-300">
|
||||
<div className="site-canvas min-h-screen">
|
||||
<Header />
|
||||
|
||||
<main id="conteudo" className="px-4 sm:px-6 pt-12 pb-24 max-w-7xl mx-auto">
|
||||
{/* Hero Section */}
|
||||
<section className="text-center mb-10 select-none">
|
||||
<div className="inline-flex items-center gap-2 bg-white dark:bg-slate-900/60 backdrop-blur-md px-4 py-2 rounded-full mb-5 border border-slate-250 dark:border-slate-800/80 shadow-xs">
|
||||
<BookOpen className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
|
||||
Enciclopédia & Conceitos do ESP32
|
||||
</span>
|
||||
</div>
|
||||
<main id="conteudo">
|
||||
<PageHero
|
||||
trail={[{ label: "Glossário" }]}
|
||||
icon={BookOpen}
|
||||
label={`${todosOsTermos.length} termos explicados`}
|
||||
title="Glossário técnico do ESP32"
|
||||
lede="Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
|
||||
/>
|
||||
|
||||
<h1 className="text-3xl sm:text-5xl md:text-6xl font-display font-extrabold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 dark:from-blue-400 dark:via-purple-400 dark:to-pink-400 bg-clip-text text-transparent mb-4 leading-tight tracking-tight">
|
||||
Glossário Técnico
|
||||
</h1>
|
||||
|
||||
<p className="text-sm sm:text-base text-slate-600 dark:text-slate-400 max-w-2xl mx-auto leading-relaxed font-normal">
|
||||
Consulte definições diretas, parâmetros de hardware e exemplos práticos dos termos fundamentais do ecossistema ESP32.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 1. PAINEL INTEGRADO DE BUSCA & FILTRAGEM (REDESIGN COMPLETO) */}
|
||||
<section className="mb-10 bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl rounded-3xl border border-slate-200 dark:border-slate-800 p-5 sm:p-7 shadow-sm transition-all">
|
||||
{/* Barra de Pesquisa Principal */}
|
||||
<div className="relative mb-5">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 p-1.5 rounded-xl bg-purple-50 dark:bg-purple-950/50 text-purple-600 dark:text-purple-400 pointer-events-none">
|
||||
<Search className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={busca}
|
||||
onChange={(e) => setBusca(e.target.value)}
|
||||
placeholder="Pesquisar por termo, sigla ou conceito (ex: GPIO, ULP, PSRAM, OTA, Deep Sleep)..."
|
||||
className="w-full bg-slate-50 dark:bg-slate-950/70 pl-13 sm:pl-14 pr-24 sm:pr-28 py-3.5 sm:py-4 rounded-2xl border border-slate-250 dark:border-slate-800 text-sm sm:text-base font-medium placeholder:text-slate-400 dark:placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all shadow-inner"
|
||||
/>
|
||||
|
||||
{/* Lado Direito da Barra de Pesquisa: Botão Limpar ou Tag de Contagem */}
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{busca ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBusca("");
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
className="text-xs px-2.5 py-1 rounded-xl bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-300 dark:hover:bg-slate-700 font-semibold flex items-center gap-1 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<span>Limpar</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="hidden sm:inline-flex text-[11px] font-bold text-slate-500 dark:text-slate-400 bg-slate-200/60 dark:bg-slate-800/80 px-2.5 py-1 rounded-lg border border-slate-250 dark:border-slate-700">
|
||||
{termosFiltrados.length} {termosFiltrados.length === 1 ? "termo" : "termos"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Atalhos de Pesquisa Rápida (Tags Populares) */}
|
||||
<div className="flex items-center gap-2 mb-6 overflow-x-auto pb-1.5 scrollbar-none text-xs">
|
||||
<span className="text-slate-500 dark:text-slate-400 font-bold shrink-0 flex items-center gap-1 text-[11px] uppercase tracking-wider">
|
||||
<Tag className="w-3 h-3" />
|
||||
Populares:
|
||||
</span>
|
||||
{buscasPopulares.map((item) => (
|
||||
<button
|
||||
key={item.rotulo}
|
||||
type="button"
|
||||
onClick={() => setBusca(item.rotulo)}
|
||||
className={`px-2.5 py-1 rounded-lg font-medium transition-all shrink-0 cursor-pointer border ${
|
||||
busca.toLowerCase() === item.rotulo.toLowerCase()
|
||||
? "bg-purple-600 text-white border-purple-600 shadow-xs"
|
||||
: "bg-slate-100/80 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-750 hover:bg-purple-50 dark:hover:bg-purple-950/40 hover:border-purple-300 dark:hover:border-purple-800"
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1">{item.emoji}</span>
|
||||
<span>{item.rotulo}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Seletor de Categorias em Grid/Flex Moderno */}
|
||||
<div className="space-y-2 mb-5">
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Filter className="w-3.5 h-3.5 text-purple-500" />
|
||||
Filtrar por Categoria
|
||||
</span>
|
||||
{categoriaSelecionada !== "todas" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCategoriaSelecionada("todas")}
|
||||
className="text-purple-600 dark:text-purple-400 hover:underline cursor-pointer lowercase first-letter:uppercase text-[11px]"
|
||||
>
|
||||
Ver todas ({todosOsTermos.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-9 gap-2">
|
||||
{/* Botão Todas */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCategoriaSelecionada("todas")}
|
||||
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
|
||||
categoriaSelecionada === "todas"
|
||||
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
|
||||
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">✨</span>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="truncate">Todas</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
|
||||
categoriaSelecionada === "todas"
|
||||
? "bg-white/20 text-white"
|
||||
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{todosOsTermos.length}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Categorias Individuais */}
|
||||
{categoriasGlossario.map((cat) => {
|
||||
const ativa = categoriaSelecionada === cat.id;
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
type="button"
|
||||
onClick={() => setCategoriaSelecionada(cat.id)}
|
||||
className={`p-2.5 rounded-xl text-xs font-bold transition-all text-left flex flex-col justify-between gap-1 cursor-pointer border ${
|
||||
ativa
|
||||
? "bg-purple-600 text-white border-purple-600 shadow-sm ring-2 ring-purple-500/20"
|
||||
: "bg-slate-50 dark:bg-slate-950/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-700"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{cat.emoji}</span>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="truncate">{cat.nome.split(" ")[0]}</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
|
||||
ativa
|
||||
? "bg-white/20 text-white"
|
||||
: "bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400"
|
||||
}`}
|
||||
>
|
||||
{cat.termos.length}
|
||||
</span>
|
||||
</div>
|
||||
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
|
||||
<div className="panel p-5 sm:p-7">
|
||||
<div className="relative">
|
||||
<label htmlFor="busca-glossario" className="sr-only">
|
||||
Buscar termo, sigla ou conceito
|
||||
</label>
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||||
<input
|
||||
id="busca-glossario"
|
||||
ref={searchInputRef}
|
||||
type="search"
|
||||
value={busca}
|
||||
onChange={(e) => setBusca(e.target.value)}
|
||||
placeholder="Buscar termo, sigla ou conceito"
|
||||
autoComplete="off"
|
||||
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
{busca && (
|
||||
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Limpar
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrubber de Índice Alfabético (A-Z) */}
|
||||
<div className="pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider shrink-0">
|
||||
<span className="text-[11px]">Índice A-Z:</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-none flex-1 justify-start sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLetraFiltro(null)}
|
||||
className={`px-2.5 py-1 rounded-lg text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
|
||||
letraFiltro === null
|
||||
? "bg-purple-600 text-white shadow-xs"
|
||||
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
}`}
|
||||
>
|
||||
Todos
|
||||
</button>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span className="mr-1 text-sm text-muted">Mais buscados</span>
|
||||
{BUSCAS_POPULARES.map((rotulo) => (
|
||||
<button
|
||||
key={rotulo}
|
||||
type="button"
|
||||
onClick={() => setBusca(rotulo)}
|
||||
aria-pressed={busca.toLowerCase() === rotulo.toLowerCase()}
|
||||
className="chip-btn"
|
||||
>
|
||||
{rotulo}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{alfabeto.map(({ letra, temTermos }) => {
|
||||
const ativo = letraFiltro === letra;
|
||||
if (!temTermos) {
|
||||
<div className="mt-7 border-t border-line pt-6">
|
||||
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" onClick={() => setCategoriaSelecionada("todas")} aria-pressed={categoriaSelecionada === "todas"} className="chip-btn">
|
||||
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
|
||||
Todas
|
||||
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
|
||||
</button>
|
||||
{categoriasGlossario.map((cat) => {
|
||||
const Icone = ICONES[cat.icone] ?? Cpu;
|
||||
return (
|
||||
<span
|
||||
key={letra}
|
||||
className="w-6 h-6 flex items-center justify-center text-[11px] font-semibold text-slate-500 dark:text-slate-450 select-none shrink-0"
|
||||
<button
|
||||
key={cat.id}
|
||||
type="button"
|
||||
onClick={() => setCategoriaSelecionada(cat.id)}
|
||||
aria-pressed={categoriaSelecionada === cat.id}
|
||||
className="chip-btn"
|
||||
>
|
||||
{letra}
|
||||
</span>
|
||||
<Icone className="h-4 w-4" aria-hidden="true" />
|
||||
{nomeCurto(cat.nome)}
|
||||
<span className="tabular-nums opacity-80">{cat.termos.length}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t border-line pt-6">
|
||||
<h2 className="text-sm font-semibold text-ink">Ordem alfabética</h2>
|
||||
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label="Filtrar pela letra inicial">
|
||||
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
|
||||
Todas
|
||||
</button>
|
||||
{alfabeto.map(({ letra, temTermos }) => (
|
||||
<button
|
||||
key={letra}
|
||||
type="button"
|
||||
onClick={() => setLetraFiltro(ativo ? null : letra)}
|
||||
className={`w-6 h-6 rounded-md flex items-center justify-center text-[11px] font-bold cursor-pointer transition-all shrink-0 ${
|
||||
ativo
|
||||
? "bg-purple-600 text-white scale-110 shadow-xs"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-purple-100 dark:hover:bg-purple-950/60"
|
||||
}`}
|
||||
disabled={!temTermos}
|
||||
onClick={() => setLetraFiltro(letraFiltro === letra ? null : letra)}
|
||||
aria-pressed={letraFiltro === letra}
|
||||
className="chip-btn min-w-11 justify-center px-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-surface"
|
||||
>
|
||||
{letra}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fita de Filtros Ativos (Aparece quando algum filtro está ativo) */}
|
||||
{temFiltroAtivo && (
|
||||
<div className="mt-4 pt-3 border-t border-slate-150 dark:border-slate-800/80 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
||||
<span className="text-slate-500 dark:text-slate-400 font-semibold mr-1">
|
||||
Filtros ativos:
|
||||
</span>
|
||||
|
||||
{busca && (
|
||||
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
|
||||
Busca: <strong>"{busca}"</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBusca("")}
|
||||
className="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{categoriaSelecionada !== "todas" && (
|
||||
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
|
||||
Categoria: <strong>{catAtivaInfo?.nome}</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCategoriaSelecionada("todas")}
|
||||
className="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{letraFiltro && (
|
||||
<span className="inline-flex items-center gap-1 bg-purple-50 dark:bg-purple-950/50 text-purple-700 dark:text-purple-300 px-2.5 py-0.5 rounded-full border border-purple-200 dark:border-purple-800 font-medium">
|
||||
Letra: <strong>{letraFiltro}</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLetraFiltro(null)}
|
||||
className="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={limparTodosFiltros}
|
||||
className="text-xs font-semibold text-red-600 dark:text-red-400 hover:underline flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
<span>Redefinir filtros</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 2. GRID DE CARDS DO GLOSSÁRIO */}
|
||||
<section>
|
||||
{termosFiltrados.length === 0 ? (
|
||||
<div className="text-center py-16 bg-white dark:bg-slate-900 rounded-3xl border border-slate-250 dark:border-slate-800 shadow-sm p-8">
|
||||
<HelpCircle className="w-12 h-12 text-slate-400 mx-auto mb-3" />
|
||||
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">
|
||||
Nenhum termo encontrado
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1 max-w-md mx-auto">
|
||||
Não encontramos resultados para sua pesquisa com os filtros selecionados.
|
||||
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
|
||||
{total} {total === 1 ? "termo" : "termos"}
|
||||
{temFiltroAtivo && <span className="font-normal text-muted"> com os filtros atuais</span>}
|
||||
</h2>
|
||||
{temFiltroAtivo && (
|
||||
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
Redefinir filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{total === 0 ? (
|
||||
<div className="card mt-6 px-6 py-14 text-center">
|
||||
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum termo encontrado</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
|
||||
Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={limparTodosFiltros}
|
||||
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-xl text-xs font-bold hover:bg-purple-700 cursor-pointer shadow-xs inline-flex items-center gap-1.5"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
<span>Limpar todos os filtros</span>
|
||||
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
|
||||
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||
Limpar filtros
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
{termosFiltrados.map((item) => {
|
||||
const Icone = ICONES[item.categoriaIcone] ?? Cpu;
|
||||
return (
|
||||
<article
|
||||
key={item.slug}
|
||||
id={item.slug}
|
||||
className="bg-white dark:bg-slate-900/90 rounded-2xl border border-slate-250 dark:border-slate-800 hover:border-purple-300 dark:hover:border-purple-500/40 p-5 sm:p-6 transition-all duration-200 shadow-xs flex flex-col justify-between scroll-mt-24 group"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Header do Card */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs px-2.5 py-0.5 rounded-full font-semibold bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
||||
<span>{item.categoriaEmoji}</span>
|
||||
<span>{item.categoriaNome}</span>
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copiarLinkTermo(item.slug)}
|
||||
title="Copiar link direto para este termo"
|
||||
className="text-xs text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 inline-flex items-center gap-1 p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors cursor-pointer"
|
||||
>
|
||||
{copiadoSlug === item.slug ? (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-semibold text-[11px]">
|
||||
Copiado!
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<Link2 className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Título do Termo */}
|
||||
<h2 className="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
|
||||
{item.termo}
|
||||
</h2>
|
||||
|
||||
{/* Definição */}
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed font-normal">
|
||||
<TextoComLinks texto={item.definicao} />
|
||||
</p>
|
||||
<article key={item.slug} id={item.slug} className="card flex scroll-mt-28 flex-col p-5 sm:p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="chip">
|
||||
<Icone className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
|
||||
{nomeCurto(item.categoriaNome)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copiarLinkTermo(item.slug)}
|
||||
aria-label={`Copiar link para ${item.termo}`}
|
||||
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
|
||||
>
|
||||
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Caixa de Exemplo Prático */}
|
||||
<div className="mt-4 pt-3 border-t border-slate-100 dark:border-slate-800/80">
|
||||
<div className="p-3.5 rounded-xl bg-purple-50/70 dark:bg-purple-950/20 border border-purple-100 dark:border-purple-900/30 text-xs leading-relaxed space-y-1">
|
||||
<span className="font-bold text-purple-700 dark:text-purple-400 flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
|
||||
<Lightbulb className="w-3.5 h-3.5" />
|
||||
Exemplo Prático
|
||||
</span>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
<TextoComLinks texto={item.exemplo} />
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="mt-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{item.termo}</h3>
|
||||
<p className="mt-2 text-[0.9375rem] leading-relaxed text-muted">
|
||||
<TextoComLinks texto={item.definicao} />
|
||||
</p>
|
||||
|
||||
<div className="callout mt-5">
|
||||
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
|
||||
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
|
||||
Na prática
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<TextoComLinks texto={item.exemplo} />
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -488,41 +266,20 @@ export default function GlossarioPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 3. CTA & PRÓXIMOS PASSOS */}
|
||||
<section className="mt-16 bg-gradient-to-br from-slate-900 to-slate-950 text-white rounded-3xl p-6 sm:p-10 border border-slate-800 shadow-xl">
|
||||
<div className="text-center max-w-2xl mx-auto space-y-4">
|
||||
<div className="inline-flex items-center gap-2 bg-purple-500/20 text-purple-400 border border-purple-500/30 px-3.5 py-1.5 rounded-full text-xs font-bold">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>Continue Explorando</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl sm:text-3xl font-display font-extrabold">
|
||||
Pronto para aplicar na prática?
|
||||
</h2>
|
||||
|
||||
<p className="text-xs sm:text-sm text-slate-400 leading-relaxed">
|
||||
Agora que você domina os conceitos técnicos, compare as especificações das séries ou use o seletor inteligente para encontrar o microcontrolador ideal para seu projeto.
|
||||
<section className="shell pb-20 md:pb-28">
|
||||
<div className="panel p-8 text-center sm:p-12">
|
||||
<h2 className="section-title mx-auto max-w-2xl">Do conceito ao chip certo.</h2>
|
||||
<p className="lede mx-auto mt-4 max-w-2xl">
|
||||
Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
|
||||
<Link
|
||||
href="/series"
|
||||
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white text-xs font-bold transition-all shadow-md hover:shadow-lg inline-flex items-center gap-2"
|
||||
>
|
||||
<span>Explorar Séries ESP32</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto">
|
||||
<Sliders className="h-4 w-4" aria-hidden="true" />
|
||||
Escolher meu chip
|
||||
</Link>
|
||||
<Link
|
||||
href="/seletor"
|
||||
className="px-5 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold border border-slate-700 transition-all inline-flex items-center gap-2"
|
||||
>
|
||||
<span>Seletor Inteligente</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/diagnostico"
|
||||
className="px-5 py-2.5 rounded-xl bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-xs font-bold border border-amber-500/40 transition-all inline-flex items-center gap-2"
|
||||
>
|
||||
<span>Guia de Diagnóstico & Erros</span>
|
||||
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
|
||||
Explorar as séries
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -564,7 +321,7 @@ function TextoComLinks({ texto }) {
|
||||
<Link
|
||||
key={i}
|
||||
href={`/series/${parte}`}
|
||||
className="font-bold text-purple-600 dark:text-purple-400 hover:underline inline-flex items-center mx-0.5"
|
||||
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
|
||||
>
|
||||
{parte}
|
||||
</Link>
|
||||
|
||||
+24
-9
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+296
-495
@@ -1,468 +1,174 @@
|
||||
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 {
|
||||
SlidersHorizontal,
|
||||
ShoppingBag,
|
||||
Cpu,
|
||||
Layers,
|
||||
Scale,
|
||||
BookOpen,
|
||||
import seriesData from "@/public/series.json";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { SITE } from "@/lib/seo";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
Compass,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
CheckCircle2,
|
||||
Zap,
|
||||
ExternalLink,
|
||||
Info,
|
||||
Radio,
|
||||
ShieldCheck,
|
||||
Code,
|
||||
HelpCircle,
|
||||
Box
|
||||
ArrowUpRight,
|
||||
BookOpen,
|
||||
Box,
|
||||
Cpu,
|
||||
Scale,
|
||||
SlidersHorizontal,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
|
||||
const FEATURED = ["ESP32", "ESP32-S3", "ESP32-C6", "ESP32-C3", "ESP32-P4", "ESP32-H2"];
|
||||
|
||||
const TOOLS = [
|
||||
{
|
||||
href: "/seletor",
|
||||
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",
|
||||
},
|
||||
{
|
||||
href: "/comparacao",
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
href: "/frameworks",
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
const ERRORS = [
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
];
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
// Ferramentas da Plataforma (100% Clicáveis)
|
||||
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: "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: "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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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: "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",
|
||||
},
|
||||
];
|
||||
|
||||
// Chips em Destaque
|
||||
const featuredChips = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
// 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",
|
||||
},
|
||||
];
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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
|
||||
</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>
|
||||
<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 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>
|
||||
|
||||
{/* 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" />
|
||||
</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>
|
||||
</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) => (
|
||||
<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 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="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 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>
|
||||
<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>
|
||||
<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="/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 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 md:grid-cols-2 gap-4">
|
||||
{commonErrors.map((err) => (
|
||||
<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={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"
|
||||
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>
|
||||
<div className="font-mono text-xs font-bold text-amber-600 dark:text-amber-400 mb-2 group-hover:underline">
|
||||
{err.title}
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
<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>
|
||||
))}
|
||||
@@ -470,71 +176,166 @@ export default function Home() {
|
||||
</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" />
|
||||
<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
|
||||
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"
|
||||
/>
|
||||
<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="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>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<a
|
||||
href="https://cienciaembarcada.com.br"
|
||||
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"
|
||||
>
|
||||
<span>Visitar Blog Ciência Embarcada</span>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-7 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<a
|
||||
href={SITE.livroAmazon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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]"
|
||||
>
|
||||
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.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+484
-964
File diff suppressed because it is too large.
Load diff
+305
-430
@@ -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,
|
||||
import {
|
||||
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,361 +142,261 @@ 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">
|
||||
<Image
|
||||
src={serie.placa}
|
||||
alt={`${key} DevKit - Placa de Desenvolvimento`}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="rounded-xl object-contain w-full h-auto max-h-[450px]"
|
||||
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}
|
||||
<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={`Placa de desenvolvimento ${key}`}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="mx-auto h-auto max-h-[28rem] w-full rounded-lg object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-muted">Placa padrão da Espressif para a série {key}.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "connections",
|
||||
label: "Diagrama de conexões",
|
||||
available: conexoes.length > 0,
|
||||
content: (
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
},
|
||||
{
|
||||
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>
|
||||
<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>
|
||||
</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
|
||||
<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"
|
||||
<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="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">
|
||||
<div className="site-canvas min-h-screen">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Séries", caminho: "/series" },
|
||||
{ nome: serie.nome_completo, caminho: `/series/${key}` },
|
||||
])),
|
||||
}}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<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. */}
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(jsonLd.trilha([
|
||||
{ nome: "Início", caminho: "/" },
|
||||
{ nome: "Séries", caminho: "/series" },
|
||||
{ nome: serie.nome_completo, caminho: `/series/${key}` },
|
||||
])),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SeriesTabMenu tabs={tabs} color={serie.cor} />
|
||||
|
||||
<PinRestrictions conexoes={conexoes} serieKey={key} cor={serie.cor} />
|
||||
|
||||
{/* 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 }} />} />
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
|
||||
<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}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="shell mt-10">
|
||||
<SeriesTabMenu tabs={tabs} />
|
||||
<PinRestrictions conexoes={conexoes} serieKey={key} />
|
||||
</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="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="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.
|
||||
<div className="mt-8 space-y-5">
|
||||
{secoes.map((secao) => (
|
||||
<SpecSection key={secao.id} {...secao} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{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>
|
||||
<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="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>
|
||||
) : (
|
||||
<span className="hidden sm:block" />
|
||||
)}
|
||||
<Link href="/comparacao" className="btn btn-ghost justify-self-center">
|
||||
<Scale className="h-4 w-4" aria-hidden="true" />
|
||||
Comparar séries
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden sm:block w-36" />
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
<div className="hidden sm:block w-36" />
|
||||
)}
|
||||
</div>
|
||||
{nextKey ? (
|
||||
<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>
|
||||
) : (
|
||||
<span className="hidden sm:block" />
|
||||
)}
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
@@ -530,27 +404,28 @@ 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}
|
||||
{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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+125
-219
@@ -1,248 +1,154 @@
|
||||
"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,
|
||||
import {
|
||||
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">
|
||||
|
||||
{/* 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>
|
||||
<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."
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{series.map(([key, serie]) => {
|
||||
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="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>
|
||||
|
||||
<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>
|
||||
|
||||
<ul className="mt-4 flex flex-1 flex-wrap content-start gap-1.5">
|
||||
{serie.aceleradores_ia && (
|
||||
<li className="chip chip-accent">
|
||||
<Sparkles className="h-3 w-3" aria-hidden="true" />
|
||||
Aceleradores de IA
|
||||
</li>
|
||||
)}
|
||||
<li className="chip">
|
||||
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
|
||||
{nivelSeguranca(serie)}
|
||||
</li>
|
||||
{serie.matter === "Sim" && (
|
||||
<li className="chip">
|
||||
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
|
||||
Matter
|
||||
</li>
|
||||
)}
|
||||
{serie.status_documentacao && (
|
||||
<li className="chip">
|
||||
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
|
||||
Documentação preliminar
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Series Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{series.map(([key, serie]) => {
|
||||
const isRiscV = serie.arquitetura.includes("RISC-V");
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<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">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
})()}
|
||||
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 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 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
+123
-239
@@ -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>
|
||||
<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."
|
||||
>
|
||||
<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>
|
||||
</PageHero>
|
||||
|
||||
<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)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 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>
|
||||
</section>
|
||||
|
||||
{/* 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.
|
||||
<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>
|
||||
|
||||
<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}
|
||||
<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="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>
|
||||
</li>
|
||||
))}
|
||||
</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.
|
||||
</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'n'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>
|
||||
@@ -288,4 +172,4 @@ export default function Sobre() {
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+46
-78
@@ -1,99 +1,67 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { Award, CheckCircle, HelpCircle, ExternalLink } from 'lucide-react';
|
||||
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
|
||||
|
||||
export default function BoardCard({ board }) {
|
||||
const mainFields = ['Nome', 'Link', 'Imagem', 'Loja Oficial'];
|
||||
|
||||
const technicalFields = Object.keys(board).filter(
|
||||
key => !mainFields.includes(key) && board[key]
|
||||
);
|
||||
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
|
||||
|
||||
const hasFeature = (value) => {
|
||||
const yesValues = ['sim', 'opcional'];
|
||||
return yesValues.includes(value?.toString().toLowerCase().trim());
|
||||
};
|
||||
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
|
||||
|
||||
// Filtrar apenas características que têm "Sim" ou "Opcional"
|
||||
const availableFeatures = technicalFields.filter(field => hasFeature(board[field]));
|
||||
const optionalFeatures = technicalFields.filter(field =>
|
||||
board[field]?.toString().toLowerCase().trim() === 'opcional'
|
||||
);
|
||||
const valor = (campo) => board[campo]?.toString().toLowerCase().trim();
|
||||
const recursos = technicalFields.filter((campo) => ["sim", "opcional"].includes(valor(campo)));
|
||||
const lojaOficial = valor("Loja Oficial") === "sim";
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-1.5 transition-all duration-300 overflow-hidden group flex flex-col h-full animate-fadeIn select-none">
|
||||
{/* Image Container with premium glass background */}
|
||||
<article className="card card-interactive flex h-full flex-col overflow-hidden">
|
||||
{board.Imagem && (
|
||||
<div
|
||||
className="mx-auto w-full p-4 flex justify-center bg-white dark:bg-white border-b border-slate-200 dark:border-slate-850/40 shrink-0"
|
||||
<div
|
||||
className="flex shrink-0 justify-center border-b border-line bg-surface-2 p-4 [&_img]:h-44 [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain"
|
||||
dangerouslySetInnerHTML={{ __html: board.Imagem }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="p-6 flex flex-col grow justify-between">
|
||||
<div>
|
||||
{/* Loja Oficial Badge */}
|
||||
{board['Loja Oficial'] && board['Loja Oficial'].toLowerCase() === 'sim' && (
|
||||
<div className="mb-2">
|
||||
<span className="inline-flex items-center gap-1 bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-wider">
|
||||
<Award className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>Loja Oficial</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex grow flex-col p-5">
|
||||
{lojaOficial && (
|
||||
<span className="chip chip-accent mb-3 w-fit">
|
||||
<Award className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Loja oficial
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Nome */}
|
||||
<h3 className="text-base font-display font-extrabold text-slate-850 dark:text-slate-100 mb-4 line-clamp-2 leading-snug group-hover:text-purple-650 dark:group-hover:text-purple-400 transition-colors">
|
||||
{board.Nome || 'Placa Genérica'}
|
||||
</h3>
|
||||
<h3 className="line-clamp-2 font-display text-lg font-semibold leading-snug tracking-[-0.02em] text-ink">
|
||||
{board.Nome || "Placa genérica"}
|
||||
</h3>
|
||||
|
||||
{/* Características Técnicas */}
|
||||
{availableFeatures.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="text-[9px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest flex items-center gap-1.5 mb-2.5">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>Recursos Embarcados</span>
|
||||
</h4>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{availableFeatures.map((field) => {
|
||||
const isOptional = optionalFeatures.includes(field);
|
||||
return (
|
||||
<div
|
||||
key={field}
|
||||
className={`text-[9px] px-2 py-0.5 rounded-lg font-bold flex items-center gap-1 ${
|
||||
isOptional
|
||||
? 'bg-amber-500/10 dark:bg-amber-500/15 border border-amber-500/20 text-amber-600 dark:text-amber-400'
|
||||
: 'bg-emerald-500/10 dark:bg-emerald-500/15 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
||||
}`}
|
||||
>
|
||||
{isOptional ? (
|
||||
<HelpCircle className="w-2.5 h-2.5 shrink-0" />
|
||||
) : (
|
||||
<CheckCircle className="w-2.5 h-2.5 shrink-0" />
|
||||
)}
|
||||
<span>{field}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{recursos.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-xs font-medium text-muted">Recursos embarcados</p>
|
||||
<ul className="mt-2 flex flex-wrap gap-1.5">
|
||||
{recursos.map((campo) => {
|
||||
const opcional = valor(campo) === "opcional";
|
||||
return (
|
||||
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
|
||||
{opcional ? (
|
||||
<CircleHelp className="h-3 w-3 text-gold" aria-hidden="true" />
|
||||
) : (
|
||||
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
|
||||
)}
|
||||
{campo}
|
||||
{opcional && <span className="sr-only"> (opcional)</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link de Compra */}
|
||||
{board.Link && (
|
||||
<a
|
||||
href={board.Link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-center px-4 py-3 rounded-xl font-bold text-xs uppercase tracking-wider hover:opacity-90 active:scale-98 transition-all shadow-md mt-auto flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>Adquirir no AliExpress</span>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
|
||||
Ver no AliExpress
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function BookCover({ className = "" }) {
|
||||
return (
|
||||
<div
|
||||
className={`book-cover dark:opacity-100 ${className}`}
|
||||
role="img"
|
||||
aria-label="Capa do livro Microcontroladores ESP32: conectividade, eficiência e desempenho, de Lucas Rayan Guerra"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{ backgroundImage: "url('/livro/capa.jpg')" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="book-spine" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+41
-102
@@ -1,19 +1,18 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Code, Copy, CheckCircle, ExternalLink, HelpCircle } from 'lucide-react'
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, Copy, ExternalLink } from "lucide-react";
|
||||
|
||||
export default function CodeExample({ framework }) {
|
||||
const examplesUrl = framework?.exemplos;
|
||||
const color = framework?.cor || '#6B7280';
|
||||
|
||||
const [sample, setSample] = useState('');
|
||||
const [sample, setSample] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSample('');
|
||||
setSample("");
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
|
||||
@@ -21,12 +20,11 @@ export default function CodeExample({ framework }) {
|
||||
|
||||
const exemploPath = framework.exemplo;
|
||||
if (!exemploPath) {
|
||||
const msg = `// Exemplos completos: ${examplesUrl || 'Ver repositório'}\n// Abra o link acima para ver exemplos e código completo.\n`;
|
||||
setSample(msg);
|
||||
setSample(`// Exemplos completos: ${examplesUrl || "ver repositório"}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = exemploPath.startsWith('/') ? exemploPath : `/${exemploPath}`;
|
||||
const url = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
@@ -41,14 +39,14 @@ export default function CodeExample({ framework }) {
|
||||
if (text && text.trim().length > 0) {
|
||||
setSample(text);
|
||||
} else {
|
||||
setSample(`// Arquivo de exemplo está vazio. Consulte o repositório oficial.\n`);
|
||||
setError('Arquivo vazio');
|
||||
setSample("// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n");
|
||||
setError("arquivo vazio");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setSample(`// Não foi possível carregar o exemplo localmente.\n// Consulte o link do GitHub acima para exemplos funcionais.\n`);
|
||||
setError(err.message || 'Falha ao carregar exemplo');
|
||||
setSample("// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n");
|
||||
setError(err.message || "falha ao carregar");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -59,7 +57,7 @@ export default function CodeExample({ framework }) {
|
||||
};
|
||||
}, [framework?.exemplo, framework?.exemplos]);
|
||||
|
||||
const lines = (sample || '').split('\n');
|
||||
const lines = (sample || "").replace(/\n$/, "").split("\n");
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
@@ -70,109 +68,50 @@ export default function CodeExample({ framework }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl mb-8 animate-fadeIn">
|
||||
<div className="md:flex items-center justify-between gap-6 mb-6">
|
||||
<div className="select-none">
|
||||
<h3 className="text-xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 flex items-center gap-2">
|
||||
<Code className="w-5 h-5" style={{ color }} />
|
||||
<span>Demonstração de Código</span>
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-semibold">Trecho de inicialização rápida. Copie e personalize em seu projeto.</p>
|
||||
<div className="card p-5 sm:p-7">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Exemplo de código</h2>
|
||||
<p className="mt-1 text-sm text-muted">Trecho de partida em {framework?.linguagem}. Ajuste pinos e configuração à sua placa.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2.5 mt-4 md:mt-0 select-none">
|
||||
<span
|
||||
className="inline-flex items-center text-[10px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: `${color}18`,
|
||||
color,
|
||||
border: `1px solid ${color}25`
|
||||
}}
|
||||
>
|
||||
{framework?.linguagem || 'Código'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-1.5 bg-slate-50 dark:bg-slate-850 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-250 px-3.5 py-2 rounded-xl font-bold text-xs shadow-xs border border-slate-300 dark:border-slate-800/80 cursor-pointer transition active:scale-95"
|
||||
title="Copiar código para a área de transferência"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span className="text-emerald-600 dark:text-emerald-450">Copiado!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Copiar</span>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button type="button" onClick={handleCopy} className="btn btn-ghost btn-sm">
|
||||
{copied ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
|
||||
<span role="status">{copied ? "Copiado" : "Copiar"}</span>
|
||||
</button>
|
||||
|
||||
{examplesUrl && (
|
||||
<a
|
||||
href={examplesUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white shadow-lg active:scale-95 transition hover:opacity-95"
|
||||
style={{
|
||||
background: `linear-gradient(90deg, ${color} 0%, rgba(147,51,234,1) 100%)`,
|
||||
boxShadow: `0 8px 20px -8px ${color}`
|
||||
}}
|
||||
title="Abrir exemplos no GitHub"
|
||||
>
|
||||
<span>Mais Exemplos</span>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<a href={examplesUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
|
||||
Mais exemplos
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl overflow-hidden border border-slate-300 dark:border-slate-800/80 shadow-md">
|
||||
<div className="select-none">
|
||||
<div className="flex items-center justify-between bg-slate-100/80 dark:bg-slate-950/70 border-b border-slate-300 dark:border-slate-850/60 px-4 py-3.5">
|
||||
<div className="text-xs font-bold text-slate-750 dark:text-slate-250 flex items-center gap-1.5">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-red-500/80" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/80" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-green-500/80" />
|
||||
<span className="ml-2 font-mono text-[10px] text-slate-500 dark:text-slate-400">{framework?.nome} example</span>
|
||||
</div>
|
||||
<div className="text-[9px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">
|
||||
{loading ? 'Buscando exemplo...' : (framework?.exemplo ? 'Local Source' : 'External GitHub')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="code-panel mt-5" aria-busy={loading}>
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2.5 text-xs text-[#a6afbd]">
|
||||
<span className="font-mono">{framework?.nome}</span>
|
||||
<span>{loading ? "Carregando exemplo…" : framework?.linguagem}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex bg-slate-950">
|
||||
<pre
|
||||
className="text-slate-200 p-5 w-full overflow-auto text-xs leading-relaxed scrollbar-thin select-text"
|
||||
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace' }}
|
||||
>
|
||||
<pre tabIndex={0} aria-label={`Código de exemplo em ${framework?.linguagem}`}>
|
||||
<code>
|
||||
{lines.map((line, idx) => (
|
||||
<div key={idx} className="flex">
|
||||
<span className="text-slate-500 pr-4 text-right w-8 select-none border-r border-slate-900/60 mr-4 font-semibold">{idx + 1}</span>
|
||||
<code className="whitespace-pre flex-1 text-left">{line === '' ? '\u00A0' : line}</code>
|
||||
</div>
|
||||
<span key={idx} className="flex">
|
||||
<span className="mr-4 w-7 shrink-0 select-none text-right text-[#7d8794]">{idx + 1}</span>
|
||||
<span className="min-w-0 whitespace-pre text-[#e9eef4]">{line || " "}</span>
|
||||
</span>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-3.5 mt-3 select-none">
|
||||
<p className="text-[10px] font-bold text-red-600 dark:text-red-400">
|
||||
Nota: Não foi possível carregar o arquivo de exemplo local ({error}). Use o botão "Mais Exemplos" para ver os códigos oficiais.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 bg-slate-50/50 dark:bg-slate-950/20 border border-slate-150/40 dark:border-slate-850/40 p-4 rounded-2xl select-none">
|
||||
<HelpCircle className="w-4.5 h-4.5 text-purple-500 shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] font-semibold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
<strong>Dica de Desenvolvimento:</strong> Personalize o trecho de inicialização com as pinagens e configurações corretas correspondentes à série do seu microcontrolador (ex: ESP32-S3, ESP32-C6). A documentação detalhada da API pode ser acessada na barra superior do framework.
|
||||
<p role="alert" className="callout callout-warn mt-4">
|
||||
Não foi possível carregar o exemplo local ({error}). Use “Mais exemplos” para ver o código oficial.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export default function CodeSample({ label, language, code, note }) {
|
||||
const lines = code.replace(/\n$/, "").split("\n");
|
||||
|
||||
return (
|
||||
<figure className="min-w-0">
|
||||
<figcaption className="mb-2.5 flex flex-wrap items-center justify-between gap-2 px-1">
|
||||
<span className="text-xs font-semibold text-ink">{label}</span>
|
||||
<span className="font-mono text-[0.6875rem] text-muted">{language}</span>
|
||||
</figcaption>
|
||||
<div className="code-panel rounded-2xl shadow-[0_18px_40px_-24px_rgba(0,0,0,0.5)]">
|
||||
<pre>
|
||||
<code>
|
||||
{lines.map((line, index) => (
|
||||
<span key={`${index}-${line}`} className="flex">
|
||||
<span className="mr-4 w-6 shrink-0 select-none text-right text-[#7d8794]">{index + 1}</span>
|
||||
<span className="min-w-0 whitespace-pre text-[#e9eef4]">{line || " "}</span>
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
{note && <p className="mt-2.5 px-1 text-xs leading-relaxed text-muted">{note}</p>}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
+266
-586
@@ -1,22 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
Search,
|
||||
X,
|
||||
Grid,
|
||||
List,
|
||||
Filter,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Tag,
|
||||
Zap,
|
||||
Sliders,
|
||||
CheckCircle,
|
||||
ExternalLink
|
||||
} from "lucide-react";
|
||||
import { AlertTriangle, CheckCircle, List, Grid, Search, Sliders, X, Zap } from "lucide-react";
|
||||
|
||||
/*
|
||||
* As cores abaixo são dado de visualização: identificam o tipo de função do
|
||||
* pino no diagrama. O texto nunca fica sobre elas; elas aparecem só como borda
|
||||
* ou ponto, e o rótulo usa as cores do tema (contraste AAA).
|
||||
*/
|
||||
const FILTER_CATEGORIES = [
|
||||
{ id: "All", name: "Todos", color: "#8b5cf6" }, // purple
|
||||
{ id: "GND", name: "Terra (GND)", color: "#64748b" }, // slate
|
||||
@@ -52,11 +44,11 @@ const type_colors = {
|
||||
export default function ConnectionsDiagram({ connections, serie }) {
|
||||
const [activeFilter, setActiveFilter] = useState("All");
|
||||
const [selectedConnection, setSelectedConnection] = useState(null);
|
||||
const [hoveredConnection, setHoveredConnection] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [viewMode, setViewMode] = useState("grid");
|
||||
const [showTooltip, setShowTooltip] = useState(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const dialogRef = useRef(null);
|
||||
const gatilhoRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -64,7 +56,8 @@ export default function ConnectionsDiagram({ connections, serie }) {
|
||||
|
||||
const isConnectionVisible = (connection) => {
|
||||
const matchesFilter = activeFilter === "All" || connection.categorias.includes(activeFilter);
|
||||
const matchesSearch = searchQuery === "" ||
|
||||
const matchesSearch =
|
||||
searchQuery === "" ||
|
||||
connection.nome.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
connection.numero.toString().toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesFilter && matchesSearch;
|
||||
@@ -75,641 +68,328 @@ export default function ConnectionsDiagram({ connections, serie }) {
|
||||
return filter?.color || "#64748b";
|
||||
};
|
||||
|
||||
const visibleConnections = useMemo(() => {
|
||||
return connections.filter(isConnectionVisible);
|
||||
}, [connections, activeFilter, searchQuery]);
|
||||
const nomeCategoria = (cat) => (cat === "Strapping" ? "Inicialização" : cat);
|
||||
|
||||
const activeCategories = useMemo(() => {
|
||||
if (activeFilter === "All") return [];
|
||||
return FILTER_CATEGORIES.filter(cat =>
|
||||
visibleConnections.some(conn => conn.categorias.includes(cat.id))
|
||||
);
|
||||
}, [visibleConnections, activeFilter]);
|
||||
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery]);
|
||||
|
||||
const abrir = (connection, evento) => {
|
||||
gatilhoRef.current = evento.currentTarget;
|
||||
setSelectedConnection(connection);
|
||||
};
|
||||
|
||||
const fechar = () => {
|
||||
setSelectedConnection(null);
|
||||
gatilhoRef.current?.focus?.();
|
||||
};
|
||||
|
||||
// Diálogo: Esc fecha, o foco entra nele, fica preso enquanto aberto e volta ao pino de origem.
|
||||
useEffect(() => {
|
||||
const handleEscape = (e) => {
|
||||
if (!selectedConnection) return;
|
||||
const dialogo = dialogRef.current;
|
||||
const focaveis = () => dialogo?.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])') ?? [];
|
||||
focaveis()[0]?.focus();
|
||||
|
||||
const aoTeclar = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
setSelectedConnection(null);
|
||||
setShowTooltip(null);
|
||||
fechar();
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Tab") return;
|
||||
const itens = [...focaveis()];
|
||||
if (itens.length === 0) return;
|
||||
const primeiro = itens[0];
|
||||
const ultimo = itens[itens.length - 1];
|
||||
if (e.shiftKey && document.activeElement === primeiro) {
|
||||
e.preventDefault();
|
||||
ultimo.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === ultimo) {
|
||||
e.preventDefault();
|
||||
primeiro.focus();
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedConnection) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("keydown", aoTeclar);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "unset";
|
||||
document.removeEventListener("keydown", aoTeclar);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [selectedConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (selectedConnection) return;
|
||||
|
||||
if (e.key === "Tab") {
|
||||
const buttons = document.querySelectorAll('[data-connection-button]');
|
||||
const currentIndex = Array.from(buttons).findIndex(btn => btn === document.activeElement);
|
||||
|
||||
if (e.shiftKey && currentIndex > 0) {
|
||||
e.preventDefault();
|
||||
buttons[currentIndex - 1].focus();
|
||||
} else if (!e.shiftKey && currentIndex < buttons.length - 1) {
|
||||
e.preventDefault();
|
||||
buttons[currentIndex + 1].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedConnection]);
|
||||
|
||||
if (!connections || connections.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div className="w-20 h-20 mb-6 rounded-2xl bg-slate-100 dark:bg-slate-900 border border-slate-300 dark:border-slate-800 flex items-center justify-center shadow-lg">
|
||||
<Sliders className="w-10 h-10 text-slate-500 dark:text-slate-400" />
|
||||
</div>
|
||||
|
||||
<p className="text-lg font-bold text-slate-800 dark:text-slate-200">
|
||||
Diagrama de conexões indisponível
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||
Este modelo ainda não possui um mapa detalhado de conexões e barramentos.
|
||||
</p>
|
||||
<div className="px-4 py-14 text-center">
|
||||
<Sliders className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
|
||||
<p className="mt-4 font-display text-xl font-semibold text-ink">Diagrama de conexões indisponível</p>
|
||||
<p className="mt-1 text-sm text-muted">Este modelo ainda não tem um mapa detalhado de conexões e barramentos.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const numeroCurto = (c) => (typeof c.numero === "number" ? c.numero : String(c.numero).substring(0, 4));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-8">
|
||||
|
||||
{/* Header info */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-4 pb-3 border-b border-slate-300 dark:border-slate-800/65 flex items-center gap-3 select-none">
|
||||
<Sliders className="w-5 h-5" style={{ color: serie.color }} />
|
||||
<span>Diagrama de Conexões Interativo</span>
|
||||
</h2>
|
||||
<p className="text-sm text-slate-550 dark:text-slate-400 leading-relaxed">
|
||||
Explore todas as conexões e multiplexação do SoC do {serie.nome_completo}. Utilize o painel de busca rápida ou filtre por categoria para acender conexões específicas e ver mapas de barramentos.
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Diagrama de conexões interativo</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">
|
||||
Todas as conexões e a multiplexação do {serie.nome_completo}. Busque por nome ou número, filtre por função e abra um pino para ver os detalhes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search Panel */}
|
||||
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 p-5 space-y-4 shadow-xl">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-slate-500 dark:text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar por nome ou número da conexão (Ex: GPIO1, TXD, XTAL)..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-12 pr-10 py-3 rounded-xl border border-slate-300 dark:border-slate-800 bg-white dark:bg-slate-950/40 focus:border-purple-500/80 focus:ring-4 focus:ring-purple-500/10 text-slate-800 dark:text-slate-200 transition-all outline-none text-sm font-semibold"
|
||||
aria-label="Search connections"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex gap-1.5 bg-slate-100 dark:bg-slate-950/80 rounded-xl p-1 shrink-0 border border-slate-250/30 dark:border-slate-850/30">
|
||||
<button
|
||||
onClick={() => setViewMode("grid")}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
|
||||
viewMode === "grid"
|
||||
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
|
||||
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
||||
}`}
|
||||
aria-label="Grid view"
|
||||
aria-pressed={viewMode === "grid"}
|
||||
>
|
||||
<Grid className="w-3.5 h-3.5" />
|
||||
<span>Grade</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setViewMode("list")}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
|
||||
viewMode === "list"
|
||||
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
|
||||
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
||||
}`}
|
||||
aria-label="List view"
|
||||
aria-pressed={viewMode === "list"}
|
||||
>
|
||||
<List className="w-3.5 h-3.5" />
|
||||
<span>Lista</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<div className="relative flex-1">
|
||||
<label htmlFor="busca-pino" className="sr-only">Buscar conexão por nome ou número</label>
|
||||
<Search className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
|
||||
<input
|
||||
id="busca-pino"
|
||||
type="search"
|
||||
placeholder="Buscar pino (ex.: GPIO1, TXD, XTAL)"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="field pl-11 pr-4 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div role="group" aria-label="Modo de exibição" className="flex gap-2">
|
||||
<button type="button" onClick={() => setViewMode("grid")} aria-pressed={viewMode === "grid"} className="chip-btn">
|
||||
<Grid className="h-4 w-4" aria-hidden="true" />
|
||||
Grade
|
||||
</button>
|
||||
<button type="button" onClick={() => setViewMode("list")} aria-pressed={viewMode === "list"} className="chip-btn">
|
||||
<List className="h-4 w-4" aria-hidden="true" />
|
||||
Lista
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 p-5 shadow-xl">
|
||||
<div className="flex items-center gap-3 mb-5 border-b border-slate-150/40 dark:border-slate-850/45 pb-3">
|
||||
<Filter className="w-4 h-4 text-purple-500" />
|
||||
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-100 select-none">
|
||||
Filtre por Função do Pino
|
||||
</h4>
|
||||
|
||||
<span className="ml-auto text-[10px] font-bold text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-950 px-3 py-1 rounded-full border border-slate-300 dark:border-slate-850/30 shadow-xs select-none">
|
||||
{visibleConnections.length} de {connections.length} ativos
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-ink">Função do pino</h3>
|
||||
<p role="status" aria-live="polite" className="text-sm text-muted">
|
||||
{visibleConnections.length} de {connections.length} pinos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div role="group" aria-label="Filtrar por função" className="mt-3 flex flex-wrap gap-2">
|
||||
{FILTER_CATEGORIES.map((filter) => {
|
||||
const isActive = activeFilter === filter.id;
|
||||
const count = connections.filter(c =>
|
||||
filter.id === "All" ? true : c.categorias.includes(filter.id)
|
||||
).length;
|
||||
|
||||
if (count === 0 && !isActive) return null; // Hide filters that don't apply to this chip
|
||||
|
||||
const count = connections.filter((c) => (filter.id === "All" ? true : c.categorias.includes(filter.id))).length;
|
||||
if (count === 0 && !isActive) return null; // filtros que não se aplicam a este chip
|
||||
return (
|
||||
<button
|
||||
key={filter.id}
|
||||
onClick={() => setActiveFilter(filter.id)}
|
||||
aria-pressed={isActive}
|
||||
aria-label={`Filter by ${filter.name}`}
|
||||
className={`group relative px-4 py-2 rounded-xl text-xs font-extrabold transition-all duration-300 focus:outline-none cursor-pointer active:scale-95 ${
|
||||
isActive
|
||||
? "shadow-lg text-white"
|
||||
: "bg-slate-50/70 dark:bg-slate-950/20 text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-850/30 border border-slate-300 dark:border-slate-850/40"
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isActive ? filter.color : undefined,
|
||||
boxShadow: isActive ? `0 8px 15px -8px ${filter.color}` : undefined
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{filter.name}</span>
|
||||
<span className={`text-[9px] px-1.5 py-0.5 rounded-full shrink-0 font-bold ${
|
||||
isActive ? "bg-white/20 text-white" : "bg-slate-200/50 dark:bg-slate-800/80 text-slate-600 dark:text-slate-400"
|
||||
}`}>
|
||||
{count}
|
||||
</span>
|
||||
</span>
|
||||
<button key={filter.id} type="button" onClick={() => setActiveFilter(filter.id)} aria-pressed={isActive} className="chip-btn">
|
||||
{filter.id !== "All" && <span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: filter.color }} aria-hidden="true" />}
|
||||
{filter.name}
|
||||
<span className="tabular-nums opacity-80">{count}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Active Categories badges */}
|
||||
{activeFilter !== "All" && activeCategories.length > 0 && (
|
||||
<div className="mt-5 pt-5 border-t border-slate-200 dark:border-slate-850/40 select-none">
|
||||
<p className="text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-3 uppercase tracking-wider">Subcategorias ativadas por esse filtro:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{activeCategories.map(cat => (
|
||||
<span
|
||||
key={cat.id}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-[10px] font-bold text-white shadow-xs"
|
||||
style={{ backgroundColor: cat.color }}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white shrink-0"></span>
|
||||
<span>{cat.name}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Diagram Workspace */}
|
||||
{visibleConnections.length === 0 ? (
|
||||
<div className="bg-slate-50/50 dark:bg-slate-900/10 backdrop-blur-xl rounded-2xl p-12 border-2 border-dashed border-slate-300 dark:border-slate-850/80 text-center shadow-inner">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 dark:bg-slate-950 flex items-center justify-center shadow-md">
|
||||
<Search className="w-6 h-6 text-slate-400" />
|
||||
</div>
|
||||
<p className="text-base font-bold text-slate-800 dark:text-slate-200 mb-1 select-none">Nenhuma conexão encontrada</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">Tente ajustar seus termos de pesquisa ou limpe os filtros rápidos.</p>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveFilter("All");
|
||||
setSearchQuery("");
|
||||
}}
|
||||
className="mt-5 px-6 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl hover:opacity-90 transition-all font-bold text-xs uppercase tracking-wider cursor-pointer active:scale-95 shadow-lg"
|
||||
>
|
||||
Limpar Filtros e Busca
|
||||
<div className="rounded-2xl border border-dashed border-line-strong px-6 py-12 text-center">
|
||||
<Search className="mx-auto h-8 w-8 text-muted" aria-hidden="true" />
|
||||
<p className="mt-3 font-semibold text-ink">Nenhuma conexão encontrada</p>
|
||||
<p className="mt-1 text-sm text-muted">Ajuste a busca ou volte para todas as funções.</p>
|
||||
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); }} className="btn btn-primary btn-sm mt-5">
|
||||
Limpar busca e filtros
|
||||
</button>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
/* Grid View Mode: Futuristic Electronic Chip Sockets */
|
||||
<div className="bg-slate-100/50 dark:bg-slate-950/20 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-850/60 shadow-inner">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{visibleConnections.map((connection, index) => {
|
||||
const isHovered = hoveredConnection === index;
|
||||
const connectionId = `connection-${index}`;
|
||||
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
||||
|
||||
return (
|
||||
<div key={index} className="relative">
|
||||
<button
|
||||
id={connectionId}
|
||||
data-connection-button
|
||||
onClick={() => setSelectedConnection(connection)}
|
||||
onMouseEnter={() => {
|
||||
setHoveredConnection(index);
|
||||
setShowTooltip(index);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredConnection(null);
|
||||
setShowTooltip(null);
|
||||
}}
|
||||
onFocus={() => setHoveredConnection(index)}
|
||||
onBlur={() => setHoveredConnection(null)}
|
||||
aria-label={`Pino ${connection.nome}, número ${connection.numero}`}
|
||||
aria-describedby={showTooltip === index ? `tooltip-${index}` : undefined}
|
||||
className="w-full flex flex-col items-center justify-center p-4 rounded-2xl border-2 transition-all duration-300 cursor-pointer active:scale-95 focus:outline-none"
|
||||
style={{
|
||||
backgroundColor: isHovered ? `${baseColor}24` : `${baseColor}0c`,
|
||||
borderColor: isHovered ? baseColor : `${baseColor}40`,
|
||||
boxShadow: isHovered ? `0 0 20px -5px ${baseColor}60` : 'none',
|
||||
transform: isHovered ? "translateY(-4px)" : undefined,
|
||||
}}
|
||||
<ul className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{visibleConnections.map((connection) => {
|
||||
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
||||
return (
|
||||
<li key={`${connection.numero}-${connection.nome}`} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => abrir(connection, e)}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={`Pino ${connection.nome}, número ${connection.numero}${connection.avisos ? ", com restrições de uso" : ""}`}
|
||||
className="flex w-full flex-col items-center rounded-xl border border-line bg-surface p-3 transition-colors hover:bg-surface-2"
|
||||
>
|
||||
<span
|
||||
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink"
|
||||
style={{ borderColor: baseColor }}
|
||||
>
|
||||
{/* Pad Contact Core Circle */}
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center text-white font-extrabold text-xs mb-3 shadow-lg select-none"
|
||||
style={{ backgroundColor: baseColor, boxShadow: `0 4px 10px -3px ${baseColor}` }}
|
||||
>
|
||||
{typeof connection.numero === "number"
|
||||
? connection.numero
|
||||
: connection.numero.substring(0, 4)}
|
||||
</div>
|
||||
|
||||
{/* Pad Name label */}
|
||||
<div className="text-xs font-bold text-center text-slate-850 dark:text-slate-200 leading-tight mb-2 select-none group-hover:text-purple-600 transition-colors">
|
||||
{connection.nome}
|
||||
</div>
|
||||
|
||||
{/* Small Category dots indicator */}
|
||||
<div className="flex flex-wrap gap-1 justify-center select-none">
|
||||
{connection.categorias.slice(0, 5).map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: getCategoryColor(cat) }}
|
||||
title={cat === "Strapping" ? "Inicialização" : cat}
|
||||
/>
|
||||
))}
|
||||
{connection.categorias.length > 5 && (
|
||||
<span className="text-[9px] text-slate-500 dark:text-slate-400 font-bold ml-0.5 leading-none">
|
||||
+{connection.categorias.length - 5}
|
||||
{numeroCurto(connection)}
|
||||
</span>
|
||||
<span className="mt-2 break-all text-center text-sm font-medium leading-tight text-ink">{connection.nome}</span>
|
||||
<span className="mt-2 flex flex-wrap justify-center gap-1" aria-hidden="true">
|
||||
{connection.categorias.slice(0, 5).map((cat) => (
|
||||
<span key={cat} className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} />
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
{connection.avisos && (
|
||||
<span className="pointer-events-none absolute -right-1.5 -top-1.5 grid h-6 w-6 place-items-center rounded-full border-2 border-surface bg-danger-soft text-danger" aria-hidden="true">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border border-line">
|
||||
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
|
||||
<caption className="sr-only">Conexões do {serie.nome_completo}</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-surface-2 text-muted">
|
||||
<th scope="col" className="px-4 py-3 font-medium">Pino</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">Nome</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">Funções</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">Uso</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
{visibleConnections.map((connection) => {
|
||||
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
||||
return (
|
||||
<tr key={`${connection.numero}-${connection.nome}`} className="hover:bg-surface-2/60">
|
||||
<td className="px-4 py-2.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => abrir(connection, e)}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={`Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border-2 bg-surface text-sm font-semibold tabular-nums text-ink hover:bg-surface-2"
|
||||
style={{ borderColor: baseColor }}
|
||||
>
|
||||
{numeroCurto(connection)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 font-medium text-ink">{connection.nome}</td>
|
||||
<td className="max-w-md px-4 py-2.5 text-muted">
|
||||
<span className="line-clamp-2">{connection.funcoes.join(", ")}</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-2.5">
|
||||
{connection.avisos ? (
|
||||
<span className="inline-flex items-center gap-1.5 font-medium text-danger">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
Restrito
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-ok">
|
||||
<CheckCircle className="h-4 w-4" aria-hidden="true" />
|
||||
Livre
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* High-Voltage warnings tags */}
|
||||
{connection.avisos && (
|
||||
<div
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 shadow-lg ring-2 ring-white dark:ring-slate-950 animate-pulse"
|
||||
aria-label="Possui restrições de uso"
|
||||
>
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Sleek Tooltip Popover */}
|
||||
{showTooltip === index && (
|
||||
<div
|
||||
id={`tooltip-${index}`}
|
||||
role="tooltip"
|
||||
className="absolute z-40 bottom-full left-1/2 -translate-x-1/2 mb-3 p-3 bg-slate-950/90 dark:bg-slate-900/90 backdrop-blur-md text-white text-[10px] rounded-xl shadow-2xl max-w-xs pointer-events-none animate-fadeIn border border-slate-850/50"
|
||||
style={{ minWidth: "180px" }}
|
||||
>
|
||||
<div className="font-extrabold text-sm mb-1">{connection.nome}</div>
|
||||
<div className="text-slate-400 mb-2 font-semibold">Pino Físico {connection.numero}</div>
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{connection.categorias.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="px-1.5 py-0.5 rounded text-[8px] font-bold"
|
||||
style={{
|
||||
backgroundColor: `${getCategoryColor(cat)}24`,
|
||||
color: getCategoryColor(cat)
|
||||
}}
|
||||
>
|
||||
{cat === "Strapping" ? "Inicialização" : cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-purple-400 font-bold text-[9px] uppercase tracking-wider">Clique para detalhes</div>
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-slate-950/95 dark:border-t-slate-900/95"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
/* List View Mode: Interactive Zebra Table */
|
||||
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl border border-slate-300 dark:border-slate-800/80 overflow-hidden shadow-xl">
|
||||
<div className="overflow-x-auto scrollbar-thin">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-slate-100/50 dark:bg-slate-950/50 border-b border-slate-300 dark:border-slate-850/60 select-none">
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Pino</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Nome do Pad</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Categorias</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Multiplexação de Funções</th>
|
||||
<th className="px-6 py-4 text-center text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Restrições</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-850/30">
|
||||
{visibleConnections.map((connection, index) => {
|
||||
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
onClick={() => setSelectedConnection(connection)}
|
||||
className="hover:bg-purple-500/5 dark:hover:bg-purple-400/5 cursor-pointer transition-colors group"
|
||||
>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-extrabold text-xs shadow-md select-none"
|
||||
style={{ backgroundColor: baseColor }}
|
||||
>
|
||||
{typeof connection.numero === "number"
|
||||
? connection.numero
|
||||
: connection.numero.substring(0, 4)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-bold text-slate-800 dark:text-slate-200 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
|
||||
{connection.nome}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-wrap gap-1.5 select-none">
|
||||
{connection.categorias.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md text-[9px] font-bold text-white shadow-xs"
|
||||
style={{ backgroundColor: getCategoryColor(cat) }}
|
||||
>
|
||||
{cat === "Strapping" ? "Inicialização" : cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 font-semibold max-w-md line-clamp-2 leading-relaxed">
|
||||
{connection.funcoes.join(", ")}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4 text-center whitespace-nowrap">
|
||||
{connection.avisos ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-red-500/10 text-red-600 dark:text-red-400 text-[10px] font-bold uppercase border border-red-500/20 select-none">
|
||||
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>Restrito</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold uppercase border border-emerald-500/20 select-none">
|
||||
<CheckCircle className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>Livre</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{connections.some((c) => c.avisos) && (
|
||||
<div className="callout callout-warn flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
|
||||
<div>
|
||||
<p className="font-semibold text-ink">Notas sobre a pinagem</p>
|
||||
<ul className="mt-1 list-disc space-y-1 pl-5">
|
||||
<li>Pinos com o ícone de alerta têm papel especial no boot ou limitações analógicas. Abra o pino para ver os detalhes.</li>
|
||||
<li>Isole os circuitos de toque capacitivo de pinos com ruído de RF acoplado.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Warning notice */}
|
||||
{connections.some(c => c.avisos) && (
|
||||
<div className="bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-2xl p-6 shadow-sm select-none">
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0">
|
||||
<div className="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center">
|
||||
<AlertTriangle className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-base font-bold text-amber-850 dark:text-amber-300 mb-1">
|
||||
Notas Importantes sobre a Pinagem
|
||||
</p>
|
||||
<ul className="space-y-1.5 text-xs text-amber-800 dark:text-amber-400 leading-relaxed">
|
||||
<li>• Pinos contendo tags de <strong>Avisos/Restrições</strong> (cor vermelha) possuem papéis especiais de Strapping na inicialização, ou limitações analógicas. Clique neles para ver detalhes.</li>
|
||||
<li>• Sempre garanta isolamento de circuitos capacitivos de toque em pinos com ruído de RF acoplado.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interactive Overlay Dialog Details Sheet */}
|
||||
{selectedConnection && mounted && createPortal(
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fadeIn"
|
||||
onClick={() => setSelectedConnection(null)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div className="dialog-scrim fixed inset-0 z-50 flex items-center justify-center bg-code/80 p-4 backdrop-blur-sm" onClick={fechar}>
|
||||
<div
|
||||
className="bg-white/95 dark:bg-slate-950/95 backdrop-blur-xl border border-slate-300 dark:border-slate-800/80 rounded-3xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col animate-slideUp"
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
className="dialog-panel flex max-h-[85vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border border-line bg-surface shadow-[var(--shadow)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Dialog Header */}
|
||||
<div className="sticky top-0 bg-white/50 dark:bg-slate-950/50 border-b border-slate-300 dark:border-slate-850/60 p-6 md:p-8 z-10">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-extrabold text-lg shadow-xl shrink-0"
|
||||
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
|
||||
>
|
||||
{typeof selectedConnection.numero === "number"
|
||||
? selectedConnection.numero
|
||||
: selectedConnection.numero.substring(0, 4)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 id="modal-title" className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 leading-none tracking-tight">
|
||||
{selectedConnection.nome}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
|
||||
Pino Físico {selectedConnection.numero}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedConnection(null)}
|
||||
aria-label="Fechar"
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 bg-slate-100 dark:bg-slate-900 rounded-xl shrink-0 cursor-pointer active:scale-95"
|
||||
<div className="flex items-start justify-between gap-4 border-b border-line p-5 sm:p-7">
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<span
|
||||
className="grid h-14 w-14 shrink-0 place-items-center rounded-full border-2 bg-surface text-lg font-semibold tabular-nums text-ink"
|
||||
style={{ borderColor: type_colors[selectedConnection.tipo] || "#64748b" }}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
{numeroCurto(selectedConnection)}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h3 id="modal-title" className="break-words font-display text-2xl font-semibold tracking-[-0.03em] text-ink">
|
||||
{selectedConnection.nome}
|
||||
</h3>
|
||||
<p className="text-sm text-muted">Pino físico {selectedConnection.numero}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={fechar} aria-label="Fechar detalhes do pino" className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink">
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dialog Body Contents */}
|
||||
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-8 scrollbar-thin">
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-5 sm:p-7">
|
||||
{selectedConnection.categorias.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
||||
<Tag className="w-4 h-4 text-purple-500" />
|
||||
<span>Categorias de Multiplexação</span>
|
||||
</h4>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">Categorias</h4>
|
||||
<ul className="mt-3 flex flex-wrap gap-2">
|
||||
{selectedConnection.categorias.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="px-3 py-1.5 rounded-xl text-xs font-bold text-white shadow-md select-none"
|
||||
style={{ backgroundColor: getCategoryColor(cat) }}
|
||||
>
|
||||
{cat === "Strapping" ? "Inicialização" : cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Power Source */}
|
||||
{selectedConnection.alimentacao && (
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
||||
<Zap className="w-4 h-4 text-red-500" />
|
||||
<span>Domínio de Alimentação Interna</span>
|
||||
</h4>
|
||||
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-300 dark:border-slate-850/50">
|
||||
<ul className="space-y-2.5">
|
||||
{selectedConnection.alimentacao.map((source, index) => (
|
||||
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||
<span
|
||||
className="shrink-0 w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: type_colors["power"] }}
|
||||
/>
|
||||
<span>{source}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Function Multiplexation */}
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
||||
<Sliders className="w-4 h-4 text-emerald-500" />
|
||||
<span>Barramentos de Funções Disponíveis</span>
|
||||
</h4>
|
||||
|
||||
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-300 dark:border-slate-850/50">
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{selectedConnection.funcoes.map((func, index) => (
|
||||
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||
<span
|
||||
className="shrink-0 w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
|
||||
/>
|
||||
<span>{func}</span>
|
||||
<li key={cat} className="chip">
|
||||
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} aria-hidden="true" />
|
||||
{nomeCategoria(cat)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{selectedConnection.alimentacao && (
|
||||
<section>
|
||||
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Zap className="h-4 w-4 text-danger" aria-hidden="true" />
|
||||
Domínio de alimentação
|
||||
</h4>
|
||||
<ul className="tile mt-3 space-y-2">
|
||||
{selectedConnection.alimentacao.map((source) => (
|
||||
<li key={source} className="text-sm text-ink-soft">{source}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">Funções disponíveis</h4>
|
||||
<ul className="tile mt-3 grid gap-2.5 sm:grid-cols-2">
|
||||
{selectedConnection.funcoes.map((func) => (
|
||||
<li key={func} className="flex items-center gap-2.5 text-sm text-ink-soft">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }} aria-hidden="true" />
|
||||
{func}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Alerts/Warnings */}
|
||||
{selectedConnection.avisos && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-5 shadow-xs">
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0">
|
||||
<div className="w-10 h-10 rounded-xl bg-red-500/15 border border-red-500/25 flex items-center justify-center text-red-500">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-bold text-red-900 dark:text-red-300 mb-2">
|
||||
Avisos Importantes de Engenharia
|
||||
</p>
|
||||
{selectedConnection.avisos.map((aviso, index) => (
|
||||
<div key={index} className="flex items-start gap-2.5 mb-1.5 text-xs text-red-800 dark:text-red-400 font-semibold leading-relaxed">
|
||||
<span>•</span>
|
||||
<span>{aviso}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="callout callout-danger">
|
||||
<h4 className="flex items-center gap-2 font-semibold text-ink">
|
||||
<AlertTriangle className="h-4 w-4 text-danger" aria-hidden="true" />
|
||||
Avisos de engenharia
|
||||
</h4>
|
||||
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
||||
{selectedConnection.avisos.map((aviso) => (
|
||||
<li key={aviso}>{aviso}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialog Footer */}
|
||||
<div className="sticky bottom-0 bg-white/50 dark:bg-slate-950/50 border-t border-slate-300 dark:border-slate-850/60 p-5">
|
||||
<button
|
||||
onClick={() => setSelectedConnection(null)}
|
||||
className="w-full py-3.5 rounded-xl font-bold text-white text-xs uppercase tracking-wider shadow-lg hover:opacity-90 active:scale-98 transition-all cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: serie.color,
|
||||
boxShadow: `0 8px 25px -8px ${serie.color}`
|
||||
}}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
Confirmar e Fechar
|
||||
</button>
|
||||
<div className="border-t border-line p-4 sm:px-7">
|
||||
<button type="button" onClick={fechar} className="btn btn-primary w-full">Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.animate-slideUp {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+145
-143
@@ -1,164 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import { SITE } from "@/lib/seo";
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
title: "Explorar",
|
||||
links: [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparador" },
|
||||
{ href: "/catalogo", label: "Catálogo de placas" },
|
||||
{ href: "/livro", label: "O livro" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Ferramentas",
|
||||
links: [
|
||||
{ href: "/seletor", label: "Seletor inteligente" },
|
||||
{ href: "/diagnostico", label: "Diagnóstico" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/frameworks", label: "Frameworks e SDKs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Projeto",
|
||||
links: [
|
||||
{ href: "/sobre", label: "Sobre o ESPDocs" },
|
||||
{ href: "/glossario", label: "Glossário" },
|
||||
{ href: SITE.repositorio, label: "Repositório", external: true },
|
||||
{ href: `${SITE.repositorio}/issues`, label: "Reportar um problema", external: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const [year, setYear] = useState(2026);
|
||||
|
||||
useEffect(() => {
|
||||
setYear(new Date().getFullYear());
|
||||
}, []);
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer role="contentinfo" aria-label="Rodapé do site ESPDocs" className="bg-slate-50 dark:bg-slate-950 text-slate-600 dark:text-slate-400 mt-24 border-t border-slate-250/30 dark:border-slate-800/80">
|
||||
<div className="max-w-7xl mx-auto px-6 py-12 md:py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10 md:gap-16">
|
||||
|
||||
{/* Coluna 1: Logo & Info */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-display font-bold text-slate-800 dark:text-slate-100">
|
||||
ESPDocs
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
|
||||
Plataforma brasileira e independente que centraliza e organiza informações técnicas sobre o ecossistema ESP32. Focada em clareza, usabilidade e agilidade no desenvolvimento de hardware.
|
||||
<footer className="mt-20 border-t border-line md:mt-28" aria-label="Rodapé do ESPDocs">
|
||||
<div className="shell py-14 md:py-20">
|
||||
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]">
|
||||
<div>
|
||||
<Link href="/" className="inline-flex min-h-11 items-center gap-2.5" aria-label="ESPDocs, página inicial">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/marca/espdocs-marca.svg" alt="" aria-hidden="true" width={28} height={28} className="h-7 w-7" />
|
||||
<span className="font-display text-2xl font-semibold tracking-[-0.045em] text-ink">ESPDocs</span>
|
||||
</Link>
|
||||
<p className="mt-5 max-w-sm text-sm leading-relaxed text-muted">
|
||||
Documentação independente do ecossistema ESP32, escrita em português brasileiro.
|
||||
Dados públicos, curadoria editorial e links para as fontes oficiais.
|
||||
</p>
|
||||
|
||||
<div className="pt-2">
|
||||
<a
|
||||
href="https://github.com/lucasrguerra/ESPDocs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Ver repositório do ESPDocs no GitHub (abre em nova aba)"
|
||||
className="inline-flex items-center gap-2 bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all cursor-pointer hover:shadow-xs active:scale-[0.98]"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>Ver no GitHub</span>
|
||||
|
||||
<Link
|
||||
href="/livro"
|
||||
className="group mt-7 grid grid-cols-[3rem_1fr_auto] items-center gap-3.5 rounded-2xl border border-line bg-surface p-3.5 transition-colors duration-300 hover:border-line-strong"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={SITE.livroCapa}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={48}
|
||||
height={72}
|
||||
className="h-[72px] w-12 rounded-md object-cover shadow-md"
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="text-xs font-semibold text-accent">Livro novo</span>
|
||||
<span className="mt-0.5 block font-display text-base font-semibold leading-tight text-ink">Microcontroladores ESP32</span>
|
||||
<span className="mt-1 block text-xs text-muted">Conectividade, eficiência e desempenho</span>
|
||||
</span>
|
||||
<ArrowUpRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" />
|
||||
</Link>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-sm">
|
||||
<a href={SITE.livroAmazon} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 text-muted transition-colors hover:text-accent">
|
||||
Amazon
|
||||
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</a>
|
||||
<a href={SITE.livroUiclap} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 text-muted transition-colors hover:text-accent">
|
||||
UICLAP
|
||||
<ArrowUpRight className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coluna 2: Links Úteis */}
|
||||
<nav aria-label="Links úteis" className="space-y-4">
|
||||
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
|
||||
Recursos
|
||||
</h4>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/componentes"
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
<span>ESP Component Registry</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/diagnostico"
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5 font-medium"
|
||||
>
|
||||
<span>Guia de Diagnóstico & Erros</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/lucasrguerra/ESPDocs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
|
||||
aria-label="Abrir repositório no GitHub"
|
||||
>
|
||||
<span>Repositório Principal</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/lucasrguerra/ESPDocs/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
|
||||
aria-label="Abrir issues do repositório"
|
||||
>
|
||||
<span>Reportar um Bug / Feedback</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://cienciaembarcada.com.br"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-purple-600 dark:hover:text-purple-400 transition-colors flex items-center gap-1.5"
|
||||
aria-label="Visitar Ciência Embarcada (abre em nova aba)"
|
||||
>
|
||||
<span className="font-semibold text-purple-600 dark:text-purple-400">Ciência Embarcada</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-60 text-purple-600 dark:text-purple-400" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
|
||||
{COLUMNS.map((column) => (
|
||||
<nav key={column.title} aria-label={column.title}>
|
||||
<h2 className="text-sm font-semibold text-ink">{column.title}</h2>
|
||||
<ul className="mt-2 space-y-0">
|
||||
{column.links.map((link) => (
|
||||
<li key={link.href}>
|
||||
{link.external ? (
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-h-11 items-center gap-1 text-sm text-muted transition-colors hover:text-accent"
|
||||
>
|
||||
{link.label}
|
||||
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
) : (
|
||||
<Link href={link.href} className="inline-flex min-h-11 min-w-11 items-center text-sm text-muted transition-colors hover:text-accent">
|
||||
{link.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
))}
|
||||
|
||||
{/* Coluna 3: Autor & Social */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
|
||||
Sobre o Autor
|
||||
</h4>
|
||||
<p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
|
||||
Desenvolvido com dedicação por{' '}
|
||||
<a
|
||||
href="https://linkedin.com/in/lucasrguerra"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold underline text-slate-700 dark:text-slate-200 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
|
||||
aria-label="Perfil LinkedIn de Lucas Rayan Guerra (abre em nova aba)"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-ink">Autor</h2>
|
||||
<p className="mt-4 text-sm leading-relaxed text-muted">
|
||||
Lucas Rayan Guerra
|
||||
</a>
|
||||
, compartilhando eletrônica, IoT e computação no Brasil.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<a
|
||||
href="https://github.com/lucasrguerra/ESPDocs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
|
||||
aria-label="GitHub do projeto"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M12 .5a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0012 .5z" fill="currentColor"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://linkedin.com/in/lucasrguerra"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-200/60 dark:bg-slate-900 hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 transition-all cursor-pointer shadow-xs"
|
||||
aria-label="LinkedIn do autor"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4.98 3.5a2.5 2.5 0 11.001 5.001A2.5 2.5 0 014.98 3.5zM3 8.98h4v12H3v-12zM9.5 8.98h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" fill="currentColor"/>
|
||||
</svg>
|
||||
</a>
|
||||
<br />
|
||||
<span className="text-xs text-faint">Ciência Embarcada</span>
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<a
|
||||
href={SITE.repositorio}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub do projeto (abre em nova aba)"
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .5a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2.1c-3.3.7-4-1.6-4-1.6-.5-1.2-1.2-1.5-1.2-1.5-1-.7.1-.7.1-.7 1.1.1 1.7 1.1 1.7 1.1 1 .1 1.6-.7 1.9-1 .1-.8.4-1.3.8-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.6.1-3.3 0 0 1-.3 3.3 1.2a11.4 11.4 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 3 .1 3.3.8.8 1.2 1.9 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.5.4.9 1.1.9 2.2v3.2c0 .3.2.7.8.6A12 12 0 0 0 12 .5z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href={SITE.autorLinkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn do autor (abre em nova aba)"
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:border-line-strong hover:bg-surface"
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001 2.5 2.5 0 0 1 0-5zM3 8.98h4v12H3v-12zm6.5 0h3.8v1.6h.1c.5-.9 1.8-1.9 3.6-1.9 3.8 0 4.5 2.5 4.5 5.8v6.5h-4v-5.8c0-1.4-.1-3.2-2-3.2-2 0-2.3 1.6-2.3 3.1v5.9h-4v-12z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divisor & Isenção */}
|
||||
<div className="mt-12 pt-8 border-t border-slate-300 dark:border-slate-800/80 flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-slate-500 dark:text-slate-400">
|
||||
<p className="text-center md:text-left leading-relaxed">
|
||||
© {year} ESPDocs. Esta plataforma independente de código aberto não possui afiliação com a Espressif Systems.
|
||||
</p>
|
||||
<p className="text-center md:text-right shrink-0">
|
||||
Feito para a comunidade maker 🇧🇷
|
||||
<div className="mt-14 flex flex-col gap-4 border-t border-line pt-6 text-xs leading-relaxed text-faint md:flex-row md:items-center md:justify-between">
|
||||
<p>© {year} ESPDocs. Projeto independente, sem afiliação com a Espressif Systems.</p>
|
||||
<p className="flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||
<a href={SITE.blog} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent">
|
||||
Ciência Embarcada
|
||||
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
<a href={SITE.autorUrl} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-11 items-center gap-1 transition-colors hover:text-accent">
|
||||
lucasrguerra.dev.br
|
||||
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
<span>Fontes oficiais: Espressif Systems.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
+268
-658
@@ -1,689 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
Home,
|
||||
Info,
|
||||
BookOpen,
|
||||
Cpu,
|
||||
SlidersHorizontal,
|
||||
Terminal,
|
||||
Scale,
|
||||
ShoppingBag,
|
||||
AlertTriangle,
|
||||
Menu,
|
||||
X,
|
||||
Sun,
|
||||
Moon,
|
||||
ChevronDown,
|
||||
Sparkles,
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
ArrowRight,
|
||||
ExternalLink,
|
||||
Compass,
|
||||
Layers,
|
||||
Box
|
||||
BookA,
|
||||
BookOpen,
|
||||
Box,
|
||||
ChevronDown,
|
||||
Cpu,
|
||||
Library,
|
||||
Menu,
|
||||
Moon,
|
||||
Scale,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Sun,
|
||||
X,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
|
||||
const MENUS = [
|
||||
{
|
||||
id: "hardware",
|
||||
label: "Hardware",
|
||||
items: [
|
||||
{ href: "/series", label: "Séries ESP32", desc: "12 chips, pinagem e especificações", icon: Cpu },
|
||||
{ href: "/comparacao", label: "Comparador", desc: "Até quatro séries lado a lado", icon: Scale },
|
||||
{ href: "/catalogo", label: "Catálogo de placas", desc: "DevKits, pinos e onde comprar", icon: Box },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Ferramentas",
|
||||
items: [
|
||||
{ href: "/seletor", label: "Seletor inteligente", desc: "O chip certo para o seu projeto", icon: SlidersHorizontal },
|
||||
{ href: "/diagnostico", label: "Diagnóstico", desc: "Brownout, bootloop, WDT e mais", icon: AlertTriangle },
|
||||
{ href: "/componentes", label: "Component Registry", desc: "Drivers e manifests idf_component.yml", icon: Search },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "library",
|
||||
label: "Biblioteca",
|
||||
items: [
|
||||
{ href: "/frameworks", label: "Frameworks e SDKs", desc: "ESP-IDF, Arduino, Rust e Matter", icon: BookOpen },
|
||||
{ href: "/glossario", label: "Glossário técnico", desc: "Os termos que aparecem nos datasheets", icon: BookA },
|
||||
{ href: "/sobre", label: "Sobre o projeto", desc: "Autoria, método e abertura", icon: Sparkles },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_LINKS = [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparador" },
|
||||
{ href: "/seletor", label: "Seletor inteligente" },
|
||||
{ href: "/diagnostico", label: "Diagnóstico" },
|
||||
{ href: "/frameworks", label: "Frameworks e SDKs" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/glossario", label: "Glossário" },
|
||||
{ href: "/catalogo", label: "Catálogo de placas" },
|
||||
{ href: "/livro", label: "O livro" },
|
||||
{ href: "/sobre", label: "Sobre" },
|
||||
];
|
||||
|
||||
function isActivePath(pathname, href) {
|
||||
return href === "/" ? pathname === "/" : pathname === href || String(pathname).startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [hardwareDropdown, setHardwareDropdown] = useState(false);
|
||||
const [toolsDropdown, setToolsDropdown] = useState(false);
|
||||
const [docsDropdown, setDocsDropdown] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [openMenu, setOpenMenu] = useState(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const navRef = useRef(null);
|
||||
const closeRef = useRef(null);
|
||||
const menuButtonRef = useRef(null);
|
||||
|
||||
const hardwareRef = useRef(null);
|
||||
const toolsRef = useRef(null);
|
||||
const docsRef = useRef(null);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
setOpenMenu(null);
|
||||
setDrawerOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Close dropdowns on outside click
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e) {
|
||||
if (hardwareRef.current && !hardwareRef.current.contains(e.target)) {
|
||||
setHardwareDropdown(false);
|
||||
}
|
||||
if (toolsRef.current && !toolsRef.current.contains(e.target)) {
|
||||
setToolsDropdown(false);
|
||||
}
|
||||
if (docsRef.current && !docsRef.current.contains(e.target)) {
|
||||
setDocsDropdown(false);
|
||||
function onPointerDown(event) {
|
||||
if (navRef.current && !navRef.current.contains(event.target)) setOpenMenu(null);
|
||||
}
|
||||
function onKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
setOpenMenu(null);
|
||||
setDrawerOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Lock scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (drawerOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
document.addEventListener("mousedown", onPointerDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
document.removeEventListener("mousedown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerOpen) return;
|
||||
const previous = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
closeRef.current?.focus();
|
||||
return () => {
|
||||
document.body.style.overflow = previous;
|
||||
menuButtonRef.current?.focus();
|
||||
};
|
||||
}, [drawerOpen]);
|
||||
|
||||
// Close menu on route change
|
||||
useEffect(() => {
|
||||
setDrawerOpen(false);
|
||||
setHardwareDropdown(false);
|
||||
setToolsDropdown(false);
|
||||
setDocsDropdown(false);
|
||||
}, [pathname]);
|
||||
|
||||
function isActive(href) {
|
||||
if (href === "/") {
|
||||
return pathname === "/";
|
||||
}
|
||||
return String(pathname).startsWith(href);
|
||||
}
|
||||
|
||||
const popularChips = [
|
||||
{ name: "ESP32", desc: "Clássico Dual-Core", href: "/series/ESP32" },
|
||||
{ name: "ESP32-S3", desc: "IA & USB Nativo", href: "/series/ESP32-S3" },
|
||||
{ name: "ESP32-C6", desc: "Wi-Fi 6 + Zigbee/Thread", href: "/series/ESP32-C6" },
|
||||
{ name: "ESP32-P4", desc: "Alto Desempenho 400MHz", href: "/series/ESP32-P4" },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-white/90 dark:bg-slate-950/80 backdrop-blur-md py-3.5 px-4 md:px-8 shadow-xs border-b border-slate-200 dark:border-slate-800/60 sticky top-0 z-50">
|
||||
<div className="flex justify-between max-w-7xl mx-auto items-center gap-4 lg:gap-8">
|
||||
{/* LOGO */}
|
||||
<Link href="/" className="group shrink-0 flex items-center gap-2.5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/marca/espdocs-marca.svg"
|
||||
alt="ESPDocs Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-7 h-7 md:w-8 md:h-8 shrink-0 transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<h1 className="text-2xl md:text-3xl font-display font-extrabold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent transition-all duration-300 group-hover:scale-[1.02]">
|
||||
ESPDocs
|
||||
</h1>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden lg:flex items-center gap-1 xl:gap-2 flex-1 justify-center" aria-label="Menu principal">
|
||||
{/* Link Início */}
|
||||
<Link
|
||||
href="/"
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 ${
|
||||
isActive("/") && pathname === "/"
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
|
||||
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
|
||||
}`}
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
<span>Início</span>
|
||||
<header className="sticky top-0 z-50 pt-3 sm:pt-4">
|
||||
<div className="shell">
|
||||
<div className="nav-pill flex h-16 items-center justify-between gap-3 rounded-full px-3 sm:px-4">
|
||||
<Link href="/" className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label="ESPDocs, página inicial">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/marca/espdocs-marca.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-[30px] w-[30px]"
|
||||
/>
|
||||
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
|
||||
</Link>
|
||||
|
||||
{/* Dropdown: Séries & Hardware */}
|
||||
<div className="relative" ref={hardwareRef}>
|
||||
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label="Navegação principal">
|
||||
{MENUS.map((menu) => {
|
||||
const active = menu.items.some((item) => isActivePath(pathname, item.href));
|
||||
const expanded = openMenu === menu.id;
|
||||
return (
|
||||
<div key={menu.id} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${active ? "is-active" : ""}`}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={`menu-${menu.id}`}
|
||||
onClick={() => setOpenMenu(expanded ? null : menu.id)}
|
||||
>
|
||||
{menu.label}
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${expanded ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
</button>
|
||||
{expanded && (
|
||||
<div
|
||||
id={`menu-${menu.id}`}
|
||||
className="absolute left-0 top-full mt-3 w-[23rem] rounded-[1.5rem] border border-line bg-surface p-2 shadow-[0_24px_60px_-20px_rgba(15,23,42,0.35)] dark:shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)]"
|
||||
>
|
||||
{menu.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="group flex items-start gap-3 rounded-2xl px-3 py-3 transition-colors hover:bg-surface-2"
|
||||
aria-current={isActivePath(pathname, item.href) ? "page" : undefined}
|
||||
>
|
||||
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent-soft text-accent transition-colors group-hover:bg-surface">
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-ink">{item.label}</span>
|
||||
<span className="mt-0.5 block text-xs leading-relaxed text-muted">{item.desc}</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Link href="/livro" className={`nav-link ${isActivePath(pathname, "/livro") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro") ? "page" : undefined}>
|
||||
Livro
|
||||
</Link>
|
||||
<Link href="/sobre" className={`nav-link ${isActivePath(pathname, "/sobre") ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre") ? "page" : undefined}>
|
||||
Sobre
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Link href="/seletor" className="btn btn-primary btn-sm hidden lg:inline-flex">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
Selecionar chip
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={() => {
|
||||
setHardwareDropdown(!hardwareDropdown);
|
||||
setToolsDropdown(false);
|
||||
setDocsDropdown(false);
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
|
||||
isActive("/series") || isActive("/comparacao") || isActive("/catalogo")
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
|
||||
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
|
||||
}`}
|
||||
aria-expanded={hardwareDropdown}
|
||||
ref={menuButtonRef}
|
||||
type="button"
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2 lg:hidden"
|
||||
aria-label="Abrir menu de navegação"
|
||||
aria-expanded={drawerOpen}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
<Cpu className="w-4 h-4" />
|
||||
<span>Séries & Chips</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${hardwareDropdown ? "rotate-180" : ""}`} />
|
||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{hardwareDropdown && (
|
||||
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
|
||||
<div className="space-y-1 mb-2">
|
||||
<Link
|
||||
href="/series"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 shrink-0">
|
||||
<Cpu className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 flex items-center gap-1.5">
|
||||
Todas as 12 Séries
|
||||
<span className="text-[10px] bg-blue-100 dark:bg-blue-950/60 text-blue-700 dark:text-blue-300 font-bold px-1.5 py-0.5 rounded-md">12</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Pinouts, especificações e restrições</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/comparacao"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<Scale className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Comparador de Séries
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Compare até 4 modelos lado a lado</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/catalogo"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400 shrink-0">
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Catálogo de Placas
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Placas de dev, conectores e compras</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Atalhos Rápidos Séries Populares */}
|
||||
<div className="pt-2 border-t border-slate-200 dark:border-slate-800/80">
|
||||
<div className="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-2.5 mb-1.5">
|
||||
Séries Populares
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{popularChips.map((chip) => (
|
||||
<Link
|
||||
key={chip.name}
|
||||
href={chip.href}
|
||||
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors text-left"
|
||||
>
|
||||
<span className="text-xs font-bold text-slate-850 dark:text-slate-100 block truncate">
|
||||
{chip.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-400 block truncate">
|
||||
{chip.desc}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown: Ferramentas */}
|
||||
<div className="relative" ref={toolsRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setToolsDropdown(!toolsDropdown);
|
||||
setHardwareDropdown(false);
|
||||
setDocsDropdown(false);
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
|
||||
isActive("/seletor") || isActive("/diagnostico")
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
|
||||
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
|
||||
}`}
|
||||
aria-expanded={toolsDropdown}
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>Ferramentas</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${toolsDropdown ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{toolsDropdown && (
|
||||
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/seletor"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 shrink-0">
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 flex items-center gap-1.5">
|
||||
Seletor Inteligente
|
||||
<span className="text-[10px] bg-purple-100 dark:bg-purple-950/60 text-purple-700 dark:text-purple-300 font-bold px-1.5 py-0.5 rounded-md">Quiz</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Descubra o chip ideal para o seu projeto</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/diagnostico"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 shrink-0">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-amber-600 dark:group-hover:text-amber-400 flex items-center gap-1.5">
|
||||
Diagnóstico de Erros
|
||||
<span className="text-[10px] bg-amber-100 dark:bg-amber-950/60 text-amber-700 dark:text-amber-300 font-bold px-1.5 py-0.5 rounded-md">30+ Erros</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Solucione Brownout, Bootloops, WDT e falhas</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown: Documentação */}
|
||||
<div className="relative" ref={docsRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDocsDropdown(!docsDropdown);
|
||||
setHardwareDropdown(false);
|
||||
setToolsDropdown(false);
|
||||
}}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl font-semibold text-sm transition-all duration-200 cursor-pointer ${
|
||||
isActive("/frameworks") || isActive("/componentes") || isActive("/glossario") || isActive("/sobre")
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400"
|
||||
: "text-slate-650 dark:text-slate-350 hover:bg-slate-100/70 dark:hover:bg-slate-800/60 hover:text-slate-850 dark:hover:text-slate-100"
|
||||
}`}
|
||||
aria-expanded={docsDropdown}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>Documentação</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${docsDropdown ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{docsDropdown && (
|
||||
<div className="absolute left-0 mt-2 w-80 bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 p-3 z-50 animate-fadeIn">
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/frameworks"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-teal-50 dark:bg-teal-950/40 text-teal-600 dark:text-teal-400 shrink-0">
|
||||
<Terminal className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Frameworks & SDKs
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">ESP-IDF, Arduino, Rust, RainMaker e mais</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/componentes"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 shrink-0">
|
||||
<Box className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Component Registry
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Catálogo de drivers e gerador idf_component.yml</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/glossario"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-pink-50 dark:bg-pink-950/40 text-pink-600 dark:text-pink-400 shrink-0">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Glossário Técnico
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Conceitos essenciais de hardware e software</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sobre"
|
||||
className="flex items-start gap-3 p-2.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/70 transition-colors group"
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-650 dark:text-slate-350 shrink-0">
|
||||
<Info className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-slate-850 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">
|
||||
Sobre o Projeto
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">História, autor e Ciência Embarcada</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Right CTA + Theme + Full Drawer Button */}
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
{/* CTA Button Desktop */}
|
||||
<Link
|
||||
href="/seletor"
|
||||
className="hidden sm:inline-flex items-center gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-2 rounded-xl font-bold text-xs shadow-sm hover:shadow-purple-500/20 hover:-translate-y-0.5 transition-all duration-200 active:scale-95 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span>Seletor de ESP32</span>
|
||||
</Link>
|
||||
|
||||
<div className="h-5 w-px bg-slate-200 dark:bg-slate-800 mx-1 hidden sm:block" />
|
||||
|
||||
{/* Theme Switcher Button */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Full Drawer Trigger Button */}
|
||||
<button
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
className="p-2 md:p-2.5 rounded-xl border border-slate-300 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-purple-500 dark:hover:border-purple-400 hover:text-purple-600 dark:hover:text-purple-400 transition-all cursor-pointer flex items-center gap-1.5"
|
||||
aria-label="Abrir menu de navegação completo"
|
||||
aria-expanded={drawerOpen}
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
<span className="text-xs font-bold hidden xl:inline">Menu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Full Screen / Side Drawer Portal */}
|
||||
{mounted && createPortal(
|
||||
<>
|
||||
{drawerOpen && (
|
||||
{mounted &&
|
||||
createPortal(
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/60 dark:bg-black/80 backdrop-blur-xs z-[100] transition-opacity duration-300"
|
||||
className={`fixed inset-0 z-[100] bg-black/45 backdrop-blur-sm transition-opacity duration-200 ${
|
||||
drawerOpen ? "opacity-100" : "hidden"
|
||||
}`}
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-screen w-96 max-w-[88vw] bg-white dark:bg-slate-900 shadow-2xl z-[101] transform transition-transform duration-300 ease-in-out flex flex-col border-l border-slate-200 dark:border-slate-800 ${
|
||||
drawerOpen ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
>
|
||||
{/* Drawer Header */}
|
||||
<div className="flex items-center justify-between p-5 border-b border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/marca/espdocs-marca.svg" alt="" aria-hidden="true" width={28} height={28} className="w-7 h-7" />
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menu de navegação"
|
||||
aria-hidden={!drawerOpen}
|
||||
inert={!drawerOpen}
|
||||
className={`fixed inset-y-0 right-0 z-[101] flex w-[min(23rem,94vw)] flex-col rounded-l-[2rem] border-l border-line bg-canvas transition-transform duration-300 ${
|
||||
drawerOpen ? "translate-x-0" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between px-5 py-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-display font-extrabold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
|
||||
Navegação ESPDocs
|
||||
</h2>
|
||||
<p className="text-[11px] text-slate-500 dark:text-slate-400">Guia Técnico em Português</p>
|
||||
<p className="text-xs font-semibold text-muted">Navegação</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">Para onde ir</p>
|
||||
</div>
|
||||
<button
|
||||
ref={closeRef}
|
||||
type="button"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="grid h-10 w-10 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
||||
aria-label="Fechar menu"
|
||||
>
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-2">
|
||||
<Link
|
||||
href="/livro"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="group grid grid-cols-[3.25rem_1fr_auto] items-center gap-3 rounded-2xl bg-[#0b0d10] p-3 text-[#f4f6f8] ring-1 ring-white/10"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/livro/capa.jpg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={52}
|
||||
height={78}
|
||||
className="h-[78px] w-[52px] rounded-md object-cover shadow-lg"
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="text-[0.6875rem] font-semibold text-[#75cdc1]">Livro novo</span>
|
||||
<span className="mt-1 block font-display text-lg font-semibold leading-tight">Microcontroladores ESP32</span>
|
||||
<span className="mt-1 block text-xs text-[#a6afbd]">Conectividade, eficiência e desempenho</span>
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 shrink-0 text-[#75cdc1] transition-transform duration-300 group-hover:translate-x-1" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel">
|
||||
{MOBILE_LINKS.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="group flex min-h-11 items-center justify-between gap-3 rounded-xl px-3 text-sm font-medium text-ink-soft transition-colors hover:bg-surface-2 hover:text-ink"
|
||||
aria-current={isActivePath(pathname, link.href) ? "page" : undefined}
|
||||
>
|
||||
{link.label}
|
||||
<ArrowRight className="h-4 w-4 text-faint transition-transform duration-300 group-hover:translate-x-0.5 group-hover:text-accent" aria-hidden="true" />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 pt-2">
|
||||
<Link href="/seletor" onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
Selecionar meu chip
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="p-2 text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-white rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 transition-all cursor-pointer"
|
||||
aria-label="Fechar menu lateral"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Action Highlights */}
|
||||
<div className="p-4 bg-slate-50 dark:bg-slate-950/50 border-b border-slate-200 dark:border-slate-800 space-y-2">
|
||||
<Link
|
||||
href="/seletor"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-sm hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Compass className="w-4 h-4 shrink-0" />
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-xs">Seletor Inteligente</div>
|
||||
<div className="text-[10px] text-purple-100">Encontre o ESP32 certo</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/diagnostico"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white shadow-sm hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-xs">Diagnóstico de Falhas</div>
|
||||
<div className="text-[10px] text-amber-100">Resolva Brownout, bootloop e erros</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Drawer Categorized Menu */}
|
||||
<nav className="flex-1 overflow-y-auto p-5 space-y-6" aria-label="Navegação móvel completa">
|
||||
{/* Seção 1: Hardware & Microcontroladores */}
|
||||
<div>
|
||||
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
|
||||
Hardware & Séries
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/series"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center justify-between p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/series") && pathname === "/series"
|
||||
? "bg-blue-50 dark:bg-blue-950/40 text-blue-600 dark:text-blue-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Cpu className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<span>Todas as Séries</span>
|
||||
</div>
|
||||
<span className="text-[11px] bg-slate-200 dark:bg-slate-800 text-slate-650 dark:text-slate-350 px-2 py-0.5 rounded-full font-bold">
|
||||
12
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/comparacao"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/comparacao")
|
||||
? "bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<Scale className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
||||
<span>Comparar Séries</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/catalogo"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/catalogo")
|
||||
? "bg-emerald-50 dark:bg-emerald-950/40 text-emerald-600 dark:text-emerald-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4 text-emerald-600 dark:text-emerald-400" />
|
||||
<span>Catálogo de Placas</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Atalhos Rápidos com Cores */}
|
||||
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-800/80">
|
||||
<div className="text-[10px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2 px-1">
|
||||
Acesso Rápido a Chips
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{popularChips.map((chip) => (
|
||||
<Link
|
||||
key={chip.name}
|
||||
href={chip.href}
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="p-2 rounded-xl bg-slate-50 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200/60 dark:border-slate-750 transition-colors"
|
||||
>
|
||||
<span className="text-xs font-bold text-slate-850 dark:text-slate-100 block">
|
||||
{chip.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-400 block truncate">
|
||||
{chip.desc}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seção 2: Ferramentas & Diagnóstico */}
|
||||
<div>
|
||||
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
|
||||
Ferramentas Interativas
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/seletor"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/seletor")
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<SlidersHorizontal className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span>Seletor Inteligente</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/diagnostico"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/diagnostico")
|
||||
? "bg-amber-50 dark:bg-amber-950/40 text-amber-600 dark:text-amber-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<span>Diagnóstico & Solução de Erros</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seção 3: Documentação & Referência */}
|
||||
<div>
|
||||
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
|
||||
Guias & Documentação
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/frameworks"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/frameworks")
|
||||
? "bg-teal-50 dark:bg-teal-950/40 text-teal-600 dark:text-teal-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<Terminal className="w-4 h-4 text-teal-600 dark:text-teal-400" />
|
||||
<span>Frameworks & SDKs</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/componentes"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/componentes")
|
||||
? "bg-purple-50 dark:bg-purple-950/40 text-purple-600 dark:text-purple-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<Box className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span>Component Registry</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/glossario"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/glossario")
|
||||
? "bg-pink-50 dark:bg-pink-950/40 text-pink-600 dark:text-pink-400 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<BookOpen className="w-4 h-4 text-pink-600 dark:text-pink-400" />
|
||||
<span>Glossário Técnico</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seção 4: Institucional & Comunidade */}
|
||||
<div>
|
||||
<div className="text-[11px] font-extrabold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-2.5 px-1">
|
||||
Comunidade & Projeto
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/sobre"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center gap-2.5 p-2.5 rounded-xl font-medium text-sm transition-colors ${
|
||||
isActive("/sobre")
|
||||
? "bg-slate-100 dark:bg-slate-800 text-slate-850 dark:text-slate-100 font-bold"
|
||||
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60"
|
||||
}`}
|
||||
>
|
||||
<Info className="w-4 h-4 text-slate-500 dark:text-slate-400" />
|
||||
<span>Sobre a Plataforma</span>
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href="https://cienciaembarcada.com.br"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-2.5 rounded-xl font-medium text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Layers className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
<span>Blog Ciência Embarcada</span>
|
||||
</div>
|
||||
<ExternalLink className="w-3.5 h-3.5 text-slate-500 dark:text-slate-400 group-hover:text-purple-600 dark:group-hover:text-purple-400" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Drawer Footer */}
|
||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/50 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||
ESPDocs • 100% em Português
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400">
|
||||
12 Séries • 8 Frameworks • 30+ Erros
|
||||
</p>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
, document.body
|
||||
)}
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/* ThemeToggle Component */
|
||||
function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -691,22 +303,20 @@ function ThemeToggle() {
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 animate-pulse border border-transparent" />
|
||||
);
|
||||
return <span className="h-11 w-11 rounded-full border border-line bg-surface-2" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
const dark = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
||||
className="w-9 h-9 md:w-10 md:h-10 rounded-xl bg-slate-100 dark:bg-slate-900 text-slate-650 dark:text-slate-350 hover:bg-slate-200 dark:hover:bg-slate-800/80 hover:text-purple-600 dark:hover:text-purple-400 border border-slate-200 dark:border-slate-800 shadow-xs flex items-center justify-center transition-all duration-200 cursor-pointer active:scale-95 shrink-0"
|
||||
aria-label="Alternar tema claro/escuro"
|
||||
type="button"
|
||||
onClick={() => setTheme(dark ? "light" : "dark")}
|
||||
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
||||
aria-label={dark ? "Ativar tema claro" : "Ativar tema escuro"}
|
||||
title={dark ? "Tema claro" : "Tema escuro"}
|
||||
>
|
||||
{resolvedTheme === "dark" ? (
|
||||
<Sun className="w-4 h-4 md:w-5 md:h-5 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4 md:w-5 md:h-5 text-indigo-600" />
|
||||
)}
|
||||
{dark ? <Sun className="h-4 w-4" aria-hidden="true" /> : <Moon className="h-4 w-4" aria-hidden="true" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Abertura padrão das páginas internas: trilha, rótulo, título, frase de apoio
|
||||
* e ações. Mesma voz da /livro e da home, para que nenhuma página pareça
|
||||
* de outro produto.
|
||||
*/
|
||||
export default function PageHero({ trail = [], icon: Icon, label, title, lede, align = "left", children }) {
|
||||
const center = align === "center";
|
||||
return (
|
||||
<section className="shell pt-8 sm:pt-12">
|
||||
{trail.length > 0 && (
|
||||
<nav aria-label="Trilha de navegação" className="flex flex-wrap items-center gap-2 text-sm text-muted">
|
||||
<Link href="/" className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
|
||||
Início
|
||||
</Link>
|
||||
{trail.map((item, i) => (
|
||||
<span key={item.label} className="flex min-h-11 items-center gap-2">
|
||||
<span aria-hidden="true" className="text-line-strong">/</span>
|
||||
{item.href && i < trail.length - 1 ? (
|
||||
<Link href={item.href} className="inline-flex min-h-11 min-w-11 items-center justify-center transition-colors hover:text-accent">
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span aria-current="page" className="text-ink">
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<div className={`${trail.length ? "mt-8 sm:mt-10" : ""} max-w-3xl ${center ? "mx-auto text-center" : ""}`}>
|
||||
{label && (
|
||||
<span className="chip chip-accent">
|
||||
{Icon && <Icon className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
<h1 className={`page-title ${label ? "mt-5" : ""}`}>{title}</h1>
|
||||
{lede && <p className={`lede mt-5 max-w-2xl ${center ? "mx-auto" : ""}`}>{lede}</p>}
|
||||
{children && <div className={`mt-7 flex flex-wrap gap-3 ${center ? "justify-center" : ""}`}>{children}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ const GRUPOS = [
|
||||
id: "entrada",
|
||||
titulo: "Somente entrada",
|
||||
icone: ArrowDownToLine,
|
||||
cor: "text-sky-500",
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Aceitam apenas leitura. Não podem ser configurados como saída e não possuem resistores de pull-up ou pull-down internos. Se precisar de um, coloque no circuito.",
|
||||
combina: (avisos) => /apenas de entrada|input only/.test(avisos),
|
||||
@@ -32,7 +32,7 @@ const GRUPOS = [
|
||||
id: "flash",
|
||||
titulo: "Ligados à memória flash/PSRAM",
|
||||
icone: HardDrive,
|
||||
cor: "text-rose-500",
|
||||
cor: "text-copper",
|
||||
explicacao:
|
||||
"Já estão conectados à memória de onde o firmware é lido. Usá-los como GPIO comum normalmente trava o chip ou corrompe a execução. Em módulos com flash interna eles nem chegam ao encapsulamento.",
|
||||
combina: (avisos) =>
|
||||
@@ -43,7 +43,7 @@ const GRUPOS = [
|
||||
id: "strapping",
|
||||
titulo: "Strapping (modo de boot)",
|
||||
icone: Power,
|
||||
cor: "text-amber-500",
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Dá para usá-los depois do boot, mas o circuito externo não pode forçar o nível errado durante o reset, senão a placa não inicia ou entra em modo de gravação sozinha.",
|
||||
combina: (avisos, cats) =>
|
||||
@@ -54,7 +54,7 @@ const GRUPOS = [
|
||||
id: "debug",
|
||||
titulo: "Gravação e depuração",
|
||||
icone: Bug,
|
||||
cor: "text-violet-500",
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"São a UART de gravação e as linhas de JTAG. Continuam utilizáveis como GPIO, mas ao reaproveitá-los você perde o caminho de gravar e depurar a placa sem desconectar o que estiver ali.",
|
||||
combina: (avisos, cats) => /debugging/.test(avisos) || cats.includes("JTAG"),
|
||||
@@ -63,7 +63,7 @@ const GRUPOS = [
|
||||
id: "usb",
|
||||
titulo: "USB nativo",
|
||||
icone: Usb,
|
||||
cor: "text-teal-500",
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Saem de fábrica ligados ao USB Serial/JTAG interno. Para usá-los como GPIO é preciso desativar essa função, o que também remove a gravação por USB direto.",
|
||||
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/.test(avisos),
|
||||
@@ -72,7 +72,7 @@ const GRUPOS = [
|
||||
id: "eletrico",
|
||||
titulo: "Não podem ficar flutuando",
|
||||
icone: Zap,
|
||||
cor: "text-orange-500",
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"Precisam de nível definido por hardware: pull-up externo ou circuito RC de atraso. Deixá-los soltos causa reset aleatório ou inicialização instável.",
|
||||
combina: (avisos) => /flutuando/.test(avisos),
|
||||
@@ -101,7 +101,7 @@ function derivarGrupos(conexoes) {
|
||||
}).filter((g) => g.pinos.length > 0);
|
||||
}
|
||||
|
||||
export default function PinRestrictions({ conexoes, serieKey, cor }) {
|
||||
export default function PinRestrictions({ conexoes, serieKey }) {
|
||||
if (!conexoes?.length) return null;
|
||||
|
||||
const grupos = derivarGrupos(conexoes);
|
||||
@@ -110,69 +110,54 @@ export default function PinRestrictions({ conexoes, serieKey, cor }) {
|
||||
const totalPinos = new Set(grupos.flatMap((g) => g.pinos)).size;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-300 dark:border-slate-800/80 shadow-2xl mb-12">
|
||||
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-3 pb-3 border-b border-slate-200 dark:border-slate-800/65 flex items-center gap-3">
|
||||
<AlertTriangle className="w-5 h-5" style={{ color: cor }} />
|
||||
<span>Pinos com Restrição de Uso</span>
|
||||
<section id="restricoes" aria-labelledby="restricoes-titulo" className="card mt-5 scroll-mt-28 p-5 sm:p-7">
|
||||
<h2 id="restricoes-titulo" className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
<span className="grid h-9 w-9 place-items-center rounded-full bg-gold-soft text-gold">
|
||||
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
Pinos com restrição de uso
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-650 dark:text-slate-350 leading-relaxed mb-8">
|
||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
||||
Nem todo pino do {serieKey} está livre para uso geral. {totalPinos} deles têm alguma
|
||||
condição: já estão ocupados por memória, são lidos durante o boot ou só funcionam
|
||||
como entrada. Vale conferir antes de fechar o esquemático.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
{grupos.map(({ id, titulo, icone: Icone, cor: corIcone, explicacao, pinos }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="bg-slate-50 dark:bg-slate-950/40 rounded-2xl p-5 border border-slate-200 dark:border-slate-800/60"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 mb-3">
|
||||
<Icone className={`w-4 h-4 shrink-0 ${corIcone}`} />
|
||||
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-200">{titulo}</h3>
|
||||
<span className="ml-auto text-[10px] font-bold text-slate-500 dark:text-slate-400 tabular">
|
||||
<div key={id} className="tile">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icone className={`h-4 w-4 shrink-0 ${corIcone}`} aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold text-ink">{titulo}</h3>
|
||||
<span className="ml-auto text-xs text-muted tabular-nums">
|
||||
{pinos.length} {pinos.length === 1 ? "pino" : "pinos"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 mb-3.5">
|
||||
<ul className="mt-3 flex flex-wrap gap-1.5">
|
||||
{pinos.map((p) => (
|
||||
<span
|
||||
key={p}
|
||||
className="px-2 py-1 rounded-lg text-[10px] font-bold font-mono"
|
||||
style={{
|
||||
backgroundColor: `${cor}18`,
|
||||
color: cor,
|
||||
border: `1px solid ${cor}30`,
|
||||
}}
|
||||
>
|
||||
<li key={p} className="rounded-md border border-line bg-surface px-2 py-1 font-mono text-xs text-ink-soft">
|
||||
{p}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{explicacao}
|
||||
</p>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted">{explicacao}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-7 pt-6 border-t border-slate-200 dark:border-slate-800/65 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed max-w-xl">
|
||||
<div className="mt-6 flex flex-col justify-between gap-4 border-t border-line pt-5 sm:flex-row sm:items-center">
|
||||
<p className="max-w-xl text-sm leading-relaxed text-muted">
|
||||
Estes grupos vêm dos mesmos dados do diagrama de conexões. Lá você vê pino a pino
|
||||
todas as funções, o domínio de alimentação e os avisos completos de cada um.
|
||||
todas as funções, o domínio de alimentação e os avisos completos.
|
||||
</p>
|
||||
<Link
|
||||
href="#connections"
|
||||
className="inline-flex items-center justify-center gap-2 shrink-0 px-5 py-3 rounded-xl font-bold text-xs uppercase tracking-wider text-white transition-opacity hover:opacity-90"
|
||||
style={{ backgroundColor: cor }}
|
||||
>
|
||||
<span>Abrir diagrama de pinos</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<Link href="#connections" className="btn btn-ghost shrink-0">
|
||||
Abrir diagrama de pinos
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** Título de seção: rótulo curto + título + frase de apoio, alinhado à esquerda. */
|
||||
export default function SectionHead({ id, eyebrow, title, lede, children }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="max-w-2xl">
|
||||
{eyebrow && <p className="eyebrow">{eyebrow}</p>}
|
||||
<h2 id={id} className={`section-title ${eyebrow ? "mt-3" : ""}`}>
|
||||
{title}
|
||||
</h2>
|
||||
{lede && <p className="mt-3 text-base leading-relaxed text-muted">{lede}</p>}
|
||||
</div>
|
||||
{children && <div className="shrink-0">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function SeriesTabMenu({ tabs, color }) {
|
||||
const availableTabs = tabs.filter(tab => tab.available);
|
||||
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'specs');
|
||||
export default function SeriesTabMenu({ tabs }) {
|
||||
const availableTabs = tabs.filter((tab) => tab.available);
|
||||
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id);
|
||||
const refs = useRef({});
|
||||
|
||||
// Troca manual limpa o hash. Sem isso, depois de abrir o diagrama pelo link e
|
||||
// mudar de aba na mão, o hash continuaria em #connections e um novo clique no
|
||||
@@ -13,7 +13,7 @@ export default function SeriesTabMenu({ tabs, color }) {
|
||||
const trocarAba = (id) => {
|
||||
setActiveTab(id);
|
||||
if (window.location.hash) {
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,93 +21,67 @@ export default function SeriesTabMenu({ tabs, color }) {
|
||||
// É o que permite a seção de restrições apontar para cá.
|
||||
useEffect(() => {
|
||||
const abrirPeloHash = () => {
|
||||
const alvo = window.location.hash.replace('#', '');
|
||||
if (alvo && availableTabs.some(t => t.id === alvo)) {
|
||||
const alvo = window.location.hash.replace("#", "");
|
||||
if (alvo && availableTabs.some((t) => t.id === alvo)) {
|
||||
setActiveTab(alvo);
|
||||
// Nenhum elemento tem id="connections", então o navegador não rola
|
||||
// sozinho: levamos até as abas na mão.
|
||||
const semAnimacao = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
document.getElementById('diagrama-abas')?.scrollIntoView({
|
||||
behavior: semAnimacao ? 'auto' : 'smooth',
|
||||
block: 'start',
|
||||
const semAnimacao = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
document.getElementById("diagrama-abas")?.scrollIntoView({
|
||||
behavior: semAnimacao ? "auto" : "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
};
|
||||
abrirPeloHash();
|
||||
window.addEventListener('hashchange', abrirPeloHash);
|
||||
return () => window.removeEventListener('hashchange', abrirPeloHash);
|
||||
window.addEventListener("hashchange", abrirPeloHash);
|
||||
return () => window.removeEventListener("hashchange", abrirPeloHash);
|
||||
}, [availableTabs]);
|
||||
|
||||
const activeTabContent = availableTabs.find(tab => tab.id === activeTab)?.content;
|
||||
// Setas, Home e End movem o foco entre as abas, como no padrão WAI-ARIA.
|
||||
const aoTeclar = (evento, indice) => {
|
||||
const total = availableTabs.length;
|
||||
const destino =
|
||||
evento.key === "ArrowRight" ? (indice + 1) % total
|
||||
: evento.key === "ArrowLeft" ? (indice - 1 + total) % total
|
||||
: evento.key === "Home" ? 0
|
||||
: evento.key === "End" ? total - 1
|
||||
: null;
|
||||
if (destino === null) return;
|
||||
evento.preventDefault();
|
||||
const id = availableTabs[destino].id;
|
||||
trocarAba(id);
|
||||
refs.current[id]?.focus();
|
||||
};
|
||||
|
||||
if (availableTabs.length === 0) return null;
|
||||
const ativa = availableTabs.find((tab) => tab.id === activeTab) ?? availableTabs[0];
|
||||
|
||||
return (
|
||||
<div className="mb-12" id="diagrama-abas">
|
||||
{/* Desktop Tabs */}
|
||||
<div className="hidden md:flex bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-xl py-2 px-3 mb-6 border border-slate-300 dark:border-slate-800/80 gap-3">
|
||||
{availableTabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => trocarAba(tab.id)}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 px-5 rounded-xl font-bold text-xs uppercase tracking-wider transition-all duration-300 cursor-pointer active:scale-98 ${
|
||||
isActive
|
||||
? 'text-white shadow-lg scale-[1.02]'
|
||||
: 'text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-800/40 hover:text-slate-800 dark:hover:text-slate-100'
|
||||
}`}
|
||||
style={
|
||||
isActive
|
||||
? { backgroundColor: color, boxShadow: `0 10px 20px -10px ${color}` }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile Dropdown Tab Selector */}
|
||||
<div className="md:hidden mb-6">
|
||||
<div className="bg-white dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl shadow-lg border border-slate-300 dark:border-slate-800/80 overflow-hidden">
|
||||
<div id="diagrama-abas" className="scroll-mt-28">
|
||||
<div role="tablist" aria-label="Placa e diagrama" className="no-scrollbar flex gap-2 overflow-x-auto pb-1">
|
||||
{availableTabs.map((tab, indice) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
const dropdown = document.getElementById('mobile-tab-dropdown');
|
||||
dropdown.classList.toggle('hidden');
|
||||
}}
|
||||
className="w-full flex items-center justify-between py-4 px-6 font-bold text-sm uppercase tracking-wider text-white transition-all duration-300"
|
||||
style={{ backgroundColor: color }}
|
||||
key={tab.id}
|
||||
ref={(el) => (refs.current[tab.id] = el)}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`aba-${tab.id}`}
|
||||
aria-selected={ativa.id === tab.id}
|
||||
aria-controls={`painel-${tab.id}`}
|
||||
tabIndex={ativa.id === tab.id ? 0 : -1}
|
||||
onClick={() => trocarAba(tab.id)}
|
||||
onKeyDown={(e) => aoTeclar(e, indice)}
|
||||
className="chip-btn shrink-0"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span>
|
||||
{availableTabs.find(tab => tab.id === activeTab)?.label}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="w-4 h-4 shrink-0 transition-transform duration-300" />
|
||||
{tab.label}
|
||||
</button>
|
||||
|
||||
<div id="mobile-tab-dropdown" className="hidden">
|
||||
{availableTabs.map((tab) => (
|
||||
tab.id !== activeTab && (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
trocarAba(tab.id);
|
||||
document.getElementById('mobile-tab-dropdown').classList.add('hidden');
|
||||
}}
|
||||
className="w-full flex items-center gap-3 py-4 px-6 text-slate-700 dark:text-slate-300 hover:bg-slate-150/40 dark:hover:bg-slate-800/30 transition-colors border-t border-slate-300 dark:border-slate-800/60 font-semibold text-xs uppercase tracking-wider text-left"
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="animate-fadeIn">
|
||||
{activeTabContent}
|
||||
<div role="tabpanel" id={`painel-${ativa.id}`} aria-labelledby={`aba-${ativa.id}`} className="mt-4">
|
||||
{ativa.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+60
@@ -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 |
@@ -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 |
@@ -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 |
+101
-200
@@ -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.
|
||||
* Uso: npm run auditar-temas
|
||||
*/
|
||||
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 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 arquivos(dir) {
|
||||
return readdirSync(dir).flatMap((nome) => {
|
||||
const caminho = join(dir, nome);
|
||||
if (statSync(caminho).isDirectory()) return arquivos(caminho);
|
||||
return /\.(jsx|js)$/.test(nome) ? [caminho] : [];
|
||||
});
|
||||
}
|
||||
|
||||
let problemas = 0;
|
||||
const relatar = (arq, linha, regra, trecho, dica) => {
|
||||
problemas++;
|
||||
console.log(` \x1b[31m✖\x1b[0m ${arq.replace(RAIZ, "")}:${linha} [${regra}] ${trecho} → ${dica}`);
|
||||
};
|
||||
|
||||
const FUNDO_CLARO = "#ffffff";
|
||||
const FUNDO_ESCURO = "#0f172a"; // slate-900, superfície de card no tema escuro
|
||||
const AA = 4.5;
|
||||
|
||||
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) {
|
||||
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));
|
||||
|
||||
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");
|
||||
// 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");
|
||||
const texto = SEM_COMENTARIO(bruto);
|
||||
const linhas = texto.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"]);
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
const secao = (titulo, itens) => {
|
||||
console.log(`\n${titulo}: ${itens.length}`);
|
||||
for (const i of itens) console.log(" ", ...i);
|
||||
};
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
process.exitCode = main();
|
||||
// 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)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user