diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md
index dfb100c..b8ef0db 100644
--- a/documentation/FEATURES.md
+++ b/documentation/FEATURES.md
@@ -27,7 +27,7 @@
### Organization
- **Folder hierarchy** - Organize notes in nested folders
- **Drag & drop** - Move notes and folders effortlessly
-- **Alphabetical sorting** - Find notes quickly
+- **Flexible sorting** - Sort notes by name (A-Z, Z-A), date (newest, oldest), or size (largest, smallest)
- **Rename anything** - Files and folders, instantly
- **Visual tree view** - Expandable/collapsible navigation
- **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar
diff --git a/frontend/app.js b/frontend/app.js
index 4acd1a0..ea09c09 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -21,6 +21,8 @@ const LOCAL_SETTINGS = {
tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false },
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false },
+ // String settings
+ sortMode: { key: 'sortMode', type: 'string', default: 'a-z' },
// Number settings with validation
sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 },
editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 },
@@ -219,6 +221,10 @@ function noteApp() {
// Tab key inserts tab character instead of changing focus
tabInsertsTab: localStorage.getItem('tabInsertsTab') === 'true',
+ // Note sorting mode (a-z, z-a, newest, oldest, largest, smallest)
+ sortMode: localStorage.getItem('sortMode') || 'a-z',
+ sortKey: 0, // Incremented to force tree re-render
+
// Icon rail / panel state
activePanel: 'files', // 'files', 'search', 'tags', 'settings'
@@ -852,6 +858,60 @@ function noteApp() {
this.autoSave();
},
+ // Sort mode configuration
+ sortModes: ['a-z', 'z-a', 'newest', 'oldest', 'largest', 'smallest'],
+ sortModeIcons: {
+ 'a-z': 'A↓',
+ 'z-a': 'Z↓',
+ 'newest': '🕐↓',
+ 'oldest': '🕐↑',
+ 'largest': '📄↓',
+ 'smallest': '📄↑'
+ },
+
+ // Cycle through sort modes
+ cycleSortMode() {
+ const currentIndex = this.sortModes.indexOf(this.sortMode);
+ const nextIndex = (currentIndex + 1) % this.sortModes.length;
+ this.sortMode = this.sortModes[nextIndex];
+ localStorage.setItem('sortMode', this.sortMode);
+ // Rebuild tree to apply new sort order
+ this.buildFolderTree();
+ },
+
+ // Get current sort icon
+ getSortIcon() {
+ return this.sortModeIcons[this.sortMode] || 'A↓';
+ },
+
+ // Get sort comparator based on current mode (for notes)
+ getSortComparator() {
+ switch (this.sortMode) {
+ case 'z-a':
+ return (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase());
+ case 'newest':
+ return (a, b) => (b.modified || '').localeCompare(a.modified || '');
+ case 'oldest':
+ return (a, b) => (a.modified || '').localeCompare(b.modified || '');
+ case 'largest':
+ return (a, b) => (b.size || 0) - (a.size || 0);
+ case 'smallest':
+ return (a, b) => (a.size || 0) - (b.size || 0);
+ case 'a-z':
+ default:
+ return (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+ }
+ },
+
+ // Get sort comparator for folders (only A-Z/Z-A, others default to A-Z)
+ getFolderSortComparator() {
+ if (this.sortMode === 'z-a') {
+ return (a, b) => b.name.toLowerCase().localeCompare(a.name.toLowerCase());
+ }
+ // Default: A-Z for all other modes
+ return (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+ },
+
// Update syntax highlight overlay (debounced, called on input)
updateSyntaxHighlight() {
if (!this.syntaxHighlightEnabled) return;
@@ -1757,7 +1817,7 @@ function noteApp() {
const sortNotes = (obj) => {
if (obj.notes && obj.notes.length > 0) {
// Create a new sorted array instead of mutating for Alpine reactivity
- obj.notes = [...obj.notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ obj.notes = [...obj.notes].sort(this.getSortComparator());
}
if (obj.children && Object.keys(obj.children).length > 0) {
Object.values(obj.children).forEach(child => sortNotes(child));
@@ -1766,7 +1826,7 @@ function noteApp() {
// Sort notes in root (create new array for reactivity)
if (tree['__root__'] && tree['__root__'].notes) {
- tree['__root__'].notes = [...tree['__root__'].notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ tree['__root__'].notes = [...tree['__root__'].notes].sort(this.getSortComparator());
}
// Sort notes in all folders
@@ -1886,7 +1946,8 @@ function noteApp() {
// Render folder recursively (helper for deep nesting)
// Uses data-* attributes to store path/name, avoiding JS string escaping issues
- renderFolderRecursive(folder, level = 0, isTopLevel = false) {
+ // sortKey parameter triggers Alpine re-render when sort mode changes
+ renderFolderRecursive(folder, level = 0, isTopLevel = false, sortKey = 0) {
if (!folder) return '';
let html = '';
@@ -1966,8 +2027,8 @@ function noteApp() {
if (folder.children && Object.keys(folder.children).length > 0) {
const children = Object.entries(folder.children)
.filter(([k, v]) => !this.hideUnderscoreFolders || !v.name.startsWith('_'))
- .sort((a, b) => a[1].name.toLowerCase().localeCompare(b[1].name.toLowerCase()));
-
+ .sort((a, b) => this.getFolderSortComparator()(a[1], b[1]));
+
children.forEach(([childKey, childFolder]) => {
html += this.renderFolderRecursive(childFolder, 0, false);
});
@@ -2033,7 +2094,8 @@ function noteApp() {
},
// Render root-level items (notes and media not in any folder)
- renderRootItems() {
+ // sortKey parameter triggers Alpine re-render when sort mode changes
+ renderRootItems(sortKey = 0) {
const root = this.folderTree['__root__'];
if (!root || !root.notes || root.notes.length === 0) {
return '';
diff --git a/frontend/index.html b/frontend/index.html
index a9ebe8d..1e7aa1f 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1591,7 +1591,15 @@
-
+