From df7d41fe962970bb437d14ad24d08acfa7f2bc69 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 18 Mar 2026 22:51:11 -0700 Subject: [PATCH] =?UTF-8?q?ui(image-assistant):=20=F0=9F=92=84=20Update=20?= =?UTF-8?q?FilterItem=20and=20FilterPanel=20components=20with=20modern=20C?= =?UTF-8?q?SS=20styling=20and=20TypeScript=20interactivity=20for=20refined?= =?UTF-8?q?=20visuals=20and=20enhanced=20filter=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/components/FilterItem.module.css | 53 ++++++ .../src/components/FilterItem.tsx | 26 +++ .../src/components/FilterPanel.module.css | 25 +++ .../src/components/FilterPanel.tsx | 152 ++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 features/image-assistant/frontend-dev/src/components/FilterItem.module.css create mode 100644 features/image-assistant/frontend-dev/src/components/FilterItem.tsx create mode 100644 features/image-assistant/frontend-dev/src/components/FilterPanel.module.css create mode 100644 features/image-assistant/frontend-dev/src/components/FilterPanel.tsx diff --git a/features/image-assistant/frontend-dev/src/components/FilterItem.module.css b/features/image-assistant/frontend-dev/src/components/FilterItem.module.css new file mode 100644 index 000000000..e10ad5e5f --- /dev/null +++ b/features/image-assistant/frontend-dev/src/components/FilterItem.module.css @@ -0,0 +1,53 @@ +.item { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.45rem 1rem; + border: none; + background: transparent; + 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 { + background: rgba(255, 255, 255, 0.04); + color: rgba(255, 255, 255, 0.85); +} + +.item.active { + background: rgba(167, 139, 250, 0.1); + color: #a78bfa; +} + +.icon { + flex-shrink: 0; + display: flex; + align-items: center; + opacity: 0.7; +} + +.item.active .icon { + opacity: 1; +} + +.label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.count { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.25); + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + +.item.active .count { + color: rgba(167, 139, 250, 0.5); +} diff --git a/features/image-assistant/frontend-dev/src/components/FilterItem.tsx b/features/image-assistant/frontend-dev/src/components/FilterItem.tsx new file mode 100644 index 000000000..40dbf8150 --- /dev/null +++ b/features/image-assistant/frontend-dev/src/components/FilterItem.tsx @@ -0,0 +1,26 @@ +import type { JSX, ReactNode } from 'react'; +import styles from './FilterItem.module.css'; + +export interface FilterItemProps { + icon: ReactNode; + label: string; + count?: string | number; + active: boolean; + onClick: () => void; +} + +export function FilterItem({ icon, label, count, active, onClick }: FilterItemProps): JSX.Element { + return ( + + ); +} diff --git a/features/image-assistant/frontend-dev/src/components/FilterPanel.module.css b/features/image-assistant/frontend-dev/src/components/FilterPanel.module.css new file mode 100644 index 000000000..d01a2579d --- /dev/null +++ b/features/image-assistant/frontend-dev/src/components/FilterPanel.module.css @@ -0,0 +1,25 @@ +.nav { + flex: 1; + padding: 0.5rem 0 1rem; + display: flex; + flex-direction: column; +} + +.section { + display: flex; + flex-direction: column; + padding: 0.75rem 0 0.25rem; +} + +.section + .section { + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.sectionTitle { + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.25); + padding: 0 1rem 0.5rem; +} diff --git a/features/image-assistant/frontend-dev/src/components/FilterPanel.tsx b/features/image-assistant/frontend-dev/src/components/FilterPanel.tsx new file mode 100644 index 000000000..70817edc0 --- /dev/null +++ b/features/image-assistant/frontend-dev/src/components/FilterPanel.tsx @@ -0,0 +1,152 @@ +import type { JSX } from 'react'; +import { + GridIcon, + ImageIcon, + VideoIcon, + LayersIcon, + HeartIcon, + MonitorIcon, + TagIcon, +} from '@lilith/ui-icons'; +import type { CategoryCount, PhotoStats } from '@/api/types'; +import { MediaType, PhotoCategory } from '@/api/types'; +import { FilterItem } from './FilterItem'; +import styles from './FilterPanel.module.css'; + +// ─── ActiveFilter — owned here, exported for consumers ─────────────────────── + +export type ActiveFilter = + | { kind: 'all' } + | { kind: 'mediaType'; value: MediaType } + | { kind: 'favorites' } + | { kind: 'screenshots' } + | { kind: 'category'; value: PhotoCategory }; + +export const INITIAL_FILTER: ActiveFilter = { kind: 'all' }; + +export function filterMatches(a: ActiveFilter, b: ActiveFilter): boolean { + if (a.kind !== b.kind) return false; + if (a.kind === 'mediaType' && b.kind === 'mediaType') return a.value === b.value; + if (a.kind === 'category' && b.kind === 'category') return a.value === b.value; + return true; +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const MEDIA_TYPE_LABELS: Record = { + image: 'Photos', + video: 'Videos', + live_photo: 'Live Photos', +}; + +const CATEGORY_LABELS: Record = { + screenshot_receipt: 'Receipts', + screenshot_conversation: 'Conversations', + screenshot_shopping: 'Shopping', + screenshot_meme: 'Memes', + screenshot_event: 'Events', + screenshot_reservation: 'Reservations', + screenshot_hottie: 'Hotties', + screenshot_other: 'Other Screenshots', + self_clothed: 'Selfies', + self_nude: 'Nude', + self_explicit: 'Explicit', + self_with_others: 'With Others', + friends: 'Friends', + unclassified: 'Unclassified', +}; + +function mediaTypeIcon(mediaType: string): JSX.Element { + switch (mediaType) { + case 'video': return ; + case 'live_photo': return ; + default: return ; + } +} + +// ─── Props ──────────────────────────────────────────────────────────────────── + +export interface FilterPanelProps { + photoStats: PhotoStats | undefined; + categoryCounts: CategoryCount[] | undefined; + activeFilter: ActiveFilter; + onSelect: (filter: ActiveFilter) => void; +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export function FilterPanel({ + photoStats, + categoryCounts, + activeFilter, + onSelect, +}: FilterPanelProps): JSX.Element { + const totalAll = photoStats?.byMediaType + .reduce((sum, r) => sum + Number(r.count), 0) + .toString(); + + const visibleCategories = (categoryCounts ?? []).filter((cc) => Number(cc.count) > 0); + + return ( + + ); +}