Public web-only release: SvxPortal front-end + auth backend, install templates and INSTALL.md
Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr), deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles live in install/ with CHANGEME placeholders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
// Shared hover info box: any element carrying a data-info attribute (URI-encoded
|
||||
// HTML) shows a floating details box while the mouse is over it. Wired once via
|
||||
// delegation, so it works for rows rendered at any time. CSP-safe (no inline JS).
|
||||
(function(){
|
||||
let box = null;
|
||||
|
||||
function ensureBox(){
|
||||
if (box) return box;
|
||||
box = document.createElement('div');
|
||||
box.id = 'rowInfo';
|
||||
box.style.cssText =
|
||||
'position:fixed;z-index:2000;display:none;max-width:340px;padding:8px 11px;'
|
||||
+ 'background:#0a1628;border:1px solid #2d5a9e;border-radius:8px;font-size:12px;'
|
||||
+ 'line-height:1.5;color:#e2e8f0;box-shadow:0 6px 20px rgba(0,0,0,.6);'
|
||||
+ 'pointer-events:none;white-space:normal;';
|
||||
document.body.appendChild(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
function place(x, y){
|
||||
if (!box) return;
|
||||
const pad = 14, w = box.offsetWidth, h = box.offsetHeight;
|
||||
let left = x + pad, top = y + pad;
|
||||
if (left + w > window.innerWidth - 8) left = x - w - pad;
|
||||
if (top + h > window.innerHeight - 8) top = y - h - pad;
|
||||
if (left < 4) left = 4;
|
||||
if (top < 4) top = 4;
|
||||
box.style.left = left + 'px';
|
||||
box.style.top = top + 'px';
|
||||
}
|
||||
|
||||
function show(html, x, y){
|
||||
const b = ensureBox();
|
||||
b.innerHTML = html;
|
||||
b.style.display = 'block';
|
||||
place(x, y);
|
||||
}
|
||||
function hide(){ if (box) box.style.display = 'none'; }
|
||||
|
||||
function infoEl(t){ return (t && t.closest) ? t.closest('[data-info]') : null; }
|
||||
|
||||
document.addEventListener('mouseover', function(e){
|
||||
const el = infoEl(e.target);
|
||||
if (!el) return;
|
||||
const raw = el.getAttribute('data-info');
|
||||
if (!raw) { hide(); return; }
|
||||
let html = '';
|
||||
try { html = decodeURIComponent(raw); } catch (_) { html = raw; }
|
||||
show(html, e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(e){
|
||||
if (!box || box.style.display !== 'block') return;
|
||||
if (infoEl(e.target)) place(e.clientX, e.clientY);
|
||||
else hide();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', function(e){
|
||||
if (!infoEl(e.target)) return;
|
||||
const to = e.relatedTarget;
|
||||
if (!to || !to.closest || !to.closest('[data-info]')) hide();
|
||||
});
|
||||
|
||||
// Build a URI-encoded data-info payload from label/value pairs.
|
||||
// rows: array of [label, value]; a value that is empty/null/undefined is skipped.
|
||||
// Pass {talking:true} to prepend a red "Talking" line.
|
||||
window.buildRowInfo = function(rows, opts){
|
||||
opts = opts || {};
|
||||
const parts = [];
|
||||
if (opts.talking) parts.push('<div style="color:#ff5252;font-weight:bold">● Talking</div>');
|
||||
for (const r of (rows || [])){
|
||||
if (!r) continue;
|
||||
const v = r[1];
|
||||
if (v === undefined || v === null || v === '') continue;
|
||||
parts.push('<div><span style="color:#7dd3fc">' + escHtml(r[0]) + ':</span> ' + escHtml(v) + '</div>');
|
||||
}
|
||||
if (!parts.length) return '';
|
||||
return encodeURIComponent(parts.join(''));
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user