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
29 changes: 23 additions & 6 deletions .github/workflows/mobile-showcase-screenshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ on:
- both
- dark
- light
theme:
description: Palette to capture (all multiplies the run by six)
required: true
default: t3-code
type: choice
options:
- t3-code
- t3-chat
- grove
- ocean
- ember
- iris
- all

permissions:
contents: read
Expand All @@ -33,7 +46,9 @@ jobs:
name: iPhone 6.9, iPhone 6.5, and iPad 13
if: inputs.platform == 'all' || inputs.platform == 'ios'
runs-on: blacksmith-12vcpu-macos-26
timeout-minutes: 60
# Capturing every palette multiplies the device matrix by six, and only the
# one native build is shared between them.
timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -62,10 +77,10 @@ jobs:
"$vp_pnpm_bin/pnpm" --version

- name: Capture iOS showcase
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}"
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}"

- name: Validate App Store Connect assets
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --validate-only
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}" --validate-only

- name: Upload iOS screenshots
if: always()
Expand All @@ -80,7 +95,9 @@ jobs:
name: Android phone, 7-inch tablet, and 10-inch tablet
if: inputs.platform == 'all' || inputs.platform == 'android'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
# Capturing every palette multiplies the device matrix by six, and only the
# one native build is shared between them.
timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }}
env:
T3_SHOWCASE_ANDROID_ABI: x86_64
steps:
Expand Down Expand Up @@ -137,10 +154,10 @@ jobs:
cores: 8
ram-size: 4096M
disable-animations: false
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}"
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}"

- name: Validate Google Play assets
run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --validate-only
run: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}" --validate-only

- name: Upload Android screenshots
if: always()
Expand Down
20 changes: 15 additions & 5 deletions apps/mobile/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
/* Page backgrounds */
--color-screen: #f2f2f7;
--color-sheet: rgba(242, 242, 247, 0.98);
--color-sheet-solid: #f2f2f7;

/* Card / surface */
--color-card: #ffffff;
Expand Down Expand Up @@ -39,13 +40,16 @@
/* Primary action */
--color-primary: #262626;
--color-primary-foreground: #ffffff;
--color-primary-shadow: rgba(0, 0, 0, 0.18);
--color-primary-shadow: #000000;

/* Secondary action */
--color-secondary: #ffffff;
--color-secondary-foreground: #262626;
--color-secondary-border: rgba(0, 0, 0, 0.08);
--color-switch-active: #34c759;
--color-switch-active-track: #34c759;
--color-switch-active-thumb: #ffffff;
--color-switch-inactive-track: rgba(0, 0, 0, 0.08);
--color-switch-inactive-thumb: #8e8e93;

/* Danger */
--color-danger: #fef2f2;
Expand All @@ -56,7 +60,7 @@
--color-input: #ffffff;
--color-input-border: rgba(0, 0, 0, 0.1);
--color-sidebar-search: rgba(118, 118, 128, 0.12);
--color-placeholder: #a3a3a3;
--color-placeholder: #737373;

/* Icons */
--color-icon: #262626;
Expand Down Expand Up @@ -90,6 +94,7 @@
--color-user-bubble: #007aff;
--color-user-bubble-foreground: #ffffff;
--color-user-bubble-foreground-muted: rgba(255, 255, 255, 0.78);
--color-user-bubble-skill-foreground: #f0abfc;

/* Drawer / modal backdrop */
--color-backdrop: rgba(0, 0, 0, 0.22);
Expand All @@ -106,6 +111,7 @@
/* Page backgrounds */
--color-screen: #0a0a0a;
--color-sheet: rgba(14, 14, 14, 0.98);
--color-sheet-solid: #0e0e0e;
Comment thread
cursor[bot] marked this conversation as resolved.

/* Card / surface */
--color-card: #171717;
Expand Down Expand Up @@ -133,13 +139,16 @@
/* Primary action */
--color-primary: #f5f5f5;
--color-primary-foreground: #0a0a0a;
--color-primary-shadow: rgba(0, 0, 0, 0.22);
--color-primary-shadow: #000000;

/* Secondary action */
--color-secondary: rgba(255, 255, 255, 0.04);
--color-secondary-foreground: #f5f5f5;
--color-secondary-border: rgba(255, 255, 255, 0.06);
--color-switch-active: #30d158;
--color-switch-active-track: #30d158;
--color-switch-active-thumb: #ffffff;
--color-switch-inactive-track: rgba(255, 255, 255, 0.06);
--color-switch-inactive-thumb: #8e8e93;

