Files
ESPDocs/components/ConnectionsDiagram.jsx
T
Lucas RayanandClaude Sonnet 5.5 5a9fe676fc
CI / Lint e auditorias (push) Successful in 1m10s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 2m39s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 18s
Release / Cria a release no Gitea (push) Successful in 5s
feat(i18n): Simplified Chinese (zh-CN), search answers per locale, fr/de/es data fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 23:28:39 -03:00

719 lines
34 KiB
React
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useMemo, useRef, Suspense } from "react";
import { createPortal } from "react-dom";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
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
{ id: "Power", name: "Alimentação", color: "#ef4444" }, // red
{ id: "GPIO", name: "GPIO", color: "#22c55e" }, // green
{ id: "Low Power", name: "GPIO LP", color: "#f59e0b" }, // amber
{ id: "ADC", name: "ADC", color: "#ec4899" }, // pink
{ id: "DAC", name: "DAC", color: "#a855f7" }, // purple
{ id: "Touch", name: "Touch", color: "#06b6d4" }, // cyan
{ id: "UART", name: "UART", color: "#3b82f6" }, // blue
{ id: "SPI", name: "SPI", color: "#f43f5e" }, // rose
{ id: "USB", name: "USB", color: "#a855f7" }, // purple
{ id: "Clock", name: "Clock", color: "#eab308" }, // yellow
{ id: "JTAG", name: "JTAG", color: "#6366f1" }, // indigo
{ id: "Strapping", name: "Inicialização", color: "#dc2626" }, // red
{ id: "DSI", name: "Display (DSI)", color: "#f97316" }, // orange
{ id: "CSI", name: "Câmera (CSI)", color: "#14b8a6" }, // teal
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" }, // sky
{ id: "SD", name: "Cartão SD", color: "#10b981" }, // emerald
{ id: "I2C", name: "I2C", color: "#f59e0b" }, // amber
{ 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 FILTER_CATEGORIES_ES = [
{ id: "All", name: "Todos", color: "#8b5cf6" },
{ id: "GND", name: "Tierra (GND)", color: "#64748b" },
{ id: "Power", name: "Alimentación", color: "#ef4444" },
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
{ id: "Low Power", name: "GPIO LP", 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: "Reloj", color: "#eab308" },
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
{ id: "Strapping", name: "Arranque (Boot)", color: "#dc2626" },
{ id: "DSI", name: "Pantalla (DSI)", color: "#f97316" },
{ id: "CSI", name: "Cámara (CSI)", color: "#14b8a6" },
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
{ id: "SD", name: "Tarjeta SD", color: "#10b981" },
{ id: "I2C", name: "I2C", color: "#f59e0b" },
{ id: "Antena", name: "Antena", color: "#06b6d4" },
];
const FILTER_CATEGORIES_FR = [
{ id: "All", name: "Tous", color: "#8b5cf6" },
{ id: "GND", name: "Masse (GND)", color: "#64748b" },
{ id: "Power", name: "Alimentation", color: "#ef4444" },
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
{ id: "Low Power", name: "GPIO LP", 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: "Horloge", color: "#eab308" },
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
{ id: "Strapping", name: "Démarrage (Boot)", color: "#dc2626" },
{ id: "DSI", name: "Affichage (DSI)", color: "#f97316" },
{ id: "CSI", name: "Caméra (CSI)", color: "#14b8a6" },
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
{ id: "SD", name: "Carte SD", color: "#10b981" },
{ id: "I2C", name: "I2C", color: "#f59e0b" },
{ id: "Antenne", name: "Antenne", color: "#06b6d4" },
];
const FILTER_CATEGORIES_DE = [
{ id: "All", name: "Alle", color: "#8b5cf6" },
{ id: "GND", name: "Masse (GND)", color: "#64748b" },
{ id: "Power", name: "Stromversorgung", 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: "Takt", color: "#eab308" },
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
{ id: "Strapping", name: "Bootmodus", color: "#dc2626" },
{ id: "DSI", name: "Display (DSI)", color: "#f97316" },
{ id: "CSI", name: "Kamera (CSI)", color: "#14b8a6" },
{ id: "Ethernet", name: "Ethernet", color: "#0284c7" },
{ id: "SD", name: "SD-Karte", color: "#10b981" },
{ id: "I2C", name: "I2C", color: "#f59e0b" },
{ id: "Antenne", name: "Antenne", color: "#06b6d4" },
];
const FILTER_CATEGORIES_ZH = [
{ id: "All", name: "全部", color: "#8b5cf6" },
{ id: "GND", name: "接地(GND)", color: "#64748b" },
{ id: "Power", name: "电源", color: "#ef4444" },
{ id: "GPIO", name: "GPIO", color: "#22c55e" },
{ id: "Low Power", name: "低功耗 GPIO", color: "#f59e0b" },
{ id: "ADC", name: "ADC", color: "#ec4899" },
{ id: "DAC", name: "DAC", color: "#a855f7" },
{ id: "Touch", name: "触摸", color: "#06b6d4" },
{ id: "UART", name: "UART", color: "#3b82f6" },
{ id: "SPI", name: "SPI", color: "#f43f5e" },
{ id: "USB", name: "USB", color: "#a855f7" },
{ id: "Clock", name: "时钟", color: "#eab308" },
{ id: "JTAG", name: "JTAG", color: "#6366f1" },
{ id: "Strapping", name: "启动配置", color: "#dc2626" },
{ id: "DSI", name: "显示(DSI)", color: "#f97316" },
{ id: "CSI", name: "摄像头(CSI)", color: "#14b8a6" },
{ id: "Ethernet", name: "以太网", color: "#0284c7" },
{ id: "SD", name: "SD 卡", color: "#10b981" },
{ id: "I2C", name: "I2C", color: "#f59e0b" },
{ id: "Antena", name: "天线", color: "#06b6d4" },
];
const type_colors = {
power: "#ef4444", // high-voltage red
ground: "#64748b", // slate gray
io: "#22c55e", // laser green
analog: "#ec4899", // dynamic pink
dedicated: "#3b82f6", // electric blue
nc: "#94a3b8", // muted gray
};
/**
* Aceita ?pinos=28,30,31&motivo=... (gerado pela busca do site): mostra só
* esses pinos, pelo número físico, com um aviso para voltar à lista completa.
* O Suspense é exigido por useSearchParams em páginas estáticas.
*/
export default function ConnectionsDiagram(props) {
return (
<Suspense fallback={null}>
<ConnectionsDiagramInner {...props} />
</Suspense>
);
}
function ConnectionsDiagramInner({ connections, serie }) {
const params = useSearchParams();
const pathname = usePathname();
const isEn = pathname?.startsWith("/en");
const isEs = pathname?.startsWith("/es");
const isFr = pathname?.startsWith("/fr");
const isDe = pathname?.startsWith("/de");
const isZh = pathname?.startsWith("/zh-CN");
const router = useRouter();
const pinosFiltro = useMemo(() => new Set((params.get("pinos") ?? "").split(",").filter(Boolean)), [params]);
const motivoFiltro = params.get("motivo") ?? "";
const [activeFilter, setActiveFilter] = useState("All");
const [selectedConnection, setSelectedConnection] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState("grid");
const [mounted, setMounted] = useState(false);
const dialogRef = useRef(null);
const gatilhoRef = useRef(null);
const filterCategories = isEn
? FILTER_CATEGORIES_EN
: isEs
? FILTER_CATEGORIES_ES
: isFr
? FILTER_CATEGORIES_FR
: isZh ? FILTER_CATEGORIES_ZH : isDe
? FILTER_CATEGORIES_DE
: FILTER_CATEGORIES;
useEffect(() => {
setMounted(true);
}, []);
const isConnectionVisible = (connection) => {
const matchesFilter = activeFilter === "All" || connection.categorias.includes(activeFilter);
const matchesSearch =
searchQuery === "" ||
connection.nome.toLowerCase().includes(searchQuery.toLowerCase()) ||
connection.numero.toString().toLowerCase().includes(searchQuery.toLowerCase());
const matchesPinos = pinosFiltro.size === 0 || pinosFiltro.has(String(connection.numero));
return matchesFilter && matchesSearch && matchesPinos;
};
const getCategoryColor = (category) => {
const filter = filterCategories.find((f) => f.id === category);
return filter?.color || "#64748b";
};
const nomeCategoria = (cat) => {
if (cat === "Strapping") {
return isEn ? "Boot mode" : isEs ? "Arranque (Boot)" : isFr ? "Démarrage (Boot)" : isZh ? "启动配置" : isDe ? "Bootmodus" : "Inicialização";
}
if (isEn) {
const found = FILTER_CATEGORIES_EN.find((f) => f.id === cat);
if (found) return found.name;
}
if (isEs) {
const found = FILTER_CATEGORIES_ES.find((f) => f.id === cat);
if (found) return found.name;
}
if (isFr) {
const found = FILTER_CATEGORIES_FR.find((f) => f.id === cat);
if (found) return found.name;
}
if (isDe) {
const found = FILTER_CATEGORIES_DE.find((f) => f.id === cat);
if (found) return found.name;
}
if (isZh) {
const found = FILTER_CATEGORIES_ZH.find((f) => f.id === cat);
if (found) return found.name;
}
return cat;
};
const visibleConnections = useMemo(() => connections.filter(isConnectionVisible), [connections, activeFilter, searchQuery, pinosFiltro]);
const mostrarTodos = () => router.replace(`${pathname}#connections`, { scroll: false });
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(() => {
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") {
fechar();
return;
}
if (e.key !== "Tab") return;
const itens = [...focaveis()];
if (itens.length === 0) return;
const primeiro = itens[0];
const ultimo = itens[itens.length - 1];
if (e.shiftKey && document.activeElement === primeiro) {
e.preventDefault();
ultimo.focus();
} else if (!e.shiftKey && document.activeElement === ultimo) {
e.preventDefault();
primeiro.focus();
}
};
document.addEventListener("keydown", aoTeclar);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", aoTeclar);
document.body.style.overflow = "";
};
}, [selectedConnection]);
if (!connections || connections.length === 0) {
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">
{isEn ? "Pinout diagram unavailable" : isEs ? "Diagrama de conexiones no disponible" : isFr ? "Brochage non disponible" : isZh ? "连接图暂不可用" : isDe ? "Pinbelegungsdiagramm nicht verfügbar" : "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."
: isEs
? "Este modelo todavía no dispone de un mapa detallado de conexiones y buses."
: isFr
? "Ce modèle ne dispose pas encore d'un plan détaillé des broches et des bus."
: isZh ? "该型号尚无详细的连接与总线图。" : isDe
? "Dieses Modell verfügt noch über keinen detaillierten Pinbelegungs- und Busplan."
: "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-6">
<div>
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
{isEn ? "Interactive pinout diagram" : isEs ? "Diagrama de conexiones interactivo" : isFr ? "Brochage interactif" : isZh ? "交互式连接图" : isDe ? "Interaktives Pinbelegungsdiagramm" : "Diagrama de conexões interativo"}
</h2>
<p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted">
{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.`
) : isEs ? (
`Todas las conexiones y la multiplexación del ${serie.nome_completo}. Busque por nombre o número, filtre por función y abra un pin para ver los detalles.`
) : isFr ? (
`Toutes les connexions et le multiplexage de l'${serie.nome_completo}. Recherchez par nom ou numéro, filtrez par fonction et ouvrez une broche pour voir les détails.`
) : isZh ? ( `${serie.nome_completo} 的全部连接与复用。可按名称或编号搜索、按功能筛选,并展开引脚查看详情。` ) : isDe ? (
`Alle Anschlüsse und das Multiplexing des ${serie.nome_completo}. Suchen Sie nach Name oder Nummer, filtern Sie nach Funktion und öffnen Sie einen Pin für 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">
{isEn ? "Filtered by search:" : isEs ? "Filtrado por la búsqueda:" : isFr ? "Filtré par la recherche :" : isZh ? "按搜索筛选:" : isDe ? "Nach Suche gefiltert:" : "Filtrado pela busca:"}
</strong>{" "}
{motivoFiltro || (isEn ? "selected pins" : isEs ? "pines seleccionados" : isFr ? "broches sélectionnées" : isZh ? "个引脚已选中" : isDe ? "ausgewählte Pins" : "pinos selecionados")} ({pinosFiltro.size}{" "}
{pinosFiltro.size === 1
? (isEn ? "pin" : isEs ? "pin" : isFr ? "broche" : isZh ? "个引脚" : isDe ? "Pin" : "pino")
: (isEn ? "pins" : isEs ? "pines" : isFr ? "broches" : isZh ? "个引脚" : isDe ? "Pins" : "pinos")})
</p>
<button type="button" onClick={mostrarTodos} className="btn btn-ghost btn-sm">
{isEn ? "Show all pins" : isEs ? "Mostrar todos los pines" : isFr ? "Afficher toutes les broches" : isZh ? "显示全部引脚" : isDe ? "Alle Pins anzeigen" : "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">
{isEn ? "Search connection by name or number" : isEs ? "Buscar conexión por nombre o número" : isFr ? "Rechercher une connexion par nom ou numéro" : isZh ? "按名称或编号搜索连接" : isDe ? "Anschluss nach Name oder Nummer suchen" : "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={
isEn
? "Search pin (e.g.: GPIO1, TXD, XTAL)"
: isEs
? "Buscar pin (ej.: GPIO1, TXD, XTAL)"
: isFr
? "Rechercher une broche (ex. : GPIO1, TXD, XTAL)"
: isZh ? "搜索引脚(例如:GPIO1、TXD、XTAL)" : isDe
? "Pin suchen (z. B.: 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={isEn ? "Display mode" : isEs ? "Modo de visualización" : isFr ? "Mode d'affichage" : isZh ? "显示模式" : isDe ? "Anzeigemodus" : "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" />
{isEn ? "Grid" : isEs ? "Cuadrícula" : isFr ? "Grille" : isZh ? "网格" : isDe ? "Raster" : "Grade"}
</button>
<button type="button" onClick={() => setViewMode("list")} aria-pressed={viewMode === "list"} className="chip-btn">
<List className="h-4 w-4" aria-hidden="true" />
{isEn ? "List" : isEs ? "Lista" : isFr ? "Liste" : isZh ? "列表" : isDe ? "Liste" : "Lista"}
</button>
</div>
</div>
<div>
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-ink">{isEn ? "Pin function" : isEs ? "Función del pin" : isFr ? "Fonction de la broche" : isZh ? "引脚功能" : isDe ? "Pin-Funktion" : "Função do pino"}</h3>
<p role="status" aria-live="polite" className="text-sm text-muted">
{isEn ? (
`${visibleConnections.length} of ${connections.length} pins`
) : isEs ? (
`${visibleConnections.length} de ${connections.length} pines`
) : isFr ? (
`${visibleConnections.length} sur ${connections.length} broches`
) : isZh ? ( `共 ${connections.length} 个引脚,显示 ${visibleConnections.length} 个` ) : isDe ? (
`${visibleConnections.length} von ${connections.length} Pins`
) : (
`${visibleConnections.length} de ${connections.length} pinos`
)}
</p>
</div>
<div role="group" aria-label={isEn ? "Filter by function" : isEs ? "Filtrar por función" : isFr ? "Filtrer par fonction" : isZh ? "按功能筛选" : isDe ? "Nach Funktion filtern" : "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
return (
<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>
</div>
{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">{isEn ? "No connections found" : isEs ? "No se encontraron conexiones" : isFr ? "Aucune connexion trouvée" : isZh ? "未找到连接" : isDe ? "Keine Anschlüsse gefunden" : "Nenhuma conexão encontrada"}</p>
<p className="mt-1 text-sm text-muted">
{isEn
? "Adjust your search or return to all functions."
: isEs
? "Ajuste la búsqueda o vuelva a todas las funciones."
: isFr
? "Ajustez votre recherche ou revenez à toutes les fonctions."
: isZh ? "请调整搜索,或返回全部功能。" : isDe
? "Passen Sie Ihre Suche an oder kehren Sie zu allen Funktionen zurück."
: "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">
{isEn ? "Clear search and filters" : isEs ? "Limpiar búsqueda y filtros" : isFr ? "Effacer la recherche et les filtres" : isZh ? "清除搜索和筛选" : isDe ? "Suche und Filter zurücksetzen" : "Limpar busca e filtros"}
</button>
</div>
) : viewMode === "grid" ? (
<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={
isEn
? `Pin ${connection.nome}, number ${connection.numero}${connection.avisos ? ", with usage restrictions" : ""}`
: isEs
? `Pin ${connection.nome}, número ${connection.numero}${connection.avisos ? ", con restricciones de uso" : ""}`
: isFr
? `Broche ${connection.nome}, numéro ${connection.numero}${connection.avisos ? ", avec restrictions d'utilisation" : ""}`
: isZh ? `引脚 ${connection.nome},编号 ${connection.numero}${connection.avisos ? ",有使用限制" : ""}` : isDe
? `Pin ${connection.nome}, Nummer ${connection.numero}${connection.avisos ? ", mit Nutzungseinschränkungen" : ""}`
: `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 }}
>
{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">
{isEn
? `Connections of ${serie.nome_completo}`
: isEs
? `Conexiones del ${serie.nome_completo}`
: isFr
? `Connexions de l'${serie.nome_completo}`
: isZh ? `${serie.nome_completo} 的连接` : isDe
? `Anschlüsse des ${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">{isEn ? "Pin" : isEs ? "Pin" : isFr ? "Broche" : isZh ? "引脚" : isDe ? "Pin" : "Pino"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Name" : isEs ? "Nombre" : isFr ? "Nom" : isZh ? "名称" : isDe ? "Name" : "Nome"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Functions" : isEs ? "Funciones" : isFr ? "Fonctions" : isZh ? "功能" : isDe ? "Funktionen" : "Funções"}</th>
<th scope="col" className="px-4 py-3 font-medium">{isEn ? "Usage" : isEs ? "Uso" : isFr ? "Utilisation" : isZh ? "用途" : isDe ? "Nutzung" : "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={
isEn
? `Open pin details for ${connection.nome}, number ${connection.numero}`
: isEs
? `Abrir detalles del pin ${connection.nome}, número ${connection.numero}`
: isFr
? `Ouvrir les détails de la broche ${connection.nome}, numéro ${connection.numero}`
: isZh ? `展开引脚 ${connection.nome} 的详情,编号 ${connection.numero}` : isDe
? `Pindetails für ${connection.nome}, Nummer ${connection.numero} öffnen`
: `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" />
{isEn ? "Restricted" : isEs ? "Restringido" : isFr ? "Restreint" : isZh ? "受限" : isDe ? "Eingeschränkt" : "Restrito"}
</span>
) : (
<span className="inline-flex items-center gap-1.5 text-ok">
<CheckCircle className="h-4 w-4" aria-hidden="true" />
{isEn ? "Free" : isEs ? "Libre" : isFr ? "Libre" : isZh ? "可用" : isDe ? "Frei" : "Livre"}
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{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">{isEn ? "Pinout notes" : isEs ? "Notas sobre el patillaje" : isFr ? "Remarques sur le brochage" : isZh ? "引脚说明" : isDe ? "Hinweise zur Pinbelegung" : "Notas sobre a pinagem"}</p>
<ul className="mt-1 list-disc space-y-1 pl-5">
{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>
</>
) : isEs ? (
<>
<li>Los pines con icono de advertencia tienen un papel especial en el arranque o limitaciones analógicas. Abra el pin para ver los detalles.</li>
<li>Aísle las pistas táctiles capacitivas del ruido de RF acoplado.</li>
</>
) : isFr ? (
<>
<li>Les broches signalées par une icône d'avertissement ont un rôle spécial au démarrage ou des restrictions analogiques. Ouvrez la broche pour voir les détails.</li>
<li>Isolez les pistes tactiles capacitives des bruits RF couplés.</li>
</>
) : isZh ? ( <>
<li>带警告图标的引脚在启动时有特殊作用,或存在模拟功能限制。展开该引脚查看详情。</li>
<li>请将电容触摸电路与耦合了射频噪声的引脚隔离开。</li>
</> ) : isDe ? (
<>
<li>Pins mit einem Warnsymbol spielen eine besondere Rolle beim Booten oder unterliegen analogen Einschränkungen. Öffnen Sie den Pin für Details.</li>
<li>Isolieren Sie kapazitive Touch-Leitungen von eingekoppeltem HF-Rauschen.</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>
)}
{selectedConnection && mounted && createPortal(
<div className="dialog-scrim fixed inset-0 z-50 flex items-center justify-center bg-code/80 p-4 backdrop-blur-sm" onClick={fechar}>
<div
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()}
>
<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" }}
>
{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">
{isEn
? `Physical pin ${selectedConnection.numero}`
: isEs
? `Pin físico ${selectedConnection.numero}`
: isFr
? `Broche physique ${selectedConnection.numero}`
: isZh ? `物理引脚 ${selectedConnection.numero}` : isDe
? `Physischer Pin ${selectedConnection.numero}`
: `Pino físico ${selectedConnection.numero}`}
</p>
</div>
</div>
<button
type="button"
onClick={fechar}
aria-label={isEn ? "Close pin details" : isEs ? "Cerrar detalles del pin" : isFr ? "Fermer les détails de la broche" : isZh ? "关闭引脚详情" : isDe ? "Pindetails schließen" : "Fechar detalhes do pino"}
className="grid h-11 w-11 shrink-0 place-items-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
>
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div 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">{isEn ? "Categories" : isEs ? "Categorías" : isFr ? "Catégories" : isZh ? "类别" : isDe ? "Kategorien" : "Categorias"}</h4>
<ul className="mt-3 flex flex-wrap gap-2">
{selectedConnection.categorias.map((cat) => (
<li key={cat} className="chip">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: getCategoryColor(cat) }} aria-hidden="true" />
{nomeCategoria(cat)}
</li>
))}
</ul>
</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" />
{isEn ? "Power domain" : isEs ? "Dominio de alimentación" : isFr ? "Domaine d'alimentation" : isZh ? "供电域" : isDe ? "Spannungsbereich" : "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">{isEn ? "Available functions" : isEs ? "Funciones disponibles" : isFr ? "Fonctions disponibles" : isZh ? "可用功能" : isDe ? "Verfügbare Funktionen" : "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>
{selectedConnection.avisos && (
<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" />
{isEn ? "Engineering notes" : isEs ? "Notas de ingeniería" : isFr ? "Notes d'ingénierie" : isZh ? "工程警告" : isDe ? "Entwicklungshinweise" : "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>
<div className="border-t border-line p-4 sm:px-7">
<button type="button" onClick={fechar} className="btn btn-primary w-full">
{isEn ? "Close" : isEs ? "Cerrar" : isFr ? "Fermer" : isZh ? "关闭" : isDe ? "Schließen" : "Fechar"}
</button>
</div>
</div>
</div>,
document.body
)}
</div>
);
}