diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index cf59bab9e..d11229f8f 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -873,7 +873,7 @@ function WorkspaceHeaderMenu({ @@ -1039,40 +1039,43 @@ function WorkspaceHeaderTitleBar({ ) : null} )} - - {isMobile && workspaceScripts.length > 0 ? ( - + - ) : null} + {isMobile && workspaceScripts.length > 0 ? ( + + ) : null} + ); } @@ -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", diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx index 54bbcacf0..ebef75520 100644 --- a/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx +++ b/packages/app/src/screens/workspace/workspace-scripts-button.test.tsx @@ -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; 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} /> ); @@ -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(); + }); }); diff --git a/packages/app/src/screens/workspace/workspace-scripts-button.tsx b/packages/app/src/screens/workspace/workspace-scripts-button.tsx index c5dd381d6..4121effcf 100644 --- a/packages/app/src/screens/workspace/workspace-scripts-button.tsx +++ b/packages/app/src/screens/workspace/workspace-scripts-button.tsx @@ -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 ( - + - + {!hideLabels && Scripts} - + {presentation === "split" ? ( + + ) : null} ({ 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],