Polish compact workspace header actions

This commit is contained in:
Mohamed Boudra
2026-05-25 19:50:31 +07:00
parent e312e0d6f3
commit f0730d5eac
3 changed files with 124 additions and 44 deletions
@@ -873,7 +873,7 @@ function WorkspaceHeaderMenu({
<DropdownMenu>
<DropdownMenuTrigger
testID="workspace-header-menu-trigger"
style={styles.headerActionButton}
style={isMobile ? styles.compactHeaderActionButton : styles.headerActionButton}
accessibilityRole="button"
accessibilityLabel="Workspace actions"
>
@@ -1039,40 +1039,43 @@ function WorkspaceHeaderTitleBar({
) : null}
</View>
)}
<WorkspaceHeaderMenu
normalizedWorkspaceId={normalizedWorkspaceId}
currentBranchName={currentBranchName}
showWorkspaceSetup={showWorkspaceSetup}
showCreateBrowserTab={showCreateBrowserTab}
isMobile={isMobile}
createTerminalDisabled={createTerminalDisabled}
importAgentDisabled={importAgentDisabled}
menuNewAgentIcon={menuNewAgentIcon}
menuNewTerminalIcon={menuNewTerminalIcon}
menuNewBrowserIcon={menuNewBrowserIcon}
menuImportIcon={menuImportIcon}
menuCopyIcon={menuCopyIcon}
menuSettingsIcon={menuSettingsIcon}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminal={onCreateTerminal}
onCreateBrowser={onCreateBrowser}
onOpenImportSheet={onOpenImportSheet}
onCopyWorkspacePath={onCopyWorkspacePath}
onCopyBranchName={onCopyBranchName}
onOpenSetupTab={onOpenSetupTab}
/>
{isMobile && workspaceScripts.length > 0 ? (
<WorkspaceScriptsButton
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
scripts={workspaceScripts}
liveTerminalIds={liveTerminalIds}
onScriptTerminalStarted={onScriptTerminalStarted}
onViewTerminal={onViewScriptTerminal}
onOpenUrlInBrowserTab={onOpenUrlInBrowserTab}
hideLabels
<View style={styles.compactHeaderMenuCluster}>
<WorkspaceHeaderMenu
normalizedWorkspaceId={normalizedWorkspaceId}
currentBranchName={currentBranchName}
showWorkspaceSetup={showWorkspaceSetup}
showCreateBrowserTab={showCreateBrowserTab}
isMobile={isMobile}
createTerminalDisabled={createTerminalDisabled}
importAgentDisabled={importAgentDisabled}
menuNewAgentIcon={menuNewAgentIcon}
menuNewTerminalIcon={menuNewTerminalIcon}
menuNewBrowserIcon={menuNewBrowserIcon}
menuImportIcon={menuImportIcon}
menuCopyIcon={menuCopyIcon}
menuSettingsIcon={menuSettingsIcon}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminal={onCreateTerminal}
onCreateBrowser={onCreateBrowser}
onOpenImportSheet={onOpenImportSheet}
onCopyWorkspacePath={onCopyWorkspacePath}
onCopyBranchName={onCopyBranchName}
onOpenSetupTab={onOpenSetupTab}
/>
) : null}
{isMobile && workspaceScripts.length > 0 ? (
<WorkspaceScriptsButton
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
scripts={workspaceScripts}
liveTerminalIds={liveTerminalIds}
onScriptTerminalStarted={onScriptTerminalStarted}
onViewTerminal={onViewScriptTerminal}
onOpenUrlInBrowserTab={onOpenUrlInBrowserTab}
hideLabels
presentation="ghost"
/>
) : null}
</View>
</View>
);
}
@@ -3105,7 +3108,7 @@ function WorkspaceScreenContent({
tooltipLabel="Toggle explorer"
tooltipKeys={EXPLORER_TOGGLE_KEYS}
tooltipSide="left"
style={styles.headerActionButton}
style={styles.compactHeaderActionButton}
accessible
accessibilityRole="button"
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
@@ -3444,7 +3447,10 @@ const styles = StyleSheet.create((theme) => ({
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
gap: {
xs: theme.spacing[1],
md: theme.spacing[2],
},
overflow: "hidden",
},
headerTitleTextGroup: {
@@ -3500,6 +3506,22 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
compactHeaderActionButton: {
width: theme.spacing[8],
height: theme.spacing[8],
padding: 0,
borderRadius: theme.borderRadius.lg,
alignItems: "center",
justifyContent: "center",
},
compactHeaderMenuCluster: {
flexDirection: "row",
alignItems: "center",
gap: {
xs: 0,
md: theme.spacing[2],
},
},
sourceControlButton: {
flexDirection: "row",
alignItems: "center",
@@ -13,7 +13,7 @@ const { theme, startWorkspaceScriptMock } = vi.hoisted(() => {
const hoistedTheme = {
spacing: { 1: 4, 1.5: 6, 2: 8, 3: 12 },
borderWidth: { 1: 1 },
borderRadius: { md: 6 },
borderRadius: { md: 6, lg: 8 },
fontSize: { xs: 11, sm: 13 },
fontWeight: { normal: "400", medium: "500" },
colors: {
@@ -150,7 +150,15 @@ function script(
const LIVE_TERMINAL_IDS: string[] = ["terminal-script-1"];
function renderScripts(scripts: WorkspaceScriptPayload[]): {
interface RenderScriptsOptions {
hideLabels?: boolean;
presentation?: "split" | "ghost";
}
function renderScripts(
scripts: WorkspaceScriptPayload[],
options: RenderScriptsOptions = {},
): {
rerender: (nextScripts: WorkspaceScriptPayload[]) => Promise<void>;
unmount: () => void;
} {
@@ -172,6 +180,8 @@ function renderScripts(scripts: WorkspaceScriptPayload[]): {
workspaceId="workspace-1"
scripts={nextScripts}
liveTerminalIds={LIVE_TERMINAL_IDS}
hideLabels={options.hideLabels}
presentation={options.presentation}
/>
</QueryClientProvider>
);
@@ -330,4 +340,22 @@ describe("WorkspaceScriptsButton", () => {
theme.colors.foregroundMuted,
);
});
it("removes the trigger caret in ghost presentation", () => {
current = renderScripts([script({ scriptName: "dev" })], {
hideLabels: true,
presentation: "ghost",
});
const trigger = document.querySelector('[data-testid="workspace-scripts-button"]');
expect(trigger?.querySelector('[data-icon="Play"]')?.getAttribute("data-size")).toBe("16");
expect(trigger?.querySelector('[data-icon="ChevronDown"]')).toBeNull();
});
it("keeps the trigger caret in split presentation", () => {
current = renderScripts([script({ scriptName: "dev" })]);
const trigger = document.querySelector('[data-testid="workspace-scripts-button"]');
expect(trigger?.querySelector('[data-icon="ChevronDown"]')).not.toBeNull();
});
});
@@ -31,6 +31,7 @@ interface WorkspaceScriptsButtonProps {
onViewTerminal?: (terminalId: string) => void;
onOpenUrlInBrowserTab?: (url: string) => void;
hideLabels?: boolean;
presentation?: "split" | "ghost";
}
interface ScriptActionButtonProps {
@@ -48,6 +49,8 @@ const ThemedGlobe = withUnistyles(Globe);
const ThemedChevronDown = withUnistyles(ChevronDown);
const ThemedExternalLink = withUnistyles(ExternalLink);
const GHOST_TRIGGER_ICON_SIZE = 16;
const foregroundColorMapping = (theme: Theme) => ({
color: theme.colors.foreground,
});
@@ -64,6 +67,7 @@ const redColorMapping = (theme: Theme) => ({
color: theme.colors.palette.red[500],
});
const playFillTransparent = { fill: "transparent" };
const ghostPlayStroke = { strokeWidth: 1.5 };
interface ScriptActionButtonChildrenProps {
hovered?: boolean;
@@ -366,6 +370,7 @@ export function WorkspaceScriptsButton({
onViewTerminal,
onOpenUrlInBrowserTab,
hideLabels,
presentation = "split",
}: WorkspaceScriptsButtonProps): ReactElement | null {
const toast = useToast();
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
@@ -397,10 +402,11 @@ export function WorkspaceScriptsButton({
const triggerStyle = useCallback(
({ hovered, pressed, open }: { hovered: boolean; pressed: boolean; open: boolean }) => [
styles.splitButtonPrimary,
(hovered || pressed || open) && styles.splitButtonPrimaryHovered,
presentation === "ghost" ? styles.ghostButton : styles.splitButtonPrimary,
(hovered || pressed || open) &&
(presentation === "ghost" ? styles.ghostButtonHovered : styles.splitButtonPrimaryHovered),
],
[],
[presentation],
);
const handleStartScript = useCallback(
@@ -414,10 +420,13 @@ export function WorkspaceScriptsButton({
const hasAnyRunning = scripts.some((s) => s.lifecycle === "running");
const triggerPlayMapping = hasAnyRunning ? blueColorMapping : mutedColorMapping;
const triggerIconSize = presentation === "ghost" ? GHOST_TRIGGER_ICON_SIZE : 14;
const triggerPlayProps =
presentation === "ghost" ? { ...playFillTransparent, ...ghostPlayStroke } : playFillTransparent;
return (
<View style={styles.row}>
<View style={styles.splitButton}>
<View style={presentation === "ghost" ? styles.ghostButtonFrame : styles.splitButton}>
<DropdownMenu>
<DropdownMenuTrigger
testID="workspace-scripts-button"
@@ -426,9 +435,15 @@ export function WorkspaceScriptsButton({
accessibilityLabel="Workspace scripts"
>
<View style={styles.splitButtonContent}>
<ThemedPlay size={14} uniProps={triggerPlayMapping} {...playFillTransparent} />
<ThemedPlay
size={triggerIconSize}
uniProps={triggerPlayMapping}
{...triggerPlayProps}
/>
{!hideLabels && <Text style={styles.splitButtonText}>Scripts</Text>}
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
{presentation === "split" ? (
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
) : null}
</View>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -475,6 +490,21 @@ const styles = StyleSheet.create((theme) => ({
borderColor: theme.colors.borderAccent,
overflow: "hidden",
},
ghostButtonFrame: {
flexDirection: "row",
alignItems: "stretch",
},
ghostButton: {
width: theme.spacing[8],
height: theme.spacing[8],
padding: 0,
borderRadius: theme.borderRadius.lg,
alignItems: "center",
justifyContent: "center",
},
ghostButtonHovered: {
backgroundColor: theme.colors.surface2,
},
splitButtonPrimary: {
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[1],