diff --git a/src/components/NoteList.css b/src/components/NoteList.css index 457d405a..d44041c4 100644 --- a/src/components/NoteList.css +++ b/src/components/NoteList.css @@ -117,12 +117,22 @@ color: #ccc; } +.note-list__pill-count { + font-size: 10px; + opacity: 0.6; + margin-left: 2px; +} + .note-list__pill--active { background: #4a9eff22; color: #4a9eff; border-color: #4a9eff44; } +.note-list__pill--active .note-list__pill-count { + opacity: 0.8; +} + .note-list__items { flex: 1; overflow-y: auto; diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 9bb2e037..46ec5da3 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -81,24 +81,24 @@ const mockEntries: VaultEntry[] = [ describe('NoteList', () => { it('shows empty state when no entries', () => { - render() + const { container } = render() expect(screen.getByText('No notes found')).toBeInTheDocument() - expect(screen.getByText('0')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('0') }) it('renders all entries with All Notes filter', () => { - render() + const { container } = render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() - expect(screen.getByText('5')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('5') }) it('filters by People', () => { - render() + const { container } = render() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('filters by Events', () => { @@ -108,21 +108,21 @@ describe('NoteList', () => { }) it('filters by section group type', () => { - render() + const { container } = render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('shows entity pinned at top with children', () => { - render( + const { container } = render( ) // Pinned entity + child (Facebook Ads Strategy belongsTo this project) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() - expect(screen.getByText('2')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('2') }) it('filters by topic (relatedTo references)', () => { @@ -140,12 +140,12 @@ describe('NoteList', () => { }) it('filters by search query (case-insensitive substring)', () => { - render() + const { container } = render() const input = screen.getByPlaceholderText('Search notes...') fireEvent.change(input, { target: { value: 'facebook' } }) expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - expect(screen.getByText('1')).toBeInTheDocument() + expect(container.querySelector('.note-list__count')!.textContent).toBe('1') }) it('sorts entries by last modified descending', () => { @@ -160,30 +160,37 @@ describe('NoteList', () => { expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest']) }) - it('renders type filter pills', () => { + it('renders type filter pills with counts and hides empty types', () => { const { container } = render() const pills = container.querySelectorAll('.note-list__pill') const labels = Array.from(pills).map((p) => p.textContent) - expect(labels).toContain('All') - expect(labels).toContain('Projects') - expect(labels).toContain('Notes') - expect(labels).toContain('Events') - expect(labels).toContain('People') + expect(labels).toContain('All 5') + expect(labels).toContain('Projects 1') + expect(labels).toContain('Notes 1') + expect(labels).toContain('Events 1') + expect(labels).toContain('People 1') + // Empty types should be hidden + expect(labels.some((l) => l?.startsWith('Experiments'))).toBe(false) + expect(labels.some((l) => l?.startsWith('Procedures'))).toBe(false) + expect(labels.some((l) => l?.startsWith('Responsibilities'))).toBe(false) }) it('filters by type pill', () => { - render() - fireEvent.click(screen.getByText('Projects')) + const { container } = render() + const projectsPill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('Projects'))! + fireEvent.click(projectsPill) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() expect(screen.queryByText('Facebook Ads Strategy')).not.toBeInTheDocument() }) it('clicking All pill resets type filter', () => { - render() - fireEvent.click(screen.getByText('People')) + const { container } = render() + const peoplePill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('People'))! + fireEvent.click(peoplePill) expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() - fireEvent.click(screen.getByText('All')) + const allPill = Array.from(container.querySelectorAll('.note-list__pill')).find((p) => p.textContent?.startsWith('All'))! + fireEvent.click(allPill) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() }) }) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index c49e0602..2a36cb4d 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -130,6 +130,15 @@ export function NoteList({ entries, selection, selectedNote, allContent, onSelec ? sorted.filter((e) => e.title.toLowerCase().includes(query)) : sorted + // Compute per-type counts from the searched results (before type filter) + const typeCounts = new Map() + typeCounts.set(null, searched.length) // "All" count + for (const entry of searched) { + if (entry.isA) { + typeCounts.set(entry.isA, (typeCounts.get(entry.isA) ?? 0) + 1) + } + } + // Type filter pills const displayed = typeFilter ? searched.filter((e) => e.isA === typeFilter) @@ -156,15 +165,21 @@ export function NoteList({ entries, selection, selectedNote, allContent, onSelec />
- {TYPE_PILLS.map(({ label, type }) => ( - - ))} + {TYPE_PILLS.filter(({ type }) => { + const count = typeCounts.get(type) ?? 0 + return type === null || count > 0 + }).map(({ label, type }) => { + const count = typeCounts.get(type) ?? 0 + return ( + + ) + })}
{displayed.length === 0 ? (