diff --git a/e2e/filtering.spec.ts b/e2e/filtering.spec.ts index b80aeea5..d2f76d73 100644 --- a/e2e/filtering.spec.ts +++ b/e2e/filtering.spec.ts @@ -64,3 +64,19 @@ test('search bar filters by title substring', async ({ page }) => { await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).not.toBeVisible() await page.screenshot({ path: 'test-results/search-budget.png' }) }) + +test('type filter pills narrow results', async ({ page }) => { + // Click "Projects" pill + await page.locator('.note-list__pill', { hasText: 'Projects' }).click() + await page.waitForTimeout(100) + const count = page.locator('.note-list__count') + await expect(count).toHaveText('1') + await expect(page.locator('.note-list__title', { hasText: 'Build Laputa App' })).toBeVisible() + await expect(page.locator('.note-list__title', { hasText: 'Matteo Cellini' })).not.toBeVisible() + await page.screenshot({ path: 'test-results/pill-projects.png' }) + + // Click "All" to reset + await page.locator('.note-list__pill', { hasText: 'All' }).click() + await page.waitForTimeout(100) + await expect(count).toHaveText('12') +}) diff --git a/src/components/NoteList.css b/src/components/NoteList.css index adb10c65..678ae6b8 100644 --- a/src/components/NoteList.css +++ b/src/components/NoteList.css @@ -55,6 +55,37 @@ border-color: #4a4a7a; } +/* Type filter pills */ +.note-list__pills { + display: flex; + gap: 4px; + padding: 8px 12px; + border-bottom: 1px solid #2a2a4a; + flex-wrap: wrap; +} + +.note-list__pill { + padding: 3px 10px; + font-size: 11px; + border: 1px solid #2a2a4a; + border-radius: 12px; + background: transparent; + color: #888; + cursor: pointer; + white-space: nowrap; +} + +.note-list__pill:hover { + background: #2a2a4a; + color: #e0e0e0; +} + +.note-list__pill--active { + background: #3a3a5a; + color: #ffffff; + border-color: #4a4a7a; +} + .note-list__items { flex: 1; } diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 6584c057..159658f6 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -160,4 +160,31 @@ describe('NoteList', () => { const titleTexts = titles.map((el) => el.textContent) expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest']) }) + + it('renders type filter pills', () => { + 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') + }) + + it('filters by type pill', () => { + render() + fireEvent.click(screen.getByText('Projects')) + 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')) + expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() + fireEvent.click(screen.getByText('All')) + expect(screen.getByText('Build Laputa App')).toBeInTheDocument() + }) }) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 0038457e..253b6904 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -55,8 +55,20 @@ function sortByModified(a: VaultEntry, b: VaultEntry): number { return (b.modifiedAt ?? 0) - (a.modifiedAt ?? 0) } +const TYPE_PILLS = [ + { label: 'All', type: null }, + { label: 'Projects', type: 'Project' }, + { label: 'Notes', type: 'Note' }, + { label: 'Events', type: 'Event' }, + { label: 'People', type: 'Person' }, + { label: 'Experiments', type: 'Experiment' }, + { label: 'Procedures', type: 'Procedure' }, + { label: 'Responsibilities', type: 'Responsibility' }, +] as const + export function NoteList({ entries, selection }: NoteListProps) { const [search, setSearch] = useState('') + const [typeFilter, setTypeFilter] = useState(null) const filtered = filterEntries(entries, selection) @@ -71,10 +83,15 @@ export function NoteList({ entries, selection }: NoteListProps) { // Search filter (title substring, case-insensitive) const query = search.trim().toLowerCase() - const displayed = query + const searched = query ? sorted.filter((e) => e.title.toLowerCase().includes(query)) : sorted + // Type filter pills + const displayed = typeFilter + ? searched.filter((e) => e.isA === typeFilter) + : searched + return (
@@ -90,6 +107,17 @@ export function NoteList({ entries, selection }: NoteListProps) { onChange={(e) => setSearch(e.target.value)} />
+
+ {TYPE_PILLS.map(({ label, type }) => ( + + ))} +
{displayed.length === 0 ? (
No notes found