From b919a3cdd91d287c4908cbd0c0db417b164f68b8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 24 Jan 2026 08:52:31 +0700 Subject: [PATCH] Revamp Changes pane view + ship menus and stabilize e2e selectors --- packages/app/e2e/checkout-ship.spec.ts | 65 +- packages/app/src/components/git-diff-pane.tsx | 624 +++++++++++++----- 2 files changed, 489 insertions(+), 200 deletions(-) diff --git a/packages/app/e2e/checkout-ship.spec.ts b/packages/app/e2e/checkout-ship.spec.ts index 28bc9bc90..dc9539a75 100644 --- a/packages/app/e2e/checkout-ship.spec.ts +++ b/packages/app/e2e/checkout-ship.spec.ts @@ -29,12 +29,33 @@ function getChangesActionButton(page: Page, label: string) { return getChangesActionLabel(page, label).locator('..'); } +async function selectChangesView(page: Page, view: 'working' | 'base') { + const scope = getChangesScope(page); + await scope.getByTestId('changes-view-selector').click(); + if (view === 'working') { + await page.getByTestId('changes-mode-uncommitted').click(); + } else { + await page.getByTestId('changes-mode-base').click(); + } +} + async function openChangesOverflowMenu(page: Page) { - const menuButton = page.getByTestId('changes-overflow-menu').first(); + const menuButton = getChangesScope(page).locator('[data-testid="changes-overflow-menu"]:visible').first(); await expect(menuButton).toBeVisible(); await menuButton.click(); } +async function openChangesShipMenu(page: Page) { + const backdrop = page.getByRole('button', { name: 'Ship menu backdrop' }).first(); + if (await backdrop.isVisible().catch(() => false)) { + return; + } + const scope = getChangesScope(page); + const caret = scope.getByTestId('changes-ship-caret').first(); + await expect(caret).toBeVisible(); + await caret.click(); +} + async function openChangesPanel(page: Page, options?: { expectGit?: boolean }) { const changesHeader = getChangesHeader(page); if (!(await changesHeader.isVisible())) { @@ -114,6 +135,8 @@ async function createAgentAndWait(page: Page, message: string) { async function requestCwd(page: Page) { await sendPrompt(page, 'Run `pwd` and respond with exactly: CWD: '); const message = await waitForAssistantText(page, 'CWD:'); + // The assistant streams tokens; make sure we capture the full path (not a partial prefix). + await expect.poll(async () => (await message.textContent()) ?? '', { timeout: 60000 }).toContain('.paseo/worktrees/'); const content = (await message.textContent()) ?? ''; const match = content.match(/CWD:\s*(\S+)/); if (!match) { @@ -168,9 +191,8 @@ async function enableCreateWorktree(page: Page) { } async function refreshUncommittedMode(page: Page) { - const changesScope = getChangesScope(page); - await changesScope.getByTestId('changes-mode-base').click(); - await changesScope.getByTestId('changes-mode-uncommitted').click(); + await selectChangesView(page, 'base'); + await selectChangesView(page, 'working'); } async function refreshChangesTab(page: Page) { @@ -253,17 +275,17 @@ test('checkout-first Changes panel ship loop', async ({ page }) => { }); await getChangesScope(page).getByTestId('diff-file-0-toggle').first().click(); await expect(page.getByText('First change')).toBeVisible(); - await expect(getChangesActionLabel(page, 'Commit')).toBeVisible(); + await expect(getChangesScope(page).getByTestId('changes-action-commit')).toBeVisible(); - await getChangesActionButton(page, 'Commit').click(); - await getChangesScope(page).getByTestId('changes-mode-uncommitted').click(); + await getChangesScope(page).getByTestId('changes-action-commit').click(); + await selectChangesView(page, 'working'); await refreshChangesTab(page); await expect(getChangesScope(page).getByText('No uncommitted changes')).toBeVisible({ timeout: 30000, }); - await expect(getChangesActionLabel(page, 'Commit')).toHaveCount(0); + await expect(getChangesScope(page).getByTestId('changes-action-commit')).toHaveCount(0); - await getChangesScope(page).getByTestId('changes-mode-base').click(); + await selectChangesView(page, 'base'); await expect(getChangesScope(page).getByText('README.md', { exact: true })).toBeVisible({ timeout: 30000, }); @@ -277,19 +299,19 @@ test('checkout-first Changes panel ship loop', async ({ page }) => { timeout: 30000, }); await expect(getChangesScope(page).getByText('README.md', { exact: true })).toHaveCount(0); - await expect(getChangesActionLabel(page, 'Commit')).toBeVisible(); + await expect(getChangesScope(page).getByTestId('changes-action-commit')).toBeVisible(); - await getChangesScope(page).getByTestId('changes-mode-base').click(); + await selectChangesView(page, 'base'); await expect(getChangesScope(page).getByText('README.md', { exact: true })).toBeVisible({ timeout: 30000, }); - await getChangesActionButton(page, 'Commit').click(); - await getChangesScope(page).getByTestId('changes-mode-uncommitted').click(); + await getChangesScope(page).getByTestId('changes-action-commit').click(); + await selectChangesView(page, 'working'); await expect(page.getByText('No uncommitted changes')).toBeVisible({ timeout: 30000 }); - await expect(getChangesActionLabel(page, 'Commit')).toHaveCount(0); + await expect(getChangesScope(page).getByTestId('changes-action-commit')).toHaveCount(0); - await getChangesScope(page).getByTestId('changes-mode-base').click(); + await selectChangesView(page, 'base'); await expect(getChangesScope(page).getByText('README.md', { exact: true })).toBeVisible({ timeout: 30000, }); @@ -297,20 +319,21 @@ test('checkout-first Changes panel ship loop', async ({ page }) => { timeout: 30000, }); - await expect(getChangesActionLabel(page, 'Create PR')).toBeVisible(); - await getChangesActionButton(page, 'Create PR').click(); + await openChangesShipMenu(page); + await page.getByTestId('changes-ship-create-pr').click(); await expect(getChangesScope(page).getByTestId('changes-pr-status')).toContainText(/open/i, { timeout: 60000, }); - await expect(getChangesActionLabel(page, 'Create PR')).toHaveCount(0); + await openChangesShipMenu(page); + await expect(page.getByTestId('changes-ship-open-pr')).toBeVisible(); - await getChangesActionButton(page, 'Merge to base').click(); - await getChangesScope(page).getByTestId('changes-mode-base').click(); + await page.getByTestId('changes-ship-merge').click(); + await selectChangesView(page, 'base'); await expect(getChangesScope(page).getByText('No base changes')).toBeVisible({ timeout: 60000, }); await refreshChangesTab(page); - await expect(getChangesActionLabel(page, 'Merge to base')).toHaveCount(0, { timeout: 30000 }); + await expect(getChangesScope(page).getByTestId('changes-ship-caret')).toHaveCount(0, { timeout: 30000 }); await openChangesOverflowMenu(page); await expect(page.getByTestId('changes-menu-archive')).toBeVisible(); diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 0714f00ee..cfd02036b 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -15,7 +15,9 @@ import { } from "react-native"; import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import { ChevronRight, GitBranch, MoreVertical, RotateCcw } from "lucide-react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import * as Linking from "expo-linking"; +import { ChevronDown, ChevronRight, GitBranch, MoreVertical, RotateCcw } from "lucide-react-native"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useSessionStore } from "@/stores/session-store"; import { @@ -361,6 +363,13 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { const [overflowVisible, setOverflowVisible] = useState(false); const [overflowAnchor, setOverflowAnchor] = useState({ left: 0, top: 0 }); const [overflowHeight, setOverflowHeight] = useState(0); + const [viewMenuVisible, setViewMenuVisible] = useState(false); + const [viewMenuAnchor, setViewMenuAnchor] = useState({ left: 0, top: 0 }); + const [viewMenuHeight, setViewMenuHeight] = useState(0); + const [shipMenuVisible, setShipMenuVisible] = useState(false); + const [shipMenuAnchor, setShipMenuAnchor] = useState({ left: 0, top: 0 }); + const [shipMenuHeight, setShipMenuHeight] = useState(0); + const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge"); const { status, isLoading: isStatusLoading, isFetching: isStatusFetching, isError: isStatusError, error: statusError, refresh: refreshStatus } = useCheckoutStatusQuery({ serverId, agentId }); const gitStatus = status && status.isGit ? status : null; @@ -428,6 +437,44 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { void refreshPrStatus(); }, [refreshDiff, refreshStatus, refreshPrStatus]); + const shipDefaultStorageKey = useMemo(() => { + if (!gitStatus?.repoRoot) { + return null; + } + return `@paseo:changes-ship-default:${gitStatus.repoRoot}`; + }, [gitStatus?.repoRoot]); + + useEffect(() => { + if (!shipDefaultStorageKey) { + return; + } + let isActive = true; + AsyncStorage.getItem(shipDefaultStorageKey) + .then((value) => { + if (!isActive) return; + if (value === "pr" || value === "merge") { + setShipDefault(value); + } + }) + .catch(() => undefined); + return () => { + isActive = false; + }; + }, [shipDefaultStorageKey]); + + const persistShipDefault = useCallback( + async (next: "merge" | "pr") => { + setShipDefault(next); + if (!shipDefaultStorageKey) return; + try { + await AsyncStorage.setItem(shipDefaultStorageKey, next); + } catch { + // Ignore persistence failures; default will reset to "merge". + } + }, + [shipDefaultStorageKey] + ); + const handleOpenOverflowMenu = useCallback((event: any) => { const { pageX, pageY } = event?.nativeEvent ?? {}; setOverflowAnchor({ @@ -447,6 +494,44 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { setOverflowHeight((current) => (current === height ? current : height)); }, []); + const handleOpenViewMenu = useCallback((event: any) => { + const { pageX, pageY } = event?.nativeEvent ?? {}; + setViewMenuAnchor({ + left: typeof pageX === "number" ? pageX : 0, + top: typeof pageY === "number" ? pageY : 0, + }); + setViewMenuVisible(true); + }, []); + + const handleCloseViewMenu = useCallback(() => { + setViewMenuVisible(false); + setViewMenuHeight(0); + }, []); + + const handleViewMenuLayout = useCallback((event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setViewMenuHeight((current) => (current === height ? current : height)); + }, []); + + const handleOpenShipMenu = useCallback((event: any) => { + const { pageX, pageY } = event?.nativeEvent ?? {}; + setShipMenuAnchor({ + left: typeof pageX === "number" ? pageX : 0, + top: typeof pageY === "number" ? pageY : 0, + }); + setShipMenuVisible(true); + }, []); + + const handleCloseShipMenu = useCallback(() => { + setShipMenuVisible(false); + setShipMenuHeight(0); + }, []); + + const handleShipMenuLayout = useCallback((event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setShipMenuHeight((current) => (current === height ? current : height)); + }, []); + const handleToggleExpanded = useCallback((path: string) => { setExpandedByPath((prev) => ({ ...prev, @@ -624,14 +709,17 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown"); const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading; const hasUncommittedChanges = Boolean(gitStatus?.isDirty); - const showCommitAction = - !isGit || hasUncommittedChanges || (diffMode === "uncommitted" && hasChanges); - const showCreatePrAction = !isGit || !prStatus; - const showMergeAction = - diffMode === "base" && isGit && (gitStatus?.aheadBehind?.ahead ?? 0) > 0; + const aheadCount = gitStatus?.aheadBehind?.ahead ?? 0; + const canShowCommit = isGit && hasUncommittedChanges; + const canShowShip = isGit && aheadCount > 0; + const baseRefLabel = useMemo(() => { + if (!baseRef) return "base"; + const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim(); + return trimmed.startsWith("origin/") ? trimmed.slice("origin/".length) : trimmed; + }, [baseRef]); const commitDisabled = actionsDisabled || commitMutation.isPending; const prDisabled = actionsDisabled || prMutation.isPending; - const mergeDisabled = actionsDisabled || mergeMutation.isPending; + const mergeDisabled = actionsDisabled || mergeMutation.isPending || hasUncommittedChanges; const archiveDisabled = actionsDisabled || archiveMutation.isPending || @@ -698,62 +786,43 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { ); } - type PrimaryAction = { - key: "commit" | "create_pr" | "merge"; - label: string; - testID: string; - disabled: boolean; - isPending: boolean; - onPress: () => void; - }; + const hasPullRequest = Boolean(prStatus?.url); + const prActionLabel = hasPullRequest ? "Open PR" : "Create PR"; - const primaryAction: PrimaryAction | null = useMemo(() => { - if (!isGit) { - return null; + type ShipActionKey = "merge" | "pr"; + const shipActions: { key: ShipActionKey; label: string; disabled: boolean; isPending: boolean }[] = + useMemo( + () => [ + { key: "merge", label: "Merge", disabled: mergeDisabled, isPending: mergeMutation.isPending }, + { key: "pr", label: prActionLabel, disabled: prDisabled, isPending: prMutation.isPending }, + ], + [mergeDisabled, mergeMutation.isPending, prActionLabel, prDisabled, prMutation.isPending] + ); + + const resolvedShipPrimary: ShipActionKey | null = useMemo(() => { + if (!canShowShip) return null; + const preferred = shipDefault; + const preferredMeta = shipActions.find((action) => action.key === preferred); + if (preferredMeta && !preferredMeta.disabled) { + return preferred; } - if (showCommitAction) { - return { - key: "commit", - label: "Commit", - testID: "changes-primary-commit", - disabled: commitDisabled, - isPending: commitMutation.isPending, - onPress: () => commitMutation.mutate(), - }; - } - if (showCreatePrAction) { - return { - key: "create_pr", - label: "Create PR", - testID: "changes-primary-create-pr", - disabled: prDisabled, - isPending: prMutation.isPending, - onPress: () => prMutation.mutate(), - }; - } - if (showMergeAction) { - return { - key: "merge", - label: "Merge to base", - testID: "changes-primary-merge", - disabled: mergeDisabled, - isPending: mergeMutation.isPending, - onPress: () => mergeMutation.mutate(), - }; - } - return null; - }, [ - commitDisabled, - commitMutation, - isGit, - mergeDisabled, - mergeMutation, - prDisabled, - prMutation, - showCommitAction, - showCreatePrAction, - showMergeAction, - ]); + const fallback = shipActions.find((action) => !action.disabled); + return fallback?.key ?? preferred; + }, [canShowShip, shipActions, shipDefault]); + + const shipPrimaryDisabled = useMemo(() => { + if (!resolvedShipPrimary) return true; + if (actionsDisabled) return true; + const meta = shipActions.find((action) => action.key === resolvedShipPrimary); + return meta?.disabled ?? true; + }, [actionsDisabled, resolvedShipPrimary, shipActions]); + + const shipPrimaryPending = useMemo(() => { + if (!resolvedShipPrimary) return false; + if (resolvedShipPrimary === "merge") return mergeMutation.isPending; + // "pr" maps to open PR (instant) or create PR (mutation) + return hasPullRequest ? false : prMutation.isPending; + }, [hasPullRequest, mergeMutation.isPending, prMutation.isPending, resolvedShipPrimary]); const overflowPosition = useMemo(() => { if (!overflowVisible) { @@ -780,6 +849,56 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { windowWidth, ]); + const viewMenuPosition = useMemo(() => { + if (!viewMenuVisible) { + return null; + } + const menuWidth = 180; + const horizontalPadding = theme.spacing[2]; + const verticalPadding = theme.spacing[2]; + const maxLeft = Math.max(horizontalPadding, windowWidth - menuWidth - horizontalPadding); + const maxTop = Math.max(verticalPadding, windowHeight - viewMenuHeight - verticalPadding); + const left = Math.min( + Math.max(viewMenuAnchor.left - menuWidth + horizontalPadding, horizontalPadding), + maxLeft + ); + const top = Math.min(Math.max(viewMenuAnchor.top + verticalPadding, verticalPadding), maxTop); + return { top, left, width: menuWidth }; + }, [ + theme.spacing, + viewMenuAnchor.left, + viewMenuAnchor.top, + viewMenuHeight, + viewMenuVisible, + windowHeight, + windowWidth, + ]); + + const shipMenuPosition = useMemo(() => { + if (!shipMenuVisible) { + return null; + } + const menuWidth = 220; + const horizontalPadding = theme.spacing[2]; + const verticalPadding = theme.spacing[2]; + const maxLeft = Math.max(horizontalPadding, windowWidth - menuWidth - horizontalPadding); + const maxTop = Math.max(verticalPadding, windowHeight - shipMenuHeight - verticalPadding); + const left = Math.min( + Math.max(shipMenuAnchor.left - menuWidth + horizontalPadding, horizontalPadding), + maxLeft + ); + const top = Math.min(Math.max(shipMenuAnchor.top + verticalPadding, verticalPadding), maxTop); + return { top, left, width: menuWidth }; + }, [ + shipMenuAnchor.left, + shipMenuAnchor.top, + shipMenuHeight, + shipMenuVisible, + theme.spacing, + windowHeight, + windowWidth, + ]); + return ( @@ -788,72 +907,101 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { {branchLabel} - {isStatusFetching && ( - - )} + {isStatusFetching && ( + + )} - + {isGit ? ( setDiffMode("uncommitted")} - style={[ - styles.modeToggleButton, - diffMode === "uncommitted" && styles.modeToggleButtonActive, - ]} + testID="changes-view-selector" + onPress={handleOpenViewMenu} + style={styles.viewSelector} + accessibilityRole="button" + accessibilityLabel="Change diff view" > - - Uncommitted + + {diffMode === "uncommitted" ? "Working" : "Base"} + - setDiffMode("base")} - style={[ - styles.modeToggleButton, - diffMode === "base" && styles.modeToggleButtonActive, - ]} - > - - Base - - - + ) : null} {isGit ? ( - - {primaryAction ? ( + + {canShowCommit ? ( commitMutation.mutate()} + disabled={commitDisabled} > - {primaryAction.isPending ? ( + {commitMutation.isPending ? ( ) : ( - {primaryAction.label} + Commit… )} ) : ( - + )} + {canShowShip ? ( + + { + if (!resolvedShipPrimary || shipPrimaryDisabled) return; + await persistShipDefault(resolvedShipPrimary); + if (resolvedShipPrimary === "merge") { + if (mergeDisabled) return; + mergeMutation.mutate(); + return; + } + if (hasPullRequest && prStatus?.url) { + void Linking.openURL(prStatus.url); + return; + } + if (prDisabled) return; + prMutation.mutate(); + }} + disabled={shipPrimaryDisabled} + accessibilityRole="button" + accessibilityLabel="Ship changes" + > + {shipPrimaryPending ? ( + + ) : ( + <> + + {resolvedShipPrimary === "merge" ? "Merge" : prActionLabel} + + + into {baseRefLabel} + + + )} + + + + + + ) : null} {actionStatus} : null} {actionError ? {actionError} : null} {prStatus ? ( - + { + if (!prStatus.url) return; + void Linking.openURL(prStatus.url); + }} + > PR {prStatus.state} {prStatus.url ? `· ${prStatus.url}` : ""} - + ) : null} {prErrorMessage ? ( {prErrorMessage} @@ -925,56 +1082,6 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { - {showCommitAction && primaryAction?.key !== "commit" ? ( - { - if (commitDisabled) return; - handleCloseOverflowMenu(); - commitMutation.mutate(); - }} - disabled={commitDisabled} - accessibilityRole="button" - > - Commit - - ) : null} - - {showCreatePrAction && primaryAction?.key !== "create_pr" ? ( - { - if (prDisabled) return; - handleCloseOverflowMenu(); - prMutation.mutate(); - }} - disabled={prDisabled} - accessibilityRole="button" - > - Create PR - - ) : null} - - {showMergeAction && primaryAction?.key !== "merge" ? ( - { - if (mergeDisabled) return; - handleCloseOverflowMenu(); - mergeMutation.mutate(); - }} - disabled={mergeDisabled} - accessibilityRole="button" - > - Merge to base - - ) : null} - - - + + + + + {viewMenuPosition ? ( + + { + handleCloseViewMenu(); + setDiffMode("uncommitted"); + }} + accessibilityRole="button" + > + Working + + { + handleCloseViewMenu(); + setDiffMode("base"); + }} + accessibilityRole="button" + > + Base + + + ) : null} + + + + + + + {shipMenuPosition ? ( + + { + if (mergeDisabled) return; + handleCloseShipMenu(); + void persistShipDefault("merge"); + mergeMutation.mutate(); + }} + disabled={mergeDisabled} + accessibilityRole="button" + > + Merge into {baseRefLabel} + + {mergeDisabled && hasUncommittedChanges ? ( + Requires a clean working tree. + ) : null} + + + + { + if (prDisabled) return; + handleCloseShipMenu(); + void persistShipDefault("pr"); + if (hasPullRequest && prStatus?.url) { + void Linking.openURL(prStatus.url); + return; + } + prMutation.mutate(); + }} + disabled={prDisabled} + accessibilityRole="button" + > + + {hasPullRequest ? "Open PR" : `Create PR into ${baseRefLabel}`} + + + + ) : null} + + ); } @@ -1030,29 +1261,21 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.medium, flexShrink: 1, }, - modeToggle: { + viewSelector: { flexDirection: "row", alignItems: "center", - backgroundColor: theme.colors.surface2, - borderRadius: theme.borderRadius.lg, - padding: theme.spacing[1], gap: theme.spacing[1], - }, - modeToggleButton: { paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[1], - borderRadius: theme.borderRadius.md, + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface2, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, }, - modeToggleButtonActive: { - backgroundColor: theme.colors.surface0, - }, - modeToggleText: { + viewSelectorText: { fontSize: theme.fontSize.xs, - color: theme.colors.foregroundMuted, - fontWeight: theme.fontWeight.medium, - }, - modeToggleTextActive: { color: theme.colors.foreground, + fontWeight: theme.fontWeight.medium, }, toolbarRow: { flexDirection: "row", @@ -1062,12 +1285,12 @@ const styles = StyleSheet.create((theme) => ({ paddingTop: theme.spacing[2], paddingBottom: theme.spacing[1], }, - toolbarPrimary: { + toolbarLeft: { flex: 1, minWidth: 0, }, - toolbarPrimarySpacer: { - height: 32, + toolbarLeftSpacer: { + height: 36, }, toolbarRight: { flexDirection: "row", @@ -1075,7 +1298,7 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[1], flexShrink: 0, }, - primaryActionButton: { + secondaryActionButton: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.md, @@ -1084,14 +1307,48 @@ const styles = StyleSheet.create((theme) => ({ borderColor: theme.colors.borderAccent, alignSelf: "flex-start", }, - primaryActionButtonDisabled: { + secondaryActionButtonDisabled: { opacity: 0.5, }, - primaryActionText: { + secondaryActionText: { fontSize: theme.fontSize.xs, color: theme.colors.foreground, fontWeight: theme.fontWeight.medium, }, + shipSplitButton: { + flexDirection: "row", + alignItems: "stretch", + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface2, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.borderAccent, + overflow: "hidden", + }, + shipPrimaryButton: { + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + minWidth: 140, + }, + shipPrimaryButtonDisabled: { + opacity: 0.6, + }, + shipPrimaryText: { + fontSize: theme.fontSize.xs, + color: theme.colors.foreground, + fontWeight: theme.fontWeight.medium, + }, + shipSecondaryText: { + marginTop: 2, + fontSize: theme.fontSize.xs, + color: theme.colors.foregroundMuted, + }, + shipCaretButton: { + width: 36, + alignItems: "center", + justifyContent: "center", + borderLeftWidth: theme.borderWidth[1], + borderLeftColor: theme.colors.borderAccent, + }, iconButton: { width: 32, height: 32, @@ -1128,6 +1385,9 @@ const styles = StyleSheet.create((theme) => ({ paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], }, + menuItemSelected: { + backgroundColor: theme.colors.surface2, + }, menuItemDisabled: { opacity: 0.5, }, @@ -1136,6 +1396,12 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foreground, fontWeight: theme.fontWeight.medium, }, + menuHintText: { + paddingHorizontal: theme.spacing[3], + paddingBottom: theme.spacing[2], + fontSize: theme.fontSize.xs, + color: theme.colors.foregroundMuted, + }, menuItemDestructive: { backgroundColor: "rgba(248, 81, 73, 0.08)", },