refactor: decompose NoteList into focused components and extract helpers
Break up NoteListInner (cc=61, 331 LoC, score 6.94) into: - NoteItem: extracted item rendering component (score 9.63) - SortDropdown: extracted into own file (score 9.68) - noteListHelpers.ts: all utility functions (score 10.0) - GroupBuilder class replaces 5-arg addResolvedGroup - filterEntries split into filterByKind/filterByFilterType - PinnedCard: unified entity/type document cards - RelationshipGroupSection: extracted group rendering - NoteList.tsx: orchestration only (score 8.79) NoteListInner reduced from cc=61 to cc=23, 331 → 105 LoC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
57
src/components/SortDropdown.tsx
Normal file
57
src/components/SortDropdown.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ArrowsDownUp, Check } from '@phosphor-icons/react'
|
||||
import { type SortOption, SORT_OPTIONS } from '../utils/noteListHelpers'
|
||||
|
||||
export function SortDropdown({ groupLabel, current, onChange }: {
|
||||
groupLabel: string
|
||||
current: SortOption
|
||||
onChange: (groupLabel: string, option: SortOption) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [open])
|
||||
|
||||
const handleSelect = (opt: SortOption) => {
|
||||
onChange(groupLabel, opt)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative" style={{ zIndex: open ? 10 : 0 }}>
|
||||
<button
|
||||
className={cn("flex items-center gap-0.5 rounded px-1 py-0.5 text-muted-foreground transition-colors hover:text-foreground hover:bg-accent", open && "bg-accent text-foreground")}
|
||||
onClick={(e) => { e.stopPropagation(); setOpen(!open) }}
|
||||
title={`Sort by ${current}`}
|
||||
data-testid={`sort-button-${groupLabel}`}
|
||||
>
|
||||
<ArrowsDownUp size={12} />
|
||||
<span className="text-[10px] font-medium">{SORT_OPTIONS.find((o) => o.value === current)?.label}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full mt-1 rounded-md border border-border bg-popover shadow-md" style={{ width: 130, padding: 4 }} data-testid={`sort-menu-${groupLabel}`}>
|
||||
{SORT_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
className={cn("flex w-full items-center gap-1.5 rounded px-2 text-[12px] text-popover-foreground hover:bg-accent", opt.value === current && "bg-accent font-medium")}
|
||||
style={{ height: 28, border: 'none', cursor: 'pointer', background: opt.value === current ? 'var(--accent)' : 'transparent' }}
|
||||
onClick={(e) => { e.stopPropagation(); handleSelect(opt.value) }}
|
||||
data-testid={`sort-option-${opt.value}`}
|
||||
>
|
||||
{opt.value === current ? <Check size={12} /> : <span style={{ width: 12, height: 12, display: 'inline-block' }} />}
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user