From 4c6da837e95ccee3e0b3d18107f11784d3b64ab1 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Mon, 26 Jan 2026 13:24:55 -0800 Subject: [PATCH] Improve hover popup styling consistency and formatting Co-Authored-By: Claude Opus 4.5 --- static/index.html | 229 +++++++++++++++++++++------------------------- 1 file changed, 105 insertions(+), 124 deletions(-) diff --git a/static/index.html b/static/index.html index 695d1fc..02843a2 100644 --- a/static/index.html +++ b/static/index.html @@ -157,6 +157,25 @@ 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 .uplink::after { content: ''; @@ -351,6 +370,15 @@ 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 { content: ''; position: absolute; @@ -390,6 +418,11 @@ 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 { opacity: 0.3; } @@ -447,6 +480,15 @@ 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 { content: ''; position: absolute; @@ -486,6 +528,11 @@ 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 { box-shadow: 0 0 0 3px #f66; } @@ -514,10 +561,11 @@ border-radius: 6px; padding: 6px 8px; font-size: 10px; - white-space: nowrap; + white-space: pre; z-index: 1000; text-align: left; line-height: 1.4; + cursor: pointer; } .node .node-info::before { @@ -549,78 +597,6 @@ 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 { position: fixed; top: 50px; @@ -1092,14 +1068,15 @@ const errOut = switchConnection.errors?.out || 0; const statsInfo = document.createElement('div'); statsInfo.className = 'error-info'; - let statsText = 'link: ' + formatLinkSpeed(switchConnection.speed); - statsText += '\nerr: rx ' + errIn + ' / tx ' + errOut; + let statsHtml = 'LINK ' + formatLinkSpeed(switchConnection.speed); + statsHtml += '\nERR RX ' + errIn + ' / TX ' + errOut; if (switchConnection.rates) { const r = switchConnection.rates; - statsText += '\nrx: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; - statsText += '\ntx: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; + statsHtml += '\nRX ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; + statsHtml += '\nTX ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; } - statsInfo.textContent = statsText; + statsInfo.innerHTML = statsHtml; + statsInfo.addEventListener('click', (e) => e.stopPropagation()); portEl.appendChild(statsInfo); div.appendChild(portEl); } @@ -1119,44 +1096,25 @@ }); ips.sort(); macs.sort(); + const lines = []; + const plainLines = []; ips.forEach(ip => { - const row = document.createElement('div'); - row.className = 'info-row'; - row.innerHTML = 'IP' + ip + ''; - 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); + lines.push('IP ' + ip); + plainLines.push('IP: ' + ip); }); macs.forEach(mac => { - const row = document.createElement('div'); - row.className = 'info-row'; - row.innerHTML = 'MAC' + mac + ''; - 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); + lines.push('MAC ' + mac); + plainLines.push('MAC: ' + mac); }); + 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); } @@ -1175,14 +1133,15 @@ const errOut = uplinkInfo.errors?.out || 0; const statsInfo = document.createElement('div'); statsInfo.className = 'error-info'; - let statsText = 'link: ' + formatLinkSpeed(uplinkInfo.speed); - statsText += '\nerr: rx ' + errIn + ' / tx ' + errOut; + let statsHtml = 'LINK ' + formatLinkSpeed(uplinkInfo.speed); + statsHtml += '\nERR RX ' + errIn + ' / TX ' + errOut; if (uplinkInfo.rates) { const r = uplinkInfo.rates; - statsText += '\nrx: ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; - statsText += '\ntx: ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; + statsHtml += '\nRX ' + formatMbps(r.inBytes) + ' (' + formatPps(r.inPkts) + ')'; + statsHtml += '\nTX ' + formatMbps(r.outBytes) + ' (' + formatPps(r.outPkts) + ')'; } - statsInfo.textContent = statsText; + statsInfo.innerHTML = statsHtml; + statsInfo.addEventListener('click', (e) => e.stopPropagation()); uplinkEl.appendChild(statsInfo); div.appendChild(uplinkEl); } @@ -1191,10 +1150,20 @@ const txEl = document.createElement('div'); txEl.className = 'dante-info tx-info'; const firstDest = danteInfo.txTo[0].split('\n')[0]; - txEl.textContent = '→ ' + firstDest; + txEl.innerHTML = ' ' + firstDest; const detail = document.createElement('div'); 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 ' ' + line; + }).join('\n'); + }).join('\n\n'); + detail.innerHTML = txHtml; + detail.addEventListener('click', (e) => e.stopPropagation()); txEl.appendChild(detail); div.appendChild(txEl); } @@ -1203,10 +1172,20 @@ const rxEl = document.createElement('div'); rxEl.className = 'dante-info rx-info'; const firstSource = danteInfo.rxFrom[0].split('\n')[0]; - rxEl.textContent = '← ' + firstSource; + rxEl.innerHTML = ' ' + firstSource; const detail = document.createElement('div'); 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 ' ' + line; + }).join('\n'); + }).join('\n\n'); + detail.innerHTML = rxHtml; + detail.addEventListener('click', (e) => e.stopPropagation()); rxEl.appendChild(detail); div.appendChild(rxEl); } @@ -1214,10 +1193,11 @@ if (artnetInfo && artnetInfo.isOut) { const outEl = document.createElement('div'); outEl.className = 'artnet-info out-info'; - outEl.textContent = '← ' + artnetInfo.outputs[0]; + outEl.innerHTML = ' ' + artnetInfo.outputs[0]; const detail = document.createElement('div'); detail.className = 'artnet-detail'; - detail.textContent = '← ' + artnetInfo.outputs.join('\n← '); + detail.innerHTML = artnetInfo.outputs.map(u => ' ' + u).join('\n'); + detail.addEventListener('click', (e) => e.stopPropagation()); outEl.appendChild(detail); div.appendChild(outEl); } @@ -1225,10 +1205,11 @@ if (artnetInfo && artnetInfo.isIn) { const inEl = document.createElement('div'); inEl.className = 'artnet-info in-info'; - inEl.textContent = '→ ' + artnetInfo.inputs[0]; + inEl.innerHTML = ' ' + artnetInfo.inputs[0]; const detail = document.createElement('div'); detail.className = 'artnet-detail'; - detail.textContent = '→ ' + artnetInfo.inputs.join('\n→ '); + detail.innerHTML = artnetInfo.inputs.map(u => ' ' + u).join('\n'); + detail.addEventListener('click', (e) => e.stopPropagation()); inEl.appendChild(detail); div.appendChild(inEl); }