import { useMemo, type ComponentType, type SVGAttributes } from 'react' import type { VaultEntry, NoteStatus } from '../types' import { cn } from '@/lib/utils' import { Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, Tag, FileText, StackSimple, File, FileDashed, } from '@phosphor-icons/react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { resolveIcon } from '../utils/iconRegistry' import { relativeDate, getDisplayDate } from '../utils/noteListHelpers' import { NoteTitleIcon } from './NoteTitleIcon' import { PropertyChips } from './note-item/PropertyChips' const TYPE_ICON_MAP: Record>> = { Project: Wrench, Experiment: Flask, Responsibility: Target, Procedure: ArrowsClockwise, Person: Users, Event: CalendarBlank, Topic: Tag, Type: StackSimple, } // eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component export function getTypeIcon(isA: string | null, customIcon?: string | null): ComponentType> { if (customIcon) return resolveIcon(customIcon) return (isA && TYPE_ICON_MAP[isA]) || FileText } const NOTE_STATUS_DOT: Record = { pendingSave: { color: 'var(--accent-green)', testId: 'pending-save-indicator', title: 'Saving to disk…' }, new: { color: 'var(--accent-green)', testId: 'new-indicator', title: 'New (uncommitted)' }, modified: { color: 'var(--accent-orange)', testId: 'modified-indicator', title: 'Modified (uncommitted)' }, } function StatusDot({ noteStatus }: { noteStatus: NoteStatus }) { const dot = NOTE_STATUS_DOT[noteStatus] if (!dot) return null return ( ) } function StateBadge({ archived }: { archived: boolean }) { if (archived) { return ( ARCHIVED ) } return null } const CHANGE_STATUS_DISPLAY: Record = { modified: { label: 'Modified', color: 'var(--accent-orange, #f59e0b)', symbol: '·' }, added: { label: 'Added', color: 'var(--accent-green, #22c55e)', symbol: '+' }, untracked: { label: 'Added', color: 'var(--accent-green, #22c55e)', symbol: '+' }, deleted: { label: 'Deleted', color: 'var(--destructive, #ef4444)', symbol: '−' }, renamed: { label: 'Renamed', color: 'var(--accent-orange, #f59e0b)', symbol: 'R' }, } function ChangeStatusIcon({ status }: { status: string }) { const display = CHANGE_STATUS_DISPLAY[status] ?? CHANGE_STATUS_DISPLAY.modified return ( {display.symbol} ) } function noteItemClassName({ isBinary, isSelected, isMultiSelected, isHighlighted, }: { isBinary: boolean isSelected: boolean isMultiSelected: boolean isHighlighted: boolean }) { return cn( 'relative border-b border-[var(--border)] transition-colors', isBinary ? 'cursor-default opacity-50' : 'cursor-pointer', isSelected && !isMultiSelected && !isBinary && 'border-l-[3px]', !isSelected && !isMultiSelected && !isBinary && 'hover:bg-muted', isHighlighted && !isSelected && !isMultiSelected && !isBinary && 'bg-muted', ) } function ChangeStatusContent({ entry, changeStatus, isSelected, isDeletedChange, }: { entry: VaultEntry changeStatus: NonNullable isSelected: boolean isDeletedChange: boolean }) { return ( <>
{entry.filename}
) } function StandardNoteContent({ entry, isBinary, noteStatus, isSelected, typeColor, displayProps, allEntries, typeEntryMap, onClickNote, }: { entry: VaultEntry isBinary: boolean noteStatus: NoteStatus isSelected: boolean typeColor: string displayProps: string[] allEntries: VaultEntry[] typeEntryMap: Record onClickNote: NoteItemProps['onClickNote'] }) { const isNonMarkdown = !!entry.fileKind && entry.fileKind !== 'markdown' const te = typeEntryMap[entry.isA ?? ''] const TypeIcon = useMemo(() => { if (isNonMarkdown) return getFileKindIcon(entry.fileKind) return getTypeIcon(entry.isA, te?.icon) }, [entry.fileKind, entry.isA, isNonMarkdown, te?.icon]) return ( <> {/* eslint-disable-next-line react-hooks/static-components -- icon lookup from static map, no internal state */}
{noteStatus !== 'clean' && !isBinary && } {entry.title} {!isBinary && }
{entry.snippet && !isBinary && (
{entry.snippet}
)} {!isBinary && displayProps.length > 0 && ( )} {!isBinary && (
{relativeDate(getDisplayDate(entry))}
)} ) } function noteItemStyle(isSelected: boolean, isMultiSelected: boolean, typeColor: string, typeLightColor: string): React.CSSProperties { const base: React.CSSProperties = { padding: isSelected && !isMultiSelected ? '14px 16px 14px 13px' : '14px 16px' } if (isMultiSelected) base.backgroundColor = 'color-mix(in srgb, var(--accent-blue) 10%, transparent)' else if (isSelected) { base.borderLeftColor = typeColor; base.backgroundColor = typeLightColor } return base } function getFileKindIcon(fileKind: string | undefined): ComponentType> { if (fileKind === 'text') return File if (fileKind === 'binary') return FileDashed return FileText } function resolveDisplayProps(entry: VaultEntry, typeEntryMap: Record, displayPropsOverride?: string[] | null): string[] { if (displayPropsOverride && displayPropsOverride.length > 0) return displayPropsOverride return typeEntryMap[entry.isA ?? '']?.listPropertiesDisplay ?? [] } type NoteItemProps = { entry: VaultEntry isSelected: boolean isMultiSelected?: boolean isHighlighted?: boolean noteStatus?: NoteStatus /** When set, renders in Changes-view style: filename + change type icon */ changeStatus?: 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed' typeEntryMap: Record allEntries?: VaultEntry[] displayPropsOverride?: string[] | null onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void onPrefetch?: (path: string) => void onContextMenu?: (entry: VaultEntry, e: React.MouseEvent) => void } function createNoteItemClickHandler( entry: VaultEntry, isBinary: boolean, onClickNote: NoteItemProps['onClickNote'], ) { if (isBinary) { return (event: React.MouseEvent) => { event.preventDefault() event.stopPropagation() } } return (event: React.MouseEvent) => onClickNote(entry, event) } export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlighted = false, noteStatus = 'clean', changeStatus, typeEntryMap, allEntries, displayPropsOverride, onClickNote, onPrefetch, onContextMenu }: NoteItemProps) { const isBinary = entry.fileKind === 'binary' const isDeletedChange = changeStatus === 'deleted' const te = typeEntryMap[entry.isA ?? ''] const displayProps = resolveDisplayProps(entry, typeEntryMap, displayPropsOverride) const typeColor = isBinary ? 'var(--muted-foreground)' : getTypeColor(entry.isA ?? 'Note', te?.color) const typeLightColor = getTypeLightColor(entry.isA ?? 'Note', te?.color) const handleClick = createNoteItemClickHandler(entry, isBinary, onClickNote) return (
onContextMenu(entry, e) : undefined} onMouseEnter={!isBinary && onPrefetch ? () => onPrefetch(entry.path) : undefined} data-testid={isMultiSelected ? 'multi-selected-item' : isBinary ? 'binary-file-item' : undefined} data-highlighted={isHighlighted || undefined} data-note-path={entry.path} data-change-status={changeStatus} title={isBinary ? 'Cannot open this file type' : undefined} > {changeStatus ? ( ) : ( )}
) }