Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start:dev": "APP_VARIANT=development expo start",
"start:preview": "APP_VARIANT=preview expo start",
"start:prod": "APP_VARIANT=production expo start",
"showcase": "APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code-dev --clear",
"showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear",
"screenshots": "node ../../scripts/mobile-showcase.ts",
"android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
"android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android",
Expand Down
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",
};
}
9 changes: 6 additions & 3 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 @@ -85,7 +86,9 @@ export function HomeRouteScreen() {
if (layout.usesSplitView) {
return (
<>
<NativeStackScreenOptions options={{ title: "", headerTitle: "" }} />
<NativeStackScreenOptions
options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }}
/>
<WorkspaceSidebarToolbar
afterSidebarButton={
<NativeHeaderToolbar.Button
Expand All @@ -107,8 +110,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";
}
3 changes: 2 additions & 1 deletion apps/mobile/src/native/StackHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,8 @@ function NativeHeaderToolbarRoot(props: {
const navigation = useNativeStackNavigation();
const items = useMemo(() => collectToolbarItems(props.children), [props.children]);

useEffect(() => {
// Swap toolbar owners before paint so split and compact headers cannot clear each other.
useLayoutEffect(() => {
if (!navigation) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/mobile-app-store-screenshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Captures wait for the real environment snapshot to hydrate and for the requested
active. Both platforms record readiness in the simulator/emulator app container. A final settle
delay allows native terminal and Git review data to finish rendering.

A full capture regenerates the selected native project with Expo's clean development prebuild before
A full capture regenerates the selected native project with Expo's clean production prebuild before
building it. Use --skip-build for repeated captures after the first build.

The harness uses its own Metro port (8199 by default), so an ordinary mobile server or another
Expand Down
13 changes: 5 additions & 8 deletions scripts/mobile-showcase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,22 +244,19 @@ it("selects a reachable LAN IPv4 address", () => {
});

it("maps capture scenes to the real application routes", () => {
assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code-dev://");
assert.equal(
showcaseSceneUrl("environments", "environment-1"),
"t3code-dev://settings/environments",
);
assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code://");
assert.equal(showcaseSceneUrl("environments", "environment-1"), "t3code://settings/environments");
assert.equal(
showcaseSceneUrl("thread", "environment-1"),
"t3code-dev://threads/environment-1/remote-command-center",
"t3code://threads/environment-1/remote-command-center",
);
assert.equal(
showcaseSceneUrl("terminal", "environment-1"),
"t3code-dev://threads/environment-1/remote-command-center/terminal?terminalId=term-1",
"t3code://threads/environment-1/remote-command-center/terminal?terminalId=term-1",
);
assert.equal(
showcaseSceneUrl("review", "environment-1"),
"t3code-dev://threads/environment-1/remote-command-center/review",
"t3code://threads/environment-1/remote-command-center/review",
);
});

Expand Down
12 changes: 6 additions & 6 deletions scripts/mobile-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ import {

const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile");
const ANDROID_PACKAGE = "com.t3tools.t3code.dev";
const APP_SCHEME = "t3code-dev";
const ANDROID_PACKAGE = "com.t3tools.t3code";
const APP_SCHEME = "t3code";
const IOS_READY_FILENAME = "T3ShowcaseReadyScene";
const SERVER_HOST = "0.0.0.0";
const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64";
const IOS_APP_PATH = NodePath.join(
MOBILE_ROOT,
".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3CodeDev.app",
".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app",
);
const ANDROID_APK_PATH = NodePath.join(
MOBILE_ROOT,
Expand All @@ -58,7 +58,7 @@ const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env);
const MOBILE_BUILD_ENV = {
...NodeProcess.env,
ANDROID_HOME: ANDROID_SDK_ROOT,
APP_VARIANT: "development",
APP_VARIANT: "production",
EXPO_NO_GIT_STATUS: "1",
JAVA_HOME:
NodeProcess.env.JAVA_HOME ??
Expand Down Expand Up @@ -664,9 +664,9 @@ async function buildIos(): Promise<string> {
"xcodebuild",
[
"-workspace",
NodePath.join(MOBILE_ROOT, "ios/T3CodeDev.xcworkspace"),
NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"),
"-scheme",
"T3CodeDev",
"T3Code",
"-configuration",
"Debug",
"-sdk",
Expand Down
Loading