diff --git a/CLAUDE.md b/CLAUDE.md index 32378b34..310dd6e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,11 @@ This is a pnpm workspace with multiple applications and packages: - Use flat keys with `.` separation (e.g., `exif.camera.model`) - Support pluralization with `_one` and `_other` suffixes - Modify English first, then other languages (ESLint auto-removes unused keys) -- Avoid nested key conflicts in flat structure +- **CRITICAL: Avoid nested key conflicts in flat structure** + - ❌ WRONG: `"action.tag.mode.and": "AND"` + `"action.tag.mode.and.tooltip": "..."` + - ✅ CORRECT: `"action.tag.mode.and": "AND"` + `"action.tag.tooltip.and": "..."` + - Rule: A key cannot be both a string value AND a parent object + - Each key must be completely independent in the flat structure ### Testing Strategy - Check README.md and package.json scripts for test commands diff --git a/apps/web/package.json b/apps/web/package.json index 995b0c6a..eceaa4c8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ "@lobehub/fluent-emoji": "2.0.0", "@maplibre/maplibre-gl-geocoder": "^1.9.0", "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-context-menu": "2.2.15", "@radix-ui/react-dialog": "1.1.14", "@radix-ui/react-dropdown-menu": "2.1.15", @@ -33,6 +34,7 @@ "@radix-ui/react-popover": "1.1.14", "@radix-ui/react-scroll-area": "1.2.9", "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tooltip": "1.2.7", "@react-hook/window-size": "3.1.1", "@remixicon/react": "4.6.0", diff --git a/apps/web/src/atoms/app.ts b/apps/web/src/atoms/app.ts index 8b448748..1e2dff2d 100644 --- a/apps/web/src/atoms/app.ts +++ b/apps/web/src/atoms/app.ts @@ -10,6 +10,7 @@ export const gallerySettingAtom = atom({ selectedCameras: [] as string[], // Selected camera display names selectedLenses: [] as string[], // Selected lens display names selectedRatings: null as number | null, // Selected minimum rating threshold + tagFilterMode: 'union' as 'union' | 'intersection', // Tag filtering logic mode tagSearchQuery: '' as string, cameraSearchQuery: '' as string, // Camera search query lensSearchQuery: '' as string, // Lens search query diff --git a/apps/web/src/components/gallery/FilterPanel.tsx b/apps/web/src/components/gallery/FilterPanel.tsx index 586aa4db..c451e6bf 100644 --- a/apps/web/src/components/gallery/FilterPanel.tsx +++ b/apps/web/src/components/gallery/FilterPanel.tsx @@ -7,6 +7,8 @@ import { gallerySettingAtom } from '~/atoms/app' import { Button } from '~/components/ui/button' import { clsxm } from '~/lib/cn' +import { Checkbox } from '../ui/checkbox' + const allTags = photoLoader.getAllTags() const allCameras = photoLoader.getAllCameras() const allLenses = photoLoader.getAllLenses() @@ -134,6 +136,16 @@ export const FilterPanel = () => { setRatingSearchQuery('') }, [setGallerySetting]) + const setTagFilterMode = useCallback( + (mode: 'union' | 'intersection') => { + setGallerySetting((prev) => ({ + ...prev, + tagFilterMode: mode, + })) + }, + [setGallerySetting], + ) + // Search handlers with useCallback const onTagSearchChange = useCallback( (e: React.ChangeEvent) => { @@ -266,13 +278,13 @@ export const FilterPanel = () => { data: [] as string[], filteredData: [] as string[], selectedItems: [] as string[], - searchQuery: '', - searchPlaceholder: '', + searchQuery: ratingSearchQuery, + searchPlaceholder: t('action.rating.search'), emptyMessage: '', notFoundMessage: '', onToggle: () => {}, onClear: clearRatings, - onSearchChange: () => {}, + onSearchChange: onRatingSearchChange, }, ], [ @@ -291,7 +303,6 @@ export const FilterPanel = () => { toggleTag, toggleCamera, toggleLens, - setRating, clearTags, clearCameras, clearLenses, @@ -328,6 +339,7 @@ export const FilterPanel = () => { selectedCameras: [], selectedLenses: [], selectedRatings: null, + tagFilterMode: 'union', tagSearchQuery: '', cameraSearchQuery: '', lensSearchQuery: '', @@ -385,7 +397,7 @@ export const FilterPanel = () => { )} - {currentTab.count > 0 && activeTab != 'ratings' && ( + {currentTab.count > 0 && activeTab !== 'ratings' && (