/* Danger */
--color-danger: rgba(239, 68, 68, 0.14);
Expand Down Expand Up @@ -184,6 +193,7 @@
--color-user-bubble: #0a84ff;
--color-user-bubble-foreground: #ffffff;
--color-user-bubble-foreground-muted: rgba(255, 255, 255, 0.78);
--color-user-bubble-skill-foreground: #f0abfc;

/* Drawer / modal backdrop */
--color-backdrop: rgba(0, 0, 0, 0.48);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class T3NativeControlsModule : Module() {
storedScene ?: appContext.currentActivity?.intent?.getStringExtra("showcaseScene")
}

// The palette is fixed for the whole capture, so it only ever arrives as a
// launch extra — unlike the scene, which the runner rewrites in place.
Function("getShowcaseTheme") {
appContext.currentActivity?.intent?.getStringExtra("showcaseTheme")
}

Function("prepareShowcaseCapture") {
// Android app data is cleared by the host runner before launch.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ public final class T3NativeControlsModule: Module {
return arguments[flagIndex + 1]
}

// The palette is fixed for the whole capture, so it only ever arrives as a
// launch argument — unlike the scene, which the runner rewrites in place.
Function("getShowcaseTheme") { () -> String? in
let arguments = ProcessInfo.processInfo.arguments
guard
let flagIndex = arguments.firstIndex(of: "--showcaseTheme"),
arguments.indices.contains(flagIndex + 1)
else {
return nil as String?
}
return arguments[flagIndex + 1]
}

