/** * History-panel subsystem — the floating frosted list of labeled * undo/redo entries that hangs off the topbar History button. * * Same docking pattern as the FX adjustment popups: drag the head to * reposition, click the minimise button to dock into the modalManager * chip chain, click the chip to restore. Esc closes. * * @param {{ * undo: () => void, * redo: () => void, * }} deps * * @returns {{ * toggleHistoryPanel: () => void, * refreshHistoryPanelIfOpen: () => void, * jumpToHistory: (offset: number) => void, * }} */ import { state } from './state.js'; import modalManager from '../modalManager.js'; import { HISTORY_ICON, relTime } from './layer-helpers.js'; import { historyPanelHTML } from './build/popups.js'; export function createHistoryPanel({ undo, redo }) { function jumpToHistory(offset) { if (offset === 0) return; if (offset < 0) { for (let i = 0; i < -offset; i++) undo(); } else { for (let i = 0; i < offset; i++) redo(); } } function closeHistoryPanel() { if (state.historyPanelEl) { if (state.historyPanelEl._escHandler) { document.removeEventListener('keydown', state.historyPanelEl._escHandler, true); } if (state.historyPanelEl._awayHandler) { document.removeEventListener('pointerdown', state.historyPanelEl._awayHandler, true); } state.historyPanelEl.remove(); state.historyPanelEl = null; } } function minimiseHistoryPanel() { if (!state.historyPanelEl) return; const panel = state.historyPanelEl; const r = panel.getBoundingClientRect(); panel._stashLeft = r.left; panel._stashTop = r.top; panel.style.display = 'none'; state.historyPanelEl = null; const modalId = panel._modalId || 'ge-history-panel-min'; panel._modalId = modalId; modalManager.register(modalId, { label: 'History', icon: HISTORY_ICON, restoreFn: () => { panel.style.left = panel._stashLeft + 'px'; panel.style.top = panel._stashTop + 'px'; panel.style.display = ''; state.historyPanelEl = panel; refreshHistoryPanelIfOpen(); }, closeFn: () => { panel.remove(); modalManager.unregister(modalId); }, }); modalManager.minimize(modalId); } function toggleHistoryPanel() { if (state.historyPanelEl) { closeHistoryPanel(); return; } const panel = document.createElement('div'); panel.id = 'ge-history-panel'; panel.className = 'ge-frosted'; panel.innerHTML = historyPanelHTML(HISTORY_ICON); document.body.appendChild(panel); state.historyPanelEl = panel; const btn = document.getElementById('ge-history-btn'); if (btn) { const r = btn.getBoundingClientRect(); panel.style.top = (r.bottom + 6) + 'px'; panel.style.left = Math.max(8, r.left) + 'px'; } panel.querySelector('.ge-adj-min').addEventListener('click', minimiseHistoryPanel); // Click anywhere outside the panel (or trigger button) closes it. setTimeout(() => { const onAway = (ev) => { if (!state.historyPanelEl) return; if (state.historyPanelEl.contains(ev.target)) return; if (btn && (ev.target === btn || btn.contains(ev.target))) return; closeHistoryPanel(); document.removeEventListener('pointerdown', onAway, true); }; document.addEventListener('pointerdown', onAway, true); panel._awayHandler = onAway; }, 0); const head = panel.querySelector('[data-history-drag]'); head.addEventListener('pointerdown', (e) => { if (e.target.closest('button')) return; e.preventDefault(); const startX = e.clientX, startY = e.clientY; const r0 = panel.getBoundingClientRect(); head.setPointerCapture(e.pointerId); head.style.cursor = 'grabbing'; const onMove = (ev) => { const nx = Math.max(0, Math.min(window.innerWidth - 60, r0.left + (ev.clientX - startX))); const ny = Math.max(0, Math.min(window.innerHeight - 30, r0.top + (ev.clientY - startY))); panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; }; const onUp = () => { head.releasePointerCapture(e.pointerId); head.style.cursor = ''; head.removeEventListener('pointermove', onMove); head.removeEventListener('pointerup', onUp); }; head.addEventListener('pointermove', onMove); head.addEventListener('pointerup', onUp); }); const onKey = (ev) => { if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); closeHistoryPanel(); } }; document.addEventListener('keydown', onKey, true); panel._escHandler = onKey; refreshHistoryPanelIfOpen(); } function refreshHistoryPanelIfOpen() { if (!state.historyPanelEl) return; const list = state.historyPanelEl.querySelector('#ge-history-list'); if (!list) return; // Chronological order — oldest at top, latest at bottom. Past // (undo) states first, then Current, then future (redo) states. const rows = []; for (let i = 0; i < state.undoStack.length; i++) { const s = state.undoStack[i]; rows.push({ offset: -(state.undoStack.length - i), label: s._label || 'Edit', ts: s._ts }); } rows.push({ offset: 0, label: 'Current', ts: Date.now(), current: true }); for (let i = state.redoStack.length - 1; i >= 0; i--) { const s = state.redoStack[i]; rows.push({ offset: (state.redoStack.length - i), label: s._label || 'Edit', ts: s._ts, future: true }); } list.innerHTML = rows.map(r => ` `).join(''); list.querySelectorAll('.ge-history-row').forEach(btn => { btn.addEventListener('click', () => { const off = parseInt(btn.dataset.offset, 10); jumpToHistory(off); }); }); // Scroll the current marker into view. const cur = list.querySelector('.current'); if (cur) cur.scrollIntoView({ block: 'center' }); } return { toggleHistoryPanel, refreshHistoryPanelIfOpen, jumpToHistory }; }