715 lines
29 KiB
React
715 lines
29 KiB
React
"use client";
|
|
|
|
import { useState, useEffect, useMemo } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import {
|
|
Search,
|
|
X,
|
|
Grid,
|
|
List,
|
|
Filter,
|
|
AlertTriangle,
|
|
Info,
|
|
Tag,
|
|
Zap,
|
|
Sliders,
|
|
CheckCircle,
|
|
ExternalLink
|
|
} from "lucide-react";
|
|
|
|
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 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
|
|
};
|
|
|
|
export default function ConnectionsDiagram({ connections, serie }) {
|
|
const [activeFilter, setActiveFilter] = useState("All");
|
|
const [selectedConnection, setSelectedConnection] = useState(null);
|
|
const [hoveredConnection, setHoveredConnection] = useState(null);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [viewMode, setViewMode] = useState("grid");
|
|
const [showTooltip, setShowTooltip] = useState(null);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
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());
|
|
return matchesFilter && matchesSearch;
|
|
};
|
|
|
|
const getCategoryColor = (category) => {
|
|
const filter = FILTER_CATEGORIES.find((f) => f.id === category);
|
|
return filter?.color || "#64748b";
|
|
};
|
|
|
|
const visibleConnections = useMemo(() => {
|
|
return connections.filter(isConnectionVisible);
|
|
}, [connections, activeFilter, searchQuery]);
|
|
|
|
const activeCategories = useMemo(() => {
|
|
if (activeFilter === "All") return [];
|
|
return FILTER_CATEGORIES.filter(cat =>
|
|
visibleConnections.some(conn => conn.categorias.includes(cat.id))
|
|
);
|
|
}, [visibleConnections, activeFilter]);
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (e) => {
|
|
if (e.key === "Escape") {
|
|
setSelectedConnection(null);
|
|
setShowTooltip(null);
|
|
}
|
|
};
|
|
|
|
if (selectedConnection) {
|
|
document.addEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "hidden";
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "unset";
|
|
};
|
|
}, [selectedConnection]);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if (selectedConnection) return;
|
|
|
|
if (e.key === "Tab") {
|
|
const buttons = document.querySelectorAll('[data-connection-button]');
|
|
const currentIndex = Array.from(buttons).findIndex(btn => btn === document.activeElement);
|
|
|
|
if (e.shiftKey && currentIndex > 0) {
|
|
e.preventDefault();
|
|
buttons[currentIndex - 1].focus();
|
|
} else if (!e.shiftKey && currentIndex < buttons.length - 1) {
|
|
e.preventDefault();
|
|
buttons[currentIndex + 1].focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [selectedConnection]);
|
|
|
|
if (!connections || connections.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-16 px-4">
|
|
<div className="w-20 h-20 mb-6 rounded-2xl bg-slate-100 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 flex items-center justify-center shadow-lg">
|
|
<Sliders className="w-10 h-10 text-slate-400 dark:text-slate-600" />
|
|
</div>
|
|
|
|
<p className="text-lg font-bold text-slate-800 dark:text-slate-200">
|
|
Diagrama de conexões indisponível
|
|
</p>
|
|
|
|
<p className="text-xs text-slate-400 dark:text-slate-500 mt-2">
|
|
Este modelo ainda não possui um mapa detalhado de conexões e barramentos.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8 pb-8">
|
|
|
|
{/* Header info */}
|
|
<div className="space-y-3">
|
|
<h2 className="text-xl font-bold text-slate-850 dark:text-slate-100 mb-4 pb-3 border-b border-slate-200/60 dark:border-slate-800/65 flex items-center gap-3 select-none">
|
|
<Sliders className="w-5 h-5" style={{ color: serie.color }} />
|
|
<span>Diagrama de Conexões Interativo</span>
|
|
</h2>
|
|
<p className="text-sm text-slate-550 dark:text-slate-400 leading-relaxed">
|
|
Explore todas as conexões e multiplexação do SoC do {serie.nome_completo}. Utilize o painel de busca rápida ou filtre por categoria para acender conexões específicas e ver mapas de barramentos.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Search Panel */}
|
|
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-200/60 dark:border-slate-800/80 p-5 space-y-4 shadow-xl">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1 relative">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-slate-400 dark:text-slate-500" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar por nome ou número da conexão (Ex: GPIO1, TXD, XTAL)..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-12 pr-10 py-3 rounded-xl border border-slate-200/60 dark:border-slate-800 bg-white dark:bg-slate-950/40 focus:border-purple-500/80 focus:ring-4 focus:ring-purple-500/10 text-slate-800 dark:text-slate-200 transition-all outline-none text-sm font-semibold"
|
|
aria-label="Search connections"
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
onClick={() => setSearchQuery("")}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors"
|
|
aria-label="Clear search"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* View Mode Toggle */}
|
|
<div className="flex gap-1.5 bg-slate-100 dark:bg-slate-950/80 rounded-xl p-1 shrink-0 border border-slate-250/30 dark:border-slate-850/30">
|
|
<button
|
|
onClick={() => setViewMode("grid")}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
|
|
viewMode === "grid"
|
|
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
|
|
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
}`}
|
|
aria-label="Grid view"
|
|
aria-pressed={viewMode === "grid"}
|
|
>
|
|
<Grid className="w-3.5 h-3.5" />
|
|
<span>Grade</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setViewMode("list")}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 ${
|
|
viewMode === "list"
|
|
? "bg-white dark:bg-slate-900 shadow-md text-slate-900 dark:text-slate-100"
|
|
: "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
|
}`}
|
|
aria-label="List view"
|
|
aria-pressed={viewMode === "list"}
|
|
>
|
|
<List className="w-3.5 h-3.5" />
|
|
<span>Lista</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters Panel */}
|
|
<div className="bg-white/60 dark:bg-slate-900/30 backdrop-blur-xl rounded-2xl border border-slate-200/60 dark:border-slate-800/80 p-5 shadow-xl">
|
|
<div className="flex items-center gap-3 mb-5 border-b border-slate-150/40 dark:border-slate-850/45 pb-3">
|
|
<Filter className="w-4 h-4 text-purple-500" />
|
|
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-100 select-none">
|
|
Filtre por Função do Pino
|
|
</h4>
|
|
|
|
<span className="ml-auto text-[10px] font-bold text-slate-400 dark:text-slate-500 bg-slate-100 dark:bg-slate-950 px-3 py-1 rounded-full border border-slate-200/30 dark:border-slate-850/30 shadow-xs select-none">
|
|
{visibleConnections.length} de {connections.length} ativos
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{FILTER_CATEGORIES.map((filter) => {
|
|
const isActive = activeFilter === filter.id;
|
|
const count = connections.filter(c =>
|
|
filter.id === "All" ? true : c.categorias.includes(filter.id)
|
|
).length;
|
|
|
|
if (count === 0 && !isActive) return null; // Hide filters that don't apply to this chip
|
|
|
|
return (
|
|
<button
|
|
key={filter.id}
|
|
onClick={() => setActiveFilter(filter.id)}
|
|
aria-pressed={isActive}
|
|
aria-label={`Filter by ${filter.name}`}
|
|
className={`group relative px-4 py-2 rounded-xl text-xs font-extrabold transition-all duration-300 focus:outline-none cursor-pointer active:scale-95 ${
|
|
isActive
|
|
? "shadow-lg text-white"
|
|
: "bg-slate-50/70 dark:bg-slate-950/20 text-slate-650 dark:text-slate-350 hover:bg-slate-100/50 dark:hover:bg-slate-850/30 border border-slate-200/40 dark:border-slate-850/40"
|
|
}`}
|
|
style={{
|
|
backgroundColor: isActive ? filter.color : undefined,
|
|
boxShadow: isActive ? `0 8px 15px -8px ${filter.color}` : undefined
|
|
}}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span>{filter.name}</span>
|
|
<span className={`text-[9px] px-1.5 py-0.5 rounded-full shrink-0 font-bold ${
|
|
isActive ? "bg-white/20 text-white" : "bg-slate-200/50 dark:bg-slate-800/80 text-slate-555 dark:text-slate-400"
|
|
}`}>
|
|
{count}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Active Categories badges */}
|
|
{activeFilter !== "All" && activeCategories.length > 0 && (
|
|
<div className="mt-5 pt-5 border-t border-slate-100 dark:border-slate-850/40 select-none">
|
|
<p className="text-[10px] font-bold text-slate-400 dark:text-slate-500 mb-3 uppercase tracking-wider">Subcategorias ativadas por esse filtro:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{activeCategories.map(cat => (
|
|
<span
|
|
key={cat.id}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-[10px] font-bold text-white shadow-xs"
|
|
style={{ backgroundColor: cat.color }}
|
|
>
|
|
<span className="w-1.5 h-1.5 rounded-full bg-white shrink-0"></span>
|
|
<span>{cat.name}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Diagram Workspace */}
|
|
{visibleConnections.length === 0 ? (
|
|
<div className="bg-slate-50/50 dark:bg-slate-900/10 backdrop-blur-xl rounded-2xl p-12 border-2 border-dashed border-slate-200 dark:border-slate-850/80 text-center shadow-inner">
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 dark:bg-slate-950 flex items-center justify-center shadow-md">
|
|
<Search className="w-6 h-6 text-slate-400" />
|
|
</div>
|
|
<p className="text-base font-bold text-slate-800 dark:text-slate-200 mb-1 select-none">Nenhuma conexão encontrada</p>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">Tente ajustar seus termos de pesquisa ou limpe os filtros rápidos.</p>
|
|
|
|
<button
|
|
onClick={() => {
|
|
setActiveFilter("All");
|
|
setSearchQuery("");
|
|
}}
|
|
className="mt-5 px-6 py-2.5 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-950 rounded-xl hover:opacity-90 transition-all font-bold text-xs uppercase tracking-wider cursor-pointer active:scale-95 shadow-lg"
|
|
>
|
|
Limpar Filtros e Busca
|
|
</button>
|
|
</div>
|
|
) : viewMode === "grid" ? (
|
|
/* Grid View Mode: Futuristic Electronic Chip Sockets */
|
|
<div className="bg-slate-100/50 dark:bg-slate-950/20 backdrop-blur-xl rounded-3xl p-6 md:p-8 border border-slate-200/50 dark:border-slate-850/60 shadow-inner">
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
|
{visibleConnections.map((connection, index) => {
|
|
const isHovered = hoveredConnection === index;
|
|
const connectionId = `connection-${index}`;
|
|
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
|
|
|
return (
|
|
<div key={index} className="relative">
|
|
<button
|
|
id={connectionId}
|
|
data-connection-button
|
|
onClick={() => setSelectedConnection(connection)}
|
|
onMouseEnter={() => {
|
|
setHoveredConnection(index);
|
|
setShowTooltip(index);
|
|
}}
|
|
onMouseLeave={() => {
|
|
setHoveredConnection(null);
|
|
setShowTooltip(null);
|
|
}}
|
|
onFocus={() => setHoveredConnection(index)}
|
|
onBlur={() => setHoveredConnection(null)}
|
|
aria-label={`Pino ${connection.nome}, número ${connection.numero}`}
|
|
aria-describedby={showTooltip === index ? `tooltip-${index}` : undefined}
|
|
className="w-full flex flex-col items-center justify-center p-4 rounded-2xl border-2 transition-all duration-300 cursor-pointer active:scale-95 focus:outline-none"
|
|
style={{
|
|
backgroundColor: isHovered ? `${baseColor}24` : `${baseColor}0c`,
|
|
borderColor: isHovered ? baseColor : `${baseColor}40`,
|
|
boxShadow: isHovered ? `0 0 20px -5px ${baseColor}60` : 'none',
|
|
transform: isHovered ? "translateY(-4px)" : undefined,
|
|
}}
|
|
>
|
|
{/* Pad Contact Core Circle */}
|
|
<div
|
|
className="w-12 h-12 rounded-full flex items-center justify-center text-white font-extrabold text-xs mb-3 shadow-lg select-none"
|
|
style={{ backgroundColor: baseColor, boxShadow: `0 4px 10px -3px ${baseColor}` }}
|
|
>
|
|
{typeof connection.numero === "number"
|
|
? connection.numero
|
|
: connection.numero.substring(0, 4)}
|
|
</div>
|
|
|
|
{/* Pad Name label */}
|
|
<div className="text-xs font-bold text-center text-slate-850 dark:text-slate-200 leading-tight mb-2 select-none group-hover:text-purple-600 transition-colors">
|
|
{connection.nome}
|
|
</div>
|
|
|
|
{/* Small Category dots indicator */}
|
|
<div className="flex flex-wrap gap-1 justify-center select-none">
|
|
{connection.categorias.slice(0, 5).map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="w-1.5 h-1.5 rounded-full shrink-0"
|
|
style={{ backgroundColor: getCategoryColor(cat) }}
|
|
title={cat === "Strapping" ? "Inicialização" : cat}
|
|
/>
|
|
))}
|
|
{connection.categorias.length > 5 && (
|
|
<span className="text-[9px] text-slate-400 dark:text-slate-500 font-bold ml-0.5 leading-none">
|
|
+{connection.categorias.length - 5}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* High-Voltage warnings tags */}
|
|
{connection.avisos && (
|
|
<div
|
|
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 shadow-lg ring-2 ring-white dark:ring-slate-950 animate-pulse"
|
|
aria-label="Possui restrições de uso"
|
|
>
|
|
<AlertTriangle className="w-3 h-3" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
|
|
{/* Sleek Tooltip Popover */}
|
|
{showTooltip === index && (
|
|
<div
|
|
id={`tooltip-${index}`}
|
|
role="tooltip"
|
|
className="absolute z-40 bottom-full left-1/2 -translate-x-1/2 mb-3 p-3 bg-slate-950/90 dark:bg-slate-900/90 backdrop-blur-md text-white text-[10px] rounded-xl shadow-2xl max-w-xs pointer-events-none animate-fadeIn border border-slate-850/50"
|
|
style={{ minWidth: "180px" }}
|
|
>
|
|
<div className="font-extrabold text-sm mb-1">{connection.nome}</div>
|
|
<div className="text-slate-400 mb-2 font-semibold">Pino Físico {connection.numero}</div>
|
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
{connection.categorias.map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="px-1.5 py-0.5 rounded text-[8px] font-bold"
|
|
style={{
|
|
backgroundColor: `${getCategoryColor(cat)}24`,
|
|
color: getCategoryColor(cat)
|
|
}}
|
|
>
|
|
{cat === "Strapping" ? "Inicialização" : cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="text-purple-400 font-bold text-[9px] uppercase tracking-wider">Clique para detalhes</div>
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 border-4 border-transparent border-t-slate-950/95 dark:border-t-slate-900/95"></div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* List View Mode: Interactive Zebra Table */
|
|
<div className="bg-white/80 dark:bg-slate-900/40 backdrop-blur-xl rounded-2xl border border-slate-200/60 dark:border-slate-800/80 overflow-hidden shadow-xl">
|
|
<div className="overflow-x-auto scrollbar-thin">
|
|
<table className="w-full text-left">
|
|
<thead>
|
|
<tr className="bg-slate-100/50 dark:bg-slate-950/50 border-b border-slate-200/60 dark:border-slate-850/60 select-none">
|
|
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Pino</th>
|
|
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Nome do Pad</th>
|
|
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Categorias</th>
|
|
<th className="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Multiplexação de Funções</th>
|
|
<th className="px-6 py-4 text-center text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">Restrições</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-850/30">
|
|
{visibleConnections.map((connection, index) => {
|
|
const baseColor = type_colors[connection.tipo] || "#94a3b8";
|
|
return (
|
|
<tr
|
|
key={index}
|
|
onClick={() => setSelectedConnection(connection)}
|
|
className="hover:bg-purple-500/5 dark:hover:bg-purple-400/5 cursor-pointer transition-colors group"
|
|
>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div
|
|
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-extrabold text-xs shadow-md select-none"
|
|
style={{ backgroundColor: baseColor }}
|
|
>
|
|
{typeof connection.numero === "number"
|
|
? connection.numero
|
|
: connection.numero.substring(0, 4)}
|
|
</div>
|
|
</td>
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-bold text-slate-800 dark:text-slate-200 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors">
|
|
{connection.nome}
|
|
</div>
|
|
</td>
|
|
|
|
<td className="px-6 py-4">
|
|
<div className="flex flex-wrap gap-1.5 select-none">
|
|
{connection.categorias.map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="inline-flex items-center px-2 py-0.5 rounded-md text-[9px] font-bold text-white shadow-xs"
|
|
style={{ backgroundColor: getCategoryColor(cat) }}
|
|
>
|
|
{cat === "Strapping" ? "Inicialização" : cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</td>
|
|
|
|
<td className="px-6 py-4">
|
|
<div className="text-xs text-slate-500 dark:text-slate-400 font-semibold max-w-md line-clamp-2 leading-relaxed">
|
|
{connection.funcoes.join(", ")}
|
|
</div>
|
|
</td>
|
|
|
|
<td className="px-6 py-4 text-center whitespace-nowrap">
|
|
{connection.avisos ? (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-red-500/10 text-red-600 dark:text-red-400 text-[10px] font-bold uppercase border border-red-500/20 select-none">
|
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
<span>Restrito</span>
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 text-[10px] font-bold uppercase border border-emerald-500/20 select-none">
|
|
<CheckCircle className="w-3.5 h-3.5 shrink-0" />
|
|
<span>Livre</span>
|
|
</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Global Warning notice */}
|
|
{connections.some(c => c.avisos) && (
|
|
<div className="bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 rounded-2xl p-6 shadow-sm select-none">
|
|
<div className="flex gap-4">
|
|
<div className="shrink-0">
|
|
<div className="w-12 h-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center">
|
|
<AlertTriangle className="w-6 h-6" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<p className="text-base font-bold text-amber-850 dark:text-amber-300 mb-1">
|
|
Notas Importantes sobre a Pinagem
|
|
</p>
|
|
<ul className="space-y-1.5 text-xs text-amber-800 dark:text-amber-400 leading-relaxed">
|
|
<li>• Pinos contendo tags de <strong>Avisos/Restrições</strong> (cor vermelha) possuem papéis especiais de Strapping na inicialização, ou limitações analógicas. Clique neles para ver detalhes.</li>
|
|
<li>• Sempre garanta isolamento de circuitos capacitivos de toque em pinos com ruído de RF acoplado.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Interactive Overlay Dialog Details Sheet */}
|
|
{selectedConnection && mounted && createPortal(
|
|
<div
|
|
className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fadeIn"
|
|
onClick={() => setSelectedConnection(null)}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
>
|
|
<div
|
|
className="bg-white/95 dark:bg-slate-950/95 backdrop-blur-xl border border-slate-200/50 dark:border-slate-800/80 rounded-3xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col animate-slideUp"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Dialog Header */}
|
|
<div className="sticky top-0 bg-white/50 dark:bg-slate-950/50 border-b border-slate-200/60 dark:border-slate-850/60 p-6 md:p-8 z-10">
|
|
<div className="flex items-start justify-between gap-6">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
<div
|
|
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-extrabold text-lg shadow-xl shrink-0"
|
|
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
|
|
>
|
|
{typeof selectedConnection.numero === "number"
|
|
? selectedConnection.numero
|
|
: selectedConnection.numero.substring(0, 4)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 id="modal-title" className="text-2xl font-display font-extrabold text-slate-850 dark:text-slate-100 mb-1 leading-none tracking-tight">
|
|
{selectedConnection.nome}
|
|
</h3>
|
|
<p className="text-xs text-slate-400 dark:text-slate-500 font-bold uppercase tracking-wider">
|
|
Pino Físico {selectedConnection.numero}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setSelectedConnection(null)}
|
|
aria-label="Fechar"
|
|
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 bg-slate-100 dark:bg-slate-900 rounded-xl shrink-0 cursor-pointer active:scale-95"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dialog Body Contents */}
|
|
<div className="flex-1 overflow-y-auto p-6 md:p-8 space-y-8 scrollbar-thin">
|
|
|
|
{/* Categories */}
|
|
{selectedConnection.categorias.length > 0 && (
|
|
<div>
|
|
<h4 className="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
|
<Tag className="w-4 h-4 text-purple-500" />
|
|
<span>Categorias de Multiplexação</span>
|
|
</h4>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{selectedConnection.categorias.map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="px-3 py-1.5 rounded-xl text-xs font-bold text-white shadow-md select-none"
|
|
style={{ backgroundColor: getCategoryColor(cat) }}
|
|
>
|
|
{cat === "Strapping" ? "Inicialização" : cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Power Source */}
|
|
{selectedConnection.alimentacao && (
|
|
<div>
|
|
<h4 className="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
|
<Zap className="w-4 h-4 text-red-500" />
|
|
<span>Domínio de Alimentação Interna</span>
|
|
</h4>
|
|
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-200/50 dark:border-slate-850/50">
|
|
<ul className="space-y-2.5">
|
|
{selectedConnection.alimentacao.map((source, index) => (
|
|
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
|
|
<span
|
|
className="shrink-0 w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: type_colors["power"] }}
|
|
/>
|
|
<span>{source}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Function Multiplexation */}
|
|
<div>
|
|
<h4 className="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2 select-none">
|
|
<Sliders className="w-4 h-4 text-emerald-500" />
|
|
<span>Barramentos de Funções Disponíveis</span>
|
|
</h4>
|
|
|
|
<div className="bg-slate-50 dark:bg-slate-900/30 rounded-2xl p-5 border border-slate-200/50 dark:border-slate-850/50">
|
|
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{selectedConnection.funcoes.map((func, index) => (
|
|
<li key={index} className="flex items-center gap-3 text-xs font-bold text-slate-700 dark:text-slate-300">
|
|
<span
|
|
className="shrink-0 w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: type_colors[selectedConnection.tipo] || "#64748b" }}
|
|
/>
|
|
<span>{func}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alerts/Warnings */}
|
|
{selectedConnection.avisos && (
|
|
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl p-5 shadow-xs">
|
|
<div className="flex gap-4">
|
|
<div className="shrink-0">
|
|
<div className="w-10 h-10 rounded-xl bg-red-500/15 border border-red-500/25 flex items-center justify-center text-red-500">
|
|
<AlertTriangle className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm font-bold text-red-900 dark:text-red-300 mb-2">
|
|
Avisos Importantes de Engenharia
|
|
</p>
|
|
{selectedConnection.avisos.map((aviso, index) => (
|
|
<div key={index} className="flex items-start gap-2.5 mb-1.5 text-xs text-red-800 dark:text-red-400 font-semibold leading-relaxed">
|
|
<span>•</span>
|
|
<span>{aviso}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Dialog Footer */}
|
|
<div className="sticky bottom-0 bg-white/50 dark:bg-slate-950/50 border-t border-slate-200/60 dark:border-slate-850/60 p-5">
|
|
<button
|
|
onClick={() => setSelectedConnection(null)}
|
|
className="w-full py-3.5 rounded-xl font-bold text-white text-xs uppercase tracking-wider shadow-lg hover:opacity-90 active:scale-98 transition-all cursor-pointer"
|
|
style={{
|
|
backgroundColor: serie.color,
|
|
boxShadow: `0 8px 25px -8px ${serie.color}`
|
|
}}
|
|
aria-label="Close modal"
|
|
>
|
|
Confirmar e Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)}
|
|
|
|
<style jsx>{`
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
transform: translateY(20px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.animate-fadeIn {
|
|
animation: fadeIn 0.2s ease-out;
|
|
}
|
|
|
|
.animate-slideUp {
|
|
animation: slideUp 0.3s ease-out;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
} |