From 1388e7ddbe1d5d404997799ab5461f3fea563d8a Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 14 Apr 2026 19:45:32 +0200 Subject: [PATCH] fix: align sidebar view count chips --- src/components/Sidebar.test.tsx | 45 +++++++++++++- src/components/sidebar/SidebarSections.tsx | 58 +----------------- src/components/sidebar/SidebarViewItem.tsx | 71 ++++++++++++++++++++++ 3 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 src/components/sidebar/SidebarViewItem.tsx diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 646bdc48..aea39c4d 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -1129,16 +1129,55 @@ describe('Sidebar', () => { const projectLabel = screen.getByText('Active Projects') const projectNavItem = projectLabel.closest('[class*="cursor-pointer"]')! // The count chip is a sibling span inside the NavItem - const projectCount = projectNavItem.querySelector('span:last-child') + const projectCount = within(projectNavItem as HTMLElement).getByTestId('view-count-chip') expect(projectCount?.textContent).toBe('1') // 'All Topics' filters for type=Topic -> mockEntries has 2 Topics const topicLabel = screen.getByText('All Topics') const topicNavItem = topicLabel.closest('[class*="cursor-pointer"]')! - const topicCount = topicNavItem.querySelector('span:last-child') + const topicCount = within(topicNavItem as HTMLElement).getByTestId('view-count-chip') expect(topicCount?.textContent).toBe('2') }) + it('styles view note count chips like the shared sidebar count pills', () => { + render( + {}} views={mockViews} /> + ) + + const viewLabel = screen.getByText('Active Projects') + const navItem = viewLabel.closest('[class*="cursor-pointer"]') as HTMLElement + const countChip = within(navItem).getByTestId('view-count-chip') + + expect(navItem).toHaveStyle({ padding: '6px 8px 6px 16px' }) + expect(countChip).toHaveStyle({ + background: 'var(--muted)', + height: '20px', + padding: '0 6px', + }) + expect(countChip.className).toContain('text-muted-foreground') + }) + + it('renders phosphor view icons without leaking the raw icon name', () => { + const iconViews = [{ + filename: 'active-projects.yml', + definition: { + name: 'Active Projects', + icon: 'rocket', + color: null, + sort: null, + filters: { all: [{ field: 'type', op: 'equals' as const, value: 'Project' }] }, + }, + }] + + render( + {}} views={iconViews} /> + ) + + const navItem = screen.getByText('Active Projects').closest('[class*="cursor-pointer"]') as HTMLElement + expect(navItem.querySelector('svg')).not.toBeNull() + expect(screen.queryByText('rocket')).not.toBeInTheDocument() + }) + it('does not show count chip for views with 0 matching notes', () => { const emptyView = [{ filename: 'empty.yml', @@ -1174,7 +1213,7 @@ describe('Sidebar', () => { const label = screen.getByText('Active Projects') const viewItem = label.closest('.group.relative') as HTMLElement const navItem = label.closest('[class*="cursor-pointer"]') as HTMLElement - const countChip = navItem.querySelector('span:last-child') as HTMLElement + const countChip = within(navItem).getByTestId('view-count-chip') expect(countChip).toBeTruthy() expect(countChip.className).toContain('transition-opacity') expect(countChip.className).toContain('group-hover:opacity-0') diff --git a/src/components/sidebar/SidebarSections.tsx b/src/components/sidebar/SidebarSections.tsx index 462e1eb8..abe2bb9a 100644 --- a/src/components/sidebar/SidebarSections.tsx +++ b/src/components/sidebar/SidebarSections.tsx @@ -2,7 +2,6 @@ import { useMemo, useCallback, type Dispatch, type Ref, type RefObject, type SetStateAction, } from 'react' import type { VaultEntry, SidebarSelection, ViewFile } from '../../types' -import { evaluateView } from '../../utils/viewFilters' import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' @@ -13,7 +12,7 @@ import { arrayMove } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { SlidersHorizontal } from 'lucide-react' import { - FileText, Trash, Archive, CaretLeft, Tray, CaretRight, CaretDown, Plus, Funnel, PencilSimple, + FileText, Archive, CaretLeft, Tray, CaretRight, CaretDown, Plus, } from '@phosphor-icons/react' import { type SectionGroup, isSelectionActive, NavItem, SectionContent, VisibilityPopover, @@ -22,6 +21,7 @@ import { TypeCustomizePopover } from '../TypeCustomizePopover' import { useDragRegion } from '../../hooks/useDragRegion' import { buildTypeEntryMap, getTypeColor, getTypeLightColor } from '../../utils/typeColors' import { NoteTitleIcon } from '../NoteTitleIcon' +import { SidebarViewItem } from './SidebarViewItem' export interface SidebarSectionProps { entries: VaultEntry[] @@ -69,58 +69,6 @@ function SidebarGroupHeader({ ) } -function ViewItem({ - view, - isActive, - onSelect, - onEditView, - onDeleteView, - entries, -}: { - view: ViewFile - isActive: boolean - onSelect: () => void - onEditView?: (filename: string) => void - onDeleteView?: (filename: string) => void - entries: VaultEntry[] -}) { - const count = useMemo(() => evaluateView(view.definition, entries).length, [view.definition, entries]) - - return ( -
- -
- {onEditView && ( - - )} - {onDeleteView && ( - - )} -
-
- ) -} - export function SidebarTopNav({ selection, onSelect, @@ -209,7 +157,7 @@ export function ViewsSection({ {!collapsed && (
{views.map((view) => ( - void + onEditView?: (filename: string) => void + onDeleteView?: (filename: string) => void + entries: VaultEntry[] +} + +export function SidebarViewItem({ + view, + isActive, + onSelect, + onEditView, + onDeleteView, + entries, +}: SidebarViewItemProps) { + const count = useMemo(() => evaluateView(view.definition, entries).length, [view.definition, entries]) + const showCount = count > 0 + const icon = view.definition.icon + ? + : + + return ( +
+
+ {icon} + {view.definition.name} + {showCount && ( + + {count} + + )} +
+
+ {onEditView && ( + + )} + {onDeleteView && ( + + )} +
+
+ ) +}