feat: add sort direction (asc/desc) to note list sorting

- Add SortDirection type and SortConfig interface to noteListHelpers
- getSortComparator now accepts optional direction parameter
- SortDropdown shows ↑/↓ arrow buttons per option in the menu
- Button label shows current direction arrow (ArrowUp/ArrowDown)
- Persistence updated to store {option, direction} with backward compat
  for old string-only preferences
- Default directions: modified/created=desc, title/status=asc
- Status sort tiebreaker always uses newest-first regardless of direction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-22 12:08:53 +01:00
parent 53c881278e
commit 1230cc4e77
3 changed files with 109 additions and 53 deletions

View File

@@ -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<SortOption, SortDirection> = {
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<string, number> = {
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<string, SortOption> {
export function loadSortPreferences(): Record<string, SortConfig> {
try {
const raw = localStorage.getItem(SORT_STORAGE_KEY)
return raw ? JSON.parse(raw) : {}
if (!raw) return {}
const parsed = JSON.parse(raw)
const result: Record<string, SortConfig> = {}
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<string, SortOption>) {
export function saveSortPreferences(prefs: Record<string, SortConfig>) {
try {
localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(prefs))
} catch { /* ignore */ }