From e4c1c03c196db961c774eac67d99c381d0afacbb Mon Sep 17 00:00:00 2001 From: Igor Abramkin Date: Thu, 29 Oct 2026 13:13:08 +0300 Subject: [PATCH 1/3] fix(clients): drop the T3 Code wordmark from the sidebar and mobile headers The sidebar top row and the mobile list headers spent space on the app's name. Native apps leave that row to the window controls and content, as Apple's HIG branding guidance recommends. Dev and Nightly builds keep their stage artwork and badge. Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/Stack.tsx | 4 +- .../src/components/CompactBrandTitle.tsx | 110 ------------------ .../src/components/HeaderStageBadge.tsx | 62 ++++++++++ .../src/features/home/ThreadListToolbar.tsx | 1 - .../home/WorkspaceConnectionTitle.tsx | 14 +-- .../PullRequestToolbar.android.tsx | 1 - .../threads/ThreadNavigationSidebar.tsx | 4 +- .../threads/sidebar-navigation-shell.tsx | 4 +- .../workspace/WorkspaceListToolbarRow.tsx | 11 +- .../workspace/WorkspaceRootScreen.tsx | 11 +- apps/mobile/src/lib/mobileBranding.ts | 6 +- apps/web/src/components/AppSidebarLayout.tsx | 19 +-- .../src/components/sidebar/SidebarChrome.tsx | 62 +--------- apps/web/src/components/threadSidebarWidth.ts | 16 +-- 14 files changed, 100 insertions(+), 225 deletions(-) delete mode 100644 apps/mobile/src/components/CompactBrandTitle.tsx create mode 100644 apps/mobile/src/components/HeaderStageBadge.tsx diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 7a0042903a3c3..55426baa157b7 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -21,7 +21,7 @@ import { useResolveClassNames } from "uniwind"; import { AppText as Text } from "./components/ui/AppText"; import { androidWorkspaceDestinationAnimation } from "./lib/android-workspace-navigation"; -import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle"; +import { getStageBadgeHeaderOptions } from "./components/HeaderStageBadge"; import { WorkspaceConnectionStatusProvider } from "./features/home/WorkspaceConnectionTitle"; import { WorkspaceRootScreen } from "./features/workspace/WorkspaceRootScreen"; import { @@ -620,7 +620,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ ...FIXED_DUO_HEADER, contentStyle: { backgroundColor: "transparent" }, headerBackVisible: false, - ...getCompactBrandHeaderOptions(), + ...getStageBadgeHeaderOptions(), }, }), PullRequest: createNativeStackScreen({ diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx deleted file mode 100644 index 7a251a2e57c16..0000000000000 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { NATIVE_WORKSPACE_COLUMNS_SUPPORTED } from "../native/NativeWorkspaceColumns"; -import Constants from "expo-constants"; -import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; -import { useState } from "react"; -import { Platform, View } from "react-native"; - -import { AppText as Text } from "./ui/AppText"; -import { T3Wordmark } from "./T3Wordmark"; -import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; -import { resolveMobileStageLabel } from "../lib/mobileBranding"; -import { useAndroidControlSizing } from "./ui/useAndroidControlSizing"; - -const BRAND_MAX_FONT_SCALE = 1.4; - -export function brandTitleOffset(): number { - if (Platform.OS !== "ios") return 0; - return Platform.isPad && !NATIVE_WORKSPACE_COLUMNS_SUPPORTED ? IPAD_HOME_TITLE_OFFSET : 0; -} - -export function CompactBrandTitle( - props: { - readonly allowFontScaling?: boolean; - readonly accessibilityLabel?: string; - /** Fills the row and hides the stage badge when the full title does not fit. */ - readonly fit?: boolean; - } = {}, -) { - const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); - const titleOffset = brandTitleOffset(); - const { scale } = useAndroidControlSizing(); - const [availableWidth, setAvailableWidth] = useState(null); - // Measured only while the badge shows, so hiding it does not make it look like it fits. - const [fullWidth, setFullWidth] = useState(null); - const showStage = - !props.fit || availableWidth === null || fullWidth === null || fullWidth <= availableWidth; - - const title = ( - setFullWidth(event.nativeEvent.layout.width) : undefined - } - aria-level={1} - accessibilityLabel={props.accessibilityLabel ?? "T3 Code, Threads"} - accessible - role="heading" - className="flex-row items-center gap-1.5" - style={[ - { marginLeft: titleOffset }, - Platform.OS === "android" && { gap: 5.25 * scale }, - props.fit && { flexShrink: 0 }, - ]} - > - - {/* A logo beside a fixed-size wordmark: it may grow with Dynamic Type, not crowd the bar. */} - - Code - - {showStage ? ( - - - {stageLabel} - - - ) : null} - - ); - - return props.fit ? ( - setAvailableWidth(event.nativeEvent.layout.width)} - > - {title} - - ) : ( - title - ); -} - -function renderCompactBrandTitle() { - return ; -} - -export function getCompactBrandHeaderOptions( - fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"], -): NativeStackNavigationOptions { - return { - headerTitle: renderCompactBrandTitle, - headerTitleStyle: fallbackTitleStyle, - title: "Threads", - unstable_headerLeftItems: undefined, - }; -} diff --git a/apps/mobile/src/components/HeaderStageBadge.tsx b/apps/mobile/src/components/HeaderStageBadge.tsx new file mode 100644 index 0000000000000..b936fe64b57fc --- /dev/null +++ b/apps/mobile/src/components/HeaderStageBadge.tsx @@ -0,0 +1,62 @@ +import { NATIVE_WORKSPACE_COLUMNS_SUPPORTED } from "../native/NativeWorkspaceColumns"; +import Constants from "expo-constants"; +import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; +import { Platform, View } from "react-native"; + +import { AppText as Text } from "./ui/AppText"; +import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; +import { resolveMobileStageLabel } from "../lib/mobileBranding"; +import { useAndroidControlSizing } from "./ui/useAndroidControlSizing"; + +const BADGE_MAX_FONT_SCALE = 1.4; + +export function headerTitleOffset(): number { + if (Platform.OS !== "ios") return 0; + return Platform.isPad && !NATIVE_WORKSPACE_COLUMNS_SUPPORTED ? IPAD_HOME_TITLE_OFFSET : 0; +} + +/** Marks Dev and Nightly builds; release builds show nothing in this slot. */ +export function HeaderStageBadge(props: { readonly allowFontScaling?: boolean } = {}) { + const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); + const { scale } = useAndroidControlSizing(); + if (stageLabel === null) return null; + + return ( + + + {stageLabel} + + + ); +} + +function renderHeaderStageBadge() { + return ; +} + +export function getStageBadgeHeaderOptions( + fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"], +): NativeStackNavigationOptions { + return { + headerTitle: renderHeaderStageBadge, + headerTitleStyle: fallbackTitleStyle, + title: "Threads", + unstable_headerLeftItems: undefined, + }; +} diff --git a/apps/mobile/src/features/home/ThreadListToolbar.tsx b/apps/mobile/src/features/home/ThreadListToolbar.tsx index 9e8c55fd2b5e5..8bf397d28d682 100644 --- a/apps/mobile/src/features/home/ThreadListToolbar.tsx +++ b/apps/mobile/src/features/home/ThreadListToolbar.tsx @@ -112,7 +112,6 @@ export function ThreadListToolbar(props: { onCloseSearch={closeSearch} onOpenSearch={openSearch} searchAccessibilityLabel="Search threads" - titleAccessibilityLabel="T3 Code, Threads" onOpenEnvironments={props.onOpenEnvironments} filter={filterButton} /> diff --git a/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx b/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx index 58cf4138483a2..e29f3e12feca7 100644 --- a/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx +++ b/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx @@ -7,10 +7,10 @@ import { SymbolView } from "../../components/ui/AppSymbol"; import { AppText as Text } from "../../components/ui/AppText"; import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing"; import { - brandTitleOffset, - CompactBrandTitle, - getCompactBrandHeaderOptions, -} from "../../components/CompactBrandTitle"; + getStageBadgeHeaderOptions, + HeaderStageBadge, + headerTitleOffset, +} from "../../components/HeaderStageBadge"; import { useDeadlinePassed } from "../../lib/clock"; import { useWorkspaceState } from "../../state/workspace"; import { @@ -169,13 +169,13 @@ export function getConnectionAwareBrandHeaderOptions(opts: { const maxWidth = Math.max(0, opts.headerWidth - 64 - 44 * (opts.trailingItemCount ?? 1)); return { - ...getCompactBrandHeaderOptions(opts.fallbackTitleStyle), + ...getStageBadgeHeaderOptions(opts.fallbackTitleStyle), headerTitle: () => ( } + brand={} maxWidth={maxWidth} onPress={opts.onOpenEnvironments} - statusOffset={brandTitleOffset()} + statusOffset={headerTitleOffset()} /> ), }; diff --git a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx index 6ba565239dcb2..c958ba81c6739 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx @@ -74,7 +74,6 @@ export function PullRequestToolbar(props: { onCloseSearch={closeSearch} onOpenSearch={openSearch} searchAccessibilityLabel="Search pull requests" - titleAccessibilityLabel="T3 Code, Pull requests" onOpenEnvironments={() => openSettings(navigation, "SettingsEnvironments")} filter={ filters ? ( diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 6ba0583a7a541..2ae793074161c 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -26,7 +26,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { SearchBarCommands } from "react-native-screens"; import { AppText as Text } from "../../components/ui/AppText"; -import { CompactBrandTitle } from "../../components/CompactBrandTitle"; +import { HeaderStageBadge } from "../../components/HeaderStageBadge"; import { Menu } from "../../components/ui/Menu"; import { SymbolView } from "../../components/ui/AppSymbol"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; @@ -865,7 +865,7 @@ function ThreadNavigationSidebarPane( size="pageTitle" brand={ - + } /> diff --git a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx index 0eeb394af87a1..dfb8e1a9d749b 100644 --- a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx +++ b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx @@ -4,7 +4,7 @@ import { type NativeStackNavigationOptions } from "@react-navigation/native-stac import type { ReactNode } from "react"; import { Platform } from "react-native"; -import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle"; +import { getStageBadgeHeaderOptions } from "../../components/HeaderStageBadge"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme"; @@ -22,7 +22,7 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { headerShadowVisible: false, headerShown: true, headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined, - ...getCompactBrandHeaderOptions({ fontSize: 18, fontWeight: "800" }), + ...getStageBadgeHeaderOptions({ fontSize: 18, fontWeight: "800" }), headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined, unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, diff --git a/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx b/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx index 07515a8d7125c..c4281c57c8c2a 100644 --- a/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { View } from "react-native"; import { AndroidHeaderIconButton } from "../../components/ui/AndroidScreenHeader"; -import { CompactBrandTitle } from "../../components/CompactBrandTitle"; +import { HeaderStageBadge } from "../../components/HeaderStageBadge"; import { useToolbarLayout } from "../../components/ui/useToolbarLayout"; import { WorkspaceConnectionTitle } from "../home/WorkspaceConnectionTitle"; @@ -17,7 +17,6 @@ export function WorkspaceListToolbarRow(props: { readonly onCloseSearch: () => void; readonly onOpenSearch: () => void; readonly searchAccessibilityLabel: string; - readonly titleAccessibilityLabel: string; readonly onOpenEnvironments: () => void; /** Stays trailing while searching, so it never moves and still narrows the results. */ readonly filter: ReactNode; @@ -41,13 +40,7 @@ export function WorkspaceListToolbarRow(props: { - } + brand={} /> void; }) { - const { state, settingsOpen } = useWorkspaceTabs()!; const maxWidth = Math.max(0, props.columnWidth - (use(HeaderItemsWidthContext) ?? 160)); - const enabled = useEnabledNavigationTabs(); - const tabTitle = settingsOpen - ? "Settings" - : enabled.length - ? TAB_TITLES[state.selectedTab] - : "Navigation"; return ( } + brand={} maxWidth={maxWidth} onPress={props.onOpenEnvironments} /> diff --git a/apps/mobile/src/lib/mobileBranding.ts b/apps/mobile/src/lib/mobileBranding.ts index c00dd9770275e..789b6407a7211 100644 --- a/apps/mobile/src/lib/mobileBranding.ts +++ b/apps/mobile/src/lib/mobileBranding.ts @@ -1,7 +1,7 @@ -export type MobileStageLabel = "Beta" | "Dev" | "Nightly"; +export type MobileStageLabel = "Dev" | "Nightly"; -export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel { +export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel | null { if (appVariant === "development") return "Dev"; if (appVariant === "preview") return "Nightly"; - return "Beta"; + return null; } diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 6c8837e63f40a..647edacb4282c 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -39,7 +39,6 @@ import { } from "../panelAnimations"; import { BackgroundThreadAnnouncer } from "./BackgroundThreadAnnouncer"; import ThreadSidebar from "./Sidebar"; -import { SidebarBrandWidthProbe } from "./sidebar/SidebarChrome"; import { isAppModalPage } from "./sidebar/mainAppLocation"; import type { SidebarRouteReveal } from "./sidebar/routeReveal"; import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; @@ -47,8 +46,8 @@ import { clampThreadSidebarWidth, resolveInitialThreadSidebarWidth, resolveThreadSidebarMaximumWidth, - resolveThreadSidebarMinimumWidth, THREAD_MAIN_CONTENT_MIN_WIDTH, + THREAD_SIDEBAR_MIN_WIDTH, THREAD_SIDEBAR_WIDTH_STORAGE_KEY, } from "./threadSidebarWidth"; import { @@ -374,7 +373,12 @@ function SidebarControl() {
{ try { removeLocalStorageItem(THREAD_SIDEBAR_WIDTH_STORAGE_KEY); @@ -436,7 +438,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { : {} ) as CSSProperties; const sidebarProviderStyle = { - "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`, + "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, THREAD_SIDEBAR_MIN_WIDTH, sidebarMaximumWidth)}px`, "--panel-animation-duration": `${panelAnimationDurationMs}ms`, ...windowControlsInsetStyle, } as CSSProperties; @@ -449,7 +451,6 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { defaultOpen style={sidebarProviderStyle} > - nextWidth <= currentWidth || wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index e233a4144d435..3ca92e00b0826 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -1,12 +1,11 @@ import { ArrowLeftIcon, ChartNoAxesColumnIcon, SettingsIcon } from "lucide-react"; import type { ReactNode } from "react"; import { memo } from "react"; -import { Link, useLocation, useNavigate } from "@tanstack/react-router"; +import { useLocation, useNavigate } from "@tanstack/react-router"; import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { usePullRequestsSupported } from "../../state/environments"; -import { T3Wordmark } from "../T3Wordmark"; import { resolveEnvironmentIdentificationPillLabel, resolveSidebarStageBackdropVariant, @@ -29,7 +28,6 @@ import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; import { SidebarProviderUpdatePill } from "./SidebarProviderUpdatePill"; import { SidebarUpdateArchitectureWarning, SidebarUpdatePill } from "./SidebarUpdatePill"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; -import { observeResize } from "~/lib/observeResize"; export const SidebarChromeHeader = memo(function SidebarChromeHeader({ isElectron, @@ -63,9 +61,8 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ />
- {pillLabel ? ( -
+
{pillLabel} @@ -76,61 +73,6 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ ); }); -export function SidebarBrandWidthProbe({ - onWidthChange, -}: { - onWidthChange: (width: number) => void; -}) { - const observeWidth = (probe: HTMLDivElement) => { - return observeResize(probe, ([entry]) => { - if (entry) onWidthChange(entry.borderBoxSize[0]?.inlineSize ?? probe.offsetWidth); - }); - }; - - return ( - - ); -} - -function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { - return ( - - - - ); -} - -function SidebarBrandMark({ onBackdrop }: { onBackdrop: boolean }) { - return ( - - - - Code - - - ); -} - function SidebarUtilityItem({ icon, label, diff --git a/apps/web/src/components/threadSidebarWidth.ts b/apps/web/src/components/threadSidebarWidth.ts index 0f55aa8593da0..42876ded295d4 100644 --- a/apps/web/src/components/threadSidebarWidth.ts +++ b/apps/web/src/components/threadSidebarWidth.ts @@ -1,17 +1,13 @@ export const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; const THREAD_SIDEBAR_DEFAULT_WIDTH = 16 * 16; -const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; +export const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; export const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; -export function resolveThreadSidebarMinimumWidth(brandWidth: number): number { - return Math.max(THREAD_SIDEBAR_MIN_WIDTH, Math.ceil(brandWidth)); -} - -export function resolveThreadSidebarMaximumWidth( - viewportWidth: number, - minimumWidth = THREAD_SIDEBAR_MIN_WIDTH, -): number { - return Math.max(minimumWidth, Math.floor(viewportWidth) - THREAD_MAIN_CONTENT_MIN_WIDTH); +export function resolveThreadSidebarMaximumWidth(viewportWidth: number): number { + return Math.max( + THREAD_SIDEBAR_MIN_WIDTH, + Math.floor(viewportWidth) - THREAD_MAIN_CONTENT_MIN_WIDTH, + ); } export function clampThreadSidebarWidth( From 44bd4264591fa46cc36885989cba21118ed3ffc7 Mon Sep 17 00:00:00 2001 From: Igor Abramkin Date: Thu, 29 Oct 2026 13:56:17 +0300 Subject: [PATCH 2/3] fix(mobile): keep an accessible screen heading in the header The header slot now always renders a heading named after the screen, with the Dev or Nightly badge inside it when there is one. Release builds keep an empty heading there, so the native header no longer falls back to its plain-text title. Co-Authored-By: Claude Opus 5.5 --- .../src/components/HeaderStageBadge.tsx | 60 ++++++++++++------- .../src/features/home/ThreadListToolbar.tsx | 1 + .../home/WorkspaceConnectionTitle.tsx | 2 +- .../PullRequestToolbar.android.tsx | 1 + .../threads/ThreadNavigationSidebar.tsx | 2 +- .../workspace/WorkspaceListToolbarRow.tsx | 3 +- .../workspace/WorkspaceRootScreen.tsx | 9 ++- 7 files changed, 52 insertions(+), 26 deletions(-) diff --git a/apps/mobile/src/components/HeaderStageBadge.tsx b/apps/mobile/src/components/HeaderStageBadge.tsx index b936fe64b57fc..74c801212b0ce 100644 --- a/apps/mobile/src/components/HeaderStageBadge.tsx +++ b/apps/mobile/src/components/HeaderStageBadge.tsx @@ -15,39 +15,55 @@ export function headerTitleOffset(): number { return Platform.isPad && !NATIVE_WORKSPACE_COLUMNS_SUPPORTED ? IPAD_HOME_TITLE_OFFSET : 0; } -/** Marks Dev and Nightly builds; release builds show nothing in this slot. */ -export function HeaderStageBadge(props: { readonly allowFontScaling?: boolean } = {}) { +/** + * The header's screen heading, visible only as the Dev or Nightly badge. Release builds still + * render this view: without it the native header falls back to its plain-text title. + */ +export function HeaderStageBadge(props: { + readonly screenTitle: string; + readonly allowFontScaling?: boolean; +}) { const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); const { scale } = useAndroidControlSizing(); - if (stageLabel === null) return null; return ( - - {stageLabel} - + {stageLabel === null ? null : ( + + + {stageLabel} + + + )} ); } +const THREADS_TITLE = "Threads"; + function renderHeaderStageBadge() { - return ; + return ; } export function getStageBadgeHeaderOptions( @@ -56,7 +72,7 @@ export function getStageBadgeHeaderOptions( return { headerTitle: renderHeaderStageBadge, headerTitleStyle: fallbackTitleStyle, - title: "Threads", + title: THREADS_TITLE, unstable_headerLeftItems: undefined, }; } diff --git a/apps/mobile/src/features/home/ThreadListToolbar.tsx b/apps/mobile/src/features/home/ThreadListToolbar.tsx index 8bf397d28d682..6cd61fae7f3a6 100644 --- a/apps/mobile/src/features/home/ThreadListToolbar.tsx +++ b/apps/mobile/src/features/home/ThreadListToolbar.tsx @@ -111,6 +111,7 @@ export function ThreadListToolbar(props: { searchField={searchField} onCloseSearch={closeSearch} onOpenSearch={openSearch} + screenTitle="Threads" searchAccessibilityLabel="Search threads" onOpenEnvironments={props.onOpenEnvironments} filter={filterButton} diff --git a/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx b/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx index e29f3e12feca7..fddb0d2aada2d 100644 --- a/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx +++ b/apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx @@ -172,7 +172,7 @@ export function getConnectionAwareBrandHeaderOptions(opts: { ...getStageBadgeHeaderOptions(opts.fallbackTitleStyle), headerTitle: () => ( } + brand={} maxWidth={maxWidth} onPress={opts.onOpenEnvironments} statusOffset={headerTitleOffset()} diff --git a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx index c958ba81c6739..5268d5ef05f81 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx @@ -73,6 +73,7 @@ export function PullRequestToolbar(props: { } onCloseSearch={closeSearch} onOpenSearch={openSearch} + screenTitle="Pull requests" searchAccessibilityLabel="Search pull requests" onOpenEnvironments={() => openSettings(navigation, "SettingsEnvironments")} filter={ diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 2ae793074161c..423289c90c058 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -865,7 +865,7 @@ function ThreadNavigationSidebarPane( size="pageTitle" brand={ - + } /> diff --git a/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx b/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx index c4281c57c8c2a..4a230ca686007 100644 --- a/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx +++ b/apps/mobile/src/features/workspace/WorkspaceListToolbarRow.tsx @@ -17,6 +17,7 @@ export function WorkspaceListToolbarRow(props: { readonly onCloseSearch: () => void; readonly onOpenSearch: () => void; readonly searchAccessibilityLabel: string; + readonly screenTitle: string; readonly onOpenEnvironments: () => void; /** Stays trailing while searching, so it never moves and still narrows the results. */ readonly filter: ReactNode; @@ -40,7 +41,7 @@ export function WorkspaceListToolbarRow(props: { } + brand={} /> void; }) { + const { state, settingsOpen } = useWorkspaceTabs()!; const maxWidth = Math.max(0, props.columnWidth - (use(HeaderItemsWidthContext) ?? 160)); + const enabled = useEnabledNavigationTabs(); + const tabTitle = settingsOpen + ? "Settings" + : enabled.length + ? TAB_TITLES[state.selectedTab] + : "Navigation"; return ( } + brand={} maxWidth={maxWidth} onPress={props.onOpenEnvironments} /> From 18667a18734bcef87faac7ed636dbcdc117cc313 Mon Sep 17 00:00:00 2001 From: Igor Abramkin Date: Thu, 29 Oct 2026 14:10:43 +0300 Subject: [PATCH 3/3] fix(mobile): give the empty release heading a touch-sized frame Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/components/HeaderStageBadge.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/components/HeaderStageBadge.tsx b/apps/mobile/src/components/HeaderStageBadge.tsx index 74c801212b0ce..449711b375bd4 100644 --- a/apps/mobile/src/components/HeaderStageBadge.tsx +++ b/apps/mobile/src/components/HeaderStageBadge.tsx @@ -34,7 +34,8 @@ export function HeaderStageBadge(props: { } accessible role="heading" - className="min-h-px min-w-px self-start" + // Release builds draw nothing here; the frame still gives touch exploration a target. + className={stageLabel === null ? "h-11 w-24 self-start" : "self-start"} style={{ marginLeft: headerTitleOffset() }} > {stageLabel === null ? null : (