From 4c0efcb9a8ffa634b376030b883af89abfd440b4 Mon Sep 17 00:00:00 2001 From: Anso Date: Thu, 30 Apr 2026 19:37:49 -0400 Subject: [PATCH] =?UTF-8?q?feat(sidebar):=20=C2=A714=20sidebar=20orchestra?= =?UTF-8?q?tion,=20filter=20chips,=20pinned=20rail,=20trailing=20column=20?= =?UTF-8?q?(#850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(sidebar): §14 sidebar orchestration (filter chips, pinned rail, trailing column) - Add All / Up / Down / Updates filter chips with live counts; active chip filters the list; chip-filtered files computed in EditorLayout with useMemo - Surface the PINNED group with a 3px cyan left rail and glow via the sidebarPinnedGroupRail token; reuses the brand token already on the active row - Compact brand row from three stacked elements to a single 44px horizontal bar - Restructure StackRow trailing column as fixed slots: label dots (max 3 + +N overflow), update-dot | git-pending icon (priority order), kebab; add reserved invisible checkbox slot for PR2 bulk mode - Export statusText / statusColor from StackRow and reuse them in StackList remote-results section to remove the duplicate inline logic - Lift filterChip state and chip-filtered files to EditorLayout; remove filterChip from StackListProps to eliminate the dual-path redundancy - Remove unused labels param from buildGroups and the void labels workaround - Wrap filteredFiles in useMemo so filterCounts memo is not defeated on every render * fix(sidebar): move statusText and statusColor to stack-status-utils react-refresh/only-export-components requires component files to export only components. Move the two utility functions and StackRowStatus type to a dedicated stack-status-utils.ts so StackRow.tsx is a pure component module. Update StackList.tsx and EditorLayout.tsx to import from the new source directly. --- frontend/src/components/EditorLayout.tsx | 33 ++++++-- .../src/components/sidebar/SidebarBrand.tsx | 6 +- .../components/sidebar/SidebarFilterChips.tsx | 62 ++++++++++++++ .../src/components/sidebar/StackGroup.tsx | 11 ++- frontend/src/components/sidebar/StackList.tsx | 19 ++--- frontend/src/components/sidebar/StackRow.tsx | 80 ++++++++++--------- .../src/components/sidebar/StackSidebar.tsx | 13 ++- .../src/components/sidebar/sidebar-styles.ts | 9 +++ .../src/components/sidebar/sidebar-types.ts | 2 + .../components/sidebar/stack-status-utils.ts | 14 ++++ 10 files changed, 187 insertions(+), 62 deletions(-) create mode 100644 frontend/src/components/sidebar/SidebarFilterChips.tsx create mode 100644 frontend/src/components/sidebar/stack-status-utils.ts 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 (