experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(tags): show usage count on tag chips in cards

Tag chips on item cards (search results, tags home, lists, groups,
pagestream) now show a small usage count next to the tag name —
e.g. `peek 158`, `todo 12` — when the caller decorates the tag
with `itemCount`. Older callers that don't pass a count get the
original chip layout, so this is opt-in per surface.

Wired up for the tags-home grid: `renderCards` builds a per-tag
count map once and passes it through `createItemCard` →
`createSearchResultCard`. The sidebar's `buildTagChip` now reuses
that same map instead of re-iterating `state.itemTags` per chip
(was O(N²) per render).

Other surfaces (search/home, lists, groups, etc.) will pick up
counts when they thread a count map through their card builders;
that's a follow-up per-surface change rather than a global one.

+46 -8
+14
app/lib/search-result-card.js
··· 211 211 chip.className = 'card-tag'; 212 212 chip.dataset.tagId = tag.id; 213 213 chip.textContent = tag.name; 214 + // Optional usage count: if the caller decorated the tag with an 215 + // `itemCount` field (number of items in the dataset that carry this 216 + // tag), render it as a small subscript-style suffix. Skip when missing 217 + // or zero; older callers that don't pass a count get the original 218 + // chip layout. Inline so every per-feature CSS surface inherits the 219 + // look without a coordinated CSS update across search/tags/lists/etc. 220 + if (typeof tag.itemCount === 'number' && tag.itemCount > 0) { 221 + const countEl = document.createElement('span'); 222 + countEl.className = 'card-tag-count'; 223 + countEl.textContent = String(tag.itemCount); 224 + countEl.title = `${tag.itemCount} item${tag.itemCount === 1 ? '' : 's'}`; 225 + countEl.style.cssText = 'margin-left:4px;font-size:9px;opacity:0.6;'; 226 + chip.appendChild(countEl); 227 + } 214 228 if (opts.onTagClick) { 215 229 chip.addEventListener('click', (e) => { 216 230 e.stopPropagation();
+32 -8
features/tags/home.js
··· 86 86 // Expose state for debugging 87 87 window._tagsState = state; 88 88 89 + /** 90 + * Build a Map<tagId, count> by counting how many items in state.itemTags 91 + * carry each tag. Cheap enough to recompute per-render for typical 92 + * Peek datasets; callers that batch (renderTagSidebar, renderCards) reuse 93 + * a single map. 94 + */ 95 + const computeTagCounts = () => { 96 + const counts = new Map(); 97 + state.itemTags.forEach(itemTags => { 98 + for (const t of itemTags) { 99 + counts.set(t.id, (counts.get(t.id) || 0) + 1); 100 + } 101 + }); 102 + return counts; 103 + }; 104 + 89 105 // Embedded editor state 90 106 let embeddedEditor = null; // CodeMirror EditorView instance 91 107 let embeddedStatusLine = null; // StatusLine instance for vim status bar ··· 1077 1093 1078 1094 tagList.innerHTML = ''; 1079 1095 1096 + // Compute per-tag counts once for this render; previously each chip 1097 + // re-iterated state.itemTags, which is O(N²) in the number of tagged items. 1098 + const tagCounts = computeTagCounts(); 1099 + 1080 1100 const selectedIds = new Set(state.activeTags.map(t => t.id)); 1081 1101 const selectedTags = tags.filter(t => selectedIds.has(t.id)); 1082 1102 const unselectedTags = tags.filter(t => !selectedIds.has(t.id)); 1083 1103 1084 1104 // Helper to build a tag chip 1085 1105 const buildTagChip = (tag, isSelected) => { 1086 - // Count items with this tag 1087 - let count = 0; 1088 - state.itemTags.forEach(itemTags => { 1089 - if (itemTags.some(t => t.id === tag.id)) count++; 1090 - }); 1106 + const count = tagCounts.get(tag.id) || 0; 1091 1107 1092 1108 const chip = document.createElement('div'); 1093 1109 chip.className = 'tag-chip'; ··· 1168 1184 1169 1185 cardsContainer.innerHTML = ''; 1170 1186 1187 + // Compute the per-tag count map once for this render — re-using it 1188 + // across every card avoids recomputing on each chip. 1189 + const tagCounts = computeTagCounts(); 1171 1190 items.forEach(item => { 1172 - const card = createItemCard(item); 1191 + const card = createItemCard(item, tagCounts); 1173 1192 cardsContainer.appendChild(card); 1174 1193 }); 1175 1194 ··· 1190 1209 /** 1191 1210 * Create a card element for an item using the shared search-result-card builder. 1192 1211 */ 1193 - const createItemCard = (item) => { 1212 + const createItemCard = (item, tagCounts) => { 1194 1213 const { tags } = getLocalItemDisplayInfo(item); 1195 1214 const rules = actionRulesCache ? actionRulesCache.getRules() : []; 1215 + const counts = tagCounts || computeTagCounts(); 1216 + 1217 + // Decorate tag objects with global usage count so search-result-card 1218 + // can render the count next to the chip name. 1219 + const tagsWithCounts = tags.map(t => ({ ...t, itemCount: counts.get(t.id) || 0 })); 1196 1220 1197 1221 const card = createSearchResultCard(item, { 1198 - tags, 1222 + tags: tagsWithCounts, 1199 1223 showOpenButton: true, 1200 1224 onOpen: (url) => openItemUrl(url), 1201 1225 onDelete: async (item) => {