Add opt-in date grouping for the All Notes list

This commit is contained in:
Yuri Karamian
2026-06-24 22:38:23 +02:00
parent 3ef0e14581
commit 987b1b9ed7
5 changed files with 83 additions and 6 deletions
+4
View File
@@ -39,6 +39,7 @@
rootNoteCount,
viewMode,
sortMode,
groupNotesByDate,
tasksLayout,
tasksHideCompleted,
tasksOnlyFlagged,
@@ -210,6 +211,7 @@
notebook_order: $notebookOrder,
note_order: $noteOrder,
sort_mode: $sortMode,
group_notes_by_date: $groupNotesByDate,
last_view_mode: $viewMode,
last_notebook: $activeNotebook?.relative_path ?? null,
last_tag: $activeTag,
@@ -553,6 +555,7 @@
$effect(() => {
$sortMode;
$groupNotesByDate;
$tasksLayout;
$tasksHideCompleted;
$tasksOnlyFlagged;
@@ -584,6 +587,7 @@
$notebookOrder = state.notebook_order ?? {};
$noteOrder = state.note_order ?? {};
if (state.sort_mode === 'created' || state.sort_mode === 'title' || state.sort_mode === 'modified' || state.sort_mode === 'custom') $sortMode = state.sort_mode;
if (typeof state.group_notes_by_date === 'boolean') $groupNotesByDate = state.group_notes_by_date;
if (state.tasks_layout === 'calendar' || state.tasks_layout === 'list') $tasksLayout = state.tasks_layout;
if (typeof state.tasks_hide_completed === 'boolean') $tasksHideCompleted = state.tasks_hide_completed;
if (typeof state.tasks_only_flagged === 'boolean') $tasksOnlyFlagged = state.tasks_only_flagged;
+59 -6
View File
@@ -9,6 +9,7 @@
activeNotebook,
activeTag,
sortMode,
groupNotesByDate,
editorDirty,
quickAccessPaths,
appConfig,
@@ -37,7 +38,7 @@
getAllTags,
createDailyNote
} from '$lib/api';
import { formatRelativeTime, formatDate } from '$lib/utils/time';
import { formatRelativeTime, formatDate, dateBucketLabel } from '$lib/utils/time';
import { openNoteWindow } from '$lib/utils/window';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import type { NoteEntry, TrashNotebookEntry, SortMode, TaskItem } from '$lib/types';
@@ -212,6 +213,24 @@
const customOrderViews = new Set(['all', 'notebook', 'tag']);
let canCustomReorder = $derived($sortMode === 'custom' && customOrderViews.has($viewMode));
// Relative-date grouping (All Notes + date sorts only). Pinned notes form their own leading group.
let groupingActive = $derived($groupNotesByDate && $viewMode === 'all' && ($sortMode === 'created' || $sortMode === 'modified'));
let noteGroups = $derived.by(() => {
if (!groupingActive) return [] as { label: string; notes: NoteEntry[] }[];
const pinned: NoteEntry[] = [];
const rest: NoteEntry[] = [];
for (const n of $sortedNotes) (n.meta.pinned ? pinned : rest).push(n);
const groups: { label: string; notes: NoteEntry[] }[] = [];
if (pinned.length) groups.push({ label: 'Pinned', notes: pinned });
let cur: { label: string; notes: NoteEntry[] } | null = null;
for (const n of rest) {
const label = dateBucketLabel($sortMode === 'created' ? n.meta.created : n.meta.modified);
if (!cur || cur.label !== label) { cur = { label, notes: [] }; groups.push(cur); }
cur.notes.push(n);
}
return groups;
});
// Virtual scroll
let listContainer = $state<HTMLDivElement>(null!);
let scrollTop = $state(0);
@@ -955,6 +974,13 @@
<line x1="4" y1="6" x2="11" y2="6" /><line x1="4" y1="12" x2="15" y2="12" /><line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
{#if $viewMode === 'all' && ($sortMode === 'created' || $sortMode === 'modified')}
<button class="icon-btn" class:active-toggle={$groupNotesByDate} onclick={() => { $groupNotesByDate = !$groupNotesByDate; }} title="Group by date">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="8" y1="6" x2="21" y2="6" /><line x1="8" y1="12" x2="21" y2="12" /><line x1="8" y1="18" x2="21" y2="18" /><line x1="3" y1="6" x2="3.01" y2="6" /><line x1="3" y1="12" x2="3.01" y2="12" /><line x1="3" y1="18" x2="3.01" y2="18" />
</svg>
</button>
{/if}
{#if $viewMode !== 'trash'}
<button class={isMobile ? 'mobile-create-btn' : 'icon-btn'} onclick={handleCreateNote} title={`New note (${modKey}+N)`}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width={isMobile ? '3' : '2'} stroke-linecap="round" stroke-linejoin="round">
@@ -1133,9 +1159,7 @@
{/each}
{/if}
<div style="height: {topPad}px"></div>
{#each visibleNotes as note, i (note.path)}
{@const noteIndex = startIndex + i}
{#snippet noteRow(note: NoteEntry, noteIndex: number)}
{#if editingNote === note.path}
<div class="note-item active">
<input
@@ -1289,8 +1313,22 @@
{/if}
</button>
{/if}
{/each}
<div style="height: {bottomPad}px"></div>
{/snippet}
{#if groupingActive}
{#each noteGroups as group (group.label)}
<div class="note-list-section">{group.label}</div>
{#each group.notes as note (note.path)}
{@render noteRow(note, -1)}
{/each}
{/each}
{:else}
<div style="height: {topPad}px"></div>
{#each visibleNotes as note, i (note.path)}
{@render noteRow(note, startIndex + i)}
{/each}
<div style="height: {bottomPad}px"></div>
{/if}
</div>
</div>
@@ -1600,6 +1638,21 @@
background: var(--text-secondary);
}
.note-list-section {
padding: 14px 12px 4px;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--accent);
border-bottom: 1px solid var(--border-light);
margin-bottom: 2px;
user-select: none;
}
.note-list-section:first-child {
padding-top: 6px;
}
.note-item {
display: block;
width: 100%;
+2
View File
@@ -18,6 +18,8 @@ export const vaultReady = writable(false);
// UI state
export const viewMode = writable<ViewMode>("all");
export const sortMode = writable<SortMode>("modified");
// Group the notes list under relative-date headers (All Notes, date sorts only). Persisted per vault.
export const groupNotesByDate = writable<boolean>(false);
export const tasksLayout = writable<"list" | "calendar">("list");
export const tasksHideCompleted = writable<boolean>(true);
export const tasksOnlyFlagged = writable<boolean>(false);
+1
View File
@@ -137,6 +137,7 @@ export interface VaultState {
notebook_order?: Record<string, number>;
note_order?: Record<string, number>;
sort_mode?: string;
group_notes_by_date?: boolean;
last_view_mode?: string;
last_notebook?: string | null;
last_tag?: string | null;
+17
View File
@@ -24,3 +24,20 @@ export function formatDate(dateStr: string): string {
minute: '2-digit'
});
}
// Relative-date bucket label for grouping the notes list:
// Today / Yesterday / Previous 7 Days / Previous 30 Days / "June" (this year) / "June 2025".
// Buckets are contiguous, so a list already sorted by date groups by consecutive label.
export function dateBucketLabel(dateStr: string, now: Date = new Date()): string {
const d = new Date(dateStr);
const dayMs = 86400000;
const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
const today = startOfDay(now);
const day = startOfDay(d);
if (day >= today) return 'Today';
if (day >= today - dayMs) return 'Yesterday';
if (day >= today - 7 * dayMs) return 'Previous 7 Days';
if (day >= today - 30 * dayMs) return 'Previous 30 Days';
if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString(undefined, { month: 'long' });
return d.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
}