From 43bbf8cb122a0c2c25ab1278c421c6a75b195923 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 19 Apr 2026 13:28:20 +0700 Subject: [PATCH] feat(app): collapse workspace header button labels on narrow rows Measure the header row width and hide text labels on the Open, Scripts, and git action split buttons below 700px so the header fits without wrapping on split-pane layouts. --- .../components/git-actions-split-button.tsx | 27 ++++++++++++------- .../src/components/headers/screen-header.tsx | 6 +++++ .../src/components/headers/screen-title.tsx | 1 + packages/app/src/hooks/use-container-width.ts | 18 +++++++++++++ .../workspace/workspace-git-actions.tsx | 9 +++++-- .../workspace-open-in-editor-button.tsx | 18 ++++++++++--- .../screens/workspace/workspace-screen.tsx | 11 ++++++++ .../workspace/workspace-scripts-button.tsx | 9 ++++++- 8 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 packages/app/src/hooks/use-container-width.ts diff --git a/packages/app/src/components/git-actions-split-button.tsx b/packages/app/src/components/git-actions-split-button.tsx index f510424caa..234d789d3b 100644 --- a/packages/app/src/components/git-actions-split-button.tsx +++ b/packages/app/src/components/git-actions-split-button.tsx @@ -16,18 +16,25 @@ import type { GitAction, GitActions } from "@/components/git-actions-policy"; interface GitActionsSplitButtonProps { gitActions: GitActions; + hideLabels?: boolean; } -export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps) { +export function GitActionsSplitButton({ + gitActions, + hideLabels, +}: GitActionsSplitButtonProps) { const { theme } = useUnistyles(); const toast = useToast(); const archiveShortcutKeys = useShortcutKeys("archive-worktree"); - const getActionDisplayLabel = useCallback((action: GitAction): string => { - if (action.status === "pending") return action.pendingLabel; - if (action.status === "success") return action.successLabel; - return action.label; - }, []); + const getActionDisplayLabel = useCallback( + (action: GitAction): string => { + if (action.status === "pending") return action.pendingLabel; + if (action.status === "success") return action.successLabel; + return action.label; + }, + [], + ); const handleActionSelect = useCallback( (action: GitAction) => { @@ -68,9 +75,11 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps ) : ( {gitActions.primary.icon} - - {getActionDisplayLabel(gitActions.primary)} - + {!hideLabels && ( + + {getActionDisplayLabel(gitActions.primary)} + + )} )} diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx index f516934e94..cc5b3b58ba 100644 --- a/packages/app/src/components/headers/screen-header.tsx +++ b/packages/app/src/components/headers/screen-header.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import type { LayoutChangeEvent } from "react-native"; import { View, type StyleProp, type ViewStyle } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; @@ -17,6 +18,7 @@ interface ScreenHeaderProps { leftStyle?: StyleProp; rightStyle?: StyleProp; borderless?: boolean; + onRowLayout?: (event: LayoutChangeEvent) => void; } /** @@ -29,6 +31,7 @@ export function ScreenHeader({ leftStyle, rightStyle, borderless, + onRowLayout, }: ScreenHeaderProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); @@ -42,6 +45,7 @@ export function ScreenHeader({ ({ flexDirection: "row", alignItems: "center", gap: theme.spacing[2], + minWidth: 0, }, right: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], + flexShrink: 0, }, borderless: { borderBottomColor: "transparent", diff --git a/packages/app/src/components/headers/screen-title.tsx b/packages/app/src/components/headers/screen-title.tsx index 57a60f60d6..de8b984496 100644 --- a/packages/app/src/components/headers/screen-title.tsx +++ b/packages/app/src/components/headers/screen-title.tsx @@ -25,6 +25,7 @@ export function ScreenTitle({ children, numberOfLines = 1, testID, style }: Scre const styles = StyleSheet.create((theme) => ({ text: { flexShrink: 1, + minWidth: 0, fontSize: theme.fontSize.base, fontWeight: { xs: "400", diff --git a/packages/app/src/hooks/use-container-width.ts b/packages/app/src/hooks/use-container-width.ts new file mode 100644 index 0000000000..35bb1b7b6f --- /dev/null +++ b/packages/app/src/hooks/use-container-width.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from "react"; +import type { LayoutChangeEvent } from "react-native"; + +/** + * Tracks the width of a container via onLayout. + */ +export function useContainerWidth(): { + onLayout: (e: LayoutChangeEvent) => void; + width: number; +} { + const [width, setWidth] = useState(0); + return { + onLayout: useCallback((e: LayoutChangeEvent) => { + setWidth(e.nativeEvent.layout.width); + }, []), + width, + }; +} diff --git a/packages/app/src/screens/workspace/workspace-git-actions.tsx b/packages/app/src/screens/workspace/workspace-git-actions.tsx index 86fb15d9c2..b30fe7bf4f 100644 --- a/packages/app/src/screens/workspace/workspace-git-actions.tsx +++ b/packages/app/src/screens/workspace/workspace-git-actions.tsx @@ -15,9 +15,14 @@ import { useGitActions } from "@/hooks/use-git-actions"; interface WorkspaceGitActionsProps { serverId: string; cwd: string; + hideLabels?: boolean; } -export function WorkspaceGitActions({ serverId, cwd }: WorkspaceGitActionsProps) { +export function WorkspaceGitActions({ + serverId, + cwd, + hideLabels, +}: WorkspaceGitActionsProps) { const { theme } = useUnistyles(); const icons = useMemo( @@ -40,5 +45,5 @@ export function WorkspaceGitActions({ serverId, cwd }: WorkspaceGitActionsProps) return null; } - return ; + return ; } diff --git a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx index 836c2469fc..a414175dfe 100644 --- a/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx +++ b/packages/app/src/screens/workspace/workspace-open-in-editor-button.tsx @@ -20,9 +20,14 @@ import { isWeb } from "@/constants/platform"; interface WorkspaceOpenInEditorButtonProps { serverId: string; cwd: string; + hideLabels?: boolean; } -export function WorkspaceOpenInEditorButton({ serverId, cwd }: WorkspaceOpenInEditorButtonProps) { +export function WorkspaceOpenInEditorButton({ + serverId, + cwd, + hideLabels, +}: WorkspaceOpenInEditorButtonProps) { const { theme } = useUnistyles(); const toast = useToast(); const client = useHostRuntimeClient(serverId); @@ -127,7 +132,7 @@ export function WorkspaceOpenInEditorButton({ serverId, cwd }: WorkspaceOpenInEd size={16} color={theme.colors.foregroundMuted} /> - Open + {!hideLabels && Open} )} @@ -196,7 +201,14 @@ const styles = StyleSheet.create((theme) => ({ }, splitButtonPrimary: { paddingLeft: theme.spacing[3], - paddingRight: 10, + paddingRight: theme.spacing[3], + paddingVertical: theme.spacing[1], + justifyContent: "center", + position: "relative", + }, + splitButtonPrimaryIconOnly: { + paddingLeft: theme.spacing[2], + paddingRight: theme.spacing[2], paddingVertical: theme.spacing[1], justifyContent: "center", position: "relative", diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index a37667ef5b..3b8ab6e4ee 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -117,6 +117,7 @@ import { findAdjacentPane } from "@/utils/split-navigation"; import { isAbsolutePath } from "@/utils/path"; import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout"; import { isWeb, isNative } from "@/constants/platform"; +import { useContainerWidth } from "@/hooks/use-container-width"; const TERMINALS_QUERY_STALE_TIME = 5_000; const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_000; @@ -939,6 +940,8 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const { closingTabIds, closeTab } = useCloseTabs(); + const { onLayout: onHeaderLayout, width: headerWidth } = useContainerWidth(); + const showCompactButtonLabels = headerWidth < 700; const closeWorkspaceTabWithCleanup = useCallback( function closeWorkspaceTabWithCleanup(input: { tabId: string; @@ -2044,6 +2047,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) {(!isFocusModeEnabled || isMobile) && ( @@ -2151,12 +2155,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) serverId={normalizedServerId} workspaceId={normalizedWorkspaceId} scripts={workspaceDescriptor.scripts} + hideLabels={showCompactButtonLabels} /> ) : null} {!isMobile ? ( ) : null} {!isMobile && isGitCheckout ? ( @@ -2164,6 +2170,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps) @@ -2413,9 +2420,11 @@ const styles = StyleSheet.create((theme) => ({ flexDirection: "row", alignItems: "center", gap: theme.spacing[2], + overflow: "hidden", }, headerTitleTextGroup: { minWidth: 0, + overflow: "hidden", flexShrink: 1, flexGrow: { xs: 1, @@ -2442,6 +2451,8 @@ const styles = StyleSheet.create((theme) => ({ md: theme.fontSize.base, }, flexShrink: 1, + minWidth: 0, + maxWidth: "60%", }, headerTitleSkeleton: { width: 190, diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.tsx index 054a619fe4..512413b70a 100644 --- a/packages/app/src/screens/workspace/workspace-scripts-button.tsx +++ b/packages/app/src/screens/workspace/workspace-scripts-button.tsx @@ -41,12 +41,14 @@ interface WorkspaceScriptsButtonProps { serverId: string; workspaceId: string; scripts: WorkspaceDescriptor["scripts"]; + hideLabels?: boolean; } export function WorkspaceScriptsButton({ serverId, workspaceId, scripts, + hideLabels, }: WorkspaceScriptsButtonProps): ReactElement | null { const { theme } = useUnistyles(); const toast = useToast(); @@ -98,7 +100,7 @@ export function WorkspaceScriptsButton({ } fill="transparent" /> - Scripts + {!hideLabels && Scripts} @@ -268,6 +270,11 @@ const styles = StyleSheet.create((theme) => ({ paddingVertical: theme.spacing[1], justifyContent: "center", }, + splitButtonPrimaryIconOnly: { + paddingHorizontal: theme.spacing[2], + paddingVertical: theme.spacing[1], + justifyContent: "center", + }, splitButtonPrimaryHovered: { backgroundColor: theme.colors.surface2, },