diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 0a69b240..1425915c 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -73,8 +73,8 @@ import { VulnerabilityScanSheet } from './VulnerabilityScanSheet'; import { StackSidebar } from '@/components/sidebar/StackSidebar'; import { usePinnedStacks } from '@/hooks/usePinnedStacks'; import { useSidebarGroupCollapse } from '@/hooks/useSidebarGroupCollapse'; -import type { StackRowStatus } from '@/components/sidebar/StackRow'; -import type { StackMenuCtx } from '@/components/sidebar/sidebar-types'; +import type { StackRowStatus } from '@/components/sidebar/stack-status-utils'; +import type { FilterChip, StackMenuCtx } from '@/components/sidebar/sidebar-types'; import { StackFileExplorer } from '@/components/files/StackFileExplorer'; interface ContainerInfo { @@ -1931,9 +1931,9 @@ export default function EditorLayout() { // Stack name is now the same as selectedFile (no extension to strip) const stackName = selectedFile || ''; - // Filter files based on search query - const filteredFiles = files.filter(file => - file.toLowerCase().includes(searchQuery.toLowerCase()) + const filteredFiles = useMemo( + () => files.filter(file => file.toLowerCase().includes(searchQuery.toLowerCase())), + [files, searchQuery], ); // Get display name for stack (now just returns the name as-is since no extension) @@ -1947,6 +1947,23 @@ export default function EditorLayout() { if (evictedOldest) toast.info('Pinned. Unpinned oldest (max 10).'); }, [evictedOldest]); + const [filterChip, setFilterChip] = useState('all'); + + const filterCounts = useMemo(() => ({ + all: filteredFiles.length, + up: filteredFiles.filter(f => stackStatuses[f] === 'running').length, + down: filteredFiles.filter(f => stackStatuses[f] === 'exited').length, + updates: filteredFiles.filter(f => !!stackUpdates[f]).length, + }), [filteredFiles, stackStatuses, stackUpdates]); + + const chipFilteredFiles = useMemo(() => { + if (filterChip === 'all') return filteredFiles; + if (filterChip === 'up') return filteredFiles.filter(f => stackStatuses[f] === 'running'); + if (filterChip === 'down') return filteredFiles.filter(f => stackStatuses[f] === 'exited'); + if (filterChip === 'updates') return filteredFiles.filter(f => !!stackUpdates[f]); + return filteredFiles; + }, [filteredFiles, filterChip, stackStatuses, stackUpdates]); + const { isCollapsed, toggle: toggleCollapse } = useSidebarGroupCollapse(activeNode?.id); const remoteResults = useMemo(() => { @@ -2286,8 +2303,11 @@ export default function EditorLayout() { canCreate={can('stack:create')} searchQuery={searchQuery} onSearchChange={setSearchQuery} + filterChip={filterChip} + filterCounts={filterCounts} + onFilterChipChange={setFilterChip} list={{ - files: filteredFiles ?? [], + files: chipFilteredFiles, isLoading, isPaid, selectedFile, @@ -2296,7 +2316,6 @@ export default function EditorLayout() { stackStatuses: stackStatuses as Record, stackUpdates, gitSourcePendingMap, - labels, pinnedFiles: pinned, isCollapsed, toggleCollapse, diff --git a/frontend/src/components/sidebar/SidebarBrand.tsx b/frontend/src/components/sidebar/SidebarBrand.tsx index 37ff15f1..d0adce90 100644 --- a/frontend/src/components/sidebar/SidebarBrand.tsx +++ b/frontend/src/components/sidebar/SidebarBrand.tsx @@ -4,17 +4,17 @@ interface SidebarBrandProps { export function SidebarBrand({ isDarkMode }: SidebarBrandProps) { return ( -
+
Sencho Logo
SENCHO ยท v{__APP_VERSION__} - Sencho + Sencho
); diff --git a/frontend/src/components/sidebar/SidebarFilterChips.tsx b/frontend/src/components/sidebar/SidebarFilterChips.tsx new file mode 100644 index 00000000..09db8644 --- /dev/null +++ b/frontend/src/components/sidebar/SidebarFilterChips.tsx @@ -0,0 +1,62 @@ +import { cn } from '@/lib/utils'; +import type { FilterChip } from './sidebar-types'; + +export interface FilterCounts { + all: number; + up: number; + down: number; + updates: number; +} + +interface SidebarFilterChipsProps { + active: FilterChip; + counts: FilterCounts; + onChange: (chip: FilterChip) => void; +} + +const chips: { id: FilterChip; label: string }[] = [ + { id: 'all', label: 'All' }, + { id: 'up', label: 'Up' }, + { id: 'down', label: 'Down' }, + { id: 'updates', label: 'Updates' }, +]; + +export function SidebarFilterChips({ active, counts, onChange }: SidebarFilterChipsProps) { + return ( +
+ {chips.map(({ id, label }) => { + const count = counts[id]; + const isActive = active === id; + const isUpdates = id === 'updates'; + const hasUpdates = isUpdates && count > 0; + + return ( + + ); + })} +
+ ); +} diff --git a/frontend/src/components/sidebar/StackGroup.tsx b/frontend/src/components/sidebar/StackGroup.tsx index e338c7cb..9bc33b2c 100644 --- a/frontend/src/components/sidebar/StackGroup.tsx +++ b/frontend/src/components/sidebar/StackGroup.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { sidebarPinnedGroupRail } from './sidebar-styles'; interface StackGroupProps { id: string; @@ -13,18 +14,22 @@ interface StackGroupProps { } export function StackGroup({ id, label, count, collapsed, onToggle, variant = 'default', children }: StackGroupProps) { - const labelColor = variant === 'pinned' ? 'text-brand/90' : 'text-stat-subtitle'; + const isPinned = variant === 'pinned'; + const labelColor = isPinned ? 'text-brand/90' : 'text-stat-subtitle'; return (