Skip to content

Commit d23b181

Browse files
authored
feat(mobile): add built-in themes (#6619)
1 parent 3583cd2 commit d23b181

76 files changed

Lines changed: 3090 additions & 838 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/mobile-showcase-screenshots.yml

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ on:
2121
- both
2222
- dark
2323
- light
24+
theme:
25+
description: Palette to capture (all multiplies the run by six)
26+
required: true
27+
default: t3-code
28+
type: choice
29+
options:
30+
- t3-code
31+
- t3-chat
32+
- grove
33+
- ocean
34+
- ember
35+
- iris
36+
- all
2437

2538
permissions:
2639
contents: read
@@ -33,7 +46,9 @@ jobs:
3346
name: iPhone 6.9, iPhone 6.5, and iPad 13
3447
if: inputs.platform == 'all' || inputs.platform == 'ios'
3548
runs-on: blacksmith-12vcpu-macos-26
36-
timeout-minutes: 60
49+
# Capturing every palette multiplies the device matrix by six, and only the
50+
# one native build is shared between them.
51+
timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }}
3752
steps:
3853
- name: Checkout
3954
uses: actions/checkout@v6
@@ -62,10 +77,10 @@ jobs:
6277
"$vp_pnpm_bin/pnpm" --version
6378
6479
- name: Capture iOS showcase
65-
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}"
80+
run: pnpm screenshots:mobile --platform ios --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}"
6681

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

7085
- name: Upload iOS screenshots
7186
if: always()
@@ -80,7 +95,9 @@ jobs:
8095
name: Android phone, 7-inch tablet, and 10-inch tablet
8196
if: inputs.platform == 'all' || inputs.platform == 'android'
8297
runs-on: blacksmith-16vcpu-ubuntu-2404
83-
timeout-minutes: 60
98+
# Capturing every palette multiplies the device matrix by six, and only the
99+
# one native build is shared between them.
100+
timeout-minutes: ${{ inputs.theme == 'all' && 300 || 60 }}
84101
env:
85102
T3_SHOWCASE_ANDROID_ABI: x86_64
86103
steps:
@@ -137,10 +154,10 @@ jobs:
137154
cores: 8
138155
ram-size: 4096M
139156
disable-animations: false
140-
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}"
157+
script: pnpm screenshots:mobile --platform android --appearance "${{ inputs.appearance }}" --theme "${{ inputs.theme }}"
141158

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

145162
- name: Upload Android screenshots
146163
if: always()

apps/mobile/global.css

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
/* Page backgrounds */
1313
--color-screen: #f2f2f7;
1414
--color-sheet: rgba(242, 242, 247, 0.98);
15+
--color-sheet-solid: #f2f2f7;
1516

1617
/* Card / surface */
1718
--color-card: #ffffff;
@@ -39,13 +40,16 @@
3940
/* Primary action */
4041
--color-primary: #262626;
4142
--color-primary-foreground: #ffffff;
42-
--color-primary-shadow: rgba(0, 0, 0, 0.18);
43+
--color-primary-shadow: #000000;
4344

4445
/* Secondary action */
4546
--color-secondary: #ffffff;
4647
--color-secondary-foreground: #262626;
4748
--color-secondary-border: rgba(0, 0, 0, 0.08);
48-
--color-switch-active: #34c759;
49+
--color-switch-active-track: #34c759;
50+
--color-switch-active-thumb: #ffffff;
51+
--color-switch-inactive-track: rgba(0, 0, 0, 0.08);
52+
--color-switch-inactive-thumb: #8e8e93;
4953

5054
/* Danger */
5155
--color-danger: #fef2f2;
@@ -56,7 +60,7 @@
5660
--color-input: #ffffff;
5761
--color-input-border: rgba(0, 0, 0, 0.1);
5862
--color-sidebar-search: rgba(118, 118, 128, 0.12);
59-
--color-placeholder: #a3a3a3;
63+
--color-placeholder: #737373;
6064

