fix(ui): tag counts, j/k nav, stream layout, search alignment

- Tag rail counts now reflect cards-only when in cards view
  (ListTags accepts cardsOnly filter, JS passes it per view)
- j/k navigation scoped to visible (intent/search filtered) list
- scrollSelectedIntoView works in both stream and cards view
- Entity items wrap title/desc/preview in .entity-content flex
  container so tags/pills align right consistently
- Title no longer eaten by description/body (flex-shrink + min-width)
- Search bar centered in header with margin auto
- switchView awaits loadEntities+loadTags to fix stale intent counts
This commit is contained in:
2026-05-16 17:51:04 -04:00
parent ab07f631a7
commit 8bfa9b15ed
5 changed files with 92 additions and 18 deletions
+31 -12
View File
@@ -110,8 +110,10 @@
});
return resp.json();
},
async listTags() {
const resp = await fetch('/api/tags');
async listTags(params = {}) {
const q = new URLSearchParams();
if (params.cards_only) q.set('cards_only', 'true');
const resp = await fetch('/api/tags?' + q);
return resp.json();
},
};
@@ -653,9 +655,9 @@
const descSnip = e.description ? `<span class="entity-desc">${escHtml(e.description)}</span>` : '';
if (e.title) {
const preview = e.body ? `<span class="entity-preview">${escHtml(e.body)}</span>` : '';
label = `<span class="entity-title">${escHtml(e.title)}</span>${descSnip}${preview}`;
label = `<span class="entity-content"><span class="entity-title">${escHtml(e.title)}</span>${descSnip}${preview}</span>`;
} else {
label = `<span class="entity-body">${escHtml(e.body)}</span>${descSnip}`;
label = `<span class="entity-content"><span class="entity-body">${escHtml(e.body)}</span>${descSnip}</span>`;
}
return `<div class="entity-item${selected}${isCard}" data-index="${idx}" data-id="${e.id}">
@@ -1124,6 +1126,7 @@
state.selectedIndex = -1;
renderEntityList();
renderDetailPane();
renderTagRail();
}
async function loadMore() {
@@ -1168,7 +1171,9 @@
}
async function loadTags() {
state.tags = await api.listTags();
const params = {};
if (state.view === 'cards') params.cards_only = true;
state.tags = await api.listTags(params);
renderTagRail();
}
@@ -1178,10 +1183,10 @@
state.selectedIndex = -1;
$$('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
window.location.hash = view === 'cards' ? '/cards' : '/';
loadEntities();
renderMonthNav();
renderTagRail();
renderCaptureBar();
loadEntities();
loadTags();
}
// ========== Toast ==========
@@ -1430,16 +1435,30 @@
const sel = state.entities[state.selectedIndex];
switch (ev.key) {
case 'j':
case 'j': {
ev.preventDefault();
selectEntity(Math.min(state.selectedIndex + 1, state.entities.length - 1));
const visible = filterBySearch(state.entities);
const sel = state.entities[state.selectedIndex];
const curPos = sel ? visible.indexOf(sel) : -1;
const nextPos = Math.min(curPos + 1, visible.length - 1);
if (visible.length > 0 && nextPos >= 0) {
selectEntity(state.entities.indexOf(visible[nextPos]));
}
scrollSelectedIntoView();
break;
case 'k':
}
case 'k': {
ev.preventDefault();
selectEntity(Math.max(state.selectedIndex - 1, 0));
const visible = filterBySearch(state.entities);
const sel = state.entities[state.selectedIndex];
const curPos = sel ? visible.indexOf(sel) : -1;
const prevPos = Math.max(curPos - 1, 0);
if (visible.length > 0) {
selectEntity(state.entities.indexOf(visible[prevPos]));
}
scrollSelectedIntoView();
break;
}
case 'n':
ev.preventDefault();
$('#capture-input').focus();
@@ -1485,7 +1504,7 @@
});
function scrollSelectedIntoView() {
const el = $(`.entity-item[data-index="${state.selectedIndex}"]`);
const el = $(`.entity-item[data-index="${state.selectedIndex}"], .card-row[data-index="${state.selectedIndex}"]`);
if (el) el.scrollIntoView({ block: 'nearest' });
}