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