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:
@@ -16,18 +16,25 @@ import type { GitAction, GitActions } from "@/components/git-actions-policy";
|
|||||||
|
|
||||||
interface GitActionsSplitButtonProps {
|
interface GitActionsSplitButtonProps {
|
||||||
gitActions: GitActions;
|
gitActions: GitActions;
|
||||||
|
hideLabels?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps) {
|
export function GitActionsSplitButton({
|
||||||
|
gitActions,
|
||||||
|
hideLabels,
|
||||||
|
}: GitActionsSplitButtonProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
const archiveShortcutKeys = useShortcutKeys("archive-worktree");
|
||||||
|
|
||||||
const getActionDisplayLabel = useCallback((action: GitAction): string => {
|
const getActionDisplayLabel = useCallback(
|
||||||
if (action.status === "pending") return action.pendingLabel;
|
(action: GitAction): string => {
|
||||||
if (action.status === "success") return action.successLabel;
|
if (action.status === "pending") return action.pendingLabel;
|
||||||
return action.label;
|
if (action.status === "success") return action.successLabel;
|
||||||
}, []);
|
return action.label;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleActionSelect = useCallback(
|
const handleActionSelect = useCallback(
|
||||||
(action: GitAction) => {
|
(action: GitAction) => {
|
||||||
@@ -68,9 +75,11 @@ export function GitActionsSplitButton({ gitActions }: GitActionsSplitButtonProps
|
|||||||
) : (
|
) : (
|
||||||
<View style={styles.splitButtonContent}>
|
<View style={styles.splitButtonContent}>
|
||||||
{gitActions.primary.icon}
|
{gitActions.primary.icon}
|
||||||
<Text style={styles.splitButtonText}>
|
{!hideLabels && (
|
||||||
{getActionDisplayLabel(gitActions.primary)}
|
<Text style={styles.splitButtonText}>
|
||||||
</Text>
|
{getActionDisplayLabel(gitActions.primary)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import type { LayoutChangeEvent } from "react-native";
|
||||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -17,6 +18,7 @@ interface ScreenHeaderProps {
|
|||||||
leftStyle?: StyleProp<ViewStyle>;
|
leftStyle?: StyleProp<ViewStyle>;
|
||||||
rightStyle?: StyleProp<ViewStyle>;
|
rightStyle?: StyleProp<ViewStyle>;
|
||||||
borderless?: boolean;
|
borderless?: boolean;
|
||||||
|
onRowLayout?: (event: LayoutChangeEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,7 @@ export function ScreenHeader({
|
|||||||
leftStyle,
|
leftStyle,
|
||||||
rightStyle,
|
rightStyle,
|
||||||
borderless,
|
borderless,
|
||||||
|
onRowLayout,
|
||||||
}: ScreenHeaderProps) {
|
}: ScreenHeaderProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -42,6 +45,7 @@ export function ScreenHeader({
|
|||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
|
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
|
||||||
<View
|
<View
|
||||||
|
onLayout={onRowLayout}
|
||||||
style={[
|
style={[
|
||||||
styles.row,
|
styles.row,
|
||||||
{
|
{
|
||||||
@@ -84,11 +88,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
right: {
|
right: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
borderless: {
|
borderless: {
|
||||||
borderBottomColor: "transparent",
|
borderBottomColor: "transparent",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function ScreenTitle({ children, numberOfLines = 1, testID, style }: Scre
|
|||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
text: {
|
text: {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
|
minWidth: 0,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: {
|
fontWeight: {
|
||||||
xs: "400",
|
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 {
|
interface WorkspaceGitActionsProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
hideLabels?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceGitActions({ serverId, cwd }: WorkspaceGitActionsProps) {
|
export function WorkspaceGitActions({
|
||||||
|
serverId,
|
||||||
|
cwd,
|
||||||
|
hideLabels,
|
||||||
|
}: WorkspaceGitActionsProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const icons = useMemo(
|
const icons = useMemo(
|
||||||
@@ -40,5 +45,5 @@ export function WorkspaceGitActions({ serverId, cwd }: WorkspaceGitActionsProps)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <GitActionsSplitButton gitActions={gitActions} />;
|
return <GitActionsSplitButton gitActions={gitActions} hideLabels={hideLabels} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ import { isWeb } from "@/constants/platform";
|
|||||||
interface WorkspaceOpenInEditorButtonProps {
|
interface WorkspaceOpenInEditorButtonProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
hideLabels?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceOpenInEditorButton({ serverId, cwd }: WorkspaceOpenInEditorButtonProps) {
|
export function WorkspaceOpenInEditorButton({
|
||||||
|
serverId,
|
||||||
|
cwd,
|
||||||
|
hideLabels,
|
||||||
|
}: WorkspaceOpenInEditorButtonProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
@@ -127,7 +132,7 @@ export function WorkspaceOpenInEditorButton({ serverId, cwd }: WorkspaceOpenInEd
|
|||||||
size={16}
|
size={16}
|
||||||
color={theme.colors.foregroundMuted}
|
color={theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.splitButtonText}>Open</Text>
|
{!hideLabels && <Text style={styles.splitButtonText}>Open</Text>}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -196,7 +201,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
splitButtonPrimary: {
|
splitButtonPrimary: {
|
||||||
paddingLeft: theme.spacing[3],
|
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],
|
paddingVertical: theme.spacing[1],
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import { findAdjacentPane } from "@/utils/split-navigation";
|
|||||||
import { isAbsolutePath } from "@/utils/path";
|
import { isAbsolutePath } from "@/utils/path";
|
||||||
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||||
import { isWeb, isNative } from "@/constants/platform";
|
import { isWeb, isNative } from "@/constants/platform";
|
||||||
|
import { useContainerWidth } from "@/hooks/use-container-width";
|
||||||
|
|
||||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||||
const WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS = 30_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 pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
||||||
const { closingTabIds, closeTab } = useCloseTabs();
|
const { closingTabIds, closeTab } = useCloseTabs();
|
||||||
|
const { onLayout: onHeaderLayout, width: headerWidth } = useContainerWidth();
|
||||||
|
const showCompactButtonLabels = headerWidth < 700;
|
||||||
const closeWorkspaceTabWithCleanup = useCallback(
|
const closeWorkspaceTabWithCleanup = useCallback(
|
||||||
function closeWorkspaceTabWithCleanup(input: {
|
function closeWorkspaceTabWithCleanup(input: {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -2044,6 +2047,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
<View style={styles.centerColumn}>
|
<View style={styles.centerColumn}>
|
||||||
{(!isFocusModeEnabled || isMobile) && (
|
{(!isFocusModeEnabled || isMobile) && (
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
|
onRowLayout={onHeaderLayout}
|
||||||
left={
|
left={
|
||||||
<>
|
<>
|
||||||
<SidebarMenuToggle />
|
<SidebarMenuToggle />
|
||||||
@@ -2151,12 +2155,14 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
workspaceId={normalizedWorkspaceId}
|
workspaceId={normalizedWorkspaceId}
|
||||||
scripts={workspaceDescriptor.scripts}
|
scripts={workspaceDescriptor.scripts}
|
||||||
|
hideLabels={showCompactButtonLabels}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!isMobile ? (
|
{!isMobile ? (
|
||||||
<WorkspaceOpenInEditorButton
|
<WorkspaceOpenInEditorButton
|
||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
cwd={normalizedWorkspaceId}
|
cwd={normalizedWorkspaceId}
|
||||||
|
hideLabels={showCompactButtonLabels}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!isMobile && isGitCheckout ? (
|
{!isMobile && isGitCheckout ? (
|
||||||
@@ -2164,6 +2170,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
<WorkspaceGitActions
|
<WorkspaceGitActions
|
||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
cwd={normalizedWorkspaceId}
|
cwd={normalizedWorkspaceId}
|
||||||
|
hideLabels={showCompactButtonLabels}
|
||||||
/>
|
/>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -2413,9 +2420,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
headerTitleTextGroup: {
|
headerTitleTextGroup: {
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
overflow: "hidden",
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
flexGrow: {
|
flexGrow: {
|
||||||
xs: 1,
|
xs: 1,
|
||||||
@@ -2442,6 +2451,8 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
md: theme.fontSize.base,
|
md: theme.fontSize.base,
|
||||||
},
|
},
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: "60%",
|
||||||
},
|
},
|
||||||
headerTitleSkeleton: {
|
headerTitleSkeleton: {
|
||||||
width: 190,
|
width: 190,
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ interface WorkspaceScriptsButtonProps {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
scripts: WorkspaceDescriptor["scripts"];
|
scripts: WorkspaceDescriptor["scripts"];
|
||||||
|
hideLabels?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceScriptsButton({
|
export function WorkspaceScriptsButton({
|
||||||
serverId,
|
serverId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
scripts,
|
scripts,
|
||||||
|
hideLabels,
|
||||||
}: WorkspaceScriptsButtonProps): ReactElement | null {
|
}: WorkspaceScriptsButtonProps): ReactElement | null {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -98,7 +100,7 @@ export function WorkspaceScriptsButton({
|
|||||||
}
|
}
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
/>
|
/>
|
||||||
<Text style={styles.splitButtonText}>Scripts</Text>
|
{!hideLabels && <Text style={styles.splitButtonText}>Scripts</Text>}
|
||||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -268,6 +270,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingVertical: theme.spacing[1],
|
paddingVertical: theme.spacing[1],
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
splitButtonPrimaryIconOnly: {
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
splitButtonPrimaryHovered: {
|
splitButtonPrimaryHovered: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user