From 2d37fba211a6272dec5f12420b3258757e2f0270 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Apr 2026 11:39:33 +0200 Subject: [PATCH] feat: Tags entry in the Library sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an expandable Tags node alongside All Photos / Rated / Duplicates / Discarded. The children are populated dynamically from useTagsQuery — one row per tag, showing the tag name and its photo count badge. Click a tag row to filter the timeline to just that tag (single-tag), with the active highlight following the filter store. Multi-tag filtering still lives in the FilterBar; the sidebar entry is the quick "show me everything in this tag" affordance. Implementation - New 'tags' library tree node with children: allTags.map(...) - 'tag-{id}' click handler in applyLibraryNode → clearAll() + setTagIds([id]) - isItemActive recognises a tag row as selected only when the filter store has exactly that single tag id, so combining it with multi-tag filter mode in the FilterBar doesn't leave a stale highlight. - Tags section is collapsed by default like other library nodes; no effect when there are no tags yet (children list is empty). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/layout/LeftSidebar.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/src/components/layout/LeftSidebar.tsx b/frontend/src/components/layout/LeftSidebar.tsx index dd5a210..f87f3e0 100644 --- a/frontend/src/components/layout/LeftSidebar.tsx +++ b/frontend/src/components/layout/LeftSidebar.tsx @@ -10,6 +10,7 @@ import { HardDrive, RefreshCw, Copy, + Tag as TagIcon, } from 'lucide-react' import clsx from 'clsx' import { sourceFolders, library, photos as photosApi, type FolderTreeNode } from '../../services/api' @@ -19,6 +20,7 @@ import { useFilterStore } from '../../store/filterStore' import { HeapsPanel } from '../heaps/HeapsPanel' import { PHOTO_DRAG_MIME } from '../timeline/PhotoThumbnail' import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery' +import { useTagsQuery } from '../../hooks/useTagsQuery' interface TreeItem { id: string @@ -44,8 +46,11 @@ export function LeftSidebar() { const setFlag = useFilterStore((s) => s.setFlag) const setFolderId = useFilterStore((s) => s.setFolderId) const setDuplicates = useFilterStore((s) => s.setDuplicates) + const setTagIds = useFilterStore((s) => s.setTagIds) const filterFolderId = useFilterStore((s) => s.folderId) const filterDuplicates = useFilterStore((s) => s.duplicates) + const filterTagIds = useFilterStore((s) => s.tagIds) + const { data: allTags = [] } = useTagsQuery() const [dropTargetId, setDropTargetId] = useState(null) // Bulk discard mutation for the drag-onto-Discarded interaction. @@ -138,6 +143,14 @@ export function LeftSidebar() { setDuplicates(true) break default: + if (id.startsWith('tag-')) { + // Tag rows: filter to that single tag. Multi-tag filtering is + // available via the FilterBar. + const tagId = id.slice('tag-'.length) + clearAllFilters() + setTagIds([tagId]) + return + } if (id.startsWith('folder-')) { // Folder rows: filter to that folder, clear other filters that // would compete (heap, discarded, etc.) so the user sees what they @@ -219,6 +232,17 @@ export function LeftSidebar() { { id: 'rated', label: 'Rated', icon: , count: 0 }, { id: 'duplicates', label: 'Duplicates', icon: , count: 0 }, { id: 'discarded', label: 'Discarded', icon: , count: 0 }, + { + id: 'tags', + label: 'Tags', + icon: , + children: allTags.map((tag) => ({ + id: `tag-${tag.id}`, + label: tag.name, + icon: , + count: tag.photo_count, + })), + }, ], }, { @@ -237,6 +261,10 @@ export function LeftSidebar() { if (id.startsWith('folder-')) { return filterFolderId === id.slice('folder-'.length) } + if (id.startsWith('tag-')) { + const tagId = id.slice('tag-'.length) + return filterTagIds.length === 1 && filterTagIds[0] === tagId + } if (id === 'all-photos') { return filterFolderId === null && selectedItem === 'all-photos' }