fix: align sidebar view count chips
This commit is contained in:
@@ -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(
|
||||
<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} 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(
|
||||
<Sidebar entries={mockEntries} selection={defaultSelection} onSelect={() => {}} 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')
|
||||
|
||||
@@ -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 (
|
||||
<div className="group relative">
|
||||
<NavItem
|
||||
icon={Funnel}
|
||||
emoji={view.definition.icon}
|
||||
label={view.definition.name}
|
||||
count={count}
|
||||
badgeClassName="transition-opacity group-hover:opacity-0 group-focus-within:opacity-0"
|
||||
isActive={isActive}
|
||||
onClick={onSelect}
|
||||
/>
|
||||
<div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
{onEditView && (
|
||||
<button
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={(event) => { event.stopPropagation(); onEditView(view.filename) }}
|
||||
title="Edit view"
|
||||
>
|
||||
<PencilSimple size={12} />
|
||||
</button>
|
||||
)}
|
||||
{onDeleteView && (
|
||||
<button
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={(event) => { event.stopPropagation(); onDeleteView(view.filename) }}
|
||||
title="Delete view"
|
||||
>
|
||||
<Trash size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarTopNav({
|
||||
selection,
|
||||
onSelect,
|
||||
@@ -209,7 +157,7 @@ export function ViewsSection({
|
||||
{!collapsed && (
|
||||
<div style={{ paddingBottom: 4 }}>
|
||||
{views.map((view) => (
|
||||
<ViewItem
|
||||
<SidebarViewItem
|
||||
key={view.filename}
|
||||
view={view}
|
||||
isActive={isSelectionActive(selection, { kind: 'view', filename: view.filename })}
|
||||
|
||||
71
src/components/sidebar/SidebarViewItem.tsx
Normal file
71
src/components/sidebar/SidebarViewItem.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { VaultEntry, ViewFile } from '../../types'
|
||||
import { evaluateView } from '../../utils/viewFilters'
|
||||
import { Funnel, PencilSimple, Trash } from '@phosphor-icons/react'
|
||||
import { NoteTitleIcon } from '../NoteTitleIcon'
|
||||
|
||||
interface SidebarViewItemProps {
|
||||
view: ViewFile
|
||||
isActive: boolean
|
||||
onSelect: () => 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
|
||||
? <NoteTitleIcon icon={view.definition.icon} size={16} />
|
||||
: <Funnel size={16} weight={isActive ? 'fill' : 'regular'} />
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div
|
||||
className={`flex cursor-pointer select-none items-center gap-2 rounded transition-colors ${isActive ? 'bg-primary/10 text-primary' : 'text-foreground hover:bg-accent'}`}
|
||||
style={{ padding: showCount ? '6px 8px 6px 16px' : '6px 16px', borderRadius: 4 }}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{icon}
|
||||
<span className="min-w-0 flex-1 truncate text-[13px] font-medium">{view.definition.name}</span>
|
||||
{showCount && (
|
||||
<span
|
||||
data-testid="view-count-chip"
|
||||
className="flex items-center justify-center text-muted-foreground transition-opacity group-hover:opacity-0 group-focus-within:opacity-0"
|
||||
style={{ height: 20, borderRadius: 9999, padding: '0 6px', fontSize: 10, background: 'var(--muted)' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
{onEditView && (
|
||||
<button
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={(event) => { event.stopPropagation(); onEditView(view.filename) }}
|
||||
title="Edit view"
|
||||
>
|
||||
<PencilSimple size={12} />
|
||||
</button>
|
||||
)}
|
||||
{onDeleteView && (
|
||||
<button
|
||||
className="rounded p-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={(event) => { event.stopPropagation(); onDeleteView(view.filename) }}
|
||||
title="Delete view"
|
||||
>
|
||||
<Trash size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user