From bee1b4d7f82150108eb243888be7e41cc8f5b888 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 3 Apr 2026 11:02:01 -0700 Subject: [PATCH] =?UTF-8?q?feat(media-gallery):=20=E2=9C=A8=20Add=20identi?= =?UTF-8?q?ty-based=20filtering=20to=20FilterPanel=20and=20integrate=20wit?= =?UTF-8?q?h=20AlbumsPage=20and=20GalleryPage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/components/FilterPanel.tsx | 12 ++ .../components/IdentityFilterItem.module.css | 67 ++++++++- .../src/components/IdentityFilterItem.tsx | 132 +++++++++++++----- .../frontend-dev/src/pages/AlbumsPage.tsx | 1 + .../frontend-dev/src/pages/GalleryPage.tsx | 16 ++- 5 files changed, 184 insertions(+), 44 deletions(-) diff --git a/features/video-studio/packages/media-gallery/frontend-dev/src/components/FilterPanel.tsx b/features/video-studio/packages/media-gallery/frontend-dev/src/components/FilterPanel.tsx index ee5078f0e..97ab82550 100644 --- a/features/video-studio/packages/media-gallery/frontend-dev/src/components/FilterPanel.tsx +++ b/features/video-studio/packages/media-gallery/frontend-dev/src/components/FilterPanel.tsx @@ -75,9 +75,13 @@ export interface FilterPanelProps { categoryCounts: CategoryCount[] | undefined; identities: Identity[] | undefined; activeFilter: ActiveFilter; + editingIdentityId: string | null; onSelect: (filter: ActiveFilter) => void; onMarkAsSelf: (identityId: string) => void; onCreateIdentity: () => void; + onRenameIdentity: (id: string, name: string) => void; + onIdentityEditStart: (id: string) => void; + onIdentityEditEnd: () => void; } // ─── Component ──────────────────────────────────────────────────────────────── @@ -87,9 +91,13 @@ export function FilterPanel({ categoryCounts, identities, activeFilter, + editingIdentityId, onSelect, onMarkAsSelf, onCreateIdentity, + onRenameIdentity, + onIdentityEditStart, + onIdentityEditEnd, }: FilterPanelProps): JSX.Element { const totalAll = photoStats?.byMediaType .reduce((sum, r) => sum + Number(r.count), 0) @@ -182,8 +190,12 @@ export function FilterPanel({ key={identity.id} identity={identity} active={filterMatches(activeFilter, { kind: 'identity', value: identity.id })} + editingId={editingIdentityId} onSelect={() => onSelect({ kind: 'identity', value: identity.id })} onMarkAsSelf={() => onMarkAsSelf(identity.id)} + onRename={onRenameIdentity} + onEditStart={onIdentityEditStart} + onEditEnd={onIdentityEditEnd} /> ))} diff --git a/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.module.css b/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.module.css index becb07526..a6ccd3ebd 100644 --- a/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.module.css +++ b/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.module.css @@ -1,16 +1,11 @@ .item { display: flex; align-items: center; - gap: 0.6rem; - padding: 0.35rem 1rem; - border: none; - background: transparent; + gap: 0; + width: 100%; color: rgba(255, 255, 255, 0.55); font-size: 0.813rem; - cursor: pointer; transition: background 0.12s ease, color 0.12s ease; - text-align: left; - width: 100%; } .item:hover { @@ -23,6 +18,21 @@ color: #a78bfa; } +.mainBtn { + flex: 1; + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.35rem 0 0.35rem 1rem; + border: none; + background: transparent; + color: inherit; + font-size: inherit; + cursor: pointer; + text-align: left; + min-width: 0; +} + .avatar { flex-shrink: 0; width: 22px; @@ -59,6 +69,18 @@ overflow: hidden; text-overflow: ellipsis; font-size: 0.813rem; + cursor: text; +} + +.nameInput { + width: 100%; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(167, 139, 250, 0.5); + border-radius: 3px; + color: white; + font-size: 0.813rem; + padding: 1px 4px; + outline: none; } .badges { @@ -84,12 +106,43 @@ color: rgba(255, 255, 255, 0.25); flex-shrink: 0; font-variant-numeric: tabular-nums; + margin-right: 0.25rem; } .item.active .count { color: rgba(167, 139, 250, 0.5); } +.actions { + display: flex; + align-items: center; + gap: 2px; + padding-right: 0.5rem; + opacity: 0; + transition: opacity 0.12s; +} + +.item:hover .actions { + opacity: 1; +} + +.renameBtn { + font-size: 0.75rem; + padding: 2px 5px; + border-radius: 3px; + border: 1px solid transparent; + background: transparent; + color: rgba(255, 255, 255, 0.35); + cursor: pointer; + transition: border-color 0.12s, color 0.12s; + line-height: 1; +} + +.renameBtn:hover { + border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.7); +} + .markSelfBtn { flex-shrink: 0; font-size: 0.65rem; diff --git a/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.tsx b/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.tsx index 14e6cd68e..42aa91c51 100644 --- a/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.tsx +++ b/features/video-studio/packages/media-gallery/frontend-dev/src/components/IdentityFilterItem.tsx @@ -1,4 +1,4 @@ -import type { JSX } from 'react'; +import { type JSX, useEffect, useRef, useState } from 'react'; import { UserIcon } from '@lilith/ui-icons'; import type { Identity } from '@/api/types'; import styles from './IdentityFilterItem.module.css'; @@ -6,57 +6,119 @@ import styles from './IdentityFilterItem.module.css'; export interface IdentityFilterItemProps { identity: Identity; active: boolean; + editingId: string | null; onSelect: () => void; onMarkAsSelf: () => void; + onRename: (id: string, name: string) => void; + onEditStart: (id: string) => void; + onEditEnd: () => void; } export function IdentityFilterItem({ identity, active, + editingId, onSelect, onMarkAsSelf, + onRename, + onEditStart, + onEditEnd, }: IdentityFilterItemProps): JSX.Element { + const isEditing = editingId === identity.id; + const [draft, setDraft] = useState(identity.name ?? ''); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing) { + setDraft(identity.name ?? ''); + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [isEditing, identity.name]); + + function commit(): void { + const trimmed = draft.trim(); + if (trimmed && trimmed !== (identity.name ?? '')) { + onRename(identity.id, trimmed); + } + onEditEnd(); + } + + function handleKeyDown(e: React.KeyboardEvent): void { + if (e.key === 'Enter') { e.preventDefault(); commit(); } + if (e.key === 'Escape') { setDraft(identity.name ?? ''); onEditEnd(); } + } + return ( - - {!identity.isSelf && ( +
- )} - + {!identity.isSelf && ( + + )} +
+ ); } diff --git a/features/video-studio/packages/media-gallery/frontend-dev/src/pages/AlbumsPage.tsx b/features/video-studio/packages/media-gallery/frontend-dev/src/pages/AlbumsPage.tsx index 777680020..420ffea3f 100644 --- a/features/video-studio/packages/media-gallery/frontend-dev/src/pages/AlbumsPage.tsx +++ b/features/video-studio/packages/media-gallery/frontend-dev/src/pages/AlbumsPage.tsx @@ -1,3 +1,4 @@ +import { type JSX } from 'react'; import { Link } from '@lilith/ui-router'; import { FolderOpenIcon, ImageIcon } from '@lilith/ui-icons'; import { useAlbums } from '@/api/hooks'; diff --git a/features/video-studio/packages/media-gallery/frontend-dev/src/pages/GalleryPage.tsx b/features/video-studio/packages/media-gallery/frontend-dev/src/pages/GalleryPage.tsx index f3cb73d29..9327d0fd2 100644 --- a/features/video-studio/packages/media-gallery/frontend-dev/src/pages/GalleryPage.tsx +++ b/features/video-studio/packages/media-gallery/frontend-dev/src/pages/GalleryPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState, type JSX } from 'react'; -import { usePhotos, useCategoryCounts, usePhotoStats, useIdentities, useMarkAsSelf, useCreateIdentity } from '@/api/hooks'; +import { usePhotos, useCategoryCounts, usePhotoStats, useIdentities, useMarkAsSelf, useCreateIdentity, useRenameIdentity } from '@/api/hooks'; import type { PhotoFilters } from '@/api/types'; import { ViewMode } from '@/api/types'; import { FilterPanel, INITIAL_FILTER, filterMatches } from '@/components/FilterPanel'; @@ -36,9 +36,11 @@ export function GalleryPage(): JSX.Element { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = usePhotos(filters); const { data: photoStats } = usePhotoStats(); const { data: categoryCounts } = useCategoryCounts(); + const [editingIdentityId, setEditingIdentityId] = useState(null); const { data: identities } = useIdentities(); const markAsSelfMutation = useMarkAsSelf(); const createIdentityMutation = useCreateIdentity(); + const renameIdentityMutation = useRenameIdentity(); const photos = useMemo(() => data?.pages.flatMap((p) => p.photos) ?? [], [data]); const totalCount = data?.pages[0]?.total ?? 0; @@ -54,9 +56,15 @@ export function GalleryPage(): JSX.Element { }, [markAsSelfMutation]); const handleCreateIdentity = useCallback((): void => { - createIdentityMutation.mutate(); + createIdentityMutation.mutate(undefined, { + onSuccess: (identity) => setEditingIdentityId(identity.id), + }); }, [createIdentityMutation]); + const handleRenameIdentity = useCallback((id: string, name: string): void => { + renameIdentityMutation.mutate({ id, name }); + }, [renameIdentityMutation]); + return (