import { useMemo, type ComponentType, type SVGAttributes } from 'react' import type { VaultEntry, NoteStatus, PinnedPropertyConfig } from '../types' import { cn } from '@/lib/utils' import { Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, Tag, FileText, StackSimple, } from '@phosphor-icons/react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { resolveIcon } from '../utils/iconRegistry' import { relativeDate, getDisplayDate } from '../utils/noteListHelpers' import { isEmoji } from '../utils/emoji' import { NoteListPinnedValues } from './NoteListPinnedValues' 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 } function defaultPinnedConfigs(entry: VaultEntry): PinnedPropertyConfig[] { if (!entry.isA) return [] const pins: PinnedPropertyConfig[] = [] if (entry.status != null) pins.push({ key: 'Status', icon: 'circle-dot' }) if (entry.belongsTo.length > 0) pins.push({ key: 'Belongs to', icon: 'arrow-up-right' }) if (entry.relatedTo.length > 0) pins.push({ key: 'Related to', icon: 'arrows-left-right' }) return pins } const THIRTY_DAYS_SECS = 86400 * 30 function TrashDateLine({ entry }: { entry: VaultEntry }) { const { isExpired, suffix } = useMemo(() => { // eslint-disable-next-line react-hooks/purity -- Date.now() intentionally memoized on trashedAt const trashedAge = entry.trashedAt ? (Date.now() / 1000 - entry.trashedAt) : 0 const expired = trashedAge >= THIRTY_DAYS_SECS return { isExpired: expired, suffix: expired ? ' — will be permanently deleted' : '', } }, [entry.trashedAt]) const style = isExpired ? { color: 'var(--destructive)', fontWeight: 500 } as const : undefined return (
Trashed {relativeDate(entry.trashedAt)}{suffix}
) } 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, trashed }: { archived: boolean; trashed: boolean }) { if (archived) { return ( ARCHIVED ) } if (trashed) { return ( TRASHED ) } return null } 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 NoteItemTitle({ entry, noteStatus, isSelected }: { entry: VaultEntry; noteStatus: NoteStatus; isSelected: boolean }) { return (
{noteStatus !== 'clean' && } {entry.icon && isEmoji(entry.icon) && {entry.icon}} {entry.title}
) } function NoteItemFooter({ entry, pinnedConfigs }: { entry: VaultEntry; pinnedConfigs: PinnedPropertyConfig[] }) { return ( <> {entry.snippet && (
{entry.snippet}
)} {pinnedConfigs.length > 0 && } {entry.trashed && entry.trashedAt ? :
{relativeDate(getDisplayDate(entry))}
} ) } export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlighted = false, noteStatus = 'clean', typeEntryMap, onClickNote, onPrefetch }: { entry: VaultEntry isSelected: boolean isMultiSelected?: boolean isHighlighted?: boolean noteStatus?: NoteStatus typeEntryMap: Record onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void onPrefetch?: (path: string) => void }) { const te = typeEntryMap[entry.isA ?? ''] const typeColor = getTypeColor(entry.isA ?? 'Note', te?.color) const typeLightColor = getTypeLightColor(entry.isA ?? 'Note', te?.color) const TypeIcon = useMemo(() => getTypeIcon(entry.isA, te?.icon), [entry.isA, te?.icon]) const pinnedConfigs = useMemo((): PinnedPropertyConfig[] => { if (te?.pinnedProperties && te.pinnedProperties.length > 0) return te.pinnedProperties return defaultPinnedConfigs(entry) }, [te, entry]) return (
onClickNote(entry, e)} onMouseEnter={onPrefetch ? () => onPrefetch(entry.path) : undefined} data-testid={isMultiSelected ? 'multi-selected-item' : undefined} data-highlighted={isHighlighted || undefined} > {/* eslint-disable-next-line react-hooks/static-components -- icon lookup from static map, no internal state */}
) }