/**
* 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,
};
})();