Polish compact workspace header actions
This commit is contained in:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user