import State from './state.js'; import Bus from './bus.js'; let windowIdCounter = 0; function createWindow(options) { const { app, title, content, w = 640, h = 480 } = options; const id = 'win-' + (++windowIdCounter); // Cascade position const count = State.get('windows').length; const offsetX = 40 + (count * 30) % 200; const offsetY = 30 + (count * 30) % 150; const layer = document.getElementById('windows-layer'); if (!layer) return null; const el = document.createElement('div'); el.className = 'm-window focused'; el.id = id; el.style.left = offsetX + 'px'; el.style.top = offsetY + 'px'; el.style.width = w + 'px'; el.style.height = h + 'px'; el.style.zIndex = 10 + count; el.innerHTML = `
${escapeHtml(title)}
`; layer.appendChild(el); // Render content const body = el.querySelector('.m-window-body'); if (typeof content === 'string') { body.innerHTML = content; } else if (content instanceof HTMLElement) { body.innerHTML = ''; body.appendChild(content); } else if (typeof content === 'function') { body.innerHTML = ''; content(body); } // Setup drag setupDrag(el, id); // Setup resize setupResize(el, id); // Setup focus el.addEventListener('mousedown', () => focusWindow(id)); // Setup header button clicks el.querySelector('.minimize-btn').addEventListener('click', (e) => { e.stopPropagation(); minimizeWindow(id); }); el.querySelector('.close-btn').addEventListener('click', (e) => { e.stopPropagation(); closeWindow(id); }); // Track in state const windows = State.get('windows'); windows.push({ id, app, title, minimized: false }); State.set('windows', windows); Bus.emit('app:open', { app, id }); return id; } function closeWindow(id) { const el = document.getElementById(id); if (!el) return; // Find window data const windows = State.get('windows'); const idx = windows.findIndex(w => w.id === id); if (idx !== -1) { const win = windows[idx]; windows.splice(idx, 1); State.set('windows', windows); Bus.emit('app:close', { app: win.app, id }); } el.remove(); } function minimizeWindow(id) { const el = document.getElementById(id); if (!el) return; el.classList.add('minimized'); const windows = State.get('windows'); const win = windows.find(w => w.id === id); if (win) win.minimized = true; State.set('windows', windows); // If this was the focused window, focus the next available const focusable = windows.filter(w => !w.minimized); if (focusable.length > 0) { focusWindow(focusable[focusable.length - 1].id); } } function focusWindow(id) { const el = document.getElementById(id); if (!el) return; // Remove focus from all document.querySelectorAll('.m-window.focused').forEach(w => w.classList.remove('focused')); el.classList.add('focused'); // Bring to front const maxZ = Math.max(0, ...Array.from(document.querySelectorAll('.m-window')).map(w => parseInt(w.style.zIndex || 0) )); el.style.zIndex = maxZ + 1; const windows = State.get('windows'); const win = windows.find(w => w.id === id); if (win) { win.minimized = false; State.set('windows', windows); } Bus.emit('window:focused', { id }); } function setupDrag(el, id) { const header = el.querySelector('.m-window-header'); if (!header) return; let isDragging = false; let startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.closest('.m-window-controls')) return; isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = el.offsetLeft; startTop = el.offsetTop; document.body.style.cursor = 'move'; const onMove = (e2) => { if (!isDragging) return; const dx = e2.clientX - startX; const dy = e2.clientY - startY; el.style.left = Math.max(0, startLeft + dx) + 'px'; el.style.top = Math.max(0, startTop + dy) + 'px'; }; const onUp = () => { isDragging = false; document.body.style.cursor = ''; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); // Touch support header.addEventListener('touchstart', (e) => { if (e.target.closest('.m-window-controls')) return; const touch = e.touches[0]; isDragging = true; startX = touch.clientX; startY = touch.clientY; startLeft = el.offsetLeft; startTop = el.offsetTop; const onMove = (e2) => { if (!isDragging) return; const touch2 = e2.touches[0]; const dx = touch2.clientX - startX; const dy = touch2.clientY - startY; el.style.left = Math.max(0, startLeft + dx) + 'px'; el.style.top = Math.max(0, startTop + dy) + 'px'; }; const onEnd = () => { isDragging = false; document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); }; document.addEventListener('touchmove', onMove); document.addEventListener('touchend', onEnd); }, { passive: true }); } function setupResize(el, id) { const handle = el.querySelector('.m-window-resize'); if (!handle) return; let isResizing = false; let startX, startY, startW, startH; handle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); isResizing = true; startX = e.clientX; startY = e.clientY; startW = el.offsetWidth; startH = el.offsetHeight; const onMove = (e2) => { if (!isResizing) return; const dx = e2.clientX - startX; const dy = e2.clientY - startY; const newW = Math.max(320, startW + dx); const newH = Math.max(240, startH + dy); el.style.width = newW + 'px'; el.style.height = newH + 'px'; }; const onUp = () => { isResizing = false; document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); // Touch support for resize handle.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; isResizing = true; startX = touch.clientX; startY = touch.clientY; startW = el.offsetWidth; startH = el.offsetHeight; const onMove = (e2) => { if (!isResizing) return; const touch2 = e2.touches[0]; const dx = touch2.clientX - startX; const dy = touch2.clientY - startY; const newW = Math.max(320, startW + dx); const newH = Math.max(240, startH + dy); el.style.width = newW + 'px'; el.style.height = newH + 'px'; }; const onEnd = () => { isResizing = false; document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); }; document.addEventListener('touchmove', onMove); document.addEventListener('touchend', onEnd); }, { passive: false }); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } const MeridianWM = { openWindow: createWindow, closeWindow, minimizeWindow, focusWindow }; export default MeridianWM; export { MeridianWM };