From 1e5b809b70517aff3a5f01c94530aecb3721dbd4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 22 Mar 2026 19:45:26 +0700 Subject: [PATCH] refactor(sidebar): use status dot overlay on icons instead of replacing them Sidebar workspace and project rows now keep their icon (Monitor/FolderGit2 for workspaces, project icon/initial for projects) and overlay a small colored status dot on the bottom-right, matching the tab icon pattern. Loading and syncing states still replace the icon entirely. --- .../src/components/sidebar-workspace-list.tsx | 157 +++++++++++------- .../src/utils/sidebar-project-row-model.ts | 8 +- 2 files changed, 104 insertions(+), 61 deletions(-) diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 12992e434..6bcd150ae 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -27,10 +27,8 @@ import { type GestureType } from "react-native-gesture-handler"; import * as Clipboard from "expo-clipboard"; import { Archive, - Check, ChevronDown, ChevronRight, - CircleHelp, Copy, FolderGit2, GitPullRequest, @@ -49,7 +47,6 @@ import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import { type SidebarProjectEntry, type SidebarWorkspaceEntry, - type SidebarStateBucket, } from "@/hooks/use-sidebar-workspaces-list"; import { useSidebarOrderStore } from "@/stores/sidebar-order-store"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; @@ -74,6 +71,7 @@ import { decideLongPressMove } from "@/utils/sidebar-gesture-arbitration"; import { confirmDialog } from "@/utils/confirm-dialog"; import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; +import { getStatusDotColor } from "@/utils/status-dot-color"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; @@ -214,22 +212,6 @@ function WorkspacePrBadge({ hint }: { hint: PrHint }) { ); } -function resolveStatusDotColor(input: { - theme: ReturnType["theme"]; - bucket: SidebarStateBucket; -}) { - const { theme, bucket } = input; - return bucket === "needs_input" - ? theme.colors.palette.amber[500] - : bucket === "failed" - ? theme.colors.palette.red[500] - : bucket === "running" - ? theme.colors.palette.blue[500] - : bucket === "attention" - ? theme.colors.palette.green[500] - : theme.colors.border; -} - function WorkspaceStatusIndicator({ bucket, workspaceKind, @@ -240,38 +222,48 @@ function WorkspaceStatusIndicator({ loading?: boolean; }) { const { theme } = useUnistyles(); - const color = resolveStatusDotColor({ theme, bucket }); const shouldShowSyncedLoader = shouldRenderSyncedStatusLoader({ bucket }); - const isIdle = !loading && !shouldShowSyncedLoader && bucket === "done"; - if (isIdle) { - const KindIcon = - workspaceKind === "local_checkout" - ? Monitor - : workspaceKind === "worktree" - ? FolderGit2 - : null; - if (!KindIcon) return null; + if (loading) { return ( - + ); } + if (shouldShowSyncedLoader) { + return ( + + + + ); + } + + const KindIcon = + workspaceKind === "local_checkout" + ? Monitor + : workspaceKind === "worktree" + ? FolderGit2 + : null; + if (!KindIcon) return null; + + const dotColor = getStatusDotColor({ theme, bucket, showDoneAsInactive: false }); + return ( - {loading ? ( - - ) : shouldShowSyncedLoader ? ( - - ) : bucket === "needs_input" ? ( - - ) : bucket === "attention" ? ( - - ) : ( - - )} + + {dotColor ? ( + + ) : null} ); } @@ -291,29 +283,72 @@ function ProjectLeadingVisual({ showChevron?: boolean; isArchiving?: boolean; }) { + const { theme } = useUnistyles(); const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(displayName); const placeholderInitial = placeholderLabel.charAt(0).toUpperCase(); const activeWorkspace = workspace; const shouldShowWorkspaceStatus = activeWorkspace !== null && (isArchiving || activeWorkspace.statusBucket !== "done"); + const shouldShowSyncedLoader = activeWorkspace + ? shouldRenderSyncedStatusLoader({ bucket: activeWorkspace.statusBucket }) + : false; + + if (showChevron && chevron !== null) { + return ( + + + + ); + } + + const projectIcon = iconDataUri ? ( + + ) : ( + + {placeholderInitial} + + ); + + if (!shouldShowWorkspaceStatus || !activeWorkspace) { + return {projectIcon}; + } + + if (isArchiving) { + return ( + + + + ); + } + + if (shouldShowSyncedLoader) { + return ( + + + + ); + } + + const dotColor = getStatusDotColor({ + theme, + bucket: activeWorkspace.statusBucket, + showDoneAsInactive: false, + }); return ( - {showChevron && chevron !== null ? ( - - ) : shouldShowWorkspaceStatus && activeWorkspace ? ( - - ) : iconDataUri ? ( - - ) : ( - - {placeholderInitial} - - )} + ) : null} ); } @@ -1993,6 +2028,7 @@ const styles = StyleSheet.create((theme) => ({ borderRadius: theme.borderRadius.sm, }, projectLeadingVisualSlot: { + position: "relative", width: theme.iconSize.md, height: theme.iconSize.md, flexShrink: 0, @@ -2124,17 +2160,22 @@ const styles = StyleSheet.create((theme) => ({ position: "relative", }, workspaceStatusDot: { + position: "relative", width: WORKSPACE_STATUS_DOT_WIDTH, height: 16, borderRadius: theme.borderRadius.full, flexShrink: 0, - alignItems: "flex-start", + alignItems: "center", justifyContent: "center", }, - workspaceStatusDotFill: { + statusDotOverlay: { + position: "absolute", + right: 0, + bottom: 0, width: 7, height: 7, borderRadius: theme.borderRadius.full, + borderWidth: 1, }, workspaceArchivingOverlay: { ...StyleSheet.absoluteFillObject, @@ -2179,7 +2220,7 @@ const styles = StyleSheet.create((theme) => ({ }, workspacePrBadgeText: { fontSize: theme.fontSize.xs, - fontWeight: theme.fontWeight.medium, + fontWeight: theme.fontWeight.normal, lineHeight: 14, }, workspacePrBadgeTextHovered: { diff --git a/packages/app/src/utils/sidebar-project-row-model.ts b/packages/app/src/utils/sidebar-project-row-model.ts index 10ba5deec..63924caf4 100644 --- a/packages/app/src/utils/sidebar-project-row-model.ts +++ b/packages/app/src/utils/sidebar-project-row-model.ts @@ -13,7 +13,7 @@ export interface SidebarProjectWorkspaceLinkRowModel { export interface SidebarProjectSectionRowModel { kind: "project_section"; - chevron: "expand" | "collapse"; + chevron: "expand" | "collapse" | null; trailingAction: "new_worktree" | "none"; } @@ -22,7 +22,7 @@ export type SidebarProjectRowModel = | SidebarProjectSectionRowModel; export function isSidebarProjectFlattened(project: SidebarProjectEntry): boolean { - return project.workspaces.length === 1; + return project.workspaces.length === 1 && project.projectKind !== "git"; } export function buildSidebarProjectRowModel(input: { @@ -50,9 +50,11 @@ export function buildSidebarProjectRowModel(input: { }; } + const collapsible = input.project.workspaces.length > 1; + return { kind: "project_section", - chevron: input.collapsed ? "expand" : "collapse", + chevron: collapsible ? (input.collapsed ? "expand" : "collapse") : null, trailingAction: input.project.projectKind === "git" ? "new_worktree" : "none", }; }