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.
This commit is contained in:
Mohamed Boudra
2026-04-19 14:26:51 +07:00
parent cfa3b009e3
commit 43bbf8cb12
8 changed files with 84 additions and 15 deletions
@@ -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
) : (
<View style={styles.splitButtonContent}>
{gitActions.primary.icon}
<Text style={styles.splitButtonText}>
{getActionDisplayLabel(gitActions.primary)}
</Text>
{!hideLabels && (
<Text style={styles.splitButtonText}>
{getActionDisplayLabel(gitActions.primary)}
</Text>
)}
</View>
)}
</Pressable>
@@ -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<ViewStyle>;
rightStyle?: StyleProp<ViewStyle>;
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({
<View style={styles.header}>
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
<View
onLayout={onRowLayout}
style={[
styles.row,
{
@@ -84,11 +88,13 @@ const styles = StyleSheet.create((theme) => ({
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
minWidth: 0,
},
right: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
flexShrink: 0,
},
borderless: {
borderBottomColor: "transparent",
@@ -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",
@@ -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,
};
}
@@ -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 <GitActionsSplitButton gitActions={gitActions} />;
return <GitActionsSplitButton gitActions={gitActions} hideLabels={hideLabels} />;
}
@@ -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}
/>
<Text style={styles.splitButtonText}>Open</Text>
{!hideLabels && <Text style={styles.splitButtonText}>Open</Text>}
</View>
)}
</Pressable>
@@ -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",
@@ -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)
<View style={styles.centerColumn}>
{(!isFocusModeEnabled || isMobile) && (
<ScreenHeader
onRowLayout={onHeaderLayout}
left={
<>
<SidebarMenuToggle />
@@ -2151,12 +2155,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
scripts={workspaceDescriptor.scripts}
hideLabels={showCompactButtonLabels}
/>
) : null}
{!isMobile ? (
<WorkspaceOpenInEditorButton
serverId={normalizedServerId}
cwd={normalizedWorkspaceId}
hideLabels={showCompactButtonLabels}
/>
) : null}
{!isMobile && isGitCheckout ? (
@@ -2164,6 +2170,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
<WorkspaceGitActions
serverId={normalizedServerId}
cwd={normalizedWorkspaceId}
hideLabels={showCompactButtonLabels}
/>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild>
@@ -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,
@@ -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"
/>
<Text style={styles.splitButtonText}>Scripts</Text>
{!hideLabels && <Text style={styles.splitButtonText}>Scripts</Text>}
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
</View>
</DropdownMenuTrigger>
@@ -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,
},