feat(i18n): English localization (phase 3)
- Translate content to English across messages/en.json, data/en, lib/data/en, and public/exemplos/en - Implement localized routing, slugs, and language negotiation with fallback to en - Build dedicated search index for English with strict book exclusion - Update components and pages to dynamically render localized strings and datasets - Verify full structural parity and exact 1:1 match on Portuguese pages via comparator Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
1 parent
22b1f26aed
commit
c1bd2c7e11
61 files changed
+18182
-1225
No files matched your search
@@ -1,8 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const HEADER_MAP_EN = {
|
||||
"Nome": "Name",
|
||||
"Descrição": "Description",
|
||||
"Série": "Series",
|
||||
"Loja Oficial": "Official Store",
|
||||
"Fabricante": "Manufacturer",
|
||||
"Processador": "Processor",
|
||||
"Memória Flash": "Flash Memory",
|
||||
"PSRAM": "PSRAM",
|
||||
"Conector USB": "USB Connector",
|
||||
"Interface USB": "USB Interface",
|
||||
"Antena": "Antenna",
|
||||
"Bateria": "Battery",
|
||||
"Display": "Display",
|
||||
"Sensores": "Sensors",
|
||||
"Botões": "Buttons",
|
||||
"LEDs": "LEDs",
|
||||
};
|
||||
|
||||
export default function BoardCard({ board }) {
|
||||
const pathname = usePathname() || "";
|
||||
const isEn = pathname === "/en" || pathname.startsWith("/en/");
|
||||
|
||||
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
|
||||
|
||||
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
|
||||
@@ -24,20 +47,21 @@ export default function BoardCard({ board }) {
|
||||
{lojaOficial && (
|
||||
<span className="chip chip-accent mb-3 w-fit">
|
||||
<Award className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Loja oficial
|
||||
{isEn ? "Official store" : "Loja oficial"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<h3 className="line-clamp-2 font-display text-lg font-semibold leading-snug tracking-[-0.02em] text-ink">
|
||||
{board.Nome || "Placa genérica"}
|
||||
{board.Nome || (isEn ? "Generic board" : "Placa genérica")}
|
||||
</h3>
|
||||
|
||||
{recursos.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-xs font-medium text-muted">Recursos embarcados</p>
|
||||
<p className="text-xs font-medium text-muted">{isEn ? "On-board features" : "Recursos embarcados"}</p>
|
||||
<ul className="mt-2 flex flex-wrap gap-1.5">
|
||||
{recursos.map((campo) => {
|
||||
const opcional = valor(campo) === "opcional";
|
||||
const label = isEn ? (HEADER_MAP_EN[campo] || campo) : campo;
|
||||
return (
|
||||
<li key={campo} className={`chip ${opcional ? "text-gold" : "text-ink-soft"}`}>
|
||||
{opcional ? (
|
||||
@@ -45,8 +69,8 @@ export default function BoardCard({ board }) {
|
||||
) : (
|
||||
<Check className="h-3 w-3 text-ok" aria-hidden="true" />
|
||||
)}
|
||||
{campo}
|
||||
{opcional && <span className="sr-only"> (opcional)</span>}
|
||||
{label}
|
||||
{opcional && <span className="sr-only">{isEn ? " (optional)" : " (opcional)"}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -56,9 +80,9 @@ export default function BoardCard({ board }) {
|
||||
|
||||
{board.Link && (
|
||||
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
|
||||
Ver no AliExpress
|
||||
{isEn ? "View on AliExpress" : "Ver no AliExpress"}
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
<span className="sr-only">{isEn ? " (opens in new tab)" : " (abre em nova aba)"}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+54
-16
@@ -2,8 +2,12 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, Copy, ExternalLink } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function CodeExample({ framework }) {
|
||||
const pathname = usePathname() || "";
|
||||
const isEn = pathname === "/en" || pathname.startsWith("/en/");
|
||||
|
||||
const examplesUrl = framework?.exemplos;
|
||||
|
||||
const [sample, setSample] = useState("");
|
||||
@@ -20,18 +24,36 @@ export default function CodeExample({ framework }) {
|
||||
|
||||
const exemploPath = framework.exemplo;
|
||||
if (!exemploPath) {
|
||||
setSample(`// Exemplos completos: ${examplesUrl || "ver repositório"}\n`);
|
||||
setSample(
|
||||
isEn
|
||||
? `// Full examples: ${examplesUrl || "see repository"}\n`
|
||||
: `// Exemplos completos: ${examplesUrl || "ver repositório"}\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
|
||||
// Se locale for en, busca primeiro em /exemplos/en/
|
||||
let url = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
|
||||
if (isEn && url.startsWith("/exemplos/") && !url.startsWith("/exemplos/en/")) {
|
||||
url = url.replace("/exemplos/", "/exemplos/en/");
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!res.ok) {
|
||||
// Fallback para o arquivo original se não achar a versão traduzida
|
||||
const fallbackUrl = exemploPath.startsWith("/") ? exemploPath : `/${exemploPath}`;
|
||||
if (fallbackUrl !== url) {
|
||||
return fetch(fallbackUrl).then((r2) => {
|
||||
if (!r2.ok) throw new Error(`HTTP ${r2.status}`);
|
||||
return r2.text();
|
||||
});
|
||||
}
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return res.text();
|
||||
})
|
||||
.then((text) => {
|
||||
@@ -39,14 +61,22 @@ export default function CodeExample({ framework }) {
|
||||
if (text && text.trim().length > 0) {
|
||||
setSample(text);
|
||||
} else {
|
||||
setSample("// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n");
|
||||
setError("arquivo vazio");
|
||||
setSample(
|
||||
isEn
|
||||
? "// The example file is empty. Refer to the official repository.\n"
|
||||
: "// O arquivo de exemplo está vazio. Consulte o repositório oficial.\n"
|
||||
);
|
||||
setError(isEn ? "empty file" : "arquivo vazio");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
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");
|
||||
setSample(
|
||||
isEn
|
||||
? "// Could not load example.\n// Refer to official repository for complete examples.\n"
|
||||
: "// Não foi possível carregar o exemplo.\n// Consulte o repositório oficial para exemplos completos.\n"
|
||||
);
|
||||
setError(err.message || (isEn ? "load failure" : "falha ao carregar"));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -55,7 +85,7 @@ export default function CodeExample({ framework }) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [framework?.exemplo, framework?.exemplos]);
|
||||
}, [framework?.exemplo, framework?.exemplos, isEn]);
|
||||
|
||||
const lines = (sample || "").replace(/\n$/, "").split("\n");
|
||||
|
||||
@@ -71,20 +101,26 @@ export default function CodeExample({ framework }) {
|
||||
<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>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
{isEn ? "Code example" : "Exemplo de código"}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn
|
||||
? `Starter snippet in ${framework?.linguagem}. Adjust pins and configuration for your board.`
|
||||
: `Trecho de partida em ${framework?.linguagem}. Ajuste pinos e configuração à sua placa.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span role="status">{copied ? (isEn ? "Copied" : "Copiado") : (isEn ? "Copy" : "Copiar")}</span>
|
||||
</button>
|
||||
{examplesUrl && (
|
||||
<a href={examplesUrl} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
|
||||
Mais exemplos
|
||||
{isEn ? "More examples" : "Mais exemplos"}
|
||||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only"> (abre em nova aba)</span>
|
||||
<span className="sr-only">{isEn ? " (opens in new tab)" : " (abre em nova aba)"}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -93,9 +129,9 @@ export default function CodeExample({ framework }) {
|
||||
<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>
|
||||
<span>{loading ? (isEn ? "Loading example…" : "Carregando exemplo…") : framework?.linguagem}</span>
|
||||
</div>
|
||||
<pre tabIndex={0} aria-label={`Código de exemplo em ${framework?.linguagem}`}>
|
||||
<pre tabIndex={0} aria-label={isEn ? `Code example in ${framework?.linguagem}` : `Código de exemplo em ${framework?.linguagem}`}>
|
||||
<code>
|
||||
{lines.map((line, idx) => (
|
||||
<span key={idx} className="flex">
|
||||
@@ -109,7 +145,9 @@ export default function CodeExample({ framework }) {
|
||||
|
||||
{error && (
|
||||
<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.
|
||||
{isEn
|
||||
? `Could not load local example (${error}). Use “More examples” to view official code.`
|
||||
: `Não foi possível carregar o exemplo local (${error}). Use “Mais exemplos” para ver o código oficial.`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,29 @@ const FILTER_CATEGORIES = [
|
||||
{ id: "Antena", name: "Antena", color: "#06b6d4" }, // cyan
|
||||
];
|
||||
|
||||
const FILTER_CATEGORIES_EN = [
|
||||
{ id: "All", name: "All", color: "#8b5cf6" },
|
||||
{ id: "GND", name: "Ground (GND)", color: "#64748b" },
|
||||
{ id: "Power", name: "Power", color: "#ef4444" },
|
||||
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
|
||||
{ id: "Low Power", name: "LP GPIO", color: "#f59e0b" },
|
||||
{ id: "ADC", name: "ADC", color: "#ec4899" },
|
||||
{ id: "DAC", name: "DAC", color: "#a855f7" },
|
||||
{ id: "Touch", name: "Touch", color: "#06b6d4" },
|
||||
{ id: "UART", name: "UART", color: "#3b82f6" },
|
||||
{ id: "SPI", name: "SPI", color: "#f43f5e" },
|
||||
{ id: "USB", name: "USB", color: "#a855f7" },
|
||||
{ id: "Clock", name: "Clock", color: "#eab308" },
|
||||
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
|
||||
{ id: "Strapping", name: "Boot mode", color: "#dc2626" },
|
||||
{ id: "DSI", name: "Display (DSI)", color: "#f97316" },
|
||||
{ id: "CSI", name: "Camera (CSI)", color: "#14b8a6" },
|
||||
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
|
||||
{ id: "SD", name: "SD Card", color: "#10b981" },
|
||||
{ id: "I2C", name: "I2C", color: "#f59e0b" },
|
||||
{ id: "Antena", name: "Antenna", color: "#06b6d4" },
|
||||
];
|
||||
|
||||
const type_colors = {
|
||||
power: "#ef4444", // high-voltage red
|
||||
ground: "#64748b", // slate gray
|
||||
@@ -58,6 +81,7 @@ export default function ConnectionsDiagram(props) {
|
||||
function ConnectionsDiagramInner({ connections, serie }) {
|
||||
const params = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const isEn = pathname?.startsWith("/en");
|
||||
const router = useRouter();
|
||||
const pinosFiltro = useMemo(() => new Set((params.get("pinos") ?? "").split(",").filter(Boolean)), [params]);
|
||||
const motivoFiltro = params.get("motivo") ?? "";
|
||||
@@ -69,6 +93,8 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
const dialogRef = useRef(null);
|
||||
const gatilhoRef = useRef(null);
|
||||
|
||||
const filterCategories = isEn ? FILTER_CATEGORIES_EN : FILTER_CATEGORIES;
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
@@ -84,11 +110,18 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
};
|
||||
|
||||
const getCategoryColor = (category) => {
|
||||
const filter = FILTER_CATEGORIES.find((f) => f.id === category);
|
||||
const filter = filterCategories.find((f) => f.id === category);
|
||||
return filter?.color || "#64748b";
|
||||
};
|
||||
|
||||
const nomeCategoria = (cat) => (cat === "Strapping" ? "Inicialização" : cat);
|
||||
const nomeCategoria = (cat) => {
|
||||
if (cat === "Strapping") return isEn ? "Boot mode" : "Inicialização";
|
||||
if (isEn) {
|
||||
const found = FILTER_CATEGORIES_EN.find((f) => f.id === cat);
|
||||
if (found) return found.name;
|
||||
}
|
||||
return cat;
|
||||
};
|
||||
|
||||
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery, pinosFiltro]);
|
||||
|
||||
@@ -142,8 +175,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
return (
|
||||
<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>
|
||||
<p className="mt-4 font-display text-xl font-semibold text-ink">
|
||||
{isEn ? "Pinout diagram unavailable" : "Diagrama de conexões indisponível"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted">
|
||||
{isEn ? "This model does not yet have a detailed pinout and bus map." : "Este modelo ainda não tem um mapa detalhado de conexões e barramentos."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -153,59 +190,71 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Diagrama de conexões interativo</h2>
|
||||
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||
{isEn ? "Interactive pinout diagram" : "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.
|
||||
{isEn ? (
|
||||
`All connections and multiplexing for the ${serie.nome_completo}. Search by name or number, filter by function, and open a pin to see details.`
|
||||
) : (
|
||||
`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>
|
||||
|
||||
{pinosFiltro.size > 0 && (
|
||||
<div role="status" className="callout flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-sm text-ink">
|
||||
<strong className="font-semibold">Filtrado pela busca:</strong> {motivoFiltro || "pinos selecionados"} ({pinosFiltro.size}{" "}
|
||||
{pinosFiltro.size === 1 ? "pino" : "pinos"})
|
||||
<strong className="font-semibold">{isEn ? "Filtered by search:" : "Filtrado pela busca:"}</strong> {motivoFiltro || (isEn ? "selected pins" : "pinos selecionados")} ({pinosFiltro.size}{" "}
|
||||
{pinosFiltro.size === 1 ? (isEn ? "pin" : "pino") : (isEn ? "pins" : "pinos")})
|
||||
</p>
|
||||
<button type="button" onClick={mostrarTodos} className="btn btn-ghost btn-sm">
|
||||
Mostrar todos os pinos
|
||||
{isEn ? "Show all pins" : "Mostrar todos os pinos"}
|
||||
</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>
|
||||
<label htmlFor="busca-pino" className="sr-only">
|
||||
{isEn ? "Search connection by name or number" : "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)"
|
||||
placeholder={isEn ? "Search pin (e.g.: GPIO1, TXD, XTAL)" : "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">
|
||||
<div role="group" aria-label={isEn ? "Display mode" : "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
|
||||
{isEn ? "Grid" : "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
|
||||
{isEn ? "List" : "Lista"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-ink">Função do pino</h3>
|
||||
<h3 className="text-sm font-semibold text-ink">{isEn ? "Pin function" : "Função do pino"}</h3>
|
||||
<p role="status" aria-live="polite" className="text-sm text-muted">
|
||||
{visibleConnections.length} de {connections.length} pinos
|
||||
{isEn ? (
|
||||
`${visibleConnections.length} of ${connections.length} pins`
|
||||
) : (
|
||||
`${visibleConnections.length} de ${connections.length} pinos`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div role="group" aria-label="Filtrar por função" className="mt-3 flex flex-wrap gap-2">
|
||||
{FILTER_CATEGORIES.map((filter) => {
|
||||
<div role="group" aria-label={isEn ? "Filter by function" : "Filtrar por função"} className="mt-3 flex flex-wrap gap-2">
|
||||
{filterCategories.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; // filtros que não se aplicam a este chip
|
||||
@@ -223,8 +272,8 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{visibleConnections.length === 0 ? (
|
||||
<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>
|
||||
<p className="mt-3 font-semibold text-ink">{isEn ? "No connections found" : "Nenhuma conexão encontrada"}</p>
|
||||
<p className="mt-1 text-sm text-muted">{isEn ? "Adjust your search or return to all functions." : "Ajuste a busca ou volte para todas as funções."}</p>
|
||||
<button type="button" onClick={() => { setActiveFilter("All"); setSearchQuery(""); if (pinosFiltro.size) mostrarTodos(); }} className="btn btn-primary btn-sm mt-5">
|
||||
Limpar busca e filtros
|
||||
</button>
|
||||
@@ -267,13 +316,15 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
) : (
|
||||
<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>
|
||||
<caption className="sr-only">
|
||||
{isEn ? `Connections of ${serie.nome_completo}` : `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>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Pin" : "Pino"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Name" : "Nome"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Functions" : "Funções"}</th>
|
||||
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Usage" : "Uso"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
@@ -286,7 +337,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
type="button"
|
||||
onClick={(e) => abrir(connection, e)}
|
||||
aria-haspopup="dialog"
|
||||
aria-label={`Abrir detalhes do pino ${connection.nome}, número ${connection.numero}`}
|
||||
aria-label={isEn ? `Open pin details for ${connection.nome}, number ${connection.numero}` : `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 }}
|
||||
>
|
||||
@@ -301,12 +352,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
{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
|
||||
{isEn ? "Restricted" : "Restrito"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-ok">
|
||||
<CheckCircle className="h-4 w-4" aria-hidden="true" />
|
||||
Livre
|
||||
{isEn ? "Free" : "Livre"}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -322,10 +373,19 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<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>
|
||||
<p className="font-semibold text-ink">{isEn ? "Pinout notes" : "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>
|
||||
{isEn ? (
|
||||
<>
|
||||
<li>Pins with a warning icon have special boot roles or analog limitations. Open the pin to see details.</li>
|
||||
<li>Isolate capacitive touch traces from coupled RF noise.</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
@@ -353,10 +413,12 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<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>
|
||||
<p className="text-sm text-muted">
|
||||
{isEn ? `Physical pin ${selectedConnection.numero}` : `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">
|
||||
<button type="button" onClick={fechar} aria-label={isEn ? "Close pin details" : "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>
|
||||
@@ -364,7 +426,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-5 sm:p-7">
|
||||
{selectedConnection.categorias.length > 0 && (
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">Categorias</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Categories" : "Categorias"}</h4>
|
||||
<ul className="mt-3 flex flex-wrap gap-2">
|
||||
{selectedConnection.categorias.map((cat) => (
|
||||
<li key={cat} className="chip">
|
||||
@@ -380,7 +442,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<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
|
||||
{isEn ? "Power domain" : "Domínio de alimentação"}
|
||||
</h4>
|
||||
<ul className="tile mt-3 space-y-2">
|
||||
{selectedConnection.alimentacao.map((source) => (
|
||||
@@ -391,7 +453,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-ink">Funções disponíveis</h4>
|
||||
<h4 className="text-sm font-semibold text-ink">{isEn ? "Available functions" : "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">
|
||||
@@ -406,7 +468,7 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
<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
|
||||
{isEn ? "Engineering notes" : "Avisos de engenharia"}
|
||||
</h4>
|
||||
<ul className="mt-2 list-disc space-y-1.5 pl-5">
|
||||
{selectedConnection.avisos.map((aviso) => (
|
||||
@@ -418,7 +480,9 @@ function ConnectionsDiagramInner({ connections, serie }) {
|
||||
</div>
|
||||
|
||||
<div className="border-t border-line p-4 sm:px-7">
|
||||
<button type="button" onClick={fechar} className="btn btn-primary w-full">Fechar</button>
|
||||
<button type="button" onClick={fechar} className="btn btn-primary w-full">
|
||||
{isEn ? "Close" : "Fechar"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
+54
-14
@@ -8,7 +8,7 @@ import { ArrowUpRight } from "lucide-react";
|
||||
import { SITE } from "@/lib/seo";
|
||||
import { LOCALES, getLocalizedHref } from "@/i18n/links";
|
||||
|
||||
const COLUMNS = [
|
||||
const COLUMNS_PT = [
|
||||
{
|
||||
title: "Explorar",
|
||||
links: [
|
||||
@@ -38,6 +38,35 @@ const COLUMNS = [
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_EN = [
|
||||
{
|
||||
title: "Explore",
|
||||
links: [
|
||||
{ href: "/series", label: "ESP32 Series" },
|
||||
{ href: "/comparacao", label: "Comparator" },
|
||||
{ href: "/catalogo", label: "Board Catalog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tools",
|
||||
links: [
|
||||
{ href: "/seletor", label: "Smart Selector" },
|
||||
{ href: "/diagnostico", label: "Diagnostics" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Project",
|
||||
links: [
|
||||
{ href: "/sobre", label: "About ESPDocs" },
|
||||
{ href: "/glossario", label: "Glossary" },
|
||||
{ href: SITE.repositorio, label: "Repository", external: true },
|
||||
{ href: `${SITE.repositorio}/issues`, label: "Report an issue", external: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const pathname = usePathname() || "";
|
||||
const year = new Date().getFullYear();
|
||||
@@ -48,23 +77,34 @@ export default function Footer() {
|
||||
|
||||
const getLinkHref = (href) => getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
|
||||
|
||||
const columns = COLUMNS.map((column) => ({
|
||||
...column,
|
||||
links: column.links.filter((link) => link.href !== "/livro" || isPt),
|
||||
}));
|
||||
const columns = isPt
|
||||
? COLUMNS_PT.map((column) => ({
|
||||
...column,
|
||||
links: column.links.filter((link) => link.href !== "/livro" || isPt),
|
||||
}))
|
||||
: COLUMNS_EN;
|
||||
|
||||
return (
|
||||
<footer className="mt-20 border-t border-line md:mt-28" aria-label="Rodapé do ESPDocs">
|
||||
<footer className="mt-20 border-t border-line md:mt-28" aria-label={isPt ? "Rodapé do ESPDocs" : "ESPDocs footer"}>
|
||||
<div className="shell py-14 md:py-20">
|
||||
<div className="grid gap-12 lg:grid-cols-[1.1fr_2fr]">
|
||||
<div>
|
||||
<Link href={getLinkHref("/")} className="inline-flex min-h-11 items-center gap-2.5" aria-label="ESPDocs, página inicial">
|
||||
<Link href={getLinkHref("/")} className="inline-flex min-h-11 items-center gap-2.5" aria-label={isPt ? "ESPDocs, página inicial" : "ESPDocs, homepage"}>
|
||||
<Logo 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.
|
||||
{isPt ? (
|
||||
<>
|
||||
Documentação independente do ecossistema ESP32, escrita em português brasileiro.
|
||||
Dados públicos, curadoria editorial e links para as fontes oficiais.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Independent documentation of the ESP32 ecosystem.
|
||||
Open data, editorial curation, and links to official sources.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{isPt && (
|
||||
@@ -133,7 +173,7 @@ export default function Footer() {
|
||||
))}
|
||||
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-ink">Autor</h2>
|
||||
<h2 className="text-sm font-semibold text-ink">{isPt ? "Autor" : "Author"}</h2>
|
||||
<p className="mt-4 text-sm leading-relaxed text-muted">
|
||||
Lucas Rayan Guerra
|
||||
<br />
|
||||
@@ -144,7 +184,7 @@ export default function Footer() {
|
||||
href={SITE.repositorio}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Gitea do projeto (abre em nova aba)"
|
||||
aria-label={isPt ? "Gitea do projeto (abre em nova aba)" : "Project Gitea (opens in new tab)"}
|
||||
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"
|
||||
>
|
||||
<GiteaMark className="h-4 w-4" />
|
||||
@@ -153,7 +193,7 @@ export default function Footer() {
|
||||
href={SITE.autorLinkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn do autor (abre em nova aba)"
|
||||
aria-label={isPt ? "LinkedIn do autor (abre em nova aba)" : "Author LinkedIn (opens in new tab)"}
|
||||
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">
|
||||
@@ -166,7 +206,7 @@ export default function Footer() {
|
||||
</div>
|
||||
|
||||
<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>© {year} {isPt ? "ESPDocs. Projeto independente, sem afiliação com a Espressif Systems." : "ESPDocs. Independent project, not affiliated with 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
|
||||
@@ -176,7 +216,7 @@ export default function Footer() {
|
||||
lucasrguerra.dev.br
|
||||
<ArrowUpRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
<span>Fontes oficiais: Espressif Systems.</span>
|
||||
<span>{isPt ? "Fontes oficiais: Espressif Systems." : "Official sources: Espressif Systems."}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+61
-18
@@ -28,7 +28,7 @@ import {
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
|
||||
const MENUS = [
|
||||
const MENUS_PT = [
|
||||
{
|
||||
id: "hardware",
|
||||
label: "Hardware",
|
||||
@@ -58,7 +58,37 @@ const MENUS = [
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_LINKS = [
|
||||
const MENUS_EN = [
|
||||
{
|
||||
id: "hardware",
|
||||
label: "Hardware",
|
||||
items: [
|
||||
{ href: "/series", label: "ESP32 Series", desc: "12 chips, pinouts and specifications", icon: Cpu },
|
||||
{ href: "/comparacao", label: "Comparator", desc: "Up to four series side by side", icon: Scale },
|
||||
{ href: "/catalogo", label: "Board Catalog", desc: "DevKits, pinouts and where to buy", icon: Box },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Tools",
|
||||
items: [
|
||||
{ href: "/seletor", label: "Smart Selector", desc: "The right chip for your project", icon: SlidersHorizontal },
|
||||
{ href: "/diagnostico", label: "Diagnostics", desc: "Brownout, bootloop, WDT and more", icon: AlertTriangle },
|
||||
{ href: "/componentes", label: "Component Registry", desc: "Drivers and idf_component.yml manifests", icon: Search },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "library",
|
||||
label: "Library",
|
||||
items: [
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs", desc: "ESP-IDF, Arduino, Rust and Matter", icon: BookOpen },
|
||||
{ href: "/glossario", label: "Technical Glossary", desc: "Terms appearing in datasheets", icon: BookA },
|
||||
{ href: "/sobre", label: "About the Project", desc: "Authorship, method and open source", icon: Sparkles },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_LINKS_PT = [
|
||||
{ href: "/series", label: "Séries ESP32" },
|
||||
{ href: "/comparacao", label: "Comparador" },
|
||||
{ href: "/seletor", label: "Seletor inteligente" },
|
||||
@@ -71,6 +101,18 @@ const MOBILE_LINKS = [
|
||||
{ href: "/sobre", label: "Sobre" },
|
||||
];
|
||||
|
||||
const MOBILE_LINKS_EN = [
|
||||
{ href: "/series", label: "ESP32 Series" },
|
||||
{ href: "/comparacao", label: "Comparator" },
|
||||
{ href: "/seletor", label: "Smart Selector" },
|
||||
{ href: "/diagnostico", label: "Diagnostics" },
|
||||
{ href: "/frameworks", label: "Frameworks & SDKs" },
|
||||
{ href: "/componentes", label: "Component Registry" },
|
||||
{ href: "/glossario", label: "Glossary" },
|
||||
{ href: "/catalogo", label: "Board Catalog" },
|
||||
{ href: "/sobre", label: "About" },
|
||||
];
|
||||
|
||||
function isActivePath(pathname, href, currentLocale) {
|
||||
const localized = getLocalizedHref(`/${currentLocale}${href}`, currentLocale);
|
||||
if (href === "/" || localized === `/${currentLocale}/` || localized === `/${currentLocale}`) {
|
||||
@@ -130,18 +172,19 @@ export default function Header() {
|
||||
};
|
||||
}, [drawerOpen]);
|
||||
|
||||
const visibleMobileLinks = isPt ? MOBILE_LINKS : MOBILE_LINKS.filter((l) => l.href !== "/livro");
|
||||
const visibleMobileLinks = isPt ? MOBILE_LINKS_PT : MOBILE_LINKS_EN;
|
||||
const MENUS = isPt ? MENUS_PT : MENUS_EN;
|
||||
|
||||
return (
|
||||
<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={getLinkHref("/")} className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label="ESPDocs, página inicial">
|
||||
<Link href={getLinkHref("/")} className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2" aria-label={isPt ? "ESPDocs, página inicial" : "ESPDocs, homepage"}>
|
||||
<Logo className="h-[30px] w-[30px]" />
|
||||
<span className="font-display text-[1.3rem] font-semibold leading-none tracking-[-0.045em] text-ink">ESPDocs</span>
|
||||
</Link>
|
||||
|
||||
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label="Navegação principal">
|
||||
<nav ref={navRef} className="hidden items-center gap-0.5 lg:flex" aria-label={isPt ? "Navegação principal" : "Main navigation"}>
|
||||
{MENUS.map((menu) => {
|
||||
const active = menu.items.some((item) => isActivePath(pathname, item.href, currentLocale));
|
||||
const expanded = openMenu === menu.id;
|
||||
@@ -194,23 +237,23 @@ export default function Header() {
|
||||
</Link>
|
||||
)}
|
||||
<Link href={getLinkHref("/sobre")} className={`nav-link ${isActivePath(pathname, "/sobre", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre", currentLocale) ? "page" : undefined}>
|
||||
Sobre
|
||||
{isPt ? "Sobre" : "About"}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Link href={getLinkHref("/seletor")} className="btn btn-primary btn-sm hidden lg:inline-flex">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
Selecionar chip
|
||||
{isPt ? "Selecionar chip" : "Select chip"}
|
||||
</Link>
|
||||
<SearchDialog />
|
||||
<LanguageSelector />
|
||||
<ThemeToggle />
|
||||
<ThemeToggle isPt={isPt} />
|
||||
<button
|
||||
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-label={isPt ? "Abrir menu de navegação" : "Open navigation menu"}
|
||||
aria-expanded={drawerOpen}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
@@ -233,7 +276,7 @@ export default function Header() {
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Menu de navegação"
|
||||
aria-label={isPt ? "Menu de navegação" : "Navigation menu"}
|
||||
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 ${
|
||||
@@ -242,15 +285,15 @@ export default function Header() {
|
||||
>
|
||||
<div className="flex items-start justify-between px-5 py-5">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-muted">Navegação</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">Para onde ir</p>
|
||||
<p className="text-xs font-semibold text-muted">{isPt ? "Navegação" : "Navigation"}</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold tracking-[-0.04em] text-ink">{isPt ? "Para onde ir" : "Where to go"}</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"
|
||||
aria-label={isPt ? "Fechar menu" : "Close menu"}
|
||||
>
|
||||
<X className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -282,7 +325,7 @@ export default function Header() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label="Navegação móvel">
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-2" aria-label={isPt ? "Navegação móvel" : "Mobile navigation"}>
|
||||
{visibleMobileLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
@@ -300,7 +343,7 @@ export default function Header() {
|
||||
<div className="p-4 pt-2">
|
||||
<Link href={getLinkHref("/seletor")} onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
Selecionar meu chip
|
||||
{isPt ? "Selecionar meu chip" : "Select my chip"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,7 +354,7 @@ export default function Header() {
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeToggle() {
|
||||
function ThemeToggle({ isPt = true }) {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -328,8 +371,8 @@ function ThemeToggle() {
|
||||
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"}
|
||||
aria-label={dark ? (isPt ? "Ativar tema claro" : "Switch to light theme") : (isPt ? "Ativar tema escuro" : "Switch to dark theme")}
|
||||
title={dark ? (isPt ? "Tema claro" : "Light theme") : (isPt ? "Tema escuro" : "Dark theme")}
|
||||
>
|
||||
{dark ? <Sun className="h-4 w-4" aria-hidden="true" /> : <Moon className="h-4 w-4" aria-hidden="true" />}
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
* diagrama nunca divergem: corrigir um pino no JSON corrige os dois.
|
||||
*/
|
||||
|
||||
const GRUPOS = [
|
||||
const GRUPOS_PT = [
|
||||
{
|
||||
id: "entrada",
|
||||
titulo: "Somente entrada",
|
||||
@@ -26,7 +26,7 @@ const GRUPOS = [
|
||||
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),
|
||||
combina: (avisos) => /apenas de entrada|input only/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "flash",
|
||||
@@ -36,8 +36,8 @@ const GRUPOS = [
|
||||
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) =>
|
||||
/(flash|psram)/.test(avisos) &&
|
||||
/(dedicad|utilizado pela mem|não deve ser utilizado|não é recomendado)/.test(avisos),
|
||||
/(flash|psram)/i.test(avisos) &&
|
||||
/(dedicad|utilizado pela mem|não deve ser utilizado|não é recomendado|connected to|dedicated to|should not be used|not recommended)/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "strapping",
|
||||
@@ -48,7 +48,7 @@ const GRUPOS = [
|
||||
"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) =>
|
||||
cats.includes("Strapping") ||
|
||||
/strapping|modo de boot|modo de inicialização/.test(avisos),
|
||||
/strapping|modo de boot|modo de inicialização|boot mode/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "debug",
|
||||
@@ -57,7 +57,7 @@ const GRUPOS = [
|
||||
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"),
|
||||
combina: (avisos, cats) => /debugging|gravação/i.test(avisos) || cats.includes("JTAG"),
|
||||
},
|
||||
{
|
||||
id: "usb",
|
||||
@@ -66,7 +66,7 @@ const GRUPOS = [
|
||||
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),
|
||||
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "eletrico",
|
||||
@@ -75,7 +75,68 @@ const GRUPOS = [
|
||||
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),
|
||||
combina: (avisos) => /flutuando|floating/i.test(avisos),
|
||||
},
|
||||
];
|
||||
|
||||
const GRUPOS_EN = [
|
||||
{
|
||||
id: "entrada",
|
||||
titulo: "Input only",
|
||||
icone: ArrowDownToLine,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Input only pins. They cannot be configured as outputs and do not have internal pull-up or pull-down resistors. If needed, add them to your external circuit.",
|
||||
combina: (avisos) => /apenas de entrada|input only/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "flash",
|
||||
titulo: "Connected to flash/PSRAM memory",
|
||||
icone: HardDrive,
|
||||
cor: "text-copper",
|
||||
explicacao:
|
||||
"Already connected to the memory where firmware is stored and executed. Using them as general-purpose GPIO typically crashes the chip or corrupts execution. In modules with internal flash, they are not brought out to the package pins.",
|
||||
combina: (avisos) =>
|
||||
/(flash|psram)/i.test(avisos) &&
|
||||
/(dedicad|utilizado pela mem|não deve ser utilizado|não é recomendado|connected to|dedicated to|should not be used|not recommended)/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "strapping",
|
||||
titulo: "Strapping pins (boot mode)",
|
||||
icone: Power,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"The chip samples the logic level of these pins during reset to determine the boot mode. You can use them after booting, but external circuitry must not force the wrong level during reset, or the board will fail to boot or enter download mode unexpectedly.",
|
||||
combina: (avisos, cats) =>
|
||||
cats.includes("Strapping") ||
|
||||
/strapping|modo de boot|modo de inicialização|boot mode/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "debug",
|
||||
titulo: "Flashing and debugging",
|
||||
icone: Bug,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"These are the flashing UART and JTAG lines. They can still be used as GPIO, but reusing them removes the ability to flash and debug the board without disconnecting connected peripherals.",
|
||||
combina: (avisos, cats) => /debugging|gravação/i.test(avisos) || cats.includes("JTAG"),
|
||||
},
|
||||
{
|
||||
id: "usb",
|
||||
titulo: "Native USB",
|
||||
icone: Usb,
|
||||
cor: "text-accent",
|
||||
explicacao:
|
||||
"Connected by default to the internal USB Serial/JTAG controller. Using them as GPIO requires disabling this peripheral, which also removes direct USB flashing.",
|
||||
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/i.test(avisos),
|
||||
},
|
||||
{
|
||||
id: "eletrico",
|
||||
titulo: "Must not float",
|
||||
icone: Zap,
|
||||
cor: "text-gold",
|
||||
explicacao:
|
||||
"Require a defined hardware logic level: external pull-up or RC delay circuit. Leaving them floating causes random resets or unstable boot behavior.",
|
||||
combina: (avisos) => /flutuando|floating/i.test(avisos),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -85,8 +146,9 @@ function rotuloDoPino(pino) {
|
||||
return gpio && gpio !== pino.nome ? `${pino.nome} · ${gpio}` : pino.nome;
|
||||
}
|
||||
|
||||
function derivarGrupos(conexoes) {
|
||||
return GRUPOS.map((grupo) => {
|
||||
function derivarGrupos(conexoes, isEn) {
|
||||
const listaGrupos = isEn ? GRUPOS_EN : GRUPOS_PT;
|
||||
return listaGrupos.map((grupo) => {
|
||||
const vistos = new Map();
|
||||
for (const pino of conexoes) {
|
||||
if (!["io", "analog", "dedicated"].includes(pino.tipo)) continue;
|
||||
@@ -101,10 +163,10 @@ function derivarGrupos(conexoes) {
|
||||
}).filter((g) => g.pinos.length > 0);
|
||||
}
|
||||
|
||||
export default function PinRestrictions({ conexoes, serieKey }) {
|
||||
export default function PinRestrictions({ conexoes, serieKey, isEn = false }) {
|
||||
if (!conexoes?.length) return null;
|
||||
|
||||
const grupos = derivarGrupos(conexoes);
|
||||
const grupos = derivarGrupos(conexoes, isEn);
|
||||
if (!grupos.length) return null;
|
||||
|
||||
const totalPinos = new Set(grupos.flatMap((g) => g.pinos)).size;
|
||||
@@ -115,13 +177,21 @@ export default function PinRestrictions({ conexoes, serieKey }) {
|
||||
<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
|
||||
{isEn ? "Pins with usage restrictions" : "Pinos com restrição de uso"}
|
||||
</h2>
|
||||
|
||||
<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.
|
||||
{isEn ? (
|
||||
<>
|
||||
Not every pin on the {serieKey} is free for general-purpose use. {totalPinos} of them have conditions: occupied by memory, sampled during boot, or function as input only. Check before finalizing your schematic.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
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="mt-6 grid gap-4 md:grid-cols-2">
|
||||
@@ -131,7 +201,7 @@ export default function PinRestrictions({ conexoes, serieKey }) {
|
||||
<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"}
|
||||
{pinos.length} {pinos.length === 1 ? (isEn ? "pin" : "pino") : (isEn ? "pins" : "pinos")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -150,11 +220,14 @@ export default function PinRestrictions({ conexoes, serieKey }) {
|
||||
|
||||
<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.
|
||||
{isEn ? (
|
||||
"These groups are derived from the same data as the pinout diagram. There you can see every pin's full functions, power domain, and complete engineering notes."
|
||||
) : (
|
||||
"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."
|
||||
)}
|
||||
</p>
|
||||
<Link href="#connections" className="btn btn-ghost shrink-0">
|
||||
Abrir diagrama de pinos
|
||||
{isEn ? "Open pinout diagram" : "Abrir diagrama de pinos"}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+52
-27
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import {
|
||||
Search,
|
||||
X,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { preparar, buscar, EXEMPLOS_DE_BUSCA } from "@/lib/searchEngine";
|
||||
import { normalizar } from "@/lib/searchTopics";
|
||||
|
||||
const TIPOS = {
|
||||
const TIPOS_PT = {
|
||||
resposta: { label: "Resposta", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
|
||||
pino: { label: "Pinos", Icon: Plug, cor: "bg-accent text-accent-contrast" },
|
||||
pagina: { label: "Página", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
|
||||
@@ -32,11 +32,23 @@ const TIPOS = {
|
||||
componente: { label: "Componente", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
|
||||
};
|
||||
|
||||
const TIPOS_EN = {
|
||||
resposta: { label: "Answer", Icon: Sparkles, cor: "bg-accent text-accent-contrast" },
|
||||
pino: { label: "Pins", Icon: Plug, cor: "bg-accent text-accent-contrast" },
|
||||
pagina: { label: "Page", Icon: FileText, cor: "bg-surface-2 text-ink-soft" },
|
||||
serie: { label: "Series", Icon: Cpu, cor: "bg-accent-soft text-accent" },
|
||||
framework: { label: "Framework", Icon: Terminal, cor: "bg-teal-soft text-teal" },
|
||||
glossario: { label: "Glossary", Icon: BookOpen, cor: "bg-copper-soft text-copper" },
|
||||
erro: { label: "Error", Icon: AlertTriangle, cor: "bg-gold-soft text-gold" },
|
||||
componente: { label: "Component", Icon: Box, cor: "bg-surface-2 text-ink-soft" },
|
||||
};
|
||||
|
||||
const INTELIGENTES = new Set(["resposta", "pino"]);
|
||||
|
||||
const EXEMPLOS_EN = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
||||
|
||||
function Destaque({ texto, termos }) {
|
||||
if (!termos.length) return texto;
|
||||
// Posições do texto normalizado só valem para o original se o comprimento não mudou.
|
||||
const norm = normalizar(texto);
|
||||
if (norm.length !== texto.length) return texto;
|
||||
const marcas = new Array(texto.length).fill(false);
|
||||
@@ -85,6 +97,13 @@ function Pilula({ children, onClick, forte = false }) {
|
||||
}
|
||||
|
||||
export default function SearchDialog() {
|
||||
const pathname = usePathname() || "";
|
||||
const locale = pathname === "/en" || pathname.startsWith("/en/") ? "en" : "pt";
|
||||
const isEn = locale === "en";
|
||||
|
||||
const TIPOS = isEn ? TIPOS_EN : TIPOS_PT;
|
||||
const EXEMPLOS = isEn ? EXEMPLOS_EN : EXEMPLOS_DE_BUSCA;
|
||||
|
||||
const [aberto, setAberto] = useState(false);
|
||||
const [montado, setMontado] = useState(false);
|
||||
const [consulta, setConsulta] = useState("");
|
||||
@@ -124,9 +143,16 @@ export default function SearchDialog() {
|
||||
const carregarIndice = useCallback(() => {
|
||||
setErro(false);
|
||||
let cancelado = false;
|
||||
fetch("/api/busca?v=2", { cache: "no-cache" })
|
||||
const url = isEn ? "/api/busca/en?v=2" : "/api/busca?v=2";
|
||||
fetch(url, { cache: "no-cache" })
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
if (!r.ok) {
|
||||
// Fallback para /api/busca caso a rota dinâmica do locale falhe
|
||||
return fetch("/api/busca?v=2", { cache: "no-cache" }).then((r2) => {
|
||||
if (!r2.ok) throw new Error(`HTTP ${r2.status}`);
|
||||
return r2.json();
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((dados) => {
|
||||
@@ -141,7 +167,7 @@ export default function SearchDialog() {
|
||||
return () => {
|
||||
cancelado = true;
|
||||
};
|
||||
}, []);
|
||||
}, [isEn]);
|
||||
|
||||
// Carrega o índice (textos + especificações + pinos) na primeira abertura.
|
||||
useEffect(() => {
|
||||
@@ -182,7 +208,6 @@ export default function SearchDialog() {
|
||||
if (!item) return;
|
||||
fechar();
|
||||
router.push(item.href);
|
||||
// Se já estamos na página de destino, só a âncora muda: avisa quem escuta (abas da série).
|
||||
if (item.href.includes("#")) setTimeout(() => window.dispatchEvent(new HashChangeEvent("hashchange")), 80);
|
||||
}
|
||||
|
||||
@@ -213,17 +238,17 @@ export default function SearchDialog() {
|
||||
id="botao-busca"
|
||||
onClick={() => setAberto(true)}
|
||||
className="flex h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full border border-line px-3 text-ink transition-colors hover:bg-surface-2 xl:px-4"
|
||||
aria-label="Buscar no ESPDocs (Ctrl+K)"
|
||||
aria-label={isEn ? "Search ESPDocs (Ctrl+K)" : "Buscar no ESPDocs (Ctrl+K)"}
|
||||
>
|
||||
<Search className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="hidden text-sm font-medium xl:inline">Buscar</span>
|
||||
<span className="hidden text-sm font-medium xl:inline">{isEn ? "Search" : "Buscar"}</span>
|
||||
<kbd className="hidden rounded-md border border-line bg-surface-2 px-1.5 py-0.5 font-mono text-xs font-semibold text-muted xl:inline">Ctrl K</kbd>
|
||||
</button>
|
||||
|
||||
{montado &&
|
||||
aberto &&
|
||||
createPortal(
|
||||
<div className="fixed inset-0 z-[120] flex items-start justify-center px-4 pt-[8vh]" role="dialog" aria-modal="true" aria-label="Buscar no ESPDocs">
|
||||
<div className="fixed inset-0 z-[120] flex items-start justify-center px-4 pt-[8vh]" role="dialog" aria-modal="true" aria-label={isEn ? "Search ESPDocs" : "Buscar no ESPDocs"}>
|
||||
<div className="dialog-scrim absolute inset-0 bg-black/45 backdrop-blur-sm" onClick={fechar} />
|
||||
<div className="dialog-panel relative flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-3xl border border-line bg-canvas shadow-2xl">
|
||||
<div className="flex items-center gap-3 border-b border-line px-5">
|
||||
@@ -235,17 +260,17 @@ export default function SearchDialog() {
|
||||
value={consulta}
|
||||
onChange={(e) => setConsulta(e.target.value)}
|
||||
onKeyDown={onInputKey}
|
||||
placeholder="Pergunte ao ESPDocs: “flash s3”, “pinos usb c6”, “gpio0 esp32”…"
|
||||
placeholder={isEn ? "Ask ESPDocs: “flash s3”, “usb pins c6”, “gpio0 esp32”…" : "Pergunte ao ESPDocs: “flash s3”, “pinos usb c6”, “gpio0 esp32”…"}
|
||||
className="flex-1 bg-transparent py-5 text-base text-ink outline-none placeholder:text-muted"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-label="Termo de busca"
|
||||
aria-label={isEn ? "Search query" : "Termo de busca"}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={fechar}
|
||||
className="grid h-9 w-9 cursor-pointer place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
|
||||
aria-label="Fechar busca"
|
||||
aria-label={isEn ? "Close search" : "Fechar busca"}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -253,7 +278,7 @@ export default function SearchDialog() {
|
||||
|
||||
{entendeu && (
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-line bg-surface px-5 py-2.5">
|
||||
<span className="text-xs font-semibold text-muted">Entendi:</span>
|
||||
<span className="text-xs font-semibold text-muted">{isEn ? "Understood:" : "Entendi:"}</span>
|
||||
{interpretacao.chips.map((c) => (
|
||||
<Pilula key={c.chave} forte>{c.nome}</Pilula>
|
||||
))}
|
||||
@@ -265,7 +290,7 @@ export default function SearchDialog() {
|
||||
|
||||
{sugestoes.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-line px-5 py-2.5">
|
||||
<span className="text-xs font-semibold text-muted">{interpretacao.chips.length ? "Perguntar sobre:" : "Para qual chip?"}</span>
|
||||
<span className="text-xs font-semibold text-muted">{interpretacao.chips.length ? (isEn ? "Ask about:" : "Perguntar sobre:") : (isEn ? "Which chip?" : "Para qual chip?")}</span>
|
||||
{sugestoes.map((s) => (
|
||||
<Pilula key={s.rotulo} onClick={() => anexar(s.anexar)}>
|
||||
{s.rotulo}
|
||||
@@ -277,36 +302,36 @@ export default function SearchDialog() {
|
||||
<div ref={listaRef} className="flex-1 overflow-y-auto p-2" role="listbox">
|
||||
{erro && (
|
||||
<div className="p-6 text-center">
|
||||
<p className="text-sm font-semibold text-danger">Não foi possível carregar o índice de busca.</p>
|
||||
<p className="text-sm font-semibold text-danger">{isEn ? "Could not load search index." : "Não foi possível carregar o índice de busca."}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={carregarIndice}
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
>
|
||||
Tentar novamente
|
||||
{isEn ? "Try again" : "Tentar novamente"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!erro && !indice && <p className="p-6 text-center text-sm text-muted">Carregando…</p>}
|
||||
{!erro && !indice && <p className="p-6 text-center text-sm text-muted">{isEn ? "Loading…" : "Carregando…"}</p>}
|
||||
|
||||
{indice && vazia && (
|
||||
<div className="px-3 pb-3 pt-1">
|
||||
<p className="text-xs font-semibold text-muted">Experimente perguntar</p>
|
||||
<p className="text-xs font-semibold text-muted">{isEn ? "Try asking" : "Experimente perguntar"}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{EXEMPLOS_DE_BUSCA.map((ex) => (
|
||||
{EXEMPLOS.map((ex) => (
|
||||
<Pilula key={ex} onClick={() => setConsulta(ex)}>
|
||||
{ex}
|
||||
</Pilula>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-xs font-semibold text-muted">Ir para</p>
|
||||
<p className="mt-4 text-xs font-semibold text-muted">{isEn ? "Go to" : "Ir para"}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{indice && !vazia && resultados.length === 0 && (
|
||||
<div className="p-8 text-center">
|
||||
<p className="font-display text-lg font-semibold text-ink">Nenhum resultado para “{consulta}”</p>
|
||||
<p className="mt-1 text-sm text-muted">Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”.</p>
|
||||
<p className="font-display text-lg font-semibold text-ink">{isEn ? `No results for “${consulta}”` : `Nenhum resultado para “${consulta}”`}</p>
|
||||
<p className="mt-1 text-sm text-muted">{isEn ? "Try “brownout”, “flash s3”, “usb pins c6” or “LVGL”." : "Tente “brownout”, “flash s3”, “pinos usb c6” ou “LVGL”."}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -369,10 +394,10 @@ export default function SearchDialog() {
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-4 border-t border-line bg-surface px-4 py-2.5 text-xs text-muted md:flex">
|
||||
<span className="flex items-center gap-1"><ArrowUp className="h-3 w-3" /><ArrowDown className="h-3 w-3" /> navegar</span>
|
||||
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" /> abrir</span>
|
||||
<span><kbd className="font-mono font-semibold">Esc</kbd> fechar</span>
|
||||
<span className="ml-auto">{vazia ? "Dica: pressione / em qualquer página" : `${resultados.length} resultado(s)`}</span>
|
||||
<span className="flex items-center gap-1"><ArrowUp className="h-3 w-3" /><ArrowDown className="h-3 w-3" /> {isEn ? "navigate" : "navegar"}</span>
|
||||
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" /> {isEn ? "open" : "abrir"}</span>
|
||||
<span><kbd className="font-mono font-semibold">Esc</kbd> {isEn ? "close" : "fechar"}</span>
|
||||
<span className="ml-auto">{vazia ? (isEn ? "Tip: press / on any page" : "Dica: pressione / em qualquer página") : `${resultados.length} ${isEn ? "result(s)" : "resultado(s)"}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
Reference in new issue
Block a user