/** * GoingsOn - Command Palette / Global Search (Cmd+K) * Full-text search across tasks, projects, emails, events, contacts. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; let overlay = null; let debounceTimer = null; let selectedIndex = -1; let currentResults = []; // ============ Overlay Creation ============ function createOverlay() { if (overlay) return overlay; overlay = document.createElement('div'); overlay.id = 'command-palette'; overlay.className = 'hidden'; overlay.innerHTML = `
🔍 Esc
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('.cp-backdrop').addEventListener('click', close); const input = overlay.querySelector('#cp-input'); input.addEventListener('input', onInput); input.addEventListener('keydown', onKeydown); document.body.appendChild(overlay); return overlay; } // ============ Open / Close ============ function open() { createOverlay(); overlay.classList.remove('hidden'); selectedIndex = -1; currentResults = []; const input = overlay.querySelector('#cp-input'); input.value = ''; input.focus(); const results = overlay.querySelector('#cp-results'); results.innerHTML = `
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('#cp-filters').innerHTML = ''; } function close() { if (overlay) { overlay.classList.add('hidden'); } } function isOpen() { return overlay && !overlay.classList.contains('hidden'); } // ============ Search ============ function onInput() { clearTimeout(debounceTimer); const query = overlay.querySelector('#cp-input').value.trim(); if (!query) { overlay.querySelector('#cp-results').innerHTML = `
Type to search across everything. Use is:overdue tag:work type:task in:ProjectName to filter.
`; overlay.querySelector('#cp-filters').innerHTML = ''; currentResults = []; selectedIndex = -1; return; } debounceTimer = setTimeout(() => runSearch(query), 150); } async function runSearch(query) { try { const response = await GoingsOn.api.search.query({ query: query, limit: 20, offset: 0, }); currentResults = response.results || []; selectedIndex = currentResults.length > 0 ? 0 : -1; renderResults(currentResults, response.total || 0); renderFilters(response.activeFilters || []); } catch (err) { overlay.querySelector('#cp-results').innerHTML = `
Search failed. Try again.
`; } } // ============ Rendering ============ const typeIcons = { task: '☑', project: '⚙', email: '✉', event: '📅', contact: '👤', }; const typeLabels = { task: 'Task', project: 'Project', email: 'Email', event: 'Event', contact: 'Contact', }; function renderResults(results, total) { const container = overlay.querySelector('#cp-results'); if (results.length === 0) { container.innerHTML = '
No results found.
'; return; } let html = ''; for (let i = 0; i < results.length; i++) { const r = results[i]; const icon = typeIcons[r.resultType] || ''; const label = typeLabels[r.resultType] || r.resultType; const selected = i === selectedIndex ? ' cp-selected' : ''; const project = r.projectName ? `${esc(r.projectName)}` : ''; const snippet = r.snippet ? `${esc(r.snippet)}` : ''; html += `
${icon}
${esc(r.title)} ${project}${snippet}
${label}
`; } if (total > results.length) { html += `
${total - results.length} more results
`; } container.innerHTML = html; } function renderFilters(filters) { const container = overlay.querySelector('#cp-filters'); if (!filters || filters.length === 0) { container.innerHTML = ''; return; } container.innerHTML = filters.map(f => `${esc(f)}`).join(''); } function updateSelection() { if (!overlay) return; const items = overlay.querySelectorAll('.cp-result'); items.forEach((el, i) => { el.classList.toggle('cp-selected', i === selectedIndex); }); if (selectedIndex >= 0 && items[selectedIndex]) { items[selectedIndex].scrollIntoView({ block: 'nearest' }); } } // ============ Keyboard Navigation ============ function onKeydown(e) { if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); if (currentResults.length > 0) { selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1); updateSelection(); } return; } if (e.key === 'ArrowUp') { e.preventDefault(); if (currentResults.length > 0) { selectedIndex = Math.max(selectedIndex - 1, 0); updateSelection(); } return; } if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && selectedIndex < currentResults.length) { navigateToResult(currentResults[selectedIndex]); } return; } } // ============ Navigation ============ function navigateToResult(result) { close(); switch (result.resultType) { case 'task': GoingsOn.navigation.switchView('tasks'); setTimeout(() => GoingsOn.tasks.openEdit(result.id), 100); break; case 'project': GoingsOn.navigation.switchView('projects'); setTimeout(() => GoingsOn.projects.openEdit(result.id), 100); break; case 'email': GoingsOn.navigation.switchView('emails'); setTimeout(() => GoingsOn.emails.open(result.id), 100); break; case 'event': GoingsOn.navigation.switchView('events'); setTimeout(() => GoingsOn.events.openEdit(result.id), 100); break; case 'contact': GoingsOn.navigation.switchView('contacts'); setTimeout(() => GoingsOn.contacts.openEdit(result.id), 100); break; } } // ============ Mouse Handlers ============ function hoverResult(index) { selectedIndex = index; updateSelection(); } function selectResult(index) { if (index >= 0 && index < currentResults.length) { navigateToResult(currentResults[index]); } } // ============ Styles ============ const style = document.createElement('style'); style.textContent = ` #command-palette { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh; } #command-palette.hidden { display: none; } .cp-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); } .cp-dialog { position: relative; width: 90%; max-width: 560px; background: var(--bg-card); border: var(--border-width) solid var(--border-color); border-radius: var(--radius-md); box-shadow: var(--shadow-brutal); overflow: hidden; } .cp-input-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); } .cp-icon { font-size: 1.1rem; opacity: 0.5; } .cp-input { flex: 1; border: none; outline: none; background: transparent; font-size: 1rem; font-family: inherit; color: var(--text-primary); } .cp-esc { font-size: 0.7rem; padding: 0.15rem 0.4rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 3px; color: var(--text-secondary); } .cp-filters { display: flex; gap: 0.4rem; padding: 0 1rem; flex-wrap: wrap; } .cp-filters:not(:empty) { padding-top: 0.5rem; padding-bottom: 0.25rem; } .cp-filter-tag { font-size: 0.75rem; padding: 0.15rem 0.5rem; background: var(--bg-secondary); border-radius: var(--radius-sm); color: var(--text-secondary); } .cp-results { max-height: 360px; overflow-y: auto; } .cp-hint, .cp-empty { padding: 1.5rem 1rem; text-align: center; font-size: 0.85rem; color: var(--text-secondary); line-height: 1.6; } .cp-hint kbd { font-size: 0.75rem; padding: 0.1rem 0.35rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 3px; } .cp-result { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 1rem; cursor: pointer; border-bottom: 1px solid var(--border-color); } .cp-result:last-child { border-bottom: none; } .cp-result:hover, .cp-result.cp-selected { background: var(--bg-secondary); } .cp-type-icon { font-size: 1rem; width: 1.5rem; text-align: center; flex-shrink: 0; } .cp-result-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; } .cp-title { font-size: 0.9rem; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cp-project { font-size: 0.75rem; color: var(--text-secondary); } .cp-snippet { font-size: 0.75rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cp-type-badge { font-size: 0.7rem; padding: 0.15rem 0.4rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-secondary); flex-shrink: 0; } .cp-more { padding: 0.5rem 1rem; text-align: center; font-size: 0.8rem; color: var(--text-secondary); opacity: 0.7; } `; document.head.appendChild(style); // ============ Export ============ GoingsOn.search = { open, close, isOpen, _hover: hoverResult, _select: selectResult, }; })();