Add snippet-library search, sort, empty states, storage monitor (M6, §02/§09D)

This commit is contained in:
2026-06-07 20:00:40 +03:00
parent 9f7bf27b7a
commit 800a313be2
19 changed files with 1461 additions and 16 deletions
+32
View File
@@ -2,8 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
loadPanelLayout,
loadPaneVisibility,
loadSnippetSort,
savePanelLayout,
savePaneVisibility,
saveSnippetSort,
} from './ux-prefs';
const KEY = 'astrolabe:ux-prefs';
@@ -98,3 +100,33 @@ describe('ux-prefs · paneVisibility', () => {
expect(stored.paneVisibility.editor).toBe(false);
});
});
describe('ux-prefs · snippetSort', () => {
beforeEach(() => vi.stubGlobal('localStorage', makeStorageStub()));
afterEach(() => vi.unstubAllGlobals());
it('defaults to Modified, descending when nothing is stored', () => {
expect(loadSnippetSort()).toEqual({ by: 'modified', order: 'desc' });
});
it('round-trips a stored sort', () => {
saveSnippetSort({ by: 'name', order: 'asc' });
expect(loadSnippetSort()).toEqual({ by: 'name', order: 'asc' });
});
it('falls back to defaults for invalid stored values', () => {
localStorage.setItem(KEY, JSON.stringify({ snippetSort: { by: 'bogus', order: 'sideways' } }));
expect(loadSnippetSort()).toEqual({ by: 'modified', order: 'desc' });
});
it('preserves panelLayout when writing the sort (separate keys)', () => {
savePanelLayout({ libraryWidth: 280 });
saveSnippetSort({ by: 'size', order: 'desc' });
const stored = JSON.parse(localStorage.getItem(KEY)!) as {
panelLayout: { libraryWidth: number };
snippetSort: { by: string; order: string };
};
expect(stored.panelLayout.libraryWidth).toBe(280);
expect(stored.snippetSort).toEqual({ by: 'size', order: 'desc' });
});
});
+41 -1
View File
@@ -4,7 +4,7 @@
* These preferences persist **separately** from UserSettings so they can change
* frequently (a drag emits many width updates) without rewriting the settings
* record. Its own key, `astrolabe:ux-prefs`, holds the snippet sort preference
* (lands with M5/M6) and the panel layout (per-pane widths + visibility).
* (spec §02 → Sort) and the panel layout (per-pane widths + visibility).
*
* This slice persists the panel **widths** and per-pane **visibility** (spec §01A).
* Read-with-fallback + write-through merge, the same contract as the settings
@@ -15,6 +15,13 @@
* `localStorage`; everything else goes through these typed functions.
*/
import {
DEFAULT_SORT_BY,
DEFAULT_SORT_ORDER,
type SortBy,
type SortOrder,
} from '@core/snippet-sort';
const KEY = 'astrolabe:ux-prefs';
/** Per-pane widths (px). Optional — a missing field falls back to its default. */
@@ -30,9 +37,16 @@ export interface PaneVisibilityPref {
preview?: boolean;
}
/** The persisted library sort (spec §02 → Sort; persists across sessions). */
export interface SnippetSortPref {
by: SortBy;
order: SortOrder;
}
interface StoredPrefs {
panelLayout?: PanelLayout;
paneVisibility?: PaneVisibilityPref;
snippetSort?: Partial<SnippetSortPref>;
[k: string]: unknown;
}
@@ -106,3 +120,29 @@ export function savePaneVisibility(visibility: PaneVisibilityPref): void {
const current = readRaw();
writeRaw({ ...current, paneVisibility: { ...current.paneVisibility, ...visibility } });
}
/** Guards against junk in storage — only the known sort fields survive. */
function sortBy(v: unknown): SortBy | undefined {
return v === 'modified' || v === 'created' || v === 'name' || v === 'size' ? v : undefined;
}
function sortOrder(v: unknown): SortOrder | undefined {
return v === 'asc' || v === 'desc' ? v : undefined;
}
/**
* The persisted library sort, falling back to the spec default (Modified, desc)
* for anything missing or invalid in storage.
*/
export function loadSnippetSort(): SnippetSortPref {
const stored = readRaw().snippetSort ?? {};
return {
by: sortBy(stored.by) ?? DEFAULT_SORT_BY,
order: sortOrder(stored.order) ?? DEFAULT_SORT_ORDER,
};
}
/** Persist the library sort, preserving every other key already in the record. */
export function saveSnippetSort(sort: SnippetSortPref): void {
const current = readRaw();
writeRaw({ ...current, snippetSort: { ...current.snippetSort, ...sort } });
}