6165
/* Icons */
6266
--color-icon: #262626;
@@ -90,6 +94,7 @@
9094
--color-user-bubble: #007aff;
9195
--color-user-bubble-foreground: #ffffff;
9296
--color-user-bubble-foreground-muted: rgba(255, 255, 255, 0.78);
97+
--color-user-bubble-skill-foreground: #f0abfc;
9398

9499
/* Drawer / modal backdrop */
95100
--color-backdrop: rgba(0, 0, 0, 0.22);
@@ -106,6 +111,7 @@
106111
/* Page backgrounds */
107112
--color-screen: #0a0a0a;
108113
--color-sheet: rgba(14, 14, 14, 0.98);
114+
--color-sheet-solid: #0e0e0e;
109115

110116
/* Card / surface */
111117
--color-card: #171717;
@@ -133,13 +139,16 @@
133139
/* Primary action */
134140
--color-primary: #f5f5f5;
135141
--color-primary-foreground: #0a0a0a;
136-
--color-primary-shadow: rgba(0, 0, 0, 0.22);
142+
--color-primary-shadow: #000000;
137143

138144
/* Secondary action */
139145
--color-secondary: rgba(255, 255, 255, 0.04);
140146
--color-secondary-foreground: #f5f5f5;
141147
--color-secondary-border: rgba(255, 255, 255, 0.06);
142-
--color-switch-active: #30d158;
148+
--color-switch-active-track: #30d158;
149+
--color-switch-active-thumb: #ffffff;
150+
--color-switch-inactive-track: rgba(255, 255, 255, 0.06);
151+
--color-switch-inactive-thumb: #8e8e93;
143152

144153
/* Danger */
145154
--color-danger: rgba(239, 68, 68, 0.14);
@@ -184,6 +193,7 @@
184193
--color-user-bubble: #0a84ff;
185194
--color-user-bubble-foreground: #ffffff;
186195
--color-user-bubble-foreground-muted: rgba(255, 255, 255, 0.78);
196+
--color-user-bubble-skill-foreground: #f0abfc;
187197

188198
/* Drawer / modal backdrop */
189199
--color-backdrop: rgba(0, 0, 0, 0.48);

apps/mobile/modules/t3-native-controls/android/src/main/java/expo/modules/t3nativecontrols/T3NativeControlsModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class T3NativeControlsModule : Module() {
2222
storedScene ?: appContext.currentActivity?.intent?.getStringExtra("showcaseScene")
2323
}
2424

25+
// The palette is fixed for the whole capture, so it only ever arrives as a
26+
// launch extra — unlike the scene, which the runner rewrites in place.
27+
Function("getShowcaseTheme") {
28+
appContext.currentActivity?.intent?.getStringExtra("showcaseTheme")
29+
}
30+
2531
Function("prepareShowcaseCapture") {
2632
// Android app data is cleared by the host runner before launch.
2733
}

apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ public final class T3NativeControlsModule: Module {
3333
return arguments[flagIndex + 1]
3434
}
3535

