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..449711b375bd4 --- /dev/null +++ b/apps/mobile/src/components/HeaderStageBadge.tsx @@ -0,0 +1,79 @@ +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; +} + +/** + * 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(); + + return ( + + {stageLabel === null ? null : ( + + + {stageLabel} + + + )} + + ); +} + +const THREADS_TITLE = "Threads"; + +function renderHeaderStageBadge() { + return ; +} + +export function getStageBadgeHeaderOptions( + fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"], +): NativeStackNavigationOptions { + return { + headerTitle: renderHeaderStageBadge, + headerTitleStyle: fallbackTitleStyle, + 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 9e8c55fd2b5e5..6cd61fae7f3a6 100644 --- a/apps/mobile/src/features/home/ThreadListToolbar.tsx +++ b/apps/mobile/src/features/home/ThreadListToolbar.tsx @@ -111,8 +111,8 @@ export function ThreadListToolbar(props: { searchField={searchField} onCloseSearch={closeSearch} onOpenSearch={openSearch} + screenTitle="Threads" 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..fddb0d2aada2d 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..5268d5ef05f81 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx @@ -73,8 +73,8 @@ export function PullRequestToolbar(props: { } onCloseSearch={closeSearch} onOpenSearch={openSearch} + screenTitle="Pull requests" 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..423289c90c058 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..4a230ca686007 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,7 @@ export function WorkspaceListToolbarRow(props: { readonly onCloseSearch: () => void; readonly onOpenSearch: () => void; readonly searchAccessibilityLabel: string; - readonly titleAccessibilityLabel: string; + readonly screenTitle: string; readonly onOpenEnvironments: () => void; /** Stays trailing while searching, so it never moves and still narrows the results. */ readonly filter: ReactNode; @@ -41,13 +41,7 @@ export function WorkspaceListToolbarRow(props: { - } + brand={} /> } + 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(