Hide keyboard shortcut badges in workspace menu on native (#1964)
* Hide keyboard shortcut badges in workspace menu on native The archive item's shortcut badge (e.g. Ctrl+Shift+⌫) rendered in the workspace kebab/context menu on mobile, where there's no physical keyboard to invoke it. Gate the trailing badge behind !isNative in both kebab menu variants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(app): hide native shortcuts in every workspace menu Workspace menus were duplicated across project and status views, so the status path missed the native gate. Centralize the menu so every sidebar view shares the same shortcut policy. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a743a22496
commit
cac71ee8f5
@@ -38,17 +38,13 @@ import { type GestureType } from "react-native-gesture-handler";
|
|||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { DiffStat } from "@/components/diff-stat";
|
import { DiffStat } from "@/components/diff-stat";
|
||||||
import {
|
import {
|
||||||
Archive,
|
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
CircleCheck,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Copy,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Settings,
|
Settings,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Pencil,
|
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
@@ -93,6 +89,7 @@ import { isEmphasizedStatusDotBucket } from "@/utils/status-dot-color";
|
|||||||
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
import type { SidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||||
import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list";
|
import { SidebarStatusWorkspaceList } from "@/components/sidebar/sidebar-status-list";
|
||||||
import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
|
import type { StatusGroup } from "@/hooks/sidebar-status-view-model";
|
||||||
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
import {
|
import {
|
||||||
SidebarWorkspaceRowFrame,
|
SidebarWorkspaceRowFrame,
|
||||||
SidebarWorkspaceRowContent,
|
SidebarWorkspaceRowContent,
|
||||||
@@ -142,15 +139,11 @@ const ThemedExternalLink = withUnistyles(ExternalLink);
|
|||||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
|
||||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||||
const ThemedPlus = withUnistyles(Plus);
|
const ThemedPlus = withUnistyles(Plus);
|
||||||
const ThemedMoreVertical = withUnistyles(MoreVertical);
|
const ThemedMoreVertical = withUnistyles(MoreVertical);
|
||||||
const ThemedTrash2 = withUnistyles(Trash2);
|
const ThemedTrash2 = withUnistyles(Trash2);
|
||||||
const ThemedSettings = withUnistyles(Settings);
|
const ThemedSettings = withUnistyles(Settings);
|
||||||
const ThemedCopy = withUnistyles(Copy);
|
|
||||||
const ThemedArchive = withUnistyles(Archive);
|
|
||||||
const ThemedPencil = withUnistyles(Pencil);
|
|
||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({
|
const foregroundColorMapping = (theme: Theme) => ({
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
@@ -358,12 +351,6 @@ function projectKebabStyle({
|
|||||||
return [styles.projectKebabButton, hovered && styles.projectKebabButtonHovered];
|
return [styles.projectKebabButton, hovered && styles.projectKebabButtonHovered];
|
||||||
}
|
}
|
||||||
|
|
||||||
function workspaceKebabStyle({
|
|
||||||
hovered = false,
|
|
||||||
}: PressableStateCallbackType & { hovered?: boolean }) {
|
|
||||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProjectWorkspaceRowStyle({
|
function getProjectWorkspaceRowStyle({
|
||||||
isDragging,
|
isDragging,
|
||||||
selected,
|
selected,
|
||||||
@@ -540,12 +527,6 @@ function ProjectRowTrailingActions({
|
|||||||
|
|
||||||
const trash2LeadingIcon = <ThemedTrash2 size={14} uniProps={foregroundMutedColorMapping} />;
|
const trash2LeadingIcon = <ThemedTrash2 size={14} uniProps={foregroundMutedColorMapping} />;
|
||||||
const settingsLeadingIcon = <ThemedSettings size={14} uniProps={foregroundMutedColorMapping} />;
|
const settingsLeadingIcon = <ThemedSettings size={14} uniProps={foregroundMutedColorMapping} />;
|
||||||
const copyLeadingIcon = <ThemedCopy size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const markAsReadLeadingIcon = (
|
|
||||||
<ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />
|
|
||||||
);
|
|
||||||
const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const openInNewWindowLeadingIcon = (
|
const openInNewWindowLeadingIcon = (
|
||||||
<ThemedExternalLink size={14} uniProps={foregroundMutedColorMapping} />
|
<ThemedExternalLink size={14} uniProps={foregroundMutedColorMapping} />
|
||||||
);
|
);
|
||||||
@@ -693,7 +674,7 @@ function WorkspaceRowRightGroup({
|
|||||||
</SidebarWorkspaceTrailingActionBase>
|
</SidebarWorkspaceTrailingActionBase>
|
||||||
<SidebarWorkspaceTrailingActionOverlay visible={showKebabInSlot}>
|
<SidebarWorkspaceTrailingActionOverlay visible={showKebabInSlot}>
|
||||||
{onArchive ? (
|
{onArchive ? (
|
||||||
<WorkspaceKebabMenu
|
<SidebarWorkspaceMenu
|
||||||
workspaceKey={workspace.workspaceKey}
|
workspaceKey={workspace.workspaceKey}
|
||||||
onCopyPath={onCopyPath}
|
onCopyPath={onCopyPath}
|
||||||
onCopyBranchName={onCopyBranchName}
|
onCopyBranchName={onCopyBranchName}
|
||||||
@@ -713,97 +694,6 @@ function WorkspaceRowRightGroup({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceKebabMenu({
|
|
||||||
workspaceKey,
|
|
||||||
onCopyPath,
|
|
||||||
onCopyBranchName,
|
|
||||||
onRename,
|
|
||||||
onMarkAsRead,
|
|
||||||
onArchive,
|
|
||||||
archiveLabel,
|
|
||||||
archiveStatus,
|
|
||||||
archivePendingLabel,
|
|
||||||
archiveShortcutKeys,
|
|
||||||
}: {
|
|
||||||
workspaceKey: string;
|
|
||||||
onCopyPath?: () => 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 ? <Shortcut chord={archiveShortcutKeys} /> : null),
|
|
||||||
[archiveShortcutKeys],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
hitSlop={8}
|
|
||||||
style={workspaceKebabStyle}
|
|
||||||
accessibilityRole={platformIsWeb ? undefined : "button"}
|
|
||||||
accessibilityLabel={t("sidebar.workspace.actions.menu")}
|
|
||||||
testID={`sidebar-workspace-kebab-${workspaceKey}`}
|
|
||||||
>
|
|
||||||
{renderKebabTriggerIcon}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" width={260}>
|
|
||||||
{onCopyPath ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-path-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyPath}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.copyPath")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onCopyBranchName ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-branch-name-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyBranchName}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.copyBranchName")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onRename ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-rename-${workspaceKey}`}
|
|
||||||
leading={renameLeadingIcon}
|
|
||||||
onSelect={onRename}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.rename")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onMarkAsRead ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-mark-as-read-${workspaceKey}`}
|
|
||||||
leading={markAsReadLeadingIcon}
|
|
||||||
onSelect={onMarkAsRead}
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
|
|
||||||
leading={archiveLeadingIcon}
|
|
||||||
trailing={archiveTrailing}
|
|
||||||
status={archiveStatus}
|
|
||||||
pendingLabel={archivePendingLabel}
|
|
||||||
onSelect={onArchive}
|
|
||||||
>
|
|
||||||
{archiveLabel ?? t("sidebar.workspace.actions.archive")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectIcon({
|
function ProjectIcon({
|
||||||
iconDataUri,
|
iconDataUri,
|
||||||
placeholderInitial,
|
placeholderInitial,
|
||||||
@@ -2872,14 +2762,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
kebabButton: {
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginLeft: 2,
|
|
||||||
},
|
|
||||||
kebabButtonHovered: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
statusDotNeedsInput: {
|
statusDotNeedsInput: {
|
||||||
backgroundColor: theme.colors.palette.amber[500],
|
backgroundColor: theme.colors.palette.amber[500],
|
||||||
borderColor: theme.colors.surface0,
|
borderColor: theme.colors.surface0,
|
||||||
|
|||||||
@@ -21,18 +21,8 @@ import {
|
|||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleDot,
|
CircleDot,
|
||||||
CircleX,
|
CircleX,
|
||||||
MoreVertical,
|
|
||||||
Copy,
|
|
||||||
Archive,
|
|
||||||
Pencil,
|
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { DiffStat } from "@/components/diff-stat";
|
import { DiffStat } from "@/components/diff-stat";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||||
@@ -43,7 +33,6 @@ import { useWorkspaceArchive } from "@/workspace/use-workspace-archive";
|
|||||||
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||||
import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning";
|
import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
@@ -56,9 +45,9 @@ import {
|
|||||||
SidebarWorkspaceTrailingActionSlot,
|
SidebarWorkspaceTrailingActionSlot,
|
||||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
} from "@/components/sidebar/sidebar-workspace-row-content";
|
||||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||||
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
|
|
||||||
// Themed icon wrappers
|
// Themed icon wrappers
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
});
|
});
|
||||||
@@ -73,18 +62,6 @@ const ThemedCircleAlert = withUnistyles(CircleAlert);
|
|||||||
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
||||||
const ThemedCircleDot = withUnistyles(CircleDot);
|
const ThemedCircleDot = withUnistyles(CircleDot);
|
||||||
const ThemedCircleX = withUnistyles(CircleX);
|
const ThemedCircleX = withUnistyles(CircleX);
|
||||||
const ThemedMoreVertical = withUnistyles(MoreVertical);
|
|
||||||
const ThemedCopy = withUnistyles(Copy);
|
|
||||||
const ThemedArchive = withUnistyles(Archive);
|
|
||||||
const ThemedPencil = withUnistyles(Pencil);
|
|
||||||
|
|
||||||
const copyLeadingIcon = <ThemedCopy size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const markAsReadLeadingIcon = (
|
|
||||||
<ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />
|
|
||||||
);
|
|
||||||
const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
|
|
||||||
interface StatusWorkspaceListProps {
|
interface StatusWorkspaceListProps {
|
||||||
groups: StatusGroup[];
|
groups: StatusGroup[];
|
||||||
projectNamesByKey: Map<string, string>;
|
projectNamesByKey: Map<string, string>;
|
||||||
@@ -641,7 +618,7 @@ function StatusWorkspaceActionSlot({
|
|||||||
</SidebarWorkspaceTrailingActionBase>
|
</SidebarWorkspaceTrailingActionBase>
|
||||||
<SidebarWorkspaceTrailingActionOverlay visible={showOverlay}>
|
<SidebarWorkspaceTrailingActionOverlay visible={showOverlay}>
|
||||||
{onArchive ? (
|
{onArchive ? (
|
||||||
<StatusKebabMenu
|
<SidebarWorkspaceMenu
|
||||||
workspaceKey={workspace.workspaceKey}
|
workspaceKey={workspace.workspaceKey}
|
||||||
onCopyPath={onCopyPath}
|
onCopyPath={onCopyPath}
|
||||||
onCopyBranchName={onCopyBranchName}
|
onCopyBranchName={onCopyBranchName}
|
||||||
@@ -659,105 +636,6 @@ function StatusWorkspaceActionSlot({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusKebabMenu({
|
|
||||||
workspaceKey,
|
|
||||||
onCopyPath,
|
|
||||||
onCopyBranchName,
|
|
||||||
onRename,
|
|
||||||
onMarkAsRead,
|
|
||||||
onArchive,
|
|
||||||
archiveLabel,
|
|
||||||
archiveStatus,
|
|
||||||
archivePendingLabel,
|
|
||||||
archiveShortcutKeys,
|
|
||||||
}: {
|
|
||||||
workspaceKey: string;
|
|
||||||
onCopyPath?: () => void;
|
|
||||||
onCopyBranchName?: () => void;
|
|
||||||
onRename?: () => void;
|
|
||||||
onMarkAsRead?: () => void;
|
|
||||||
onArchive: () => void;
|
|
||||||
archiveLabel?: string;
|
|
||||||
archiveStatus?: "idle" | "pending" | "success";
|
|
||||||
archivePendingLabel?: string;
|
|
||||||
archiveShortcutKeys?: ShortcutKey[][] | null;
|
|
||||||
}) {
|
|
||||||
const archiveTrailing = useMemo(
|
|
||||||
() => (archiveShortcutKeys ? <Shortcut chord={archiveShortcutKeys} /> : null),
|
|
||||||
[archiveShortcutKeys],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
hitSlop={8}
|
|
||||||
style={kebabStyle}
|
|
||||||
accessibilityRole={platformIsWeb ? undefined : "button"}
|
|
||||||
accessibilityLabel="Workspace actions"
|
|
||||||
testID={`sidebar-workspace-kebab-${workspaceKey}`}
|
|
||||||
>
|
|
||||||
{({ hovered }: { hovered?: boolean }) => (
|
|
||||||
<ThemedMoreVertical
|
|
||||||
size={14}
|
|
||||||
uniProps={hovered ? foregroundColorMapping : foregroundMutedColorMapping}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" width={260}>
|
|
||||||
{onCopyPath ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-path-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyPath}
|
|
||||||
>
|
|
||||||
Copy path
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onCopyBranchName ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-branch-name-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyBranchName}
|
|
||||||
>
|
|
||||||
Copy branch name
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onRename ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-rename-${workspaceKey}`}
|
|
||||||
leading={renameLeadingIcon}
|
|
||||||
onSelect={onRename}
|
|
||||||
>
|
|
||||||
Rename workspace
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onMarkAsRead ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-mark-as-read-${workspaceKey}`}
|
|
||||||
leading={markAsReadLeadingIcon}
|
|
||||||
onSelect={onMarkAsRead}
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
|
|
||||||
leading={archiveLeadingIcon}
|
|
||||||
trailing={archiveTrailing}
|
|
||||||
status={archiveStatus}
|
|
||||||
pendingLabel={archivePendingLabel}
|
|
||||||
onSelect={onArchive}
|
|
||||||
>
|
|
||||||
{archiveLabel ?? "Archive"}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function kebabStyle({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) {
|
|
||||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusWorkspaceRowStyle({
|
function getStatusWorkspaceRowStyle({
|
||||||
selected,
|
selected,
|
||||||
isHovered,
|
isHovered,
|
||||||
@@ -860,12 +738,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
sidebarRowSelected: {
|
sidebarRowSelected: {
|
||||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||||
},
|
},
|
||||||
kebabButton: {
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginLeft: 2,
|
|
||||||
},
|
|
||||||
kebabButtonHovered: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 = <ThemedCopy size={14} uniProps={foregroundMutedColorMapping} />;
|
||||||
|
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
|
||||||
|
const markAsReadLeadingIcon = (
|
||||||
|
<ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />
|
||||||
|
);
|
||||||
|
const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedColorMapping} />;
|
||||||
|
|
||||||
|
function renderTriggerIcon({ hovered }: { hovered?: boolean }) {
|
||||||
|
return (
|
||||||
|
<ThemedMoreVertical
|
||||||
|
size={14}
|
||||||
|
uniProps={hovered ? foregroundColorMapping : foregroundMutedColorMapping}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ? <Shortcut chord={archiveShortcutKeys} /> : null),
|
||||||
|
[archiveShortcutKeys],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
hitSlop={8}
|
||||||
|
style={triggerStyle}
|
||||||
|
accessibilityRole={isWeb ? undefined : "button"}
|
||||||
|
accessibilityLabel={t("sidebar.workspace.actions.menu")}
|
||||||
|
testID={`sidebar-workspace-kebab-${workspaceKey}`}
|
||||||
|
>
|
||||||
|
{renderTriggerIcon}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" width={260}>
|
||||||
|
{onCopyPath ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={`sidebar-workspace-menu-copy-path-${workspaceKey}`}
|
||||||
|
leading={copyLeadingIcon}
|
||||||
|
onSelect={onCopyPath}
|
||||||
|
>
|
||||||
|
{t("sidebar.workspace.actions.copyPath")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
{onCopyBranchName ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={`sidebar-workspace-menu-copy-branch-name-${workspaceKey}`}
|
||||||
|
leading={copyLeadingIcon}
|
||||||
|
onSelect={onCopyBranchName}
|
||||||
|
>
|
||||||
|
{t("sidebar.workspace.actions.copyBranchName")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
{onRename ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={`sidebar-workspace-menu-rename-${workspaceKey}`}
|
||||||
|
leading={renameLeadingIcon}
|
||||||
|
onSelect={onRename}
|
||||||
|
>
|
||||||
|
{t("sidebar.workspace.actions.rename")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
{onMarkAsRead ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={`sidebar-workspace-menu-mark-as-read-${workspaceKey}`}
|
||||||
|
leading={markAsReadLeadingIcon}
|
||||||
|
onSelect={onMarkAsRead}
|
||||||
|
>
|
||||||
|
Mark as read
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
|
||||||
|
leading={archiveLeadingIcon}
|
||||||
|
trailing={archiveTrailing}
|
||||||
|
status={archiveStatus}
|
||||||
|
pendingLabel={archivePendingLabel}
|
||||||
|
onSelect={onArchive}
|
||||||
|
>
|
||||||
|
{archiveLabel ?? t("sidebar.workspace.actions.archive")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,22 +1,13 @@
|
|||||||
import { memo, useCallback, useMemo, useState, type Ref } from "react";
|
import { memo, useCallback, useMemo, useState, type Ref } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View, Text, Pressable, type PressableStateCallbackType } from "react-native";
|
import { View, Text, Pressable } from "react-native";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Archive, CircleCheck, Copy, MoreVertical, Pencil } from "lucide-react-native";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import type { Theme } from "@/styles/theme";
|
|
||||||
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||||
import type { DraggableListDragHandleProps } from "@/components/draggable-list.types";
|
import type { DraggableListDragHandleProps } from "@/components/draggable-list.types";
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
import { DiffStat } from "@/components/diff-stat";
|
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 { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
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 { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention";
|
||||||
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
||||||
import { requireWorkspaceDirectory, resolveWorkspaceDirectory } from "@/utils/workspace-directory";
|
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 { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||||
|
import { SidebarWorkspaceMenu } from "@/components/sidebar/sidebar-workspace-menu";
|
||||||
import {
|
import {
|
||||||
SidebarWorkspaceRowFrame,
|
SidebarWorkspaceRowFrame,
|
||||||
SidebarWorkspaceRowContent,
|
SidebarWorkspaceRowContent,
|
||||||
@@ -38,31 +30,6 @@ import {
|
|||||||
SidebarWorkspaceTrailingActionSlot,
|
SidebarWorkspaceTrailingActionSlot,
|
||||||
} from "@/components/sidebar/sidebar-workspace-row-content";
|
} 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 = <ThemedCopy size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const renameLeadingIcon = <ThemedPencil size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
const markAsReadLeadingIcon = (
|
|
||||||
<ThemedCircleCheck size={14} uniProps={foregroundMutedColorMapping} />
|
|
||||||
);
|
|
||||||
const archiveLeadingIcon = <ThemedArchive size={14} uniProps={foregroundMutedColorMapping} />;
|
|
||||||
|
|
||||||
function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) {
|
|
||||||
return (
|
|
||||||
<ThemedMoreVertical
|
|
||||||
size={14}
|
|
||||||
uniProps={hovered ? foregroundColorMapping : foregroundMutedColorMapping}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|
||||||
interface SidebarWorkspaceRowProps {
|
interface SidebarWorkspaceRowProps {
|
||||||
@@ -458,7 +425,7 @@ function WorkspaceRowTrailingActions({
|
|||||||
</SidebarWorkspaceTrailingActionBase>
|
</SidebarWorkspaceTrailingActionBase>
|
||||||
<SidebarWorkspaceTrailingActionOverlay visible={showKebabInSlot}>
|
<SidebarWorkspaceTrailingActionOverlay visible={showKebabInSlot}>
|
||||||
{onArchive ? (
|
{onArchive ? (
|
||||||
<WorkspaceKebabMenu
|
<SidebarWorkspaceMenu
|
||||||
workspaceKey={workspace.workspaceKey}
|
workspaceKey={workspace.workspaceKey}
|
||||||
onCopyPath={onCopyPath}
|
onCopyPath={onCopyPath}
|
||||||
onCopyBranchName={onCopyBranchName}
|
onCopyBranchName={onCopyBranchName}
|
||||||
@@ -478,103 +445,6 @@ function WorkspaceRowTrailingActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceKebabMenu({
|
|
||||||
workspaceKey,
|
|
||||||
onCopyPath,
|
|
||||||
onCopyBranchName,
|
|
||||||
onRename,
|
|
||||||
onMarkAsRead,
|
|
||||||
onArchive,
|
|
||||||
archiveLabel,
|
|
||||||
archiveStatus,
|
|
||||||
archivePendingLabel,
|
|
||||||
archiveShortcutKeys,
|
|
||||||
}: {
|
|
||||||
workspaceKey: string;
|
|
||||||
onCopyPath?: () => 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 ? <Shortcut chord={archiveShortcutKeys} /> : null),
|
|
||||||
[archiveShortcutKeys],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
hitSlop={8}
|
|
||||||
style={workspaceKebabStyle}
|
|
||||||
accessibilityRole={platformIsWeb ? undefined : "button"}
|
|
||||||
accessibilityLabel={t("sidebar.workspace.actions.menu")}
|
|
||||||
testID={`sidebar-workspace-kebab-${workspaceKey}`}
|
|
||||||
>
|
|
||||||
{renderKebabTriggerIcon}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" width={260}>
|
|
||||||
{onCopyPath ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-path-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyPath}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.copyPath")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onCopyBranchName ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-copy-branch-name-${workspaceKey}`}
|
|
||||||
leading={copyLeadingIcon}
|
|
||||||
onSelect={onCopyBranchName}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.copyBranchName")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onRename ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-rename-${workspaceKey}`}
|
|
||||||
leading={renameLeadingIcon}
|
|
||||||
onSelect={onRename}
|
|
||||||
>
|
|
||||||
{t("sidebar.workspace.actions.rename")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
{onMarkAsRead ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-mark-as-read-${workspaceKey}`}
|
|
||||||
leading={markAsReadLeadingIcon}
|
|
||||||
onSelect={onMarkAsRead}
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : null}
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID={`sidebar-workspace-menu-archive-${workspaceKey}`}
|
|
||||||
leading={archiveLeadingIcon}
|
|
||||||
trailing={archiveTrailing}
|
|
||||||
status={archiveStatus}
|
|
||||||
pendingLabel={archivePendingLabel}
|
|
||||||
onSelect={onArchive}
|
|
||||||
>
|
|
||||||
{archiveLabel ?? t("sidebar.workspace.actions.archive")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function workspaceKebabStyle({
|
|
||||||
hovered = false,
|
|
||||||
}: PressableStateCallbackType & { hovered?: boolean }) {
|
|
||||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspaceRowStyle({
|
function getWorkspaceRowStyle({
|
||||||
isDragging,
|
isDragging,
|
||||||
selected,
|
selected,
|
||||||
@@ -630,12 +500,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
kebabButton: {
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginLeft: 2,
|
|
||||||
},
|
|
||||||
kebabButtonHovered: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user