Unified · Split
| @@ -11,7 +11,7 @@ · 10 unmodified lines | ||
| 11 | 11 | "start:dev": "APP_VARIANT=development expo start", |
| 12 | 12 | "start:preview": "APP_VARIANT=preview expo start", |
| 13 | 13 | "start:prod": "APP_VARIANT=production expo start", |
| 14 | − "showcase": "APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code-dev --clear", | |
| 14 | + "showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear", | |
| 15 | 15 | "screenshots": "node ../../scripts/mobile-showcase.ts", |
| 16 | 16 | "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android", |
| 17 | 17 | "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android", |
| @@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re · 14 unmodified lines | ||
| 15 | 15 | import { useResolveClassNames } from "uniwind"; |
| 16 | 16 | |
| 17 | 17 | import { AppText as Text } from "./components/AppText"; |
| 18 | +import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle"; | |
| 18 | 19 | import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; |
| 19 | 20 | import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; |
| 20 | 21 | import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent"; |
| @@ -392,7 +393,7 @@ export const RootStack = createNativeStackNavigator({ · 371 unmodified lines | ||
| 392 | 393 | ...GLASS_HEADER_OPTIONS, |
| 393 | 394 | contentStyle: { backgroundColor: "transparent" }, |
| 394 | 395 | headerBackVisible: false, |
| 395 | − title: "Threads", | |
| 396 | + ...getCompactBrandHeaderOptions(), | |
| 396 | 397 | }, |
| 397 | 398 | }), |
| 398 | 399 | Thread: createNativeStackScreen({ |
| @@ -0,0 +1,121 @@ | ||
| 1 | +import Constants from "expo-constants"; | |
| 2 | +import type { | |
| 3 | + NativeStackHeaderItem, | |
| 4 | + NativeStackNavigationOptions, | |
| 5 | +} from "@react-navigation/native-stack"; | |
| 6 | +import { Platform, View } from "react-native"; | |
| 7 | + | |
| 8 | +import { AppText as Text } from "./AppText"; | |
| 9 | +import { T3Wordmark } from "./T3Wordmark"; | |
| 10 | +import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; | |
| 11 | +import { resolveMobileStageLabel } from "../lib/mobileBranding"; | |
| 12 | +import { useThemeColor } from "../lib/useThemeColor"; | |
| 13 | +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass"; | |
| 14 | + | |
| 15 | +// Native leading items inherit different UIKit margins than title views. | |
| 16 | +const IOS_NATIVE_LEADING_TITLE_OFFSET = -6; | |
| 17 | +const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * Compact brand lockup sized for native navigation bars. | |
| 21 | + */ | |
| 22 | +export function CompactBrandTitle( | |
| 23 | + props: { | |
| 24 | + readonly nativeLeadingItem?: boolean; | |
| 25 | + } = {}, | |
| 26 | +) { | |
| 27 | + const iconColor = useThemeColor("--color-icon"); | |
| 28 | + const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 29 | + const subtleColor = useThemeColor("--color-subtle"); | |
| 30 | + const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); | |
| 31 | + const titleOffset = | |
| 32 | + Platform.OS !== "ios" | |
| 33 | + ? 0 | |
| 34 | + : props.nativeLeadingItem | |
| 35 | + ? Platform.isPad | |
| 36 | + ? IPAD_NATIVE_LEADING_TITLE_OFFSET | |
| 37 | + : IOS_NATIVE_LEADING_TITLE_OFFSET | |
| 38 | + : Platform.isPad | |
| 39 | + ? IPAD_HOME_TITLE_OFFSET | |
| 40 | + : 0; | |
| 41 | + | |
| 42 | + return ( | |
| 43 | + <View | |
| 44 | + aria-level={1} | |
| 45 | + accessibilityLabel="T3 Code, Threads" | |
| 46 | + accessible | |
| 47 | + role="heading" | |
| 48 | + style={{ | |
| 49 | + alignItems: "center", | |
| 50 | + flexDirection: "row", | |
| 51 | + gap: 6, | |
| 52 | + marginLeft: titleOffset, | |
| 53 | + }} | |
| 54 | + > | |
| 55 | + <T3Wordmark color={iconColor} height={15} /> | |
| 56 | + <Text | |
| 57 | + style={{ | |
| 58 | + color: mutedColor, | |
| 59 | + fontFamily: "DMSans-Medium", | |
| 60 | + fontSize: 21, | |
| 61 | + letterSpacing: -0.5, | |
| 62 | + }} | |
| 63 | + > | |
| 64 | + Code | |
| 65 | + </Text> | |
| 66 | + <View | |
| 67 | + style={{ | |
| 68 | + backgroundColor: subtleColor, | |
| 69 | + borderRadius: 999, | |
| 70 | + paddingHorizontal: 6, | |
| 71 | + paddingVertical: 2, | |
| 72 | + }} | |
| 73 | + > | |
| 74 | + <Text | |
| 75 | + style={{ | |
| 76 | + color: mutedColor, | |
| 77 | + fontFamily: "DMSans-Bold", | |
| 78 | + fontSize: 9, | |
| 79 | + letterSpacing: 0.9, | |
| 80 | + textTransform: "uppercase", | |
| 81 | + }} | |
| 82 | + > | |
| 83 | + {stageLabel} | |
| 84 | + </Text> | |
| 85 | + </View> | |
| 86 | + </View> | |
| 87 | + ); | |
| 88 | +} | |
| 89 | + | |
| 90 | +export function renderCompactBrandTitle() { | |
| 91 | + return <CompactBrandTitle />; | |
| 92 | +} | |
| 93 | + | |
| 94 | +export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { | |
| 95 | + return [ | |
| 96 | + { | |
| 97 | + element: <CompactBrandTitle nativeLeadingItem />, | |
| 98 | + hidesSharedBackground: true, | |
| 99 | + type: "custom", | |
| 100 | + }, | |
| 101 | + ]; | |
| 102 | +} | |
| 103 | + | |
| 104 | +export function getCompactBrandHeaderOptions( | |
| 105 | + fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"], | |
| 106 | +): NativeStackNavigationOptions { | |
| 107 | + if (Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED) { | |
| 108 | + return { | |
| 109 | + headerTitle: "Threads", | |
| 110 | + headerTitleStyle: { color: "transparent", fontSize: 18, fontWeight: "800" }, | |
| 111 | + title: "Threads", | |
| 112 | + unstable_headerLeftItems: renderCompactBrandHeaderItems, | |
| 113 | + }; | |
| 114 | + } | |
| 115 | + | |
| 116 | + return { | |
| 117 | + headerTitle: renderCompactBrandTitle, | |
| 118 | + headerTitleStyle: fallbackTitleStyle, | |
| 119 | + title: "Threads", | |
| 120 | + }; | |
| 121 | +} | |
| @@ -3,6 +3,7 @@ import * as Order from "effect/Order"; · 2 unmodified lines | ||
| 3 | 3 | import { useNavigation } from "@react-navigation/native"; |
| 4 | 4 | import { useEffect, useMemo, useState } from "react"; |
| 5 | 5 | |
| 6 | +import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle"; | |
| 6 | 7 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 7 | 8 | import { useProjects, useThreadShells } from "../../state/entities"; |
| 8 | 9 | import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; |
| @@ -93,7 +94,9 @@ export function HomeRouteScreen() { · 84 unmodified lines | ||
| 93 | 94 | if (layout.usesSplitView) { |
| 94 | 95 | return ( |
| 95 | 96 | <> |
| 96 | − <NativeStackScreenOptions options={{ title: "", headerTitle: "" }} /> | |
| 97 | + <NativeStackScreenOptions | |
| 98 | + options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }} | |
| 99 | + /> | |
| 97 | 100 | <WorkspaceSidebarToolbar |
| 98 | 101 | afterSidebarButton={ |
| 99 | 102 | <NativeHeaderToolbar.Button |
| @@ -115,8 +118,8 @@ export function HomeRouteScreen() { · 15 unmodified lines | ||
| 115 | 118 | onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} |
| 116 | 119 | > |
| 117 | 120 | <> |
| 118 | − {/* Restore the compact title in case the split branch blanked it. */} | |
| 119 | − <NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} /> | |
| 121 | + {/* Restore the compact title after the split branch blanks the detail header. */} | |
| 122 | + <NativeStackScreenOptions options={getCompactBrandHeaderOptions()} /> | |
| 120 | 123 | <HomeHeader |
| 121 | 124 | environments={environments} |
| 122 | 125 | projects={projectFilterOptions} |
| @@ -11,6 +11,7 @@ import { · 10 unmodified lines | ||
| 11 | 11 | import type { ReactNode } from "react"; |
| 12 | 12 | import { Platform, useColorScheme } from "react-native"; |
| 13 | 13 | |
| 14 | +import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle"; | |
| 14 | 15 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 15 | 16 | import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; |
| 16 | 17 | |
| @@ -35,10 +36,9 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { · 18 unmodified lines | ||
| 35 | 36 | headerShadowVisible: false, |
| 36 | 37 | headerShown: true, |
| 37 | 38 | headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined, |
| 38 | − headerTitleStyle: { fontSize: 18, fontWeight: "800" }, | |
| 39 | + ...getCompactBrandHeaderOptions({ fontSize: 18, fontWeight: "800" }), | |
| 39 | 40 | headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, |
| 40 | 41 | scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined, |
| 41 | − title: "Threads", | |
| 42 | 42 | unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, |
| 43 | 43 | }; |
| 44 | 44 | |
| @@ -15,6 +15,7 @@ import { AppText as Text } from "../../components/AppText"; · 14 unmodified lines | ||
| 15 | 15 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 16 | 16 | import { ProjectFavicon } from "../../components/ProjectFavicon"; |
| 17 | 17 | import { cn } from "../../lib/cn"; |
| 18 | +import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics"; | |
| 18 | 19 | import { relativeTime } from "../../lib/time"; |
| 19 | 20 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 20 | 21 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; |
| @@ -33,7 +34,7 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match"; · 12 unmodified lines | ||
| 33 | 34 | export type ThreadListVariant = "compact" | "sidebar"; |
| 34 | 35 | |
| 35 | 36 | /** Left inset that aligns compact secondary rows with the title column. */ |
| 36 | −export const THREAD_LIST_COMPACT_INSET = 20; | |
| 37 | +export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET; | |
| 37 | 38 | const SIDEBAR_ROW_RADIUS = 12; |
| 38 | 39 | |
| 39 | 40 | function pullRequestTintColor( |
| @@ -0,0 +1,5 @@ | ||
| 1 | +/** Horizontal inset shared by the home header and compact thread list. */ | |
| 2 | +export const HOME_HORIZONTAL_INSET = 20; | |
| 3 | + | |
| 4 | +/** Compensates for the tighter native sidebar title margin on iPad. */ | |
| 5 | +export const IPAD_HOME_TITLE_OFFSET = 10; | |
| @@ -0,0 +1,14 @@ | ||
| 1 | +import { describe, expect, it } from "vite-plus/test"; | |
| 2 | + | |
| 3 | +import { resolveMobileStageLabel } from "./mobileBranding"; | |
| 4 | + | |
| 5 | +describe("resolveMobileStageLabel", () => { | |
| 6 | + it.each([ | |
| 7 | + ["development", "Dev"], | |
| 8 | + ["preview", "Nightly"], | |
| 9 | + ["production", "Alpha"], | |
| 10 | + [undefined, "Alpha"], | |
| 11 | + ])("maps %s builds to %s", (appVariant, expected) => { | |
| 12 | + expect(resolveMobileStageLabel(appVariant)).toBe(expected); | |
| 13 | + }); | |
| 14 | +}); | |
| @@ -0,0 +1,7 @@ | ||
| 1 | +export type MobileStageLabel = "Alpha" | "Dev" | "Nightly"; | |
| 2 | + | |
| 3 | +export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel { | |
| 4 | + if (appVariant === "development") return "Dev"; | |
| 5 | + if (appVariant === "preview") return "Nightly"; | |
| 6 | + return "Alpha"; | |
| 7 | +} | |
| @@ -370,7 +370,8 @@ function NativeHeaderToolbarRoot(props: { · 369 unmodified lines | ||
| 370 | 370 | const navigation = useNativeStackNavigation(); |
| 371 | 371 | const items = useMemo(() => collectToolbarItems(props.children), [props.children]); |
| 372 | 372 | |
| 373 | − useEffect(() => { | |
| 373 | + // Swap toolbar owners before paint so split and compact headers cannot clear each other. | |
| 374 | + useLayoutEffect(() => { | |
| 374 | 375 | if (!navigation) { |
| 375 | 376 | return; |
| 376 | 377 | } |
| @@ -43,7 +43,7 @@ Captures wait for the real environment snapshot to hydrate and for the requested · 42 unmodified lines | ||
| 43 | 43 | active. Both platforms record readiness in the simulator/emulator app container. A final settle |
| 44 | 44 | delay allows native terminal and Git review data to finish rendering. |
| 45 | 45 | |
| 46 | −A full capture regenerates the selected native project with Expo's clean development prebuild before | |
| 46 | +A full capture regenerates the selected native project with Expo's clean production prebuild before | |
| 47 | 47 | building it. Use --skip-build for repeated captures after the first build. |
| 48 | 48 | |
| 49 | 49 | The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is |
| @@ -244,22 +244,19 @@ it("selects a reachable LAN IPv4 address", () => { · 243 unmodified lines | ||
| 244 | 244 | }); |
| 245 | 245 | |
| 246 | 246 | it("maps capture scenes to the real application routes", () => { |
| 247 | − assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code-dev://"); | |
| 248 | − assert.equal( | |
| 249 | − showcaseSceneUrl("environments", "environment-1"), | |
| 250 | − "t3code-dev://settings/environments", | |
| 251 | − ); | |
| 247 | + assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code://"); | |
| 248 | + assert.equal(showcaseSceneUrl("environments", "environment-1"), "t3code://settings/environments"); | |
| 252 | 249 | assert.equal( |
| 253 | 250 | showcaseSceneUrl("thread", "environment-1"), |
| 254 | − "t3code-dev://threads/environment-1/remote-command-center", | |
| 251 | + "t3code://threads/environment-1/remote-command-center", | |
| 255 | 252 | ); |
| 256 | 253 | assert.equal( |
| 257 | 254 | showcaseSceneUrl("terminal", "environment-1"), |
| 258 | − "t3code-dev://threads/environment-1/remote-command-center/terminal?terminalId=term-1", | |
| 255 | + "t3code://threads/environment-1/remote-command-center/terminal?terminalId=term-1", | |
| 259 | 256 | ); |
| 260 | 257 | assert.equal( |
| 261 | 258 | showcaseSceneUrl("review", "environment-1"), |
| 262 | − "t3code-dev://threads/environment-1/remote-command-center/review", | |
| 259 | + "t3code://threads/environment-1/remote-command-center/review", | |
| 263 | 260 | ); |
| 264 | 261 | }); |
| 265 | 262 | |
| @@ -31,14 +31,14 @@ import { · 30 unmodified lines | ||
| 31 | 31 | |
| 32 | 32 | const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), ".."); |
| 33 | 33 | const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile"); |
| 34 | −const ANDROID_PACKAGE = "com.t3tools.t3code.dev"; | |
| 35 | −const APP_SCHEME = "t3code-dev"; | |
| 34 | +const ANDROID_PACKAGE = "com.t3tools.t3code"; | |
| 35 | +const APP_SCHEME = "t3code"; | |
| 36 | 36 | const IOS_READY_FILENAME = "T3ShowcaseReadyScene"; |
| 37 | 37 | const SERVER_HOST = "0.0.0.0"; |
| 38 | 38 | const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64"; |
| 39 | 39 | const IOS_APP_PATH = NodePath.join( |
| 40 | 40 | MOBILE_ROOT, |
| 41 | − ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3CodeDev.app", | |
| 41 | + ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app", | |
| 42 | 42 | ); |
| 43 | 43 | const ANDROID_APK_PATH = NodePath.join( |
| 44 | 44 | MOBILE_ROOT, |
| @@ -58,7 +58,7 @@ const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env); · 13 unmodified lines | ||
| 58 | 58 | const MOBILE_BUILD_ENV = { |
| 59 | 59 | ...NodeProcess.env, |
| 60 | 60 | ANDROID_HOME: ANDROID_SDK_ROOT, |
| 61 | − APP_VARIANT: "development", | |
| 61 | + APP_VARIANT: "production", | |
| 62 | 62 | EXPO_NO_GIT_STATUS: "1", |
| 63 | 63 | JAVA_HOME: |
| 64 | 64 | NodeProcess.env.JAVA_HOME ?? |
| @@ -664,9 +664,9 @@ async function buildIos(): Promise<string> { · 599 unmodified lines | ||
| 664 | 664 | "xcodebuild", |
| 665 | 665 | [ |
| 666 | 666 | "-workspace", |
| 667 | − NodePath.join(MOBILE_ROOT, "ios/T3CodeDev.xcworkspace"), | |
| 667 | + NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"), | |
| 668 | 668 | "-scheme", |
| 669 | − "T3CodeDev", | |
| 669 | + "T3Code", | |
| 670 | 670 | "-configuration", |
| 671 | 671 | "Debug", |
| 672 | 672 | "-sdk", |