Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re
import { useResolveClassNames } from "uniwind";

import { AppText as Text } from "./components/AppText";
import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent";
Expand Down Expand Up @@ -383,7 +384,7 @@ export const RootStack = createNativeStackNavigator({
...GLASS_HEADER_OPTIONS,
contentStyle: { backgroundColor: "transparent" },
headerBackVisible: false,
title: "Threads",
...getCompactBrandHeaderOptions(),
},
}),
Thread: createNativeStackScreen({
Expand Down
121 changes: 121 additions & 0 deletions apps/mobile/src/components/CompactBrandTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Constants from "expo-constants";
import type {
NativeStackHeaderItem,
NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { Platform, View } from "react-native";

import { AppText as Text } from "./AppText";
import { T3Wordmark } from "./T3Wordmark";
import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
import { resolveMobileStageLabel } from "../lib/mobileBranding";
import { useThemeColor } from "../lib/useThemeColor";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";

// Native leading items inherit different UIKit margins than title views.
const IOS_NATIVE_LEADING_TITLE_OFFSET = -6;
const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7;

/**
* Compact brand lockup sized for native navigation bars.
*/
export function CompactBrandTitle(
props: {
readonly nativeLeadingItem?: boolean;
} = {},
) {
const iconColor = useThemeColor("--color-icon");
const mutedColor = useThemeColor("--color-foreground-muted");
const subtleColor = useThemeColor("--color-subtle");
const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
const titleOffset =
Platform.OS !== "ios"
? 0
: props.nativeLeadingItem
? Platform.isPad
? IPAD_NATIVE_LEADING_TITLE_OFFSET
: IOS_NATIVE_LEADING_TITLE_OFFSET
: Platform.isPad
? IPAD_HOME_TITLE_OFFSET
: 0;

return (
<View
aria-level={1}
accessibilityLabel="T3 Code, Threads"
accessible
role="heading"
style={{
alignItems: "center",
flexDirection: "row",
gap: 6,
marginLeft: titleOffset,
}}
>
<T3Wordmark color={iconColor} height={15} />
<Text
style={{
color: mutedColor,
fontFamily: "DMSans-Medium",
fontSize: 21,
letterSpacing: -0.5,
}}
>
Code
</Text>
<View
style={{
backgroundColor: subtleColor,
borderRadius: 999,
paddingHorizontal: 6,
paddingVertical: 2,
}}
>
<Text
style={{
color: mutedColor,
fontFamily: "DMSans-Bold",
fontSize: 9,
letterSpacing: 0.9,
textTransform: "uppercase",
}}
>
{stageLabel}
</Text>
</View>
</View>
);
}

export function renderCompactBrandTitle() {
return <CompactBrandTitle />;
}

export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] {
return [
{
element: <CompactBrandTitle nativeLeadingItem />,
hidesSharedBackground: true,
type: "custom",
},
];
}

export function getCompactBrandHeaderOptions(
fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"],
): NativeStackNavigationOptions {
if (Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED) {
return {
headerTitle: "Threads",
headerTitleStyle: { color: "transparent", fontSize: 18, fontWeight: "800" },
title: "Threads",
unstable_headerLeftItems: renderCompactBrandHeaderItems,
};
}

return {
headerTitle: renderCompactBrandTitle,
headerTitleStyle: fallbackTitleStyle,
title: "Threads",
};
}
5 changes: 3 additions & 2 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Order from "effect/Order";
import { useNavigation } from "@react-navigation/native";
import { useEffect, useMemo, useState } from "react";

import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useProjects, useThreadShells } from "../../state/entities";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
Expand Down Expand Up @@ -107,8 +108,8 @@ export function HomeRouteScreen() {
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
>
<>
{/* Restore the compact title in case the split branch blanked it. */}
Comment thread
cursor[bot] marked this conversation as resolved.
<NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />
{/* Restore the compact title after the split branch blanks the detail header. */}
<NativeStackScreenOptions options={getCompactBrandHeaderOptions()} />
<HomeHeader
environments={environments}
projects={projectFilterOptions}
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/features/threads/sidebar-navigation-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import type { ReactNode } from "react";
import { Platform, useColorScheme } from "react-native";

import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";

Expand All @@ -35,10 +36,9 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = {
headerShadowVisible: false,
headerShown: true,
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
...getCompactBrandHeaderOptions({ fontSize: 18, fontWeight: "800" }),
headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined,
title: "Threads",
unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
};

Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AppText as Text } from "../../components/AppText";
import { ControlPillMenu } from "../../components/ControlPill";
import { ProjectFavicon } from "../../components/ProjectFavicon";
import { cn } from "../../lib/cn";
import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
import { relativeTime } from "../../lib/time";
import { useThemeColor } from "../../lib/useThemeColor";
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
Expand All @@ -31,7 +32,7 @@ import { resolveThreadStatus } from "./threadPresentation";
export type ThreadListVariant = "compact" | "sidebar";

/** Left inset that aligns compact secondary rows with the title column. */
export const THREAD_LIST_COMPACT_INSET = 20;
export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET;
const SIDEBAR_ROW_RADIUS = 12;

function pullRequestTintColor(
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/lib/layoutMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Horizontal inset shared by the home header and compact thread list. */
export const HOME_HORIZONTAL_INSET = 20;

/** Compensates for the tighter native sidebar title margin on iPad. */
export const IPAD_HOME_TITLE_OFFSET = 10;
14 changes: 14 additions & 0 deletions apps/mobile/src/lib/mobileBranding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it } from "vite-plus/test";

import { resolveMobileStageLabel } from "./mobileBranding";

describe("resolveMobileStageLabel", () => {
it.each([
["development", "Dev"],
["preview", "Nightly"],
["production", "Alpha"],
[undefined, "Alpha"],
])("maps %s builds to %s", (appVariant, expected) => {
expect(resolveMobileStageLabel(appVariant)).toBe(expected);
});
});
7 changes: 7 additions & 0 deletions apps/mobile/src/lib/mobileBranding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type MobileStageLabel = "Alpha" | "Dev" | "Nightly";

export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel {
if (appVariant === "development") return "Dev";
if (appVariant === "preview") return "Nightly";
return "Alpha";
}
Loading