36+
// The palette is fixed for the whole capture, so it only ever arrives as a
37+
// launch argument — unlike the scene, which the runner rewrites in place.
38+
Function("getShowcaseTheme") { () -> String? in
39+
let arguments = ProcessInfo.processInfo.arguments
40+
guard
41+
let flagIndex = arguments.firstIndex(of: "--showcaseTheme"),
42+
arguments.indices.contains(flagIndex + 1)
43+
else {
44+
return nil as String?
45+
}
46+
return arguments[flagIndex + 1]
47+
}
48+
3649
Function("getShowcaseOrientation") { () -> String? in
3750
let arguments = ProcessInfo.processInfo.arguments
3851
guard

apps/mobile/src/App.tsx

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { BlurTargetView } from "expo-blur";
22
import * as Linking from "expo-linking";
33
import * as SplashScreen from "expo-splash-screen";
44
import { useEffect } from "react";
5-
import { StatusBar, useColorScheme } from "react-native";
5+
import { StatusBar } from "react-native";
66
import { GestureHandlerRootView } from "react-native-gesture-handler";
77
import { KeyboardProvider } from "react-native-keyboard-controller";
88
import { SafeAreaProvider } from "react-native-safe-area-context";
9-
import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigation/native";
9+
import { createStaticNavigation } from "@react-navigation/native";
1010

1111
import { RegistryContext } from "@effect/atom-react";
1212
import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
@@ -22,6 +22,7 @@ import { appAtomRegistry } from "./state/atom-registry";
2222
import { OverlayPortalHost } from "./components/OverlayPortal";
2323
import { appBlurTargetRef } from "./lib/appBlurTarget";
2424
import { useThemeColor } from "./lib/useThemeColor";
25+
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
2526

2627
import "../global.css";
2728

@@ -58,45 +59,51 @@ function SplashScreenCoordinator() {
5859
}
5960

6061
export default function App() {
61-
const colorScheme = useColorScheme();
62-
const statusBarBg = useThemeColor("--color-status-bar");
63-
6462
return (
6563
<RegistryContext.Provider value={appAtomRegistry}>
6664
<CloudAuthProvider>
6765
<AppearancePreferencesProvider>
68-
<SplashScreenCoordinator />
69-
<GestureHandlerRootView className="flex-1">
70-
<KeyboardProvider statusBarTranslucent>
71-
<SafeAreaProvider>
72-
<StatusBar
73-
barStyle={colorScheme === "dark" ? "light-content" : "dark-content"}
74-
backgroundColor={statusBarBg}
75-
translucent
76-
/>
77-
{/* The navigation theme drives the NATIVE header appearance: native-stack
78-
forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without
79-
this, React Navigation defaults to its light theme and every native
80-
header (glass buttons, title, materials) is forced light even when
81-
the system is in dark mode. */}
82-
{/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}
83-
<BlurTargetView ref={appBlurTargetRef} style={{ flex: 1 }}>
84-
<IncomingShareProvider>
85-
<Navigation
86-
linking={appLinking}
87-
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
88-
/>
89-
</IncomingShareProvider>
90-
<ConfirmDialogHost />
91-
</BlurTargetView>
92-
{/* Anchored-menu overlays render here — in-window, so the
93-
keyboard stays up while a dropdown is open. */}
94-
<OverlayPortalHost />
95-
</SafeAreaProvider>
96-
</KeyboardProvider>
97-
</GestureHandlerRootView>
66+
<AppContent />
9867
</AppearancePreferencesProvider>
9968
</CloudAuthProvider>
10069
</RegistryContext.Provider>
10170
);
10271
}
72+
73+
function AppContent() {
74+
const { themeAppearance } = useAppearancePreferences();
75+
const statusBarBg = useThemeColor("--color-status-bar");
76+
const navigationTheme = useMobileNavigationTheme(themeAppearance);
77+
78+
return (
79+
<>
80+
<SplashScreenCoordinator />
81+
<GestureHandlerRootView className="flex-1">
82+
<KeyboardProvider statusBarTranslucent>
83+
<SafeAreaProvider>
84+
<StatusBar
85+
barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"}
86+
backgroundColor={statusBarBg}
87+
translucent
88+
/>
89+
{/* The navigation theme drives the NATIVE header appearance: native-stack
90+
forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without
91+
this, React Navigation defaults to its light theme and every native
92+
header (glass buttons, title, materials) is forced light even when
93+
the system is in dark mode. */}
94+
{/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}
95+
<BlurTargetView ref={appBlurTargetRef} style={{ flex: 1 }}>
96+
<IncomingShareProvider>
97+
<Navigation linking={appLinking} theme={navigationTheme} />
98+
</IncomingShareProvider>
99+
<ConfirmDialogHost />
100+
</BlurTargetView>
101+
{/* Anchored-menu overlays render here — in-window, so the
102+
keyboard stays up while a dropdown is open. */}
103+
<OverlayPortalHost />
104+
</SafeAreaProvider>
105+
</KeyboardProvider>
106+
</GestureHandlerRootView>
107+
</>
108+
);
109+
}

apps/mobile/src/Stack.tsx

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type NativeStackNavigationOptions,
1212
} from "@react-navigation/native-stack";
1313
import { useEffect, useRef } from "react";
14-
import { Platform, Pressable, ScrollView, StyleSheet } from "react-native";
14+
import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native";
1515
import { useResolveClassNames } from "uniwind";
1616

1717
import { AppText as Text } from "./components/AppText";
@@ -71,11 +71,7 @@ import {
7171
} from "./features/sharing/incoming-share-presentation";
7272
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
7373
import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
74-
import {
75-
FORM_SHEET_PRESENTATION_OPTIONS,
76-
NATIVE_SHEET_SURFACE_COLOR,
77-
NATIVE_SHEET_SURFACE_CONTENT_STYLE,
78-
} from "./native/sheet-surface";
74+
import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface";
7975
import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
8076

8177
const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
@@ -96,11 +92,7 @@ const GLASS_HEADER_OPTIONS: AppScreenOptions = {
9692
headerLargeTitle: false,
9793
headerShadowVisible: false,
9894
headerShown: true,
99-
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED
100-
? { backgroundColor: "transparent" }
101-
: NATIVE_SHEET_SURFACE_COLOR !== undefined
102-
? { backgroundColor: NATIVE_SHEET_SURFACE_COLOR as unknown as string }
103-
: undefined,
95+
headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
10496
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
10597
headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
10698
scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
@@ -115,12 +107,6 @@ const SOLID_HEADER_OPTIONS: AppScreenOptions = {
115107
headerLargeTitle: false,
116108
headerShadowVisible: false,
117109
headerShown: true,
118-
headerStyle:
119-
NATIVE_SHEET_SURFACE_COLOR !== undefined
120-
? // native-stack types this as `string`, but the native side accepts any
121-
// ColorValue including DynamicColorIOS.
122-
{ backgroundColor: NATIVE_SHEET_SURFACE_COLOR as unknown as string }
123-
: undefined,
124110
headerTitleStyle: { fontSize: 18, fontWeight: "800" },
125111
headerTransparent: false,
126112
unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined,
@@ -507,7 +493,6 @@ export const RootStack = createNativeStackNavigator({
507493
linking: `${THREAD_LINKING_PREFIX}/files`,
508494
options: {
509495
...GLASS_HEADER_OPTIONS,
510-
contentStyle: NATIVE_SHEET_SURFACE_CONTENT_STYLE,
511496
title: "Files",
512497
},
513498
}),
@@ -636,7 +621,11 @@ export const RootStack = createNativeStackNavigator({
636621
// The whole new-task flow (choose project → draft → add project) shares
637622
// draft state via NewTaskFlowProvider. The expo-router era mounted it in
638623
// app/new/_layout.tsx; this layout wrapper is the native-stack equivalent.
639-
layout: ({ children }) => <NewTaskFlowProvider>{children}</NewTaskFlowProvider>,
624+
layout: ({ children }) => (
625+
<NewTaskFlowProvider>
626+
<View className="flex-1 bg-sheet-solid">{children}</View>
627+
</NewTaskFlowProvider>
628+
),
640629
options: {
641630
gestureEnabled: true,
642631
headerShown: false,

apps/mobile/src/components/AndroidAnchoredMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { BlurView } from "expo-blur";
33
import type { ReactNode } from "react";
44
import { useCallback, useEffect, useRef, useState } from "react";
55
import type { StyleProp, ViewStyle } from "react-native";
6-
import { BackHandler, Pressable, ScrollView, useColorScheme, View } from "react-native";
6+
import { BackHandler, Pressable, ScrollView, View } from "react-native";
77
import { useKeyboardState } from "react-native-keyboard-controller";
88
import Animated, { FadeIn } from "react-native-reanimated";
99

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

82-
const isDarkMode = useColorScheme() === "dark";
83+
const { themeAppearance } = useAppearancePreferences();
84+
const isDarkMode = themeAppearance === "dark";
8385
const keyboardVisible = useKeyboardState((state) => state.isVisible);
8486
const keyboardHeight = useKeyboardState((state) => state.height);
8587
const rippleColor = useThemeColor("--color-subtle");

0 commit comments

Comments
 (0)