feat: fix category counts and add matchContext for search highlights
All checks were successful
Build & Publish / publish (push) Successful in 58s

- Category tab counts now reflect query-filtered results, not total data
- Added matchContext field to SearchResult for server-side match snippets
- Renders <em>-tagged highlight text below meta in muted style

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-25 09:26:55 +01:00
parent 499c86b680
commit 90e3de2cdf
3 changed files with 38 additions and 12 deletions

View File

@@ -277,6 +277,23 @@
overflow-y: auto;
}
/* Match context snippet */
.matchContext {
font-size: 11px;
color: var(--text-faint);
font-family: var(--font-mono);
margin-top: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.matchContext em {
font-style: normal;
color: var(--amber);
font-weight: 600;
}
/* Match highlight */
.mark {
background: none;

View File

@@ -92,14 +92,10 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
}
}, [open])
// Filter results
const filtered = useMemo(() => {
// Stage 1: apply text query + scope filters (used for counts)
const queryFiltered = useMemo(() => {
let results = data
if (activeCategory !== 'all') {
results = results.filter((r) => r.category === activeCategory)
}
if (query.trim()) {
const q = query.toLowerCase()
results = results.filter(
@@ -107,7 +103,6 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
)
}
// Apply scope filters
for (const sf of scopeFilters) {
results = results.filter((r) =>
r.category === sf.field || r.title.toLowerCase().includes(sf.value.toLowerCase()),
@@ -115,7 +110,13 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
}
return results
}, [data, query, activeCategory, scopeFilters])
}, [data, query, scopeFilters])
// Stage 2: apply category filter (used for display)
const filtered = useMemo(() => {
if (activeCategory === 'all') return queryFiltered
return queryFiltered.filter((r) => r.category === activeCategory)
}, [queryFiltered, activeCategory])
// Group results by category
const grouped = useMemo(() => {
@@ -130,14 +131,14 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
// Flatten for keyboard nav
const flatResults = useMemo(() => filtered, [filtered])
// Counts per category
// Counts per category (from query-filtered, before category filter)
const categoryCounts = useMemo(() => {
const counts: Record<string, number> = { all: data.length }
for (const r of data) {
const counts: Record<string, number> = { all: queryFiltered.length }
for (const r of queryFiltered) {
counts[r.category] = (counts[r.category] ?? 0) + 1
}
return counts
}, [data])
}, [queryFiltered])
function handleKeyDown(e: React.KeyboardEvent) {
switch (e.key) {
@@ -303,6 +304,12 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
<div className={styles.itemMeta}>
{highlightText(result.meta, query)}
</div>
{result.matchContext && (
<div
className={styles.matchContext}
dangerouslySetInnerHTML={{ __html: result.matchContext }}
/>
)}
</div>
{result.expandedContent && (
<button

View File

@@ -15,6 +15,8 @@ export interface SearchResult {
matchRanges?: [number, number][]
/** Skip client-side query filtering (result already matched server-side) */
serverFiltered?: boolean
/** Server-side match snippet with <em> tags around matched text */
matchContext?: string
}
export interface ScopeFilter {