Improve hover popup styling consistency and formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-26 13:24:55 -08:00
parent e43982df4e
commit 4c6da837e9

View File

@@ -157,6 +157,25 @@
line-height: 1.4; line-height: 1.4;
} }
.node .switch-port .error-info::before,
.node .uplink .error-info::before {
content: '';
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 8px;
}
.error-info .lbl,
.node-info .lbl,
.dante-info .lbl,
.dante-detail .lbl,
.artnet-info .lbl,
.artnet-detail .lbl {
color: #888;
}
.node .switch-port::after, .node .switch-port::after,
.node .uplink::after { .node .uplink::after {
content: ''; content: '';
@@ -351,6 +370,15 @@
line-height: 1.4; line-height: 1.4;
} }
.node .dante-info .dante-detail::before {
content: '';
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 8px;
}
.node .dante-info::after { .node .dante-info::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -390,6 +418,11 @@
margin-top: 4px; margin-top: 4px;
} }
body.dante-mode .node.dante-tx.dante-rx .dante-info.rx-info .dante-detail::before {
top: auto;
bottom: 100%;
}
body.artnet-mode .node { body.artnet-mode .node {
opacity: 0.3; opacity: 0.3;
} }
@@ -447,6 +480,15 @@
line-height: 1.4; line-height: 1.4;
} }
.node .artnet-info .artnet-detail::before {
content: '';
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 8px;
}
.node .artnet-info::after { .node .artnet-info::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -486,6 +528,11 @@
margin-top: 4px; margin-top: 4px;
} }
body.artnet-mode .node.artnet-out.artnet-in .artnet-info.in-info .artnet-detail::before {
top: auto;
bottom: 100%;
}
.node.has-error { .node.has-error {
box-shadow: 0 0 0 3px #f66; box-shadow: 0 0 0 3px #f66;
} }
@@ -514,10 +561,11 @@
border-radius: 6px; border-radius: 6px;
padding: 6px 8px; padding: 6px 8px;
font-size: 10px; font-size: 10px;
white-space: nowrap; white-space: pre;
z-index: 1000; z-index: 1000;
text-align: left; text-align: left;
line-height: 1.4; line-height: 1.4;
cursor: pointer;
} }
.node .node-info::before { .node .node-info::before {
@@ -549,78 +597,6 @@
display: none; display: none;
} }
.node .node-info .info-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
}
.node .node-info .info-label {
color: #888;
min-width: 28px;
}
.node .node-info .info-value {
color: #eee;
font-family: monospace;
}
.node .node-info .copy-btn {
padding: 2px 4px;
border: none;
background: transparent;
color: #888;
cursor: pointer;
font-size: 12px;
line-height: 1;
width: 20px;
text-align: center;
}
.node .node-info .copy-btn:hover {
color: #ccc;
}
.node .node-info .copy-btn.copied {
color: #4f4;
}
.node .node-info .info-row {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
}
.node .node-info .info-label {
color: #888;
min-width: 28px;
}
.node .node-info .info-value {
color: #eee;
font-family: monospace;
}
.node .node-info .copy-btn {
padding: 2px 4px;
border: none;
background: transparent;
color: #888;
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.node .node-info .copy-btn:hover {
color: #ccc;
}
.node .node-info .copy-btn.copied {
color: #4f4;
}
#error-panel { #error-panel {
position: fixed; position: fixed;
top: 50px; top: 50px;
@@ -1092,14 +1068,15 @@
const errOut = switchConnection.errors?.out || 0; const errOut = switchConnection.errors?.out || 0;
const statsInfo = document.createElement('div'); const statsInfo = document.createElement('div');
statsInfo.className = 'error-info'; statsInfo.className = 'error-info';
let statsText = 'link: ' + formatLinkSpeed(switchConnection.speed); let statsHtml = '<span class="lbl">LINK</span> ' + formatLinkSpeed(switchConnection.speed);
statsText += '\nerr: rx ' + errIn + ' / tx ' + errOut; statsHtml += '\n<span class="lbl">ERR</span> RX ' + errIn + ' / TX ' + errOut;
if (switchConnection.rates) { if (switchConnection.rates) {
const r = switchConnection.rates; const r = switchConnection.rates;
statsText += '\nrx: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; statsHtml += '\n<span class="lbl">RX</span> ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
statsText += '\ntx: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; statsHtml += '\n<span class="lbl">TX</span> ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
} }
statsInfo.textContent = statsText; statsInfo.innerHTML = statsHtml;
statsInfo.addEventListener('click', (e) => e.stopPropagation());
portEl.appendChild(statsInfo); portEl.appendChild(statsInfo);
div.appendChild(portEl); div.appendChild(portEl);
} }
@@ -1119,44 +1096,25 @@
}); });
ips.sort(); ips.sort();
macs.sort(); macs.sort();
const lines = [];
const plainLines = [];
ips.forEach(ip => { ips.forEach(ip => {
const row = document.createElement('div'); lines.push('<span class="lbl">IP</span> ' + ip);
row.className = 'info-row'; plainLines.push('IP: ' + ip);
row.innerHTML = '<span class="info-label">IP</span><span class="info-value">' + ip + '</span>';
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = '⧉';
btn.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(ip).then(() => {
btn.classList.add('copied');
btn.textContent = '✓';
setTimeout(() => { btn.classList.remove('copied'); btn.textContent = '⧉'; }, 500);
});
});
row.appendChild(btn);
nodeInfo.appendChild(row);
}); });
macs.forEach(mac => { macs.forEach(mac => {
const row = document.createElement('div'); lines.push('<span class="lbl">MAC</span> ' + mac);
row.className = 'info-row'; plainLines.push('MAC: ' + mac);
row.innerHTML = '<span class="info-label">MAC</span><span class="info-value">' + mac + '</span>';
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = '⧉';
btn.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(mac).then(() => {
btn.classList.add('copied');
btn.textContent = '✓';
setTimeout(() => { btn.classList.remove('copied'); btn.textContent = '⧉'; }, 500);
});
});
row.appendChild(btn);
nodeInfo.appendChild(row);
}); });
if (lines.length > 0) {
nodeInfo.innerHTML = lines.join('\n');
nodeInfo.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard.writeText(plainLines.join('\n'));
});
}
} }
if (nodeInfo.children.length > 0) { if (nodeInfo.textContent) {
div.appendChild(nodeInfo); div.appendChild(nodeInfo);
} }
@@ -1175,14 +1133,15 @@
const errOut = uplinkInfo.errors?.out || 0; const errOut = uplinkInfo.errors?.out || 0;
const statsInfo = document.createElement('div'); const statsInfo = document.createElement('div');
statsInfo.className = 'error-info'; statsInfo.className = 'error-info';
let statsText = 'link: ' + formatLinkSpeed(uplinkInfo.speed); let statsHtml = '<span class="lbl">LINK</span> ' + formatLinkSpeed(uplinkInfo.speed);
statsText += '\nerr: rx ' + errIn + ' / tx ' + errOut; statsHtml += '\n<span class="lbl">ERR</span> RX ' + errIn + ' / TX ' + errOut;
if (uplinkInfo.rates) { if (uplinkInfo.rates) {
const r = uplinkInfo.rates; const r = uplinkInfo.rates;
statsText += '\nrx: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; statsHtml += '\n<span class="lbl">RX</span> ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')';
statsText += '\ntx: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; statsHtml += '\n<span class="lbl">TX</span> ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')';
} }
statsInfo.textContent = statsText; statsInfo.innerHTML = statsHtml;
statsInfo.addEventListener('click', (e) => e.stopPropagation());
uplinkEl.appendChild(statsInfo); uplinkEl.appendChild(statsInfo);
div.appendChild(uplinkEl); div.appendChild(uplinkEl);
} }
@@ -1191,10 +1150,20 @@
const txEl = document.createElement('div'); const txEl = document.createElement('div');
txEl.className = 'dante-info tx-info'; txEl.className = 'dante-info tx-info';
const firstDest = danteInfo.txTo[0].split('\n')[0]; const firstDest = danteInfo.txTo[0].split('\n')[0];
txEl.textContent = '→ ' + firstDest; txEl.innerHTML = '<span class="lbl">→</span> ' + firstDest;
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'dante-detail'; detail.className = 'dante-detail';
detail.textContent = '→ ' + danteInfo.txTo.join('\n\n→ '); const txHtml = danteInfo.txTo.map(entry => {
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '<span class="lbl">→</span> ' + line;
}).join('\n');
}).join('\n\n');
detail.innerHTML = txHtml;
detail.addEventListener('click', (e) => e.stopPropagation());
txEl.appendChild(detail); txEl.appendChild(detail);
div.appendChild(txEl); div.appendChild(txEl);
} }
@@ -1203,10 +1172,20 @@
const rxEl = document.createElement('div'); const rxEl = document.createElement('div');
rxEl.className = 'dante-info rx-info'; rxEl.className = 'dante-info rx-info';
const firstSource = danteInfo.rxFrom[0].split('\n')[0]; const firstSource = danteInfo.rxFrom[0].split('\n')[0];
rxEl.textContent = '← ' + firstSource; rxEl.innerHTML = '<span class="lbl">←</span> ' + firstSource;
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'dante-detail'; detail.className = 'dante-detail';
detail.textContent = '← ' + danteInfo.rxFrom.join('\n\n← '); const rxHtml = danteInfo.rxFrom.map(entry => {
const lines = entry.split('\n');
return lines.map(line => {
if (line.startsWith(' ')) {
return ' ' + line.trim();
}
return '<span class="lbl">←</span> ' + line;
}).join('\n');
}).join('\n\n');
detail.innerHTML = rxHtml;
detail.addEventListener('click', (e) => e.stopPropagation());
rxEl.appendChild(detail); rxEl.appendChild(detail);
div.appendChild(rxEl); div.appendChild(rxEl);
} }
@@ -1214,10 +1193,11 @@
if (artnetInfo && artnetInfo.isOut) { if (artnetInfo && artnetInfo.isOut) {
const outEl = document.createElement('div'); const outEl = document.createElement('div');
outEl.className = 'artnet-info out-info'; outEl.className = 'artnet-info out-info';
outEl.textContent = '← ' + artnetInfo.outputs[0]; outEl.innerHTML = '<span class="lbl">←</span> ' + artnetInfo.outputs[0];
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'artnet-detail'; detail.className = 'artnet-detail';
detail.textContent = '← ' + artnetInfo.outputs.join('\n'); detail.innerHTML = artnetInfo.outputs.map(u => '<span class="lbl">←</span> ' + u).join('\n');
detail.addEventListener('click', (e) => e.stopPropagation());
outEl.appendChild(detail); outEl.appendChild(detail);
div.appendChild(outEl); div.appendChild(outEl);
} }
@@ -1225,10 +1205,11 @@
if (artnetInfo && artnetInfo.isIn) { if (artnetInfo && artnetInfo.isIn) {
const inEl = document.createElement('div'); const inEl = document.createElement('div');
inEl.className = 'artnet-info in-info'; inEl.className = 'artnet-info in-info';
inEl.textContent = '→ ' + artnetInfo.inputs[0]; inEl.innerHTML = '<span class="lbl">→</span> ' + artnetInfo.inputs[0];
const detail = document.createElement('div'); const detail = document.createElement('div');
detail.className = 'artnet-detail'; detail.className = 'artnet-detail';
detail.textContent = '→ ' + artnetInfo.inputs.join('\n'); detail.innerHTML = artnetInfo.inputs.map(u => '<span class="lbl">→</span> ' + u).join('\n');
detail.addEventListener('click', (e) => e.stopPropagation());
inEl.appendChild(detail); inEl.appendChild(detail);
div.appendChild(inEl); div.appendChild(inEl);
} }