From 68024745956107f60c76ddc94930784215aeda5e Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sat, 16 May 2026 09:47:35 -0400 Subject: [PATCH] fix(ui): broken peek editing + wire up search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix startEditBody/startEditField selectors (.detail-body → .peek-body, .detail-title → .peek-title) — double-click editing works again - Wire up search input: client-side filter by body/title/description text - Search supports #tag inline filters (e.g. "proxy #ops") - Debounced input (150ms), Escape clears and blurs - Shows "no matches" when search has no results --- web/app.js | 56 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/web/app.js b/web/app.js index 1e5fe57..9ece617 100644 --- a/web/app.js +++ b/web/app.js @@ -32,6 +32,7 @@ runChecked: new Set(), fillValues: {}, fillActive: 0, + searchQuery: '', }; const $ = (sel) => document.querySelector(sel); @@ -406,30 +407,31 @@ function renderEntityList() { const list = $('#entity-list'); + const filtered = filterBySearch(state.entities); - if (state.entities.length === 0) { - list.innerHTML = '
no entities yet
'; - renderCardsHeader(false); + if (filtered.length === 0) { + list.innerHTML = `
${state.searchQuery ? 'no matches' : 'no entities yet'}
`; + renderCardsHeader(state.view === 'cards'); return; } let html = ''; if (state.view === 'stream') { renderCardsHeader(false); - const groups = groupByDate(state.entities); + const groups = groupByDate(filtered); let idx = 0; for (const g of groups) { html += `
${g.label}
`; for (const e of g.entities) { - html += renderEntityItem(e, idx); + const realIdx = state.entities.indexOf(e); + html += renderEntityItem(e, realIdx); idx++; } } } else { renderCardsHeader(true); - const pinned = state.entities.filter(e => e.pinned); - const rest = state.entities.filter(e => !e.pinned); - let idx = 0; + const pinned = filtered.filter(e => e.pinned); + const rest = filtered.filter(e => !e.pinned); if (pinned.length) { html += '
★ pinned
'; for (const e of pinned) { @@ -889,7 +891,7 @@ function startEditBody() { const e = state.entities[state.selectedIndex]; if (!e) return; - const el = $(`.detail-body[data-id="${e.id}"]`); + const el = $(`.peek-body[data-id="${e.id}"]`); if (!el || el.tagName === 'TEXTAREA') return; const ta = document.createElement('textarea'); @@ -921,8 +923,7 @@ function startEditField(field) { const e = state.entities[state.selectedIndex]; if (!e) return; - const cls = field === 'title' ? '.detail-title' : '.detail-desc'; - const el = $(`${cls}[data-id="${e.id}"]`); + const el = $(`.peek-title[data-id="${e.id}"]`); if (!el || el.tagName === 'INPUT') return; const input = document.createElement('input'); @@ -1381,6 +1382,39 @@ window.addEventListener('hashchange', handleHash); + // ========== Search ========== + + const searchInput = $('#search-input'); + let searchDebounce = null; + + searchInput.addEventListener('input', () => { + clearTimeout(searchDebounce); + searchDebounce = setTimeout(() => { + state.searchQuery = searchInput.value.trim().toLowerCase(); + renderEntityList(); + }, 150); + }); + + searchInput.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape') { searchInput.value = ''; state.searchQuery = ''; renderEntityList(); searchInput.blur(); } + }); + + function filterBySearch(entities) { + if (!state.searchQuery) return entities; + let query = state.searchQuery; + let filterTags = []; + query = query.replace(/#(\S+)/g, (_, tag) => { filterTags.push(tag); return ''; }).trim(); + return entities.filter(e => { + if (filterTags.length) { + const eTags = (e.tags || []).map(t => t.toLowerCase()); + if (!filterTags.every(ft => eTags.includes(ft))) return false; + } + if (!query) return true; + const haystack = ((e.body || '') + ' ' + (e.title || '') + ' ' + (e.description || '')).toLowerCase(); + return haystack.includes(query); + }); + } + // ========== Utils ========== function escHtml(s) {