Function("getShowcaseOrientation") { () -> String? in
let arguments = ProcessInfo.processInfo.arguments
guard
Expand Down
77 changes: 42 additions & 35 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { BlurTargetView } from "expo-blur";
import * as Linking from "expo-linking";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { StatusBar, useColorScheme } from "react-native";
import { StatusBar } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigation/native";
import { createStaticNavigation } from "@react-navigation/native";

import { RegistryContext } from "@effect/atom-react";
import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
Expand All @@ -22,6 +22,7 @@ import { appAtomRegistry } from "./state/atom-registry";
import { OverlayPortalHost } from "./components/OverlayPortal";
import { appBlurTargetRef } from "./lib/appBlurTarget";
import { useThemeColor } from "./lib/useThemeColor";
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";

import "../global.css";

Expand Down Expand Up @@ -58,45 +59,51 @@ function SplashScreenCoordinator() {
}

export default function App() {
const colorScheme = useColorScheme();
const statusBarBg = useThemeColor("--color-status-bar");

return (
<RegistryContext.Provider value={appAtomRegistry}>
<CloudAuthProvider>
<AppearancePreferencesProvider>
<SplashScreenCoordinator />
<GestureHandlerRootView className="flex-1">
<KeyboardProvider statusBarTranslucent>
<SafeAreaProvider>
<StatusBar
barStyle={colorScheme === "dark" ? "light-content" : "dark-content"}
backgroundColor={statusBarBg}
translucent
/>
{/* The navigation theme drives the NATIVE header appearance: native-stack
forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without
this, React Navigation defaults to its light theme and every native
header (glass buttons, title, materials) is forced light even when
the system is in dark mode. */}
{/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}
<BlurTargetView ref={appBlurTargetRef} style={{ flex: 1 }}>
<IncomingShareProvider>
<Navigation
linking={appLinking}
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
/>
</IncomingShareProvider>
<ConfirmDialogHost />
</BlurTargetView>
{/* Anchored-menu overlays render here — in-window, so the
keyboard stays up while a dropdown is open. */}
<OverlayPortalHost />
</SafeAreaProvider>
</KeyboardProvider>
</GestureHandlerRootView>
<AppContent />
</AppearancePreferencesProvider>
</CloudAuthProvider>
</RegistryContext.Provider>
);
}

function AppContent() {
const { themeAppearance } = useAppearancePreferences();
const statusBarBg = useThemeColor("--color-status-bar");
const navigationTheme = useMobileNavigationTheme(themeAppearance);

return (
<>
<SplashScreenCoordinator />
<GestureHandlerRootView className="flex-1">
<KeyboardProvider statusBarTranslucent>
<SafeAreaProvider>
<StatusBar
barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"}
backgroundColor={statusBarBg}
translucent
/>
{/* The navigation theme drives the NATIVE header appearance: native-stack
forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without
this, React Navigation defaults to its light theme and every native
header (glass buttons, title, materials) is forced light even when
the system is in dark mode. */}
{/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}
<BlurTargetView ref={appBlurTargetRef} style={{ flex: 1 }}>
<IncomingShareProvider>
<Navigation linking={appLinking} theme={navigationTheme} />
</IncomingShareProvider>
<ConfirmDialogHost />
</BlurTargetView>
{/* Anchored-menu overlays render here — in-window, so the
keyboard stays up while a dropdown is open. */}
<OverlayPortalHost />
</SafeAreaProvider>
</KeyboardProvider>
</GestureHandlerRootView>
</>
);
}
27 changes: 8 additions & 19 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { useEffect, useRef } from "react";
import { Platform, Pressable, ScrollView, StyleSheet } from "react-native";
import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native";
import { useResolveClassNames } from "uniwind";

import { AppText as Text } from "./components/AppText";
Expand Down Expand Up @@ -71,11 +71,7 @@ import {
} from "./features/sharing/incoming-share-presentation";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
import {
FORM_SHEET_PRESENTATION_OPTIONS,
NATIVE_SHEET_SURFACE_COLOR,
NATIVE_SHEET_SURFACE_CONTENT_STYLE,
} from "./native/sheet-surface";
import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface";
import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";

const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
Expand All @@ -96,11 +92,7 @@ const GLASS_HEADER_OPTIONS: AppScreenOptions = {
headerLargeTitle: false,
headerShadowVisible: false,
headerShown: true,
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED
? { backgroundColor: "transparent" }
: NATIVE_SHEET_SURFACE_COLOR !== undefined
? { backgroundColor: NATIVE_SHEET_SURFACE_COLOR as unknown as string }
: undefined,
Comment thread
cursor[bot] marked this conversation as resolved.
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
Expand All @@ -115,12 +107,6 @@ const SOLID_HEADER_OPTIONS: AppScreenOptions = {
headerLargeTitle: false,
headerShadowVisible: false,
headerShown: true,
headerStyle:
NATIVE_SHEET_SURFACE_COLOR !== undefined
? // native-stack types this as `string`, but the native side accepts any
// ColorValue including DynamicColorIOS.
{ backgroundColor: NATIVE_SHEET_SURFACE_COLOR as unknown as string }
: undefined,
Comment thread
cursor[bot] marked this conversation as resolved.
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
headerTransparent: false,
unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined,
Expand Down Expand Up @@ -507,7 +493,6 @@ export const RootStack = createNativeStackNavigator({
linking: `${THREAD_LINKING_PREFIX}/files`,
options: {
...GLASS_HEADER_OPTIONS,
contentStyle: NATIVE_SHEET_SURFACE_CONTENT_STYLE,
Comment thread
cursor[bot] marked this conversation as resolved.
title: "Files",
},
}),
Expand Down Expand Up @@ -636,7 +621,11 @@ export const RootStack = createNativeStackNavigator({
// The whole new-task flow (choose project → draft → add project) shares
// draft state via NewTaskFlowProvider. The expo-router era mounted it in
// app/new/_layout.tsx; this layout wrapper is the native-stack equivalent.
layout: ({ children }) => <NewTaskFlowProvider>{children}</NewTaskFlowProvider>,
layout: ({ children }) => (
<NewTaskFlowProvider>
<View className="flex-1 bg-sheet-solid">{children}</View>
</NewTaskFlowProvider>
),
options: {
gestureEnabled: true,
headerShown: false,
Expand Down
6 changes: 4 additions & 2 deletions apps/mobile/src/components/AndroidAnchoredMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { BlurView } from "expo-blur";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { StyleProp, ViewStyle } from "react-native";
import { BackHandler, Pressable, ScrollView, useColorScheme, View } from "react-native";
import { BackHandler, Pressable, ScrollView, View } from "react-native";
import { useKeyboardState } from "react-native-keyboard-controller";
import Animated, { FadeIn } from "react-native-reanimated";

import { appBlurTargetRef } from "../lib/appBlurTarget";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
import { useThemeColor } from "../lib/useThemeColor";
import { cn } from "../lib/cn";
import { type AppSymbolName, SymbolView } from "./AppSymbol";
Expand Down Expand Up @@ -79,7 +80,8 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
const anchorRef = useRef<View>(null);
const overlayRef = useRef<View>(null);

const isDarkMode = useColorScheme() === "dark";
const { themeAppearance } = useAppearancePreferences();
const isDarkMode = themeAppearance === "dark";
const keyboardVisible = useKeyboardState((state) => state.isVisible);
const keyboardHeight = useKeyboardState((state) => state.height);
const rippleColor = useThemeColor("--color-subtle");
Expand Down
Loading
Loading