feat: virtualized tag manager with filter, sort, and light-theme support

Replaces the flat TagManageRow list with a virtualized grid of
TagManageTile cards using @tanstack/react-virtual and dynamic measured
heights (46px idle, 82px when editing/confirming). Adds a filter input
and a sort dropdown (most-used / least-used / A-Z / Z-A). renameTag and
deleteTag no longer clear exploreTagEntries on invalidation so the
manager keeps its filter/sort state during the background refresh.
Light-theme overrides cover all new tag-manager class names.
This commit is contained in:
2026-06-29 16:29:12 +01:00
parent 9144be2518
commit af3c8418ee
4 changed files with 312 additions and 49 deletions
+14 -2
View File
@@ -1,5 +1,5 @@
import { emit } from "@tauri-apps/api/event";
import type { Album, Folder, ImageRecord, ImageTag, SortOrder } from "../store";
import type { Album, ExploreTagEntry, Folder, ImageRecord, ImageTag, SortOrder } from "../store";
import { compareImages, createMockDb, mockExif } from "./mockFixtures";
import { getMockScenario } from "./mockScenarios";
@@ -58,12 +58,22 @@ function searchTags(payload: unknown) {
.filter(([, tags]) => tags.some((tag) => tag.tag.toLowerCase().includes(query)))
.map(([imageId]) => Number(imageId)),
);
const images = filterImages(db.images, payload)
const images = filterImages(db.images, { params: { ...p, query: "", search: "" } })
.filter((image) => matchingIds.has(image.id))
.sort(compareImages((p.sort ?? "date_desc") as SortOrder));
return page(images, Number(p.offset ?? 0), Number(p.limit ?? 200));
}
function searchTagsAutocomplete(payload: unknown): ExploreTagEntry[] {
const p = params(payload);
const query = String(p.query ?? "").trim().toLowerCase();
const limit = Number(p.limit ?? 10);
return db.exploreTags
.filter((entry) => !query || entry.tag.toLowerCase().includes(query))
.sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag))
.slice(0, limit);
}
function semanticSearch(payload: unknown): ImageRecord[] {
const p = params(payload);
const query = String(p.query ?? "").toLowerCase();
@@ -233,6 +243,8 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise
return semanticSearch(payload);
case "search_images_by_tag":
return searchTags(payload);
case "search_tags_autocomplete":
return searchTagsAutocomplete(payload);
case "get_images_by_ids":
return (p.image_ids ?? []).map((id: number) => db.images.find((image) => image.id === id)).filter(Boolean);
case "find_similar_images":