* 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>
152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
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,
|
|
},
|
|
}));
|