function setupModeToggle() {
document.getElementById('meta-mode')?.addEventListener('click', toggleViewSide);
document.addEventListener('keydown', e => {
if (window.isPromptPopupOpen?.()) return; if ((e.key !== 't' && e.key !== 'T') || e.metaKey || e.ctrlKey || e.altKey) return;
const t = e.target;
if (t && (/^(input|textarea|select)$/i.test(t.tagName) || t.isContentEditable)) return;
toggleViewSide();
});
}
window.flyoutHeader = (function () {
let home = null, homeNext = null, bar = null, owner = null, ownerEl = null;
return {
mount(container, key) {
if (owner || document.fullscreenElement || !container) return;
const header = document.querySelector('header');
if (!header) return;
home = header.parentElement; homeNext = header.nextSibling;
bar = document.createElement('div');
bar.className = 'fs-bar visible';
bar.append(header);
container.appendChild(bar);
container.classList.add('fly-header-host');
container.style.paddingTop = bar.offsetHeight + 'px';
owner = key; ownerEl = container;
},
unmount(key) {
if (owner !== key) return;
const header = document.querySelector('header');
if (home && header) home.insertBefore(header, homeNext);
bar?.remove();
ownerEl?.classList.remove('fly-header-host');
if (ownerEl) ownerEl.style.paddingTop = '';
bar = null; home = null; owner = null; ownerEl = null;
}
};
})();
function updateWarnCount() {
const warnEl = document.getElementById('nav-warn-count');
if (!warnEl) return;
const n = window.warningTypeCount?.(currentLevel()) ?? 0;
warnEl.textContent = n ? String(n) : '';
}
function updateHeader() {
const meta = window.META;
const hasBaseline = meta.baseline !== null;
const hasCurrent = meta.current !== null;
const isReview = !hasBaseline || !hasCurrent;
document.body.classList.toggle('mode-review', isReview);
const titleEl = document.getElementById('title');
titleEl.textContent = meta.target;
titleEl.title = meta.target;
const modeEl = document.getElementById('meta-mode');
modeEl.style.display = isReview ? 'none' : '';
modeEl.textContent = 'toggle';
modeEl.title = 'Click to toggle baseline ⇄ current (press t)';
document.querySelector('.snap-group[data-snap="baseline"]').style.display = hasBaseline ? '' : 'none';
const baselineName = document.getElementById('meta-baseline-name');
baselineName.textContent = hasBaseline ? meta.baseline.name : '';
baselineName.title = hasBaseline ? meta.baseline.name : '';
document.getElementById('meta-baseline-commit').textContent = hasBaseline && meta.baseline.commit ? ` ${meta.baseline.commit}` : '';
document.querySelector('.snap-group[data-snap="current"]').style.display = hasCurrent ? '' : 'none';
const currentName = document.getElementById('meta-current-name');
currentName.textContent = hasCurrent ? meta.current.name : '';
currentName.title = hasCurrent ? meta.current.name : '';
document.getElementById('meta-current-commit').textContent = hasCurrent && meta.current.commit ? ` ${meta.current.commit}` : '';
if (!hasCurrent) window.viewSide = 'baseline';
else if (!hasBaseline) window.viewSide = 'current';
updateActiveSnapGroup();
}
function updateFilesTab() {}
function buildSnapPopupHTML(snap, refSnap, sideLabel) {
if (!snap) return '';
const sections = [];
if (sideLabel) {
const shown = sideLabel.toLowerCase() === window.viewSide;
sections.push(`<div class="sp-side ${shown ? 'sp-side-shown' : ''}">${sideLabel}${shown ? ' · currently shown' : ''}</div>`);
}
const genRows = [];
if (snap.generated_at) {
let dateStr = fmtDate(snap.generated_at);
if (refSnap?.generated_at) {
const diffMs = new Date(snap.generated_at) - new Date(refSnap.generated_at);
dateStr += ` <span class="sp-gen-diff">(diff ${fmtDuration(Math.abs(diffMs))})</span>`;
}
genRows.push(`<div class="sp-row"><span class="sp-lbl">Generated</span><span>${dateStr}</span></div>`);
}
if (snap.command)
genRows.push(`<div class="sp-cmd-block"><div class="sp-cmd-bar"><span class="sp-lbl">Command</span><button class="sp-copy-btn" data-copy="${escAttr(snap.command)}" title="Copy">⧉</button></div><textarea class="sp-cmd-input" readonly rows="3">${escHtml(snap.command)}</textarea></div>`);
if (genRows.length)
sections.push(`<div class="sp-section-label">General</div>${genRows.join('')}`);
if (snap.git && Object.keys(snap.git).length) {
const gitRows = Object.entries(snap.git).map(([k, v]) => {
const cls = k === 'origin' ? ' class="sp-origin"' : '';
return `<div class="sp-row"><span class="sp-lbl">${escHtml(k)}</span><span${cls}>${escHtml(String(v ?? ''))}</span></div>`;
}).join('');
sections.push(`<div class="sp-section-label">Git</div>${gitRows}`);
}
if (snap.versions && Object.keys(snap.versions).length) {
const vrows = Object.entries(snap.versions).map(([k, v]) =>
`<div class="sp-row"><span class="sp-lbl">${escHtml(k)}</span><span>${escHtml(v)}</span></div>`
).join('');
sections.push(`<div class="sp-section-label">Versions</div>${vrows}`);
}
if (snap.timings?.length) {
const trows = snap.timings.map(t =>
`<div class="sp-row"><span class="sp-lbl">${escHtml(t.stage)}</span><span>${fmtMs(t.ms)}</span></div>`
).join('');
sections.push(`<div class="sp-section-label">Duration</div>${trows}`);
}
const acts = [];
if (sideLabel === 'Baseline') {
acts.push('<button class="sp-action" data-act="upload-baseline">↑ Replace baseline</button>');
if (window.CURRENT) acts.push('<button class="sp-action sp-action-x" data-act="remove-baseline">✕ Remove baseline</button>');
if (!window.CURRENT) acts.push('<button class="sp-action" data-act="upload-current">↑ Set current</button>');
} else if (sideLabel === 'Current') {
acts.push('<button class="sp-action" data-act="upload-current">↑ Replace current</button>');
if (window.BASELINE) acts.push('<button class="sp-action sp-action-x" data-act="remove-current">✕ Remove current</button>');
if (!window.BASELINE) acts.push('<button class="sp-action" data-act="upload-baseline">↑ Set baseline</button>');
}
if (acts.length)
sections.push(`<div class="sp-section-label">Actions</div><div class="sp-actions">${acts.join('')}</div>`);
return sections.join('');
}
function setupSnapPopup() {
let popup = null, openFor = null;
function getPopup() {
if (!popup) {
popup = document.createElement('div');
popup.id = 'snap-popup';
(document.fullscreenElement || document.body).appendChild(popup);
}
return popup;
}
function hide() { if (popup) popup.style.display = 'none'; openFor = null; }
function show(snap, anchor, refSnap, sideLabel) {
if (!snap) return;
const p = getPopup();
p.innerHTML = buildSnapPopupHTML(snap, refSnap, sideLabel);
p.querySelectorAll('.sp-copy-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
navigator.clipboard?.writeText(btn.dataset.copy).then(() => {
btn.textContent = '✓';
setTimeout(() => { btn.textContent = '⧉'; }, 1500);
});
});
});
p.querySelectorAll('.sp-action').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const act = btn.dataset.act;
if (act === 'upload-baseline') document.getElementById('input-baseline').click();
else if (act === 'upload-current') document.getElementById('input-current').click();
else if (act === 'remove-baseline') { window.BASELINE = null; recomputeAll(); }
else if (act === 'remove-current') { window.CURRENT = null; recomputeAll(); }
hide();
});
});
p.style.display = 'block';
openFor = anchor;
requestAnimationFrame(() => {
const r = anchor.getBoundingClientRect();
let left = r.left;
let top = r.bottom + 6;
if (left + p.offsetWidth > window.innerWidth - 8) left = window.innerWidth - p.offsetWidth - 8;
if (top + p.offsetHeight > window.innerHeight - 8) top = r.top - p.offsetHeight - 6;
p.style.left = left + 'px';
p.style.top = top + 'px';
});
}
document.querySelectorAll('.snap-group').forEach((grp, i) => {
grp.addEventListener('click', () => setViewSide(i === 0 ? 'baseline' : 'current'));
grp.querySelector('.snap-edit')?.addEventListener('click', e => {
e.stopPropagation();
if (openFor === grp) { hide(); return; }
const snap = i === 0 ? window.BASELINE : window.CURRENT;
const ref = i === 1 ? window.BASELINE : null;
show(snap, grp, ref, i === 0 ? 'Baseline' : 'Current');
});
});
document.addEventListener('click', e => {
if (openFor && popup && !popup.contains(e.target) && !openFor.contains(e.target)) hide();
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') hide(); });
}
function updateActiveSnapGroup() {
const active = window.CURRENT ? window.viewSide : null;
document.querySelectorAll('.snap-group[data-snap]').forEach(grp => {
grp.classList.toggle('snap-active', grp.dataset.snap === active);
});
}
function readEmbeddedSnapshot(id) {
const el = document.getElementById(id);
if (!el) return null;
const t = el.textContent.trim();
return t && t !== 'null' ? JSON.parse(t) : null;
}
function extractSnapshotFromText(text) {
const s = text.trim();
if (s.startsWith('{')) return JSON.parse(s);
const doc = new DOMParser().parseFromString(text, 'text/html');
const read = id => {
const t = doc.getElementById(id)?.textContent?.trim();
return t && t !== 'null' ? JSON.parse(t) : null;
};
return read('cs-current') || read('cs-baseline');
}
function setupFileControls() {
const inputBaseline = document.getElementById('input-baseline');
const inputCurrent = document.getElementById('input-current');
inputBaseline.addEventListener('change', () => {
const file = inputBaseline.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try { window.BASELINE = extractSnapshotFromText(e.target.result); } catch { alert('Invalid snapshot file'); return; }
recomputeAll();
};
reader.readAsText(file);
inputBaseline.value = '';
});
inputCurrent.addEventListener('change', () => {
const file = inputCurrent.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try { window.CURRENT = extractSnapshotFromText(e.target.result); } catch { alert('Invalid snapshot file'); return; }
recomputeAll();
};
reader.readAsText(file);
inputCurrent.value = '';
});
}