/* Life OS - UI Interactions */ document.addEventListener('DOMContentLoaded', () => { // Theme toggle const theme = localStorage.getItem('lifeos-theme') || 'dark'; document.documentElement.setAttribute('data-theme', theme); // Domain tree collapse document.querySelectorAll('.domain-header').forEach(header => { header.addEventListener('click', () => { const children = header.nextElementSibling; if (children && children.classList.contains('domain-children')) { children.classList.toggle('collapsed'); const key = 'sidebar-' + header.dataset.domainId; localStorage.setItem(key, children.classList.contains('collapsed')); } }); }); // Restore collapsed state document.querySelectorAll('.domain-children').forEach(el => { const key = 'sidebar-' + el.dataset.domainId; if (localStorage.getItem(key) === 'true') { el.classList.add('collapsed'); } }); // Auto-submit filters on change document.querySelectorAll('.filter-select[data-auto-submit]').forEach(select => { select.addEventListener('change', () => { select.closest('form').submit(); }); }); // Confirm delete dialogs document.querySelectorAll('form[data-confirm]').forEach(form => { form.addEventListener('submit', (e) => { if (!confirm(form.dataset.confirm)) { e.preventDefault(); } }); }); }); // Theme toggle function function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); localStorage.setItem('lifeos-theme', next); } // Collapsed style document.head.insertAdjacentHTML('beforeend', '' ); // ---- Search Modal ---- let searchDebounce = null; function openSearch() { const modal = document.getElementById('search-modal'); if (!modal) return; modal.classList.remove('hidden'); const input = document.getElementById('search-input'); input.value = ''; input.focus(); document.getElementById('search-results').innerHTML = ''; } function closeSearch() { const modal = document.getElementById('search-modal'); if (modal) modal.classList.add('hidden'); } document.addEventListener('keydown', (e) => { // Cmd/Ctrl + K opens search if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } // Escape closes search if (e.key === 'Escape') { closeSearch(); } }); // Live search with debounce document.addEventListener('DOMContentLoaded', () => { const input = document.getElementById('search-input'); if (!input) return; input.addEventListener('input', () => { clearTimeout(searchDebounce); const q = input.value.trim(); if (q.length < 1) { document.getElementById('search-results').innerHTML = ''; return; } searchDebounce = setTimeout(() => doSearch(q), 200); }); // Navigate results with arrow keys input.addEventListener('keydown', (e) => { const results = document.querySelectorAll('.search-result-item'); const active = document.querySelector('.search-result-item.active'); let idx = Array.from(results).indexOf(active); if (e.key === 'ArrowDown') { e.preventDefault(); if (active) active.classList.remove('active'); idx = (idx + 1) % results.length; results[idx]?.classList.add('active'); } else if (e.key === 'ArrowUp') { e.preventDefault(); if (active) active.classList.remove('active'); idx = idx <= 0 ? results.length - 1 : idx - 1; results[idx]?.classList.add('active'); } else if (e.key === 'Enter') { e.preventDefault(); const activeItem = document.querySelector('.search-result-item.active'); if (activeItem) { window.location.href = activeItem.dataset.url; } } }); }); async function doSearch(query) { const container = document.getElementById('search-results'); try { const resp = await fetch(`/search/api?q=${encodeURIComponent(query)}&limit=8`); const data = await resp.json(); if (!data.results || data.results.length === 0) { container.innerHTML = '
No results found
'; return; } // Group by type const grouped = {}; data.results.forEach(r => { if (!grouped[r.type_label]) grouped[r.type_label] = []; grouped[r.type_label].push(r); }); let html = ''; for (const [label, items] of Object.entries(grouped)) { html += `
${label}
`; items.forEach((item, i) => { const isFirst = i === 0 && label === Object.keys(grouped)[0]; html += ` ${escHtml(item.name)} ${item.context ? `${escHtml(item.context)}` : ''} ${item.status ? `${item.status.replace('_', ' ')}` : ''} `; }); } container.innerHTML = html; } catch (err) { container.innerHTML = '
Search error
'; } } function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ---- Timer Pill (topbar running timer) ---- let timerStartAt = null; let timerInterval = null; function formatElapsed(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) { return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } return m + ':' + String(s).padStart(2, '0'); } function updateTimerPill() { if (!timerStartAt) return; const now = new Date(); const secs = Math.floor((now - timerStartAt) / 1000); const el = document.getElementById('timer-pill-elapsed'); if (el) el.textContent = formatElapsed(secs); } async function pollTimer() { try { const resp = await fetch('/time/running'); const data = await resp.json(); const pill = document.getElementById('timer-pill'); if (!pill) return; if (data.running) { pill.classList.remove('hidden'); timerStartAt = new Date(data.start_at); const taskEl = document.getElementById('timer-pill-task'); if (taskEl) { taskEl.textContent = data.task_title; taskEl.href = '/tasks/' + data.task_id; } updateTimerPill(); // Start 1s interval if not already running if (!timerInterval) { timerInterval = setInterval(updateTimerPill, 1000); } } else { pill.classList.add('hidden'); timerStartAt = null; if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } } } catch (err) { // Silently ignore polling errors } } // Poll on load, then every 30s document.addEventListener('DOMContentLoaded', () => { pollTimer(); setInterval(pollTimer, 30000); }); // ---- Mobile More Panel ---- document.addEventListener('DOMContentLoaded', function() { var btn = document.getElementById('mobMoreBtn'); var panel = document.getElementById('mobMore'); var overlay = document.getElementById('mobOverlay'); if (!btn || !panel || !overlay) return; btn.addEventListener('click', function() { panel.classList.toggle('open'); overlay.classList.toggle('open'); }); overlay.addEventListener('click', function() { panel.classList.remove('open'); overlay.classList.remove('open'); }); });