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 f510424ca..234d789d3 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 f516934e9..cc5b3b58b 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 57a60f60d..de8b98449 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 000000000..35bb1b7b6 --- /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 86fb15d9c..b30fe7bf4 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 836c2469f..a414175df 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 a37667ef5..3b8ab6e4e 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 054a619fe..512413b70 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, },