From e191c883f03672d9b3e10c349b1c65cea66239e0 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:47:22 -0700 Subject: [PATCH] fix(mobile): dictation buttons line up with the send button on every composer On the tall iPad/sidebar composer the dictation confirm button sat flush against the composer's rounded edge, while the send button is inset. The confirm button now uses the send button's 62pt slot and Android scaling, the dictation toolbar's stop/resume/send buttons get the large size, and the toolbar uses the same vertical padding as the draft row (it sat 4pt low on the Android floating composer). Co-Authored-By: Claude Opus 5.5 --- .../src/features/threads/ThreadComposer.tsx | 8 +++++-- .../voice-input/ComposerDictationControl.tsx | 22 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 3f5529b345f3a..54e142d6623ca 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -576,6 +576,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps const floatingSidebarComposer = useAndroidFloatingTabs() && layout.usesSplitView; const usesLargeControls = useSidebarWorkspaceTabs() === "picker" || (Platform.OS === "ios" && Platform.isPad); + const surfacePaddingVertical = usesLargeControls ? 9 : floatingSidebarComposer ? 6 : 2; const [compactRowWidth, setCompactRowWidth] = useState(0); const compactAttachmentLimit = compactRowWidth >= 500 ? 3 : 1; const compactEditor = useCompactComposerEditor(bodyText, draftMessage); @@ -871,7 +872,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps borderRadius: usesLargeControls ? 31 : 27, borderCurve: "circular", overflow: "hidden", - paddingVertical: usesLargeControls ? 9 : floatingSidebarComposer ? 6 : 2, + paddingVertical: surfacePaddingVertical, }} > {isExpanded ? editingAttachments : null} @@ -1122,7 +1123,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps collapsable={false} importantForAccessibility={isToolbarVisible ? "auto" : "no-hide-descendants"} pointerEvents={isToolbarVisible ? "auto" : "none"} - style={{ position: "absolute", bottom: usesLargeControls ? 9 : 2, left: 0, right: 0 }} + style={{ position: "absolute", bottom: surfacePaddingVertical, left: 0, right: 0 }} > {showStopAction ? ( ) : showResumeAction && voicePresentation.showsSend ? ( ) : voicePresentation.showsSend ? ( void; readonly variant?: "plain" | "primary"; }) { + const { scale } = useAndroidControlSizing(); const variant = props.variant ?? "plain"; const loading = props.loading === true; + const large = props.size === "large"; + const circleSize = Math.round((large ? 44 : 30) * scale); + const iconSize = Math.round( + (variant === "primary" ? (large ? 20 : 16) : large ? 24 : 20) * scale, + ); const primaryStyle = useAnimatedStyle(() => ({ opacity: withTiming(loading ? 0 : 1, DICTATION_TIMING), })); @@ -272,17 +279,18 @@ function VoiceActionButton(props: { className="size-[44px] shrink-0 items-center justify-center active:opacity-70" disabled={props.disabled} onPress={props.onPress} - style={{ opacity: props.disabled && !props.loading ? 0.4 : 1 }} + style={{ + opacity: props.disabled && !props.loading ? 0.4 : 1, + // Matches ComposerActionButton so the circle stays concentric with the large composer corner. + width: variant === "primary" && large ? 62 : undefined, + }} > {variant === "primary" ? (