// ── Search ── import { $, esc, escRe, fuzzyMatch, countSubstring, highlight } from './util.js'; import { state, parseTags, tagChipHtml } from './state.js'; import { renderMd, stripMd } from './markdown.js'; const overlay = $('search'); const input = $('search-input'); const results = $('search-results'); const footer = $('search-footer'); const previewEmpty = $('search-preview-empty'); const previewContent = $('search-preview-content'); const bucketFilter = $('search-bucket-filter'); let active = -1, hits = []; export function openSearch() { window._closeTasks?.(); input.value = ''; results.innerHTML = ''; footer.classList.add('hidden'); active = -1; overlay.classList.add('open'); input.focus(); clearPreview(); renderHome(); } export function closeSearch() { overlay.classList.remove('open'); } function clearPreview() { previewEmpty.classList.remove('hidden'); previewContent.classList.add('hidden'); } function getPool() { const bv = bucketFilter.value; if (!bv) return state.cards; if (bv === 'none') return state.cards.filter(c => !c.bucket_id); return state.cards.filter(c => c.bucket_id == bv); } function updatePreview() { const card = hits[active]; if (!card) { clearPreview(); return; } previewEmpty.classList.add('hidden'); previewContent.classList.remove('hidden'); $('search-preview-color').style.background = card.color; const q = input.value.trim(); const words = q ? q.toLowerCase().split(/\s+/).filter(Boolean) : []; $('search-preview-title').innerHTML = words.length ? highlight(esc(card.title), words) : esc(card.title); let html = renderMd(card.body); if (words.length) { for (const w of words) html = html.replace(new RegExp('(>)([^<]*?)(' + escRe(w) + ')', 'gi'), '$1$2$3'); } $('search-preview-body').innerHTML = html; const ptags = parseTags(card); const ptagEl = $('search-preview-tags'); if (ptags.length) { ptagEl.classList.remove('hidden'); ptagEl.innerHTML = ptags.map(t => tagChipHtml(t)).join(''); ptagEl.querySelectorAll('.tag-seg[data-tag]').forEach(seg => { seg.addEventListener('click', e => { closeSearch(); const tag = seg.dataset.tag; if (e.ctrlKey || e.metaKey) { const idx = state.activeTags.indexOf(tag); if (idx >= 0) state.activeTags.splice(idx, 1); else state.activeTags.push(tag); } else { state.activeTags = [tag]; } window._renderTagBar?.(); window._renderBoard(); }); }); } else { ptagEl.classList.add('hidden'); } $('search-preview-meta').textContent = '> created: ' + card.created_at + ' | modified: ' + card.updated_at + (card.bucket_name ? ' | bucket: ' + card.bucket_name : ''); } function renderHome() { const recent = [...getPool()].sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || '')).slice(0, 6); if (!recent.length) { results.innerHTML = `
no cards with ${tagFilters.map(t => '#' + t).join(' + ')}
no results for "${esc(query)}"