181 lines
7.2 KiB
JavaScript
181 lines
7.2 KiB
JavaScript
// ═══════════════════════════════════════════════════════════
|
|
// MNEMOSYNE — Memory Inspect Panel (issue #1227)
|
|
// ═══════════════════════════════════════════════════════════
|
|
//
|
|
// Side-panel detail view for memory crystals.
|
|
// Opens when a crystal is clicked; auto-closes on empty-space click.
|
|
//
|
|
// Usage from app.js:
|
|
// MemoryInspect.init({ onNavigate: fn });
|
|
// MemoryInspect.show(memData, regionDef);
|
|
// MemoryInspect.hide();
|
|
// MemoryInspect.isOpen();
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
const MemoryInspect = (() => {
|
|
let _panel = null;
|
|
let _onNavigate = null; // callback(memId) — navigate to a linked memory
|
|
|
|
// ─── INIT ────────────────────────────────────────────────
|
|
function init(opts = {}) {
|
|
_onNavigate = opts.onNavigate || null;
|
|
_panel = document.getElementById('memory-inspect-panel');
|
|
if (!_panel) {
|
|
console.warn('[MemoryInspect] Panel element #memory-inspect-panel not found in DOM');
|
|
}
|
|
}
|
|
|
|
// ─── SHOW ────────────────────────────────────────────────
|
|
function show(data, regionDef) {
|
|
if (!_panel) return;
|
|
|
|
const region = regionDef || {};
|
|
const colorHex = region.color
|
|
? '#' + region.color.toString(16).padStart(6, '0')
|
|
: '#4af0c0';
|
|
const strength = data.strength != null ? data.strength : 0.7;
|
|
const vitality = Math.round(Math.max(0, Math.min(1, strength)) * 100);
|
|
|
|
let vitalityColor = '#4af0c0';
|
|
if (vitality < 30) vitalityColor = '#ff4466';
|
|
else if (vitality < 60) vitalityColor = '#ffaa22';
|
|
|
|
const ts = data.timestamp ? new Date(data.timestamp) : null;
|
|
const created = ts && !isNaN(ts) ? ts.toLocaleString() : '—';
|
|
|
|
// Linked memories
|
|
let linksHtml = '';
|
|
if (data.connections && data.connections.length > 0) {
|
|
linksHtml = data.connections
|
|
.map(id => `<button class="mi-link-btn" data-memid="${_esc(id)}">${_esc(id)}</button>`)
|
|
.join('');
|
|
} else {
|
|
linksHtml = '<span class="mi-empty">No linked memories</span>';
|
|
}
|
|
|
|
_panel.innerHTML = `
|
|
<div class="mi-header" style="border-left:3px solid ${colorHex}">
|
|
<span class="mi-region-glyph">${region.glyph || '\u25C8'}</span>
|
|
<div class="mi-header-text">
|
|
<div class="mi-id" title="${_esc(data.id || '')}">${_esc(_truncate(data.id || '\u2014', 28))}</div>
|
|
<div class="mi-region" style="color:${colorHex}">${_esc(region.label || data.category || '\u2014')}</div>
|
|
</div>
|
|
<button class="mi-close" id="mi-close-btn" aria-label="Close inspect panel">\u2715</button>
|
|
</div>
|
|
<div class="mi-body">
|
|
<div class="mi-section">
|
|
<div class="mi-section-label">CONTENT</div>
|
|
<div class="mi-content">${_esc(data.content || '(empty)')}</div>
|
|
</div>
|
|
<div class="mi-section">
|
|
<div class="mi-section-label">VITALITY</div>
|
|
<div class="mi-vitality-row">
|
|
<div class="mi-vitality-bar-track">
|
|
<div class="mi-vitality-bar" style="width:${vitality}%;background:${vitalityColor}"></div>
|
|
</div>
|
|
<span class="mi-vitality-pct" style="color:${vitalityColor}">${vitality}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="mi-section">
|
|
<div class="mi-section-label">LINKED MEMORIES</div>
|
|
<div class="mi-links" id="mi-links">${linksHtml}</div>
|
|
</div>
|
|
<div class="mi-section">
|
|
<div class="mi-section-label">META</div>
|
|
<div class="mi-meta-row">
|
|
<span class="mi-meta-key">Source</span>
|
|
<span class="mi-meta-val">${_esc(data.source || '\u2014')}</span>
|
|
</div>
|
|
<div class="mi-meta-row">
|
|
<span class="mi-meta-key">Created</span>
|
|
<span class="mi-meta-val">${created}</span>
|
|
</div>
|
|
</div>
|
|
<div class="mi-actions">
|
|
<button class="mi-action-btn" id="mi-copy-btn">\u2398 Copy</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Wire close button
|
|
const closeBtn = _panel.querySelector('#mi-close-btn');
|
|
if (closeBtn) closeBtn.addEventListener('click', hide);
|
|
|
|
// Wire copy button
|
|
const copyBtn = _panel.querySelector('#mi-copy-btn');
|
|
if (copyBtn) {
|
|
copyBtn.addEventListener('click', () => {
|
|
const text = data.content || '';
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
copyBtn.textContent = '\u2713 Copied';
|
|
setTimeout(() => { copyBtn.textContent = '\u2398 Copy'; }, 1500);
|
|
}).catch(() => _fallbackCopy(text));
|
|
} else {
|
|
_fallbackCopy(text);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wire link navigation
|
|
const linksContainer = _panel.querySelector('#mi-links');
|
|
if (linksContainer) {
|
|
linksContainer.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.mi-link-btn');
|
|
if (btn && _onNavigate) _onNavigate(btn.dataset.memid);
|
|
});
|
|
}
|
|
|
|
_panel.style.display = 'flex';
|
|
// Trigger CSS animation
|
|
requestAnimationFrame(() => _panel.classList.add('mi-visible'));
|
|
}
|
|
|
|
// ─── HIDE ─────────────────────────────────────────────────
|
|
function hide() {
|
|
if (!_panel) return;
|
|
_panel.classList.remove('mi-visible');
|
|
// Wait for CSS transition before hiding
|
|
const onEnd = () => {
|
|
_panel.style.display = 'none';
|
|
_panel.removeEventListener('transitionend', onEnd);
|
|
};
|
|
_panel.addEventListener('transitionend', onEnd);
|
|
// Safety fallback if transition doesn't fire
|
|
setTimeout(() => { if (_panel) _panel.style.display = 'none'; }, 350);
|
|
}
|
|
|
|
// ─── QUERY ────────────────────────────────────────────────
|
|
function isOpen() {
|
|
return _panel != null && _panel.style.display !== 'none';
|
|
}
|
|
|
|
// ─── HELPERS ──────────────────────────────────────────────
|
|
function _esc(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function _truncate(str, n) {
|
|
return str.length > n ? str.slice(0, n - 1) + '\u2026' : str;
|
|
}
|
|
|
|
function _fallbackCopy(text) {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.left = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
}
|
|
|
|
return { init, show, hide, isOpen };
|
|
})();
|
|
|
|
export { MemoryInspect };
|