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
+48
View File
@@ -25,6 +25,12 @@ import {
type Snippet,
} from '@core/snippet';
import { recomputeDatasetRefs, renameDatasetInSpec } from '@core/spec-refs';
import {
DEFAULT_SORT_BY,
DEFAULT_SORT_ORDER,
type SortBy,
type SortOrder,
} from '@core/snippet-sort';
/** Which version of the active snippet the editor is showing (spec §03D). */
export type EditorView = 'draft' | 'published';
@@ -44,6 +50,17 @@ export interface SnippetState {
*/
bufferEpoch: number;
/**
* Library view state (spec §02 → Search / Sort). These affect only which
* snippets are shown and in what order — never `activeSnippetId` or any data.
* The component derives its visible list from these primitives via `useMemo`
* (core `filterAndSortSnippets`), NOT through a selector that builds a fresh
* array (which would loop the app — MEMORY → "Zustand stable selectors").
*/
searchQuery: string;
sortBy: SortBy;
sortOrder: SortOrder;
/** Replace the library from storage and choose an active snippet. */
hydrate: (snippets: Snippet[], activeId?: string | null) => void;
/**
@@ -72,6 +89,16 @@ export interface SnippetState {
* an unchanged comment. `now` injectable.
*/
setComment: (id: string, comment: string, now?: Date) => void;
/** Set the live search query (spec §02 → Search). Visibility only — never
* touches `activeSnippetId` or any snippet data. */
setSearch: (query: string) => void;
/**
* Set the library sort (spec §02 → Sort): re-selecting the **active** field
* flips direction; choosing a **different** field switches to it and resets to
* descending. Persisted by an orchestration subscriber; never touches the
* active snippet. `order` may be passed to hydrate an exact stored state.
*/
setSort: (by: SortBy, order?: SortOrder) => void;
/**
* Duplicate the active snippet (spec §02 → Duplicate): flushes the live buffer
* into the source draft first so the copy reflects in-progress edits, then
@@ -143,6 +170,9 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
draftText: '',
editorView: 'draft',
bufferEpoch: 0,
searchQuery: '',
sortBy: DEFAULT_SORT_BY,
sortOrder: DEFAULT_SORT_ORDER,
hydrate: (snippets, activeId) => {
const id = activeId !== undefined ? activeId : newestId(snippets);
@@ -243,6 +273,21 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
});
},
setSearch: (searchQuery) => set({ searchQuery }),
setSort: (by, order) => {
set((s) => {
// An explicit order hydrates a stored state directly (orchestration init).
if (order !== undefined) return { sortBy: by, sortOrder: order };
// Re-selecting the active field flips direction; a different field resets
// to descending (spec §02 → Sort).
if (by === s.sortBy) {
return { sortOrder: s.sortOrder === 'desc' ? 'asc' : 'desc' };
}
return { sortBy: by, sortOrder: 'desc' };
});
},
duplicateActiveSnippet: (now, id) => {
// Flush the live buffer into the source draft first, so the copy faithfully
// mirrors what the user currently sees, not the last auto-saved draft.
@@ -396,6 +441,9 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
draftText: '',
editorView: 'draft',
bufferEpoch: 0,
searchQuery: '',
sortBy: DEFAULT_SORT_BY,
sortOrder: DEFAULT_SORT_ORDER,
}),
}));