diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index d7611099e..fbcac3ff7 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -38,17 +38,13 @@ import { type GestureType } from "react-native-gesture-handler"; import * as Clipboard from "expo-clipboard"; import { DiffStat } from "@/components/diff-stat"; import { - Archive, CircleAlert, - CircleCheck, ChevronDown, ChevronRight, - Copy, ExternalLink, GitPullRequest, Settings, MoreVertical, - Pencil, Plus, Trash2, } from "lucide-react-native"; @@ -93,6 +89,7 @@ import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color"; import type { SidebarStateBucket } from "@/utils/sidebar-agent-state"; import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list"; import type { StatusGroup } from "@/hooks/sidebar-status-view-model"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; import { SidebarWorkspaceRowFrame, SidebarWorkspaceRowContent, @@ -142,15 +139,11 @@ const ThemedExternalLink = withUnistyles(ExternalLink); const ThemedGitPullRequest = withUnistyles(GitPullRequest); const ThemedActivityIndicator = withUnistyles(ActivityIndicator); const ThemedCircleAlert = withUnistyles(CircleAlert); -const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedSyncedLoader = withUnistyles(SyncedLoader); const ThemedPlus = withUnistyles(Plus); const ThemedMoreVertical = withUnistyles(MoreVertical); const ThemedTrash2 = withUnistyles(Trash2); const ThemedSettings = withUnistyles(Settings); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground, @@ -358,12 +351,6 @@ function projectKebabStyle({ return [styles.projectKebabButton, hovered && styles.projectKebabButtonHovered]; } -function workspaceKebabStyle({ - hovered = false, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getProjectWorkspaceRowStyle({ isDragging, selected, @@ -540,12 +527,6 @@ function ProjectRowTrailingActions({ const trash2LeadingIcon = ; const settingsLeadingIcon = ; -const copyLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; -const renameLeadingIcon = ; const openInNewWindowLeadingIcon = ( ); @@ -693,7 +674,7 @@ function WorkspaceRowRightGroup({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const { t } = useTranslation(); - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {renderKebabTriggerIcon} - - - {onCopyPath ? ( - - {t("sidebar.workspace.actions.copyPath")} - - ) : null} - {onCopyBranchName ? ( - - {t("sidebar.workspace.actions.copyBranchName")} - - ) : null} - {onRename ? ( - - {t("sidebar.workspace.actions.rename")} - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? t("sidebar.workspace.actions.archive")} - - - - ); -} - function ProjectIcon({ iconDataUri, placeholderInitial, @@ -2872,14 +2762,6 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, flexShrink: 0, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, statusDotNeedsInput: { backgroundColor: theme.colors.palette.amber[500], borderColor: theme.colors.surface0, diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index 693dc0ebe..4db918332 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -21,18 +21,8 @@ import { CircleCheck, CircleDot, CircleX, - MoreVertical, - Copy, - Archive, - Pencil, } from "lucide-react-native"; import { DiffStat } from "@/components/diff-stat"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; import { useToast } from "@/contexts/toast-context"; import { useMutation } from "@tanstack/react-query"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; @@ -43,7 +33,6 @@ import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning"; import * as Clipboard from "expo-clipboard"; -import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; @@ -56,9 +45,9 @@ import { SidebarWorkspaceTrailingActionSlot, } from "@/components/sidebar/sidebar-workspace-row-content"; import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; // Themed icon wrappers -const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); @@ -73,18 +62,6 @@ const ThemedCircleAlert = withUnistyles(CircleAlert); const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedCircleDot = withUnistyles(CircleDot); const ThemedCircleX = withUnistyles(CircleX); -const ThemedMoreVertical = withUnistyles(MoreVertical); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); - -const copyLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; -const renameLeadingIcon = ; - interface StatusWorkspaceListProps { groups: StatusGroup[]; projectNamesByKey: Map; @@ -641,7 +618,7 @@ function StatusWorkspaceActionSlot({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {({ hovered }: { hovered?: boolean }) => ( - - )} - - - {onCopyPath ? ( - - Copy path - - ) : null} - {onCopyBranchName ? ( - - Copy branch name - - ) : null} - {onRename ? ( - - Rename workspace - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? "Archive"} - - - - ); -} - -function kebabStyle({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getStatusWorkspaceRowStyle({ selected, isHovered, @@ -860,12 +738,4 @@ const styles = StyleSheet.create((theme) => ({ sidebarRowSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, })); diff --git a/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx new file mode 100644 index 000000000..5e16ea688 --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-workspace-menu.tsx @@ -0,0 +1,151 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { type PressableStateCallbackType } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { Archive, CircleCheck, Copy, MoreVertical, Pencil } from "lucide-react-native"; +import { isNative, isWeb } from "@/constants/platform"; +import type { Theme } from "@/styles/theme"; +import type { ShortcutKey } from "@/utils/format-shortcut"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Shortcut } from "@/components/ui/shortcut"; + +const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); +const foregroundMutedColorMapping = (theme: Theme) => ({ + color: theme.colors.foregroundMuted, +}); + +const ThemedMoreVertical = withUnistyles(MoreVertical); +const ThemedCopy = withUnistyles(Copy); +const ThemedArchive = withUnistyles(Archive); +const ThemedPencil = withUnistyles(Pencil); +const ThemedCircleCheck = withUnistyles(CircleCheck); + +const copyLeadingIcon = ; +const renameLeadingIcon = ; +const markAsReadLeadingIcon = ( + +); +const archiveLeadingIcon = ; + +function renderTriggerIcon({ hovered }: { hovered?: boolean }) { + return ( + + ); +} + +interface SidebarWorkspaceMenuProps { + workspaceKey: string; + onCopyPath?: () => void; + onCopyBranchName?: () => void; + onRename?: () => void; + onMarkAsRead?: () => void; + onArchive: () => void; + archiveLabel?: string; + archiveStatus?: "idle" | "pending" | "success"; + archivePendingLabel?: string; + archiveShortcutKeys?: ShortcutKey[][] | null; +} + +export function SidebarWorkspaceMenu({ + workspaceKey, + onCopyPath, + onCopyBranchName, + onRename, + onMarkAsRead, + onArchive, + archiveLabel, + archiveStatus, + archivePendingLabel, + archiveShortcutKeys, +}: SidebarWorkspaceMenuProps) { + const { t } = useTranslation(); + const archiveTrailing = useMemo( + () => (archiveShortcutKeys && !isNative ? : null), + [archiveShortcutKeys], + ); + + return ( + + + {renderTriggerIcon} + + + {onCopyPath ? ( + + {t("sidebar.workspace.actions.copyPath")} + + ) : null} + {onCopyBranchName ? ( + + {t("sidebar.workspace.actions.copyBranchName")} + + ) : null} + {onRename ? ( + + {t("sidebar.workspace.actions.rename")} + + ) : null} + {onMarkAsRead ? ( + + Mark as read + + ) : null} + + {archiveLabel ?? t("sidebar.workspace.actions.archive")} + + + + ); +} + +function triggerStyle({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) { + return [styles.trigger, hovered && styles.triggerHovered]; +} + +const styles = StyleSheet.create((theme) => ({ + trigger: { + padding: 2, + borderRadius: 4, + marginLeft: 2, + }, + triggerHovered: { + backgroundColor: theme.colors.surface2, + }, +})); diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index 2967d857f..cf8bab866 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -1,22 +1,13 @@ import { memo, useCallback, useMemo, useState, type Ref } from "react"; import { useTranslation } from "react-i18next"; -import { View, Text, Pressable, type PressableStateCallbackType } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { Archive, CircleCheck, Copy, MoreVertical, Pencil } from "lucide-react-native"; +import { View, Text, Pressable } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; import { useMutation } from "@tanstack/react-query"; import * as Clipboard from "expo-clipboard"; -import type { Theme } from "@/styles/theme"; import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; import type { DraggableListDragHandleProps } from "@/components/draggable-list.types"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { DiffStat } from "@/components/diff-stat"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; -import { Shortcut } from "@/components/ui/shortcut"; import { AdaptiveRenameModal } from "@/components/rename-modal"; import { useToast } from "@/contexts/toast-context"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; @@ -28,8 +19,9 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention"; import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { requireWorkspaceDirectory, resolveWorkspaceDirectory } from "@/utils/workspace-directory"; -import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform"; +import { isNative as platformIsNative } from "@/constants/platform"; import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction"; +import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu"; import { SidebarWorkspaceRowFrame, SidebarWorkspaceRowContent, @@ -38,31 +30,6 @@ import { SidebarWorkspaceTrailingActionSlot, } from "@/components/sidebar/sidebar-workspace-row-content"; -const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); -const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); - -const ThemedMoreVertical = withUnistyles(MoreVertical); -const ThemedCopy = withUnistyles(Copy); -const ThemedArchive = withUnistyles(Archive); -const ThemedPencil = withUnistyles(Pencil); -const ThemedCircleCheck = withUnistyles(CircleCheck); - -const copyLeadingIcon = ; -const renameLeadingIcon = ; -const markAsReadLeadingIcon = ( - -); -const archiveLeadingIcon = ; - -function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) { - return ( - - ); -} - function noop() {} interface SidebarWorkspaceRowProps { @@ -458,7 +425,7 @@ function WorkspaceRowTrailingActions({ {onArchive ? ( - void; - onCopyBranchName?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - onArchive: () => void; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - archiveShortcutKeys?: ShortcutKey[][] | null; -}) { - const { t } = useTranslation(); - const archiveTrailing = useMemo( - () => (archiveShortcutKeys ? : null), - [archiveShortcutKeys], - ); - return ( - - - {renderKebabTriggerIcon} - - - {onCopyPath ? ( - - {t("sidebar.workspace.actions.copyPath")} - - ) : null} - {onCopyBranchName ? ( - - {t("sidebar.workspace.actions.copyBranchName")} - - ) : null} - {onRename ? ( - - {t("sidebar.workspace.actions.rename")} - - ) : null} - {onMarkAsRead ? ( - - Mark as read - - ) : null} - - {archiveLabel ?? t("sidebar.workspace.actions.archive")} - - - - ); -} - -function workspaceKebabStyle({ - hovered = false, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.kebabButton, hovered && styles.kebabButtonHovered]; -} - function getWorkspaceRowStyle({ isDragging, selected, @@ -630,12 +500,4 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, flexShrink: 0, }, - kebabButton: { - padding: 2, - borderRadius: 4, - marginLeft: 2, - }, - kebabButtonHovered: { - backgroundColor: theme.colors.surface2, - }, }));