diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx
index d35552b0..bed98d63 100644
--- a/src/components/NoteList.tsx
+++ b/src/components/NoteList.tsx
@@ -8,14 +8,14 @@ import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
import { NoteItem, getTypeIcon } from './NoteItem'
import { SortDropdown } from './SortDropdown'
import {
- type SortOption, type RelationshipGroup,
- getSortComparator,
+ type SortOption, type SortDirection, type SortConfig, type RelationshipGroup,
+ DEFAULT_DIRECTIONS, getSortComparator,
buildRelationshipGroups, filterEntries,
sortByModified, relativeDate, getDisplayDate,
loadSortPreferences, saveSortPreferences,
} from '../utils/noteListHelpers'
-// eslint-disable-next-line react-refresh/only-export-components -- re-exports for consumers
+// Re-export for consumers
export { sortByModified, filterEntries, buildRelationshipGroups, getSortComparator }
export type { SortOption }
@@ -38,10 +38,9 @@ function PinnedCard({ entry, typeEntryMap, onSelectNote, showDate }: {
const te = typeEntryMap[entry.isA ?? '']
const color = getTypeColor(entry.isA ?? '', te?.color)
const bgColor = getTypeLightColor(entry.isA ?? '', te?.color)
- const Icon = useMemo(() => getTypeIcon(entry.isA, te?.icon), [entry.isA, te?.icon])
+ const Icon = getTypeIcon(entry.isA, te?.icon)
return (
onSelectNote(entry)}>
- {/* eslint-disable-next-line react-hooks/static-components -- icon lookup from static map, no internal state */}
{entry.title}
{entry.snippet}
@@ -53,13 +52,13 @@ function PinnedCard({ entry, typeEntryMap, onSelectNote, showDate }: {
function RelationshipGroupSection({ group, isCollapsed, sortPrefs, onToggle, handleSortChange, renderItem }: {
group: RelationshipGroup
isCollapsed: boolean
- sortPrefs: Record
+ sortPrefs: Record
onToggle: () => void
- handleSortChange: (groupLabel: string, option: SortOption) => void
+ handleSortChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void
renderItem: (entry: VaultEntry) => React.ReactNode
}) {
- const groupSort = sortPrefs[group.label] ?? 'modified'
- const sortedEntries = [...group.entries].sort(getSortComparator(groupSort))
+ const groupConfig = sortPrefs[group.label] ?? { option: 'modified' as SortOption, direction: 'desc' as SortDirection }
+ const sortedEntries = [...group.entries].sort(getSortComparator(groupConfig.option, groupConfig.direction))
return (
@@ -68,7 +67,7 @@ function RelationshipGroupSection({ group, isCollapsed, sortPrefs, onToggle, han
{group.entries.length}
-
+
@@ -118,8 +117,8 @@ function useTypeEntryMap(entries: VaultEntry[]) {
function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, onToggleGroup, onSortChange, renderItem, typeEntryMap, onSelectNote }: {
entity: VaultEntry; groups: RelationshipGroup[]; query: string
- collapsedGroups: Set; sortPrefs: Record
- onToggleGroup: (label: string) => void; onSortChange: (label: string, opt: SortOption) => void
+ collapsedGroups: Set; sortPrefs: Record
+ onToggleGroup: (label: string) => void; onSortChange: (label: string, opt: SortOption, dir: SortDirection) => void
renderItem: (entry: VaultEntry) => React.ReactNode
typeEntryMap: Record; onSelectNote: (entry: VaultEntry) => void
}) {
@@ -175,10 +174,10 @@ function countExpiredTrash(entries: VaultEntry[]): number {
interface NoteListDataParams {
entries: VaultEntry[]; selection: SidebarSelection; allContent: Record
- query: string; listSort: SortOption
+ query: string; listSort: SortOption; listDirection: SortDirection; modifiedFiles?: ModifiedFile[]
}
-function useNoteListData({ entries, selection, allContent, query, listSort }: NoteListDataParams) {
+function useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedFiles }: NoteListDataParams) {
const isEntityView = selection.kind === 'entity'
const isTrashView = selection.kind === 'filter' && selection.filter === 'trash'
@@ -189,9 +188,9 @@ function useNoteListData({ entries, selection, allContent, query, listSort }: No
const searched = useMemo(() => {
if (isEntityView) return []
- const sorted = [...filterEntries(entries, selection)].sort(getSortComparator(listSort))
+ const sorted = [...filterEntries(entries, selection, modifiedFiles)].sort(getSortComparator(listSort, listDirection))
return filterByQuery(sorted, query)
- }, [entries, selection, isEntityView, listSort, query])
+ }, [entries, selection, modifiedFiles, isEntityView, listSort, listDirection, query])
const searchedGroups = useMemo(() => {
if (!isEntityView) return []
@@ -209,24 +208,26 @@ function useNoteListData({ entries, selection, allContent, query, listSort }: No
// --- Main component ---
-function NoteListInner({ entries, selection, selectedNote, allContent, onSelectNote, onCreateNote }: NoteListProps) {
+function NoteListInner({ entries, selection, selectedNote, allContent, modifiedFiles, onSelectNote, onCreateNote }: NoteListProps) {
const [search, setSearch] = useState('')
const [searchVisible, setSearchVisible] = useState(false)
const [collapsedGroups, setCollapsedGroups] = useState>(new Set())
- const [sortPrefs, setSortPrefs] = useState>(loadSortPreferences)
+ const [sortPrefs, setSortPrefs] = useState>(loadSortPreferences)
- const handleSortChange = useCallback((groupLabel: string, option: SortOption) => {
- setSortPrefs((prev) => { const next = { ...prev, [groupLabel]: option }; saveSortPreferences(next); return next })
+ const handleSortChange = useCallback((groupLabel: string, option: SortOption, direction: SortDirection) => {
+ setSortPrefs((prev) => { const next = { ...prev, [groupLabel]: { option, direction } }; saveSortPreferences(next); return next })
}, [])
const toggleGroup = useCallback((label: string) => {
- setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(label)) next.delete(label); else next.add(label); return next })
+ setCollapsedGroups((prev) => { const next = new Set(prev); next.has(label) ? next.delete(label) : next.add(label); return next })
}, [])
const typeEntryMap = useTypeEntryMap(entries)
const query = search.trim().toLowerCase()
- const listSort = sortPrefs['__list__'] ?? 'modified'
- const { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort })
+ const listConfig = sortPrefs['__list__'] ?? { option: 'modified' as SortOption, direction: 'desc' as SortDirection }
+ const listSort = listConfig.option
+ const listDirection = listConfig.direction
+ const { isEntityView, isTrashView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedFiles })
const renderItem = useCallback((entry: VaultEntry) => (
@@ -237,7 +238,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, onSelectN
{resolveHeaderTitle(selection, typeDocument)}
- {!isEntityView &&
}
+ {!isEntityView &&
}
diff --git a/src/components/SortDropdown.tsx b/src/components/SortDropdown.tsx
index 3b44969f..13e37133 100644
--- a/src/components/SortDropdown.tsx
+++ b/src/components/SortDropdown.tsx
@@ -1,12 +1,13 @@
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'
+import { ArrowUp, ArrowDown } from '@phosphor-icons/react'
+import { type SortOption, type SortDirection, DEFAULT_DIRECTIONS, SORT_OPTIONS } from '../utils/noteListHelpers'
-export function SortDropdown({ groupLabel, current, onChange }: {
+export function SortDropdown({ groupLabel, current, direction, onChange }: {
groupLabel: string
current: SortOption
- onChange: (groupLabel: string, option: SortOption) => void
+ direction: SortDirection
+ onChange: (groupLabel: string, option: SortOption, direction: SortDirection) => void
}) {
const [open, setOpen] = useState(false)
const ref = useRef
(null)
@@ -20,11 +21,13 @@ export function SortDropdown({ groupLabel, current, onChange }: {
return () => document.removeEventListener('mousedown', handleClick)
}, [open])
- const handleSelect = (opt: SortOption) => {
- onChange(groupLabel, opt)
+ const handleSelect = (opt: SortOption, dir: SortDirection) => {
+ onChange(groupLabel, opt, dir)
setOpen(false)
}
+ const DirectionIcon = direction === 'asc' ? ArrowUp : ArrowDown
+
return (
{open && (
-
- {SORT_OPTIONS.map((opt) => (
-
- ))}
+
+ {SORT_OPTIONS.map((opt) => {
+ const isActive = opt.value === current
+ return (
+
{ e.stopPropagation(); handleSelect(opt.value, isActive ? direction : DEFAULT_DIRECTIONS[opt.value]) }}
+ >
+
+ {opt.label}
+
+
+
+
+
+
+ )
+ })}
)}
diff --git a/src/utils/noteListHelpers.ts b/src/utils/noteListHelpers.ts
index 2d9edbb6..8bc0068e 100644
--- a/src/utils/noteListHelpers.ts
+++ b/src/utils/noteListHelpers.ts
@@ -53,6 +53,19 @@ export function sortByModified(a: VaultEntry, b: VaultEntry): number {
}
export type SortOption = 'modified' | 'created' | 'title' | 'status'
+export type SortDirection = 'asc' | 'desc'
+
+export interface SortConfig {
+ option: SortOption
+ direction: SortDirection
+}
+
+export const DEFAULT_DIRECTIONS: Record
= {
+ modified: 'desc',
+ created: 'desc',
+ title: 'asc',
+ status: 'asc',
+}
export const SORT_OPTIONS: { value: SortOption; label: string }[] = [
{ value: 'modified', label: 'Modified' },
@@ -65,36 +78,51 @@ const STATUS_ORDER: Record = {
Active: 0, Paused: 1, Done: 2, Finished: 3,
}
-export function getSortComparator(option: SortOption): (a: VaultEntry, b: VaultEntry) => number {
+export function getSortComparator(option: SortOption, direction?: SortDirection): (a: VaultEntry, b: VaultEntry) => number {
+ const dir = direction ?? DEFAULT_DIRECTIONS[option]
+ const flip = dir === 'asc' ? 1 : -1
switch (option) {
case 'modified':
- return sortByModified
+ return (a, b) => flip * ((getDisplayDate(a) ?? 0) - (getDisplayDate(b) ?? 0))
case 'created':
- return (a, b) => (b.createdAt ?? b.modifiedAt ?? 0) - (a.createdAt ?? a.modifiedAt ?? 0)
+ return (a, b) => flip * ((a.createdAt ?? a.modifiedAt ?? 0) - (b.createdAt ?? b.modifiedAt ?? 0))
case 'title':
- return (a, b) => a.title.localeCompare(b.title)
+ return (a, b) => flip * a.title.localeCompare(b.title)
case 'status':
return (a, b) => {
const sa = STATUS_ORDER[a.status ?? ''] ?? 999
const sb = STATUS_ORDER[b.status ?? ''] ?? 999
- if (sa !== sb) return sa - sb
- return sortByModified(a, b)
+ if (sa !== sb) return flip * (sa - sb)
+ // Tiebreaker: always newest first regardless of direction
+ return (getDisplayDate(b) ?? 0) - (getDisplayDate(a) ?? 0)
}
}
}
const SORT_STORAGE_KEY = 'laputa-sort-preferences'
-export function loadSortPreferences(): Record {
+export function loadSortPreferences(): Record {
try {
const raw = localStorage.getItem(SORT_STORAGE_KEY)
- return raw ? JSON.parse(raw) : {}
+ if (!raw) return {}
+ const parsed = JSON.parse(raw)
+ const result: Record = {}
+ for (const [key, value] of Object.entries(parsed)) {
+ if (typeof value === 'string') {
+ // Migrate old format: bare SortOption string → SortConfig
+ const opt = value as SortOption
+ result[key] = { option: opt, direction: DEFAULT_DIRECTIONS[opt] }
+ } else {
+ result[key] = value as SortConfig
+ }
+ }
+ return result
} catch {
return {}
}
}
-export function saveSortPreferences(prefs: Record) {
+export function saveSortPreferences(prefs: Record) {
try {
localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(prefs))
} catch { /* ignore */ }