Add shared trigger components and horizontal desktop workspace arguments

- Add DropdownTrigger and ComboboxTrigger with an overridable chevron slot and a 1 px translateY caret nudge for consistent vertical alignment.

- Convert text-based dropdown/combobox triggers across model selector, agent controls, mode control, settings, and new workspace to use the shared components.

- Lay out New Workspace arguments horizontally on desktop without labels while keeping the mobile vertical stack; align the project icon left edge with the title.
This commit is contained in:
Mohamed Boudra
2026-06-16 14:56:07 +07:00
parent c464c2bcb4
commit 36a136d811
8 changed files with 365 additions and 258 deletions
@@ -12,14 +12,8 @@ import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb as platformIsWeb } from "@/constants/platform";
import {
AlertTriangle,
ChevronDown,
ChevronRight,
Search,
Settings,
Star,
} from "lucide-react-native";
import { AlertTriangle, ChevronRight, Search, Settings, Star } from "lucide-react-native";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { SheetHeader } from "@/components/adaptive-modal-sheet";
import { useProviderSettingsStore } from "@/stores/provider-settings-store";
@@ -784,35 +778,43 @@ export function CombinedModelSelector({
return (
<>
<Pressable
ref={anchorRef}
collapsable={false}
disabled={disabled}
onPress={handleTriggerPress}
style={triggerStyle}
accessibilityRole="button"
accessibilityLabel={t("modelSelector.selectedModel", { model: selectedModelLabel })}
testID="combined-model-selector"
>
{renderTrigger ? (
renderTrigger({
{renderTrigger ? (
<Pressable
ref={anchorRef}
collapsable={false}
disabled={disabled}
onPress={handleTriggerPress}
style={triggerStyle}
accessibilityRole="button"
accessibilityLabel={t("modelSelector.selectedModel", { model: selectedModelLabel })}
testID="combined-model-selector"
>
{renderTrigger({
selectedModelLabel: triggerLabel,
onPress: handleTriggerPress,
disabled,
isOpen,
})
) : (
<>
{ProviderIcon ? (
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
) : null}
<Text style={styles.triggerText} numberOfLines={1} ellipsizeMode="tail">
{triggerLabel}
</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</>
)}
</Pressable>
})}
</Pressable>
) : (
<ComboboxTrigger
ref={anchorRef}
collapsable={false}
disabled={disabled}
onPress={handleTriggerPress}
style={triggerStyle}
accessibilityRole="button"
accessibilityLabel={t("modelSelector.selectedModel", { model: selectedModelLabel })}
testID="combined-model-selector"
>
{ProviderIcon ? (
<ProviderIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
) : null}
<Text style={styles.triggerText} numberOfLines={1} ellipsizeMode="tail">
{triggerLabel}
</Text>
</ComboboxTrigger>
)}
<Combobox
options={EMPTY_COMBOBOX_OPTIONS}
value=""
@@ -0,0 +1,71 @@
import { forwardRef, useCallback, type ReactElement, type ReactNode } from "react";
import {
Pressable,
View,
type PressableProps,
type PressableStateCallbackType,
type ViewStyle,
type StyleProp,
} from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { ChevronDown } from "lucide-react-native";
import { ICON_SIZE, type Theme } from "@/styles/theme";
const ThemedChevronDown = withUnistyles(ChevronDown);
const chevronColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
interface TriggerState {
pressed: boolean;
hovered: boolean;
}
type TriggerStyleProp = StyleProp<ViewStyle> | ((state: TriggerState) => StyleProp<ViewStyle>);
interface ComboboxTriggerProps extends Omit<PressableProps, "style" | "children"> {
style?: TriggerStyleProp;
children?: ReactNode;
chevron?: ReactNode | null;
}
export const ComboboxTrigger = forwardRef<View, ComboboxTriggerProps>(function ComboboxTrigger(
{ children, chevron, style, ...props },
ref,
): ReactElement {
const pressableStyle = useCallback(
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => {
if (typeof style === "function") {
return style({ pressed, hovered });
}
return style;
},
[style],
);
return (
<Pressable ref={ref} collapsable={false} style={pressableStyle} {...props}>
<View style={styles.row}>
{children}
{chevron !== null &&
(chevron ?? (
<View style={styles.chevronContainer}>
<ThemedChevronDown size={ICON_SIZE.sm} uniProps={chevronColorMapping} />
</View>
))}
</View>
</Pressable>
);
});
const styles = StyleSheet.create((theme) => ({
row: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
chevronContainer: {
transform: [{ translateY: 1 }],
},
}));
@@ -303,7 +303,7 @@ interface TriggerState {
}
type TriggerStyleProp = StyleProp<ViewStyle> | ((state: TriggerState) => StyleProp<ViewStyle>);
interface DropdownMenuTriggerProps extends Omit<PressableProps, "style" | "children"> {
export interface DropdownMenuTriggerProps extends Omit<PressableProps, "style" | "children"> {
style?: TriggerStyleProp;
children: ReactNode | ((state: TriggerState) => ReactNode);
}
@@ -0,0 +1,48 @@
import type { ReactElement, ReactNode } from "react";
import { View } from "react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { ChevronDown } from "lucide-react-native";
import { ICON_SIZE, type Theme } from "@/styles/theme";
import { DropdownMenuTrigger, type DropdownMenuTriggerProps } from "@/components/ui/dropdown-menu";
const ThemedChevronDown = withUnistyles(ChevronDown);
interface DropdownTriggerProps extends Omit<DropdownMenuTriggerProps, "children"> {
children?: ReactNode;
chevron?: ReactNode | null;
}
const chevronColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
export function DropdownTrigger({
children,
chevron,
...props
}: DropdownTriggerProps): ReactElement {
return (
<DropdownMenuTrigger {...props}>
<View style={styles.row}>
{children}
{chevron !== null &&
(chevron ?? (
<View style={styles.chevronContainer}>
<ThemedChevronDown size={ICON_SIZE.sm} uniProps={chevronColorMapping} />
</View>
))}
</View>
</DropdownMenuTrigger>
);
}
const styles = StyleSheet.create((theme) => ({
row: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
chevronContainer: {
transform: [{ translateY: 1 }],
},
}));
@@ -20,7 +20,9 @@ import {
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { Brain, ChevronDown, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
import { Brain, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
import { DropdownTrigger } from "@/components/ui/dropdown-trigger";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import {
@@ -37,12 +39,7 @@ import {
toggleFavoriteModel,
useFormPreferences,
} from "@/hooks/use-form-preferences";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { DraftAgentModeControl, AgentModeControl } from "@/composer/agent-controls/mode-control";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
@@ -764,7 +761,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
<>
{providerOptions && providerOptions.length > 0 ? (
<>
<Pressable
<ComboboxTrigger
ref={providerAnchorRef}
collapsable={false}
disabled={disabled || !canSelectProvider}
@@ -775,8 +772,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
testID="agent-provider-selector"
>
<Text style={styles.modeBadgeText}>{displayProvider}</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable>
</ComboboxTrigger>
<Combobox
options={comboboxProviderOptions}
value={selectedProviderId ?? ""}
@@ -821,7 +817,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
<>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild triggerRefProp="ref">
<Pressable
<ComboboxTrigger
ref={thinkingAnchorRef}
collapsable={false}
disabled={disabled || !canSelectThinking}
@@ -835,8 +831,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
>
<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable>
</ComboboxTrigger>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>{t(getAgentControlHintKey("thinking"))}</Text>
@@ -1173,7 +1168,7 @@ function DesktopFeatureItem({
<DropdownMenu open={openSelector === featureSelector} onOpenChange={handleFeatureOpenChange}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild triggerRefProp="ref">
<DropdownMenuTrigger
<DropdownTrigger
disabled={disabled}
style={selectPressableStyle}
accessibilityRole="button"
@@ -1182,8 +1177,7 @@ function DesktopFeatureItem({
>
<FeatureIcon size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<Text style={styles.modeBadgeText}>{selectedOption?.label ?? feature.label}</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
</DropdownTrigger>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>{getFeatureTooltip(feature)}</Text>
@@ -1288,7 +1282,7 @@ function SheetFeatureItem({
open={openSelector === featureSelector}
onOpenChange={handleFeatureOpenChange}
>
<DropdownMenuTrigger
<DropdownTrigger
disabled={disabled}
style={togglePressableStyle}
accessibilityRole="button"
@@ -1296,8 +1290,7 @@ function SheetFeatureItem({
testID={`agent-feature-${feature.id}`}
>
<Text style={styles.sheetSelectText}>{selectedOption?.label ?? feature.label}</Text>
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger>
</DropdownTrigger>
<DropdownMenuContent side="top" align="start">
{feature.options.map((option) => (
<FeatureOptionMenuItem
@@ -8,18 +8,12 @@ import {
type ReactElement,
} from "react";
import { useTranslation } from "react-i18next";
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
import { Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
import {
Bot,
ChevronDown,
ShieldAlert,
ShieldCheck,
ShieldOff,
ShieldQuestionMark,
} from "lucide-react-native";
import { Bot, ShieldAlert, ShieldCheck, ShieldOff, ShieldQuestionMark } from "lucide-react-native";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { type SheetHeader } from "@/components/adaptive-modal-sheet";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { useSessionStore } from "@/stores/session-store";
@@ -198,7 +192,7 @@ function AgentModeControlView({
return (
<>
<Pressable
<ComboboxTrigger
ref={anchorRef}
collapsable={false}
disabled={disabled}
@@ -212,8 +206,7 @@ function AgentModeControlView({
>
{Icon ? <Icon size={theme.iconSize.md} color={iconColor} /> : null}
<Text style={labelStyle}>{selectedModeLabel}</Text>
<ChevronDown size={theme.iconSize.sm} color={iconColor} />
</Pressable>
</ComboboxTrigger>
<Combobox
options={options}
value={selectedMode.id}
+184 -175
View File
@@ -8,7 +8,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { createNameId } from "mnemonic-id";
import { useQuery } from "@tanstack/react-query";
import { Check, ChevronDown, Folder, GitBranch, GitPullRequest, X } from "lucide-react-native";
import { Check, Folder, GitBranch, GitPullRequest, X } from "lucide-react-native";
import { Composer } from "@/composer";
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
import { splitComposerAttachmentsForSubmit } from "@/composer/attachments/submit";
@@ -16,6 +16,7 @@ import { FileDropZone } from "@/components/file-drop-zone";
import { ProjectIconView } from "@/components/project-icon-view";
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { SidebarMenuToggle } from "@/components/headers/menu-header";
@@ -166,7 +167,6 @@ function RefPickerBadgeContent({
<Text style={styles.badgeText} numberOfLines={1}>
{triggerLabel}
</Text>
<ChevronDown size={iconSize} color={iconColor} />
</>
);
}
@@ -197,7 +197,7 @@ function RefPickerTrigger({
return (
<Tooltip>
<TooltipTrigger asChild triggerRefProp="ref">
<Pressable
<ComboboxTrigger
ref={pickerAnchorRef}
testID="new-workspace-ref-picker-trigger"
onPress={onPress}
@@ -212,7 +212,7 @@ function RefPickerTrigger({
iconColor={iconColor}
iconSize={iconSize}
/>
</Pressable>
</ComboboxTrigger>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>{tooltipLabel}</Text>
@@ -247,7 +247,7 @@ function ProjectPickerTrigger({
return (
<Tooltip>
<TooltipTrigger asChild triggerRefProp="ref">
<Pressable
<ComboboxTrigger
ref={pickerAnchorRef}
testID="new-workspace-project-picker-trigger"
onPress={onPress}
@@ -273,8 +273,7 @@ function ProjectPickerTrigger({
<Text style={styles.badgeText} numberOfLines={1}>
{label}
</Text>
<ChevronDown size={iconSize} color={iconColor} />
</Pressable>
</ComboboxTrigger>
</TooltipTrigger>
<TooltipContent side="top" align="center" offset={8}>
<Text style={styles.tooltipText}>Choose project</Text>
@@ -652,7 +651,7 @@ function IsolationPickerTrigger({
iconSize: number;
}) {
return (
<Pressable
<ComboboxTrigger
ref={pickerAnchorRef}
testID="workspace-create-backing-trigger"
onPress={onPress}
@@ -671,21 +670,14 @@ function IsolationPickerTrigger({
<Text style={styles.badgeText} numberOfLines={1}>
{label}
</Text>
<ChevronDown size={iconSize} color={iconColor} />
</Pressable>
</ComboboxTrigger>
);
}
// Each row aligns its label glyph with the screen heading glyph and lets the
// label's natural width push the control. The label and control sit side by
// side with no held horizontal space.
function LabeledRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
{children}
</View>
);
// Wraps a single argument control in the mobile vertical stack. On desktop the
// controls are laid out in one horizontal row, so no per-control wrapper is used.
function FormRow({ children }: { children: React.ReactNode }) {
return <View style={styles.row}>{children}</View>;
}
interface WorkspaceBackingState {
@@ -1533,155 +1525,167 @@ export function NewWorkspaceScreen({
const backingTriggerLabel = backingLabel(t, effectiveBacking);
const formStack = useMemo(
() => (
<View testID="new-workspace-ref-picker-row" style={styles.formStack}>
<LabeledRow label={t("newWorkspace.fields.project")}>
<View>
<ProjectPickerTrigger
pickerAnchorRef={projectPickerAnchorRef}
onPress={openProjectPicker}
disabled={isPending || projectPickerOptions.length === 0}
badgePressableStyle={badgePressableStyle}
label={projectTriggerLabel}
projectKey={selectedProject?.projectKey ?? null}
iconDataUri={
selectedProject
? (projectIconDataByProjectKey.get(selectedProject.projectKey) ?? null)
: null
}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={projectPickerOptions}
value={selectedProjectOptionId}
onSelect={handleSelectProjectOption}
searchable
searchPlaceholder="Search projects"
title="Project"
open={projectPickerOpen}
onOpenChange={handleProjectPickerOpenChange}
desktopPlacement="bottom-start"
anchorRef={projectPickerAnchorRef}
emptyText="No projects available."
renderOption={renderProjectOption}
/>
</View>
</LabeledRow>
{/* The Isolation row keeps its height for non-git projects so switching
projects never shifts the form; worktree backing is git-only, so a
non-git project renders an invisible spacer matching the trigger
height exactly. */}
{canCreateWorktree ? (
<LabeledRow label={t("newWorkspace.backing.label")}>
<View>
<IsolationPickerTrigger
pickerAnchorRef={backingPickerAnchorRef}
onPress={openBackingPicker}
disabled={isPending}
badgePressableStyle={badgePressableStyle}
backing={effectiveBacking}
label={backingTriggerLabel}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={backingOptions}
value={effectiveBacking}
onSelect={handleSelectBackingOption}
title={t("newWorkspace.backing.label")}
open={backingPickerOpen}
onOpenChange={handleBackingPickerOpenChange}
desktopPlacement="bottom-start"
anchorRef={backingPickerAnchorRef}
renderOption={renderBackingOption}
/>
</View>
</LabeledRow>
) : (
<View style={styles.baseSpacer} />
)}
{/* The Base row keeps its height so toggling Isolation never shifts the
form; on Local backing it renders an invisible spacer with no label
or control, matching the trigger height exactly. */}
{showRefPicker ? (
<LabeledRow label={t("newWorkspace.fields.base")}>
<View>
<RefPickerTrigger
pickerAnchorRef={pickerAnchorRef}
onPress={openPicker}
disabled={isPending || !selectedSourceDirectory}
badgePressableStyle={badgePressableStyle}
selectedItem={selectedItem}
triggerLabel={triggerLabel}
accessibilityLabel={t("newWorkspace.refPicker.startingRef")}
tooltipLabel={t("newWorkspace.refPicker.chooseStart")}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={options}
value={selectedOptionId}
onSelect={handleSelectOption}
searchable
searchPlaceholder={t("newWorkspace.refPicker.searchPlaceholder")}
title={t("newWorkspace.refPicker.title")}
open={pickerOpen}
onOpenChange={handlePickerOpenChange}
onSearchQueryChange={setPickerSearchQuery}
desktopPlacement="bottom-start"
anchorRef={pickerAnchorRef}
emptyText={pickerEmptyText}
renderOption={renderPickerOption}
/>
</View>
</LabeledRow>
) : (
<View style={styles.baseSpacer} />
)}
const formStack = useMemo(() => {
const projectControl = (
<View>
<ProjectPickerTrigger
pickerAnchorRef={projectPickerAnchorRef}
onPress={openProjectPicker}
disabled={isPending || projectPickerOptions.length === 0}
badgePressableStyle={badgePressableStyle}
label={projectTriggerLabel}
projectKey={selectedProject?.projectKey ?? null}
iconDataUri={
selectedProject
? (projectIconDataByProjectKey.get(selectedProject.projectKey) ?? null)
: null
}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={projectPickerOptions}
value={selectedProjectOptionId}
onSelect={handleSelectProjectOption}
searchable
searchPlaceholder="Search projects"
title="Project"
open={projectPickerOpen}
onOpenChange={handleProjectPickerOpenChange}
desktopPlacement="bottom-start"
anchorRef={projectPickerAnchorRef}
emptyText="No projects available."
renderOption={renderProjectOption}
/>
</View>
),
[
backingOptions,
backingPickerOpen,
backingTriggerLabel,
badgePressableStyle,
canCreateWorktree,
effectiveBacking,
handleBackingPickerOpenChange,
handlePickerOpenChange,
handleProjectPickerOpenChange,
handleSelectBackingOption,
handleSelectOption,
handleSelectProjectOption,
isPending,
openBackingPicker,
openPicker,
openProjectPicker,
options,
pickerEmptyText,
pickerOpen,
projectPickerOpen,
projectPickerOptions,
projectTriggerLabel,
projectIconDataByProjectKey,
renderBackingOption,
renderPickerOption,
renderProjectOption,
selectedItem,
selectedOptionId,
selectedProject,
selectedProjectOptionId,
selectedSourceDirectory,
setPickerSearchQuery,
showRefPicker,
t,
theme.colors.foregroundMuted,
theme.iconSize.sm,
triggerLabel,
],
);
);
const isolationControl = canCreateWorktree ? (
<View>
<IsolationPickerTrigger
pickerAnchorRef={backingPickerAnchorRef}
onPress={openBackingPicker}
disabled={isPending}
badgePressableStyle={badgePressableStyle}
backing={effectiveBacking}
label={backingTriggerLabel}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={backingOptions}
value={effectiveBacking}
onSelect={handleSelectBackingOption}
title={t("newWorkspace.backing.label")}
open={backingPickerOpen}
onOpenChange={handleBackingPickerOpenChange}
desktopPlacement="bottom-start"
anchorRef={backingPickerAnchorRef}
renderOption={renderBackingOption}
/>
</View>
) : null;
const baseControl = showRefPicker ? (
<View>
<RefPickerTrigger
pickerAnchorRef={pickerAnchorRef}
onPress={openPicker}
disabled={isPending || !selectedSourceDirectory}
badgePressableStyle={badgePressableStyle}
selectedItem={selectedItem}
triggerLabel={triggerLabel}
accessibilityLabel={t("newWorkspace.refPicker.startingRef")}
tooltipLabel={t("newWorkspace.refPicker.chooseStart")}
iconColor={theme.colors.foregroundMuted}
iconSize={theme.iconSize.sm}
/>
<Combobox
options={options}
value={selectedOptionId}
onSelect={handleSelectOption}
searchable
searchPlaceholder={t("newWorkspace.refPicker.searchPlaceholder")}
title={t("newWorkspace.refPicker.title")}
open={pickerOpen}
onOpenChange={handlePickerOpenChange}
onSearchQueryChange={setPickerSearchQuery}
desktopPlacement="bottom-start"
anchorRef={pickerAnchorRef}
emptyText={pickerEmptyText}
renderOption={renderPickerOption}
/>
</View>
) : null;
if (isCompact) {
return (
<View testID="new-workspace-ref-picker-row" style={styles.formStack}>
<FormRow>{projectControl}</FormRow>
{/* The Isolation row keeps its height for non-git projects so switching
projects never shifts the form; worktree backing is git-only, so a
non-git project renders an invisible spacer matching the trigger
height exactly. */}
{isolationControl ? (
<FormRow>{isolationControl}</FormRow>
) : (
<View style={styles.baseSpacer} />
)}
{/* The Base row keeps its height so toggling Isolation never shifts the
form; on Local backing it renders an invisible spacer with no label
or control, matching the trigger height exactly. */}
{baseControl ? <FormRow>{baseControl}</FormRow> : <View style={styles.baseSpacer} />}
</View>
);
}
return (
<View testID="new-workspace-ref-picker-row" style={styles.formStackDesktop}>
{projectControl}
{isolationControl}
{baseControl}
</View>
);
}, [
backingOptions,
backingPickerOpen,
backingTriggerLabel,
badgePressableStyle,
canCreateWorktree,
effectiveBacking,
handleBackingPickerOpenChange,
handlePickerOpenChange,
handleProjectPickerOpenChange,
handleSelectBackingOption,
handleSelectOption,
handleSelectProjectOption,
isCompact,
isPending,
openBackingPicker,
openPicker,
openProjectPicker,
options,
pickerEmptyText,
pickerOpen,
projectIconDataByProjectKey,
projectPickerOpen,
projectPickerOptions,
projectTriggerLabel,
renderBackingOption,
renderPickerOption,
renderProjectOption,
selectedItem,
selectedOptionId,
selectedProject,
selectedProjectOptionId,
selectedSourceDirectory,
setPickerSearchQuery,
showRefPicker,
t,
theme.colors.foregroundMuted,
theme.iconSize.sm,
triggerLabel,
]);
const composerFooter = useMemo(
() => (
@@ -1804,19 +1808,24 @@ const styles = StyleSheet.create((theme) => ({
marginBottom: theme.spacing[8],
gap: theme.spacing[2],
},
formStackDesktop: {
flexDirection: "row",
alignItems: "center",
marginBottom: theme.spacing[8],
// The badge adds its own left padding; offset it so the project icon's left
// edge lands exactly on the "New workspace" title's left edge.
paddingLeft: theme.spacing[4],
gap: theme.spacing[2],
},
// The row's left inset matches the heading's text x (composerTitleContainer
// paddingLeft) so each label glyph aligns with the "New workspace" glyph. The
// label keeps its natural width and the control sits immediately beside it.
// paddingLeft) so the control aligns with the "New workspace" glyph. The badge
// adds its own left padding, so the row inset is reduced by that amount.
row: {
flexDirection: "row",
alignItems: "center",
paddingLeft: theme.spacing[6],
paddingLeft: theme.spacing[4],
gap: theme.spacing[1],
},
rowLabel: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
baseSpacer: {
height: BADGE_HEIGHT,
},
+9 -18
View File
@@ -26,7 +26,6 @@ import type { TFunction } from "i18next";
import { Buffer } from "buffer";
import {
ArrowLeft,
ChevronDown,
Monitor,
Settings,
Palette,
@@ -43,6 +42,8 @@ import {
FolderGit2,
SquareTerminal,
} from "lucide-react-native";
import { DropdownTrigger } from "@/components/ui/dropdown-trigger";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
import { SidebarSeparator } from "@/components/sidebar/sidebar-separator";
import { ScreenTitle } from "@/components/headers/screen-title";
@@ -78,12 +79,7 @@ import { KeyboardShortcutsSection } from "@/screens/settings/keyboard-shortcuts-
import { Button } from "@/components/ui/button";
import { CommunityLinks } from "@/components/community-links";
import { SegmentedControl } from "@/components/ui/segmented-control";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { IntegrationsSection } from "@/desktop/components/integrations-section";
@@ -312,10 +308,8 @@ function GeneralSection({
handleLanguageChange,
handleTerminalScrollbackLinesChange,
}: GeneralSectionProps) {
const { theme } = useUnistyles();
const { t, i18n } = useTranslation();
const activeLocale = getActiveLocale(i18n.language);
const iconColor = theme.colors.foregroundMuted;
const sendBehaviorOptions = useMemo(() => getSendBehaviorOptions(t), [t]);
const sendBehaviorDescriptionKey =
settings.sendBehavior === "interrupt"
@@ -377,14 +371,13 @@ function GeneralSection({
<Text style={settingsStyles.rowHint}>{t("settings.general.language.description")}</Text>
</View>
<DropdownMenu>
<DropdownMenuTrigger
<DropdownTrigger
accessibilityRole="button"
accessibilityLabel={selectedLanguageLabel}
style={themeTriggerStyle}
>
<Text style={styles.themeTriggerText}>{selectedLanguageLabel}</Text>
<ChevronDown size={theme.iconSize.sm} color={iconColor} />
</DropdownMenuTrigger>
</DropdownTrigger>
<DropdownMenuContent side="bottom" align="end" width={220}>
{LANGUAGE_OPTIONS.map((option) => (
<LanguageMenuItem
@@ -407,12 +400,11 @@ function GeneralSection({
</Text>
</View>
<DropdownMenu>
<DropdownMenuTrigger style={themeTriggerStyle}>
<DropdownTrigger style={themeTriggerStyle}>
<Text style={styles.themeTriggerText}>
{getServiceUrlBehaviorLabel(t, settings.serviceUrlBehavior)}
</Text>
<ChevronDown size={theme.iconSize.sm} color={iconColor} />
</DropdownMenuTrigger>
</DropdownTrigger>
<DropdownMenuContent side="bottom" align="end" width={200}>
{SERVICE_URL_BEHAVIOR_VALUES.map((value) => (
<ServiceUrlBehaviorMenuItem
@@ -1067,7 +1059,7 @@ function HostPicker({
return (
<>
<Pressable
<ComboboxTrigger
ref={triggerRef}
style={triggerStyle}
onPress={handleOpen}
@@ -1079,8 +1071,7 @@ function HostPicker({
<Text style={sidebarStyles.pickerTriggerLabel} numberOfLines={1}>
{activeHost?.label ?? t("settings.groups.host")}
</Text>
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
</Pressable>
</ComboboxTrigger>
<Combobox
options={options}
value={activeServerId ?? ""}