From 0dd3639a0a8ba65398d4b91c3b771dc7c2b5c1af Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 11:43:21 +0200 Subject: [PATCH 01/12] fix(mobile): decode project SVG and ICO favicons Use expo-image for project favicons so the shared mobile UI can render the formats returned by the favicon resolver. Co-authored-by: codex --- apps/mobile/package.json | 1 + apps/mobile/src/components/ProjectFavicon.tsx | 5 +++-- pnpm-lock.yaml | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ae09235519e..145a91d4608 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -80,6 +80,7 @@ "expo-font": "~56.0.7", "expo-glass-effect": "~56.0.4", "expo-haptics": "~56.0.3", + "expo-image": "~56.0.11", "expo-image-picker": "~56.0.18", "expo-linking": "~56.0.14", "expo-network": "~56.0.5", diff --git a/apps/mobile/src/components/ProjectFavicon.tsx b/apps/mobile/src/components/ProjectFavicon.tsx index ba306c5a9fe..1378e204f5e 100644 --- a/apps/mobile/src/components/ProjectFavicon.tsx +++ b/apps/mobile/src/components/ProjectFavicon.tsx @@ -1,6 +1,7 @@ import { SymbolView } from "expo-symbols"; +import { Image } from "expo-image"; import { useState } from "react"; -import { Image, View } from "react-native"; +import { View } from "react-native"; import type { EnvironmentId } from "@t3tools/contracts"; import { useThemeColor } from "../lib/useThemeColor"; import { useAssetUrl } from "../state/assets"; @@ -78,7 +79,7 @@ function ProjectFaviconImage(props: { borderRadius: props.size * 0.16, ...(showImage ? {} : { position: "absolute" as const, opacity: 0 }), }} - resizeMode="contain" + contentFit="contain" onLoad={() => { if (props.faviconUrl) loadedFaviconUrls.add(props.faviconUrl); setStatus("loaded"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d53bf5cd70e..ed67594e846 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -306,6 +306,9 @@ importers: expo-haptics: specifier: ~56.0.3 version: 56.0.3(expo@56.0.12) + expo-image: + specifier: ~56.0.11 + version: 56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-image-picker: specifier: ~56.0.18 version: 56.0.18(expo@56.0.12) @@ -6562,6 +6565,17 @@ packages: peerDependencies: expo: '*' + expo-image@56.0.11: + resolution: {integrity: sha512-k2xwxGk14xi6zxmEGAU4rUTb1lK5qf0y0Qb8+Jaggnul0KaJJxcq9qvyDp9iyJBW35cp9isONAUnNtIiooZ/Pw==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + expo-json-utils@56.0.0: resolution: {integrity: sha512-lUqyv9aIGDbYTQ5Nux2FnH2/Dz0w5uJ8Pr080eS0StXi2jr5OmuMNErpzUnpfnYOU55xKotd4AHv68PfV/ludg==} @@ -16695,6 +16709,13 @@ snapshots: expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-loader: 56.0.3(expo@56.0.12) + expo-image@56.0.11(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + dependencies: + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + sf-symbols-typescript: 2.2.0 + expo-json-utils@56.0.0: {} expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): From f29ae8519b4986bd60077d72bd51cf659c9fa81c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 11:43:30 +0200 Subject: [PATCH 02/12] fix(mobile): gate startup on font loading Keep the native splash visible until fonts settle, render with the system font if loading fails, and avoid stale text measurements after a cold start. Co-authored-by: codex --- apps/mobile/src/App.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index d0880a39798..d031b474ae5 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -26,20 +26,33 @@ const appLinking = { prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], }; +// Keep the native splash up until fonts resolve; without this Expo +// auto-hides it on first render and the font gate shows a blank screen. +SplashScreen.preventAutoHideAsync().catch(() => undefined); + const Navigation = createStaticNavigation(RootStack); export default function App() { - const [fontsLoaded] = useFonts({ + const [fontsLoaded, fontError] = useFonts({ DMSans_400Regular, DMSans_500Medium, DMSans_700Bold, }); const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); + const fontsResolved = fontsLoaded || fontError !== null; useEffect(() => { - if (fontsLoaded) SplashScreen.hide(); - }, [fontsLoaded]); + if (fontsResolved) SplashScreen.hide(); + }, [fontsResolved]); + + // Text measured with the fallback font keeps its stale width when the + // custom font swaps in, clipping trailing glyphs. Hold rendering (behind + // the splash screen) until fonts resolve; on load failure, proceed with + // the system font rather than blocking the app. + if (!fontsResolved) { + return null; + } return ( From 143cdb04f73f34cc0dfc3074eea9446f53b1bbe5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 11:53:38 +0200 Subject: [PATCH 03/12] fix(mobile): embed DM Sans in native builds Register the three app font weights through the expo-font config plugin, align Uniwind and native surfaces to their native family names, and remove the runtime font gate so providers mount immediately. Co-authored-by: codex --- apps/mobile/app.config.ts | 35 ++++++++++++++++++- apps/mobile/global.css | 13 +++---- .../ios/T3ComposerEditorView.swift | 4 +-- apps/mobile/src/App.tsx | 30 ---------------- .../features/files/FileMarkdownPreview.tsx | 16 ++++----- .../terminal/NativeTerminalSurface.tsx | 2 +- .../src/features/threads/ThreadComposer.tsx | 2 +- .../src/features/threads/ThreadFeed.tsx | 28 +++++++-------- .../threads/ThreadNavigationSidebar.tsx | 2 +- .../src/native/T3ComposerEditor.ios.tsx | 2 +- apps/mobile/src/native/T3ComposerEditor.tsx | 2 +- 11 files changed, 70 insertions(+), 66 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 3597e0161cd..7f848226a01 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -63,6 +63,16 @@ function resolveAppVariant(value: string | undefined): AppVariant { const variant = VARIANT_CONFIG[APP_VARIANT]; +const dmSansFonts = { + regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf", + medium: "@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf", + bold: "@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf", +} as const; + +// These aliases match the fonts' PostScript names on iOS. Register the same +// names on Android so React Native and the native composer use one set of +// family names without waiting for runtime font loading. + const config: ExpoConfig = { name: variant.appName, slug: "t3-code", @@ -121,7 +131,30 @@ const config: ExpoConfig = { favicon: "./assets/favicon.png", }, plugins: [ - "expo-font", + [ + "expo-font", + { + ios: { + fonts: [dmSansFonts.regular, dmSansFonts.medium, dmSansFonts.bold], + }, + android: { + fonts: [ + { + fontFamily: "DMSans-Regular", + fontDefinitions: [{ path: dmSansFonts.regular, weight: 400 }], + }, + { + fontFamily: "DMSans-Medium", + fontDefinitions: [{ path: dmSansFonts.medium, weight: 500 }], + }, + { + fontFamily: "DMSans-Bold", + fontDefinitions: [{ path: dmSansFonts.bold, weight: 700 }], + }, + ], + }, + }, + ], "expo-secure-store", "expo-sqlite", ["@clerk/expo", { theme: "./clerk-theme.json" }], diff --git a/apps/mobile/global.css b/apps/mobile/global.css index f76300ee524..539ed7bf2ce 100644 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -194,9 +194,10 @@ /* ─── Typography ────────────────────────────────────────────────────── */ @theme { - --font-sans: "DMSans_400Regular"; - --font-medium: "DMSans_500Medium"; - --font-bold: "DMSans_700Bold"; + /* Keep these native family names aligned with app.config.ts. */ + --font-sans: "DMSans-Regular"; + --font-medium: "DMSans-Medium"; + --font-bold: "DMSans-Bold"; /* Keep this scale aligned with src/lib/typography.ts for native style props. */ --text-3xs: 11px; @@ -221,14 +222,14 @@ /* ─── Custom utilities ──────────────────────────────────────────────── */ @utility font-t3-medium { - font-family: "DMSans_500Medium"; + font-family: "DMSans-Medium"; } @utility font-t3-bold { - font-family: "DMSans_700Bold"; + font-family: "DMSans-Bold"; } @utility font-t3-extrabold { - font-family: "DMSans_700Bold"; + font-family: "DMSans-Bold"; font-weight: 800; } diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 8515abfbae8..180da203008 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -300,7 +300,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { skillText: "#a21caf", fileTint: "#737373" ) - private var fontFamily = "DMSans_400Regular" + private var fontFamily = "DMSans-Regular" private var fontSize: CGFloat = 14 private var lineHeight: CGFloat = 20 private var contentInsetVertical: CGFloat = 0 @@ -608,7 +608,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { iconImage: UIImage?, style: ComposerChipStyle ) -> UIImage { - let font = UIFont(name: "DMSans_500Medium", size: max(12, fontSize - 2)) + let font = UIFont(name: "DMSans-Medium", size: max(12, fontSize - 2)) ?? UIFont.systemFont(ofSize: max(12, fontSize - 2), weight: .medium) let fallbackIcon = UIImage( systemName: iconName, diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index d031b474ae5..e9c9c222de1 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -1,11 +1,4 @@ -import { - DMSans_400Regular, - DMSans_500Medium, - DMSans_700Bold, - useFonts, -} from "@expo-google-fonts/dm-sans"; import * as Linking from "expo-linking"; -import * as SplashScreen from "expo-splash-screen"; import { StatusBar, useColorScheme } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; @@ -13,7 +6,6 @@ import { SafeAreaProvider } from "react-native-safe-area-context"; import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigation/native"; import { RegistryContext } from "@effect/atom-react"; -import { useEffect } from "react"; import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider"; import { RootStack } from "./Stack"; @@ -26,33 +18,11 @@ const appLinking = { prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], }; -// Keep the native splash up until fonts resolve; without this Expo -// auto-hides it on first render and the font gate shows a blank screen. -SplashScreen.preventAutoHideAsync().catch(() => undefined); - const Navigation = createStaticNavigation(RootStack); export default function App() { - const [fontsLoaded, fontError] = useFonts({ - DMSans_400Regular, - DMSans_500Medium, - DMSans_700Bold, - }); const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); - const fontsResolved = fontsLoaded || fontError !== null; - - useEffect(() => { - if (fontsResolved) SplashScreen.hide(); - }, [fontsResolved]); - - // Text measured with the fallback font keeps its stale width when the - // custom font swaps in, clipping trailing glyphs. Hold rendering (behind - // the splash screen) until fonts resolve; on load failure, proceed with - // the system font rather than blocking the app. - if (!fontsResolved) { - return null; - } return ( diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index 4109ce2a999..b6a4eb449e8 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -57,7 +57,7 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { }} style={{ color: link, - fontFamily: "DMSans_500Medium", + fontFamily: "DMSans-Medium", textDecorationLine: "none", }} > @@ -86,21 +86,21 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { styles: { text: { color: body, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", fontSize: markdownFontSizes.m, lineHeight: markdownFontSizes.bodyLineHeight, }, heading: { color: strong, - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", }, strong: { color: strong, - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", }, link: { color: link, - fontFamily: "DMSans_500Medium", + fontFamily: "DMSans-Medium", }, blockquote: { backgroundColor: blockquoteBackground, @@ -141,9 +141,9 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans_400Regular", - headingFontFamily: "DMSans_700Bold", - boldFontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Regular", + headingFontFamily: "DMSans-Bold", + boldFontFamily: "DMSans-Bold", }, }; }, [ diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index ad174a2502d..d99e10e25ab 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -169,7 +169,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter className="text-2xs" style={{ color: theme.foreground, - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", }} > Ctrl-C diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 3192e47667b..363fd572532 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -797,7 +797,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer textStyle={{ ...bodyText, color: foregroundColor, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", }} /> diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index c95af3979b8..98fad6c27c1 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -235,7 +235,7 @@ const markdownLinkStyles = StyleSheet.create({ borderRadius: 3, }, file: { - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", fontWeight: "700", }, }); @@ -255,7 +255,7 @@ const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { }} style={{ color: props.color, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", textDecorationLine: "none", }} > @@ -365,8 +365,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe h6: markdownFontSizes.h6, }, fontFamilies: { - regular: "DMSans_400Regular", - heading: "DMSans_700Bold", + regular: "DMSans-Regular", + heading: "DMSans-Bold", mono: "ui-monospace", }, headingWeight: "700", @@ -388,7 +388,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe bold: { fontWeight: "700", color: markdownStrongColor, - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", }, italic: { fontStyle: "italic" }, link: { @@ -404,7 +404,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe marginVertical: 10, }, heading: { - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", color: markdownStrongColor, marginTop: 18, marginBottom: 8, @@ -492,7 +492,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe width: ordered ? 22 : 12, marginRight: 5, color: inlineTextColor, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", fontSize: markdownFontSizes.m, lineHeight: markdownFontSizes.bodyLineHeight, textAlign: ordered ? "right" : "center", @@ -601,7 +601,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe bold: { fontWeight: "700", color: markdownUserBodyColor, - fontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Bold", }, heading: { ...baseStyles.heading, @@ -655,9 +655,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans_400Regular", - headingFontFamily: "DMSans_700Bold", - boldFontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Regular", + headingFontFamily: "DMSans-Bold", + boldFontFamily: "DMSans-Bold", }, }, assistant: { @@ -686,9 +686,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans_400Regular", - headingFontFamily: "DMSans_700Bold", - boldFontFamily: "DMSans_700Bold", + fontFamily: "DMSans-Regular", + headingFontFamily: "DMSans-Bold", + boldFontFamily: "DMSans-Bold", }, }, }; diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index c639bd60f53..2ff4b905910 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -819,7 +819,7 @@ const styles = StyleSheet.create({ height: 34, paddingVertical: 0, paddingHorizontal: 0, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", }, threadList: { flex: 1, diff --git a/apps/mobile/src/native/T3ComposerEditor.ios.tsx b/apps/mobile/src/native/T3ComposerEditor.ios.tsx index 1089ab3ac1f..38372afb6ca 100644 --- a/apps/mobile/src/native/T3ComposerEditor.ios.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.ios.tsx @@ -228,7 +228,7 @@ export function ComposerEditor({ fontFamily={ typeof resolvedTextStyle.fontFamily === "string" ? resolvedTextStyle.fontFamily - : "DMSans_400Regular" + : "DMSans-Regular" } fontSize={ typeof resolvedTextStyle.fontSize === "number" diff --git a/apps/mobile/src/native/T3ComposerEditor.tsx b/apps/mobile/src/native/T3ComposerEditor.tsx index 27e61e1b99c..85725f32112 100644 --- a/apps/mobile/src/native/T3ComposerEditor.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.tsx @@ -48,7 +48,7 @@ export function ComposerEditor({ flex: 1, minHeight: 0, color: foregroundColor, - fontFamily: "DMSans_400Regular", + fontFamily: "DMSans-Regular", ...bodyText, paddingVertical: contentInsetVertical, }, From 5cfa40f63a28df48803144ffd3f5260b52cf8065 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 11:58:04 +0200 Subject: [PATCH 04/12] refactor(mobile): use font utilities for text Co-authored-by: codex --- .../mobile/src/features/files/FileMarkdownPreview.tsx | 2 +- .../src/features/terminal/NativeTerminalSurface.tsx | 8 +------- apps/mobile/src/features/threads/ThreadFeed.tsx | 11 ++++------- .../src/features/threads/ThreadNavigationSidebar.tsx | 3 +-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index b6a4eb449e8..add098c3d20 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -50,6 +50,7 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { const renderers: CustomRenderers = { link: ({ href, children }) => ( { if (href) { void tryOpenExternalUrl(href, "markdown-link"); @@ -57,7 +58,6 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { }} style={{ color: link, - fontFamily: "DMSans-Medium", textDecorationLine: "none", }} > diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index d99e10e25ab..e9665a7dab3 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -165,13 +165,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter })} onPress={() => props.onInput("\u0003")} > - + Ctrl-C diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 98fad6c27c1..dd0df5a49b3 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -234,10 +234,6 @@ const markdownLinkStyles = StyleSheet.create({ favicon: { borderRadius: 3, }, - file: { - fontFamily: "DMSans-Bold", - fontWeight: "700", - }, }); const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { @@ -250,12 +246,12 @@ const MarkdownExternalLink = memo(function MarkdownExternalLink(props: { return ( { void Linking.openURL(props.href); }} style={{ color: props.color, - fontFamily: "DMSans-Regular", textDecorationLine: "none", }} > @@ -428,8 +424,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe if (presentation.kind === "file") { return ( onLinkPress(href)} - style={[markdownLinkStyles.file, { color: inlineTextColor }]} + style={{ color: inlineTextColor }} > void): MarkdownStyleSe }} > @@ -819,7 +819,6 @@ const styles = StyleSheet.create({ height: 34, paddingVertical: 0, paddingHorizontal: 0, - fontFamily: "DMSans-Regular", }, threadList: { flex: 1, From 722dc15e83d4bd6bc47d7a197855d7d6a95f22ab Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 12:01:59 +0200 Subject: [PATCH 05/12] refactor(mobile): resolve native fonts from theme Co-authored-by: codex --- apps/mobile/global.css | 6 ++-- .../features/files/FileMarkdownPreview.tsx | 21 +++++++---- .../src/features/threads/ThreadComposer.tsx | 1 - .../src/features/threads/ThreadFeed.tsx | 35 ++++++++++++------- apps/mobile/src/lib/useFontFamily.ts | 15 ++++++++ .../src/native/T3ComposerEditor.ios.tsx | 6 ++-- apps/mobile/src/native/T3ComposerEditor.tsx | 4 ++- 7 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 apps/mobile/src/lib/useFontFamily.ts diff --git a/apps/mobile/global.css b/apps/mobile/global.css index 539ed7bf2ce..ac90aec15a9 100644 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -222,14 +222,14 @@ /* ─── Custom utilities ──────────────────────────────────────────────── */ @utility font-t3-medium { - font-family: "DMSans-Medium"; + font-family: var(--font-medium); } @utility font-t3-bold { - font-family: "DMSans-Bold"; + font-family: var(--font-bold); } @utility font-t3-extrabold { - font-family: "DMSans-Bold"; + font-family: var(--font-bold); font-weight: 800; } diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index add098c3d20..1efa7dc21f6 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -8,6 +8,7 @@ import { import { RefreshControl, ScrollView, Text as NativeText, View } from "react-native"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; +import { useFontFamily } from "../../lib/useFontFamily"; import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, @@ -45,6 +46,9 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { const codeBackground = String(useThemeColor("--color-md-code-bg")); const codeText = String(useThemeColor("--color-md-code-text")); const horizontalRule = String(useThemeColor("--color-md-hr")); + const regularFontFamily = useFontFamily("regular"); + const mediumFontFamily = useFontFamily("medium"); + const boldFontFamily = useFontFamily("bold"); return useMemo(() => { const renderers: CustomRenderers = { @@ -86,21 +90,21 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { styles: { text: { color: body, - fontFamily: "DMSans-Regular", + fontFamily: regularFontFamily, fontSize: markdownFontSizes.m, lineHeight: markdownFontSizes.bodyLineHeight, }, heading: { color: strong, - fontFamily: "DMSans-Bold", + fontFamily: boldFontFamily, }, strong: { color: strong, - fontFamily: "DMSans-Bold", + fontFamily: boldFontFamily, }, link: { color: link, - fontFamily: "DMSans-Medium", + fontFamily: mediumFontFamily, }, blockquote: { backgroundColor: blockquoteBackground, @@ -141,9 +145,9 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans-Regular", - headingFontFamily: "DMSans-Bold", - boldFontFamily: "DMSans-Bold", + fontFamily: regularFontFamily, + headingFontFamily: boldFontFamily, + boldFontFamily, }, }; }, [ @@ -155,8 +159,11 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { horizontalRule, link, markdownFontSizes, + mediumFontFamily, nativeMarkdownTypography, + regularFontFamily, strong, + boldFontFamily, ]); } diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 363fd572532..d65c670d359 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -797,7 +797,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer textStyle={{ ...bodyText, color: foregroundColor, - fontFamily: "DMSans-Regular", }} /> diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index dd0df5a49b3..290e5230db0 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -52,6 +52,7 @@ import Animated, { type SharedValue, } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; +import { useFontFamily } from "../../lib/useFontFamily"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { hasNativeSelectableMarkdownText, @@ -310,6 +311,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe ); const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"]; const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); + const regularFontFamily = useFontFamily("regular"); + const boldFontFamily = useFontFamily("bold"); return useMemo(() => { const markdownBodyColor = colors.body; @@ -361,8 +364,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe h6: markdownFontSizes.h6, }, fontFamilies: { - regular: "DMSans-Regular", - heading: "DMSans-Bold", + regular: regularFontFamily, + heading: boldFontFamily, mono: "ui-monospace", }, headingWeight: "700", @@ -384,7 +387,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe bold: { fontWeight: "700", color: markdownStrongColor, - fontFamily: "DMSans-Bold", + fontFamily: boldFontFamily, }, italic: { fontStyle: "italic" }, link: { @@ -400,7 +403,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe marginVertical: 10, }, heading: { - fontFamily: "DMSans-Bold", + fontFamily: boldFontFamily, color: markdownStrongColor, marginTop: 18, marginBottom: 8, @@ -598,7 +601,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe bold: { fontWeight: "700", color: markdownUserBodyColor, - fontFamily: "DMSans-Bold", + fontFamily: boldFontFamily, }, heading: { ...baseStyles.heading, @@ -652,9 +655,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans-Regular", - headingFontFamily: "DMSans-Bold", - boldFontFamily: "DMSans-Bold", + fontFamily: regularFontFamily, + headingFontFamily: boldFontFamily, + boldFontFamily, }, }, assistant: { @@ -683,13 +686,21 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe fontSize: nativeMarkdownTypography.fontSize, lineHeight: nativeMarkdownTypography.lineHeight, headingFontSizes: nativeMarkdownTypography.headingFontSizes, - fontFamily: "DMSans-Regular", - headingFontFamily: "DMSans-Bold", - boldFontFamily: "DMSans-Bold", + fontFamily: regularFontFamily, + headingFontFamily: boldFontFamily, + boldFontFamily, }, }, }; - }, [colors, inlineSkillForeground, markdownFontSizes, nativeMarkdownTypography, onLinkPress]); + }, [ + boldFontFamily, + colors, + inlineSkillForeground, + markdownFontSizes, + nativeMarkdownTypography, + onLinkPress, + regularFontFamily, + ]); } function renderFeedEntry( diff --git a/apps/mobile/src/lib/useFontFamily.ts b/apps/mobile/src/lib/useFontFamily.ts new file mode 100644 index 00000000000..09805ae1154 --- /dev/null +++ b/apps/mobile/src/lib/useFontFamily.ts @@ -0,0 +1,15 @@ +import { useCSSVariable } from "uniwind"; + +const FONT_FAMILY_VARIABLES = { + regular: "--font-sans", + medium: "--font-medium", + bold: "--font-bold", +} as const; + +/** + * Resolves a font family for APIs that require a style object or native prop. + * Prefer Uniwind font classes when the target component accepts `className`. + */ +export function useFontFamily(weight: keyof typeof FONT_FAMILY_VARIABLES): string { + return useCSSVariable(FONT_FAMILY_VARIABLES[weight]) as string; +} diff --git a/apps/mobile/src/native/T3ComposerEditor.ios.tsx b/apps/mobile/src/native/T3ComposerEditor.ios.tsx index 38372afb6ca..4e9d62ad2c2 100644 --- a/apps/mobile/src/native/T3ComposerEditor.ios.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.ios.tsx @@ -15,6 +15,7 @@ import { Image, StyleSheet } from "react-native"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; import { useThemeColor } from "../lib/useThemeColor"; +import { useFontFamily } from "../lib/useFontFamily"; import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; import { acknowledgeComposerNativeEvent, @@ -117,6 +118,7 @@ export function ComposerEditor({ const skillBorder = useThemeColor("--color-inline-skill-border"); const skillText = useThemeColor("--color-inline-skill-foreground"); const fileTint = useThemeColor("--color-icon-muted"); + const fontFamily = useFontFamily("regular"); useImperativeHandle( ref, @@ -226,9 +228,7 @@ export function ComposerEditor({ themeJson={themeJson} placeholder={props.placeholder ?? ""} fontFamily={ - typeof resolvedTextStyle.fontFamily === "string" - ? resolvedTextStyle.fontFamily - : "DMSans-Regular" + typeof resolvedTextStyle.fontFamily === "string" ? resolvedTextStyle.fontFamily : fontFamily } fontSize={ typeof resolvedTextStyle.fontSize === "number" diff --git a/apps/mobile/src/native/T3ComposerEditor.tsx b/apps/mobile/src/native/T3ComposerEditor.tsx index 85725f32112..8995f6bcb0a 100644 --- a/apps/mobile/src/native/T3ComposerEditor.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.tsx @@ -3,6 +3,7 @@ import { useImperativeHandle, useRef } from "react"; import { TextInput, type TextInput as RNTextInput } from "react-native"; import { useThemeColor } from "../lib/useThemeColor"; +import { useFontFamily } from "../lib/useFontFamily"; import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; import { useNativePaste } from "../lib/useNativePaste"; import type { ComposerEditorProps } from "./T3ComposerEditor.types"; @@ -21,6 +22,7 @@ export function ComposerEditor({ const bodyText = useScaledTextRole("body"); const foregroundColor = useThemeColor("--color-foreground"); const placeholderColor = useThemeColor("--color-placeholder"); + const fontFamily = useFontFamily("regular"); const handlePaste = useNativePaste((uris) => onPasteImages?.(uris)); useImperativeHandle( @@ -48,7 +50,7 @@ export function ComposerEditor({ flex: 1, minHeight: 0, color: foregroundColor, - fontFamily: "DMSans-Regular", + fontFamily, ...bodyText, paddingVertical: contentInsetVertical, }, From 4a8875dbb30b8b92810bfbbc25a3cc46f077baa8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 12:25:29 +0200 Subject: [PATCH 06/12] refactor(mobile): prefer uniwind utilities app-wide Co-authored-by: codex --- apps/mobile/global.css | 2 + apps/mobile/src/components/AppText.tsx | 8 +- apps/mobile/src/components/BrandMark.tsx | 9 +- .../components/ComposerAttachmentStrip.tsx | 14 +- .../src/components/ComposerToolbarTrigger.tsx | 6 +- .../src/components/GlassSafeAreaView.tsx | 20 +-- .../archive/ArchivedThreadsScreen.tsx | 11 +- .../cloud/CloudWaitlistEnrollment.tsx | 85 ++--------- .../cloud/ConnectOnboardingRouteScreen.tsx | 2 +- .../connection/CloudEnvironmentRows.tsx | 6 +- .../connection/ConnectionEnvironmentRow.tsx | 18 +-- .../connection/ConnectionSheetButton.tsx | 52 +++---- .../connection/ConnectionsNewRouteScreen.tsx | 22 +-- .../connection/ConnectionsRouteScreen.tsx | 7 +- .../src/features/files/FileTreeBrowser.tsx | 2 +- .../features/files/ThreadFilesRouteScreen.tsx | 9 +- apps/mobile/src/features/home/HomeScreen.tsx | 2 +- .../features/home/thread-swipe-actions.tsx | 61 ++------ .../layout/AdaptiveWorkspaceLayout.tsx | 7 +- .../layout/workspace-inspector-pane.tsx | 5 +- .../layout/workspace-pane-divider.tsx | 11 +- .../features/projects/AddProjectScreen.tsx | 18 +-- .../review/ReviewCommentComposerSheet.tsx | 15 +- .../src/features/review/ReviewSheet.tsx | 4 +- .../features/review/reviewDiffRendering.tsx | 4 +- .../SettingsAppearanceRouteScreen.tsx | 6 +- .../SettingsClientStorageRouteScreen.tsx | 16 +-- .../SettingsEnvironmentsRouteScreen.tsx | 10 +- .../features/settings/SettingsRouteScreen.tsx | 12 +- .../components/FontSizeSliderRow.tsx | 2 +- .../settings/components/SettingsRow.tsx | 7 +- .../settings/components/SettingsSection.tsx | 5 +- .../settings/components/SettingsSwitchRow.tsx | 7 +- .../terminal/NativeTerminalSurface.tsx | 17 +-- .../terminal/ThreadTerminalRouteScreen.tsx | 6 +- .../threads/ComposerCommandPopover.tsx | 27 +--- .../threads/GitActionProgressOverlay.tsx | 3 +- .../features/threads/NewTaskDraftScreen.tsx | 16 +-- .../features/threads/NewTaskRouteScreen.tsx | 21 ++- .../src/features/threads/ThreadComposer.tsx | 43 ++---- .../features/threads/ThreadDetailScreen.tsx | 15 +- .../src/features/threads/ThreadFeed.tsx | 51 +++---- .../threads/ThreadNavigationSidebar.tsx | 133 ++++-------------- .../features/threads/git/GitBranchesSheet.tsx | 56 ++------ .../features/threads/git/GitCommitSheet.tsx | 47 ++----- .../features/threads/git/GitConfirmSheet.tsx | 12 +- .../features/threads/git/GitOverviewSheet.tsx | 25 ++-- .../threads/git/gitSheetComponents.tsx | 60 +++----- .../threads/sidebar-filter-button.tsx | 17 +-- .../threads/sidebar-header-actions.tsx | 22 +-- .../thread-inspector-content-stack.tsx | 2 +- .../features/threads/thread-list-items.tsx | 81 +++++------ .../src/features/threads/thread-work-log.tsx | 5 +- apps/mobile/src/native/T3KeyboardCommands.tsx | 2 +- 54 files changed, 319 insertions(+), 807 deletions(-) diff --git a/apps/mobile/global.css b/apps/mobile/global.css index ac90aec15a9..e048760dc50 100644 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -51,6 +51,7 @@ /* Inputs */ --color-input: #ffffff; --color-input-border: rgba(0, 0, 0, 0.1); + --color-sidebar-search: rgba(118, 118, 128, 0.12); --color-placeholder: #a3a3a3; /* Icons */ @@ -144,6 +145,7 @@ /* Inputs */ --color-input: #141414; --color-input-border: rgba(255, 255, 255, 0.08); + --color-sidebar-search: rgba(118, 118, 128, 0.24); --color-placeholder: #8e8e93; /* Icons */ diff --git a/apps/mobile/src/components/AppText.tsx b/apps/mobile/src/components/AppText.tsx index 33bffb1f8cd..0288e02420c 100644 --- a/apps/mobile/src/components/AppText.tsx +++ b/apps/mobile/src/components/AppText.tsx @@ -4,7 +4,6 @@ import { type TextInputProps as RNTextInputProps, type TextProps as RNTextProps, } from "react-native"; -import { useThemeColor } from "../lib/useThemeColor"; import { cn } from "../lib/cn"; @@ -33,8 +32,6 @@ export function AppTextInput({ ref, ...props }: AppTextInputProps) { - const placeholderColor = useThemeColor("--color-placeholder"); - return ( ); diff --git a/apps/mobile/src/components/BrandMark.tsx b/apps/mobile/src/components/BrandMark.tsx index 78aaf6b0ce4..4d1ac01b1ff 100644 --- a/apps/mobile/src/components/BrandMark.tsx +++ b/apps/mobile/src/components/BrandMark.tsx @@ -22,14 +22,9 @@ export function BrandMark(props: { readonly compact?: boolean; readonly stageLab /> - - T3 Code - + T3 Code - + {stageLabel} diff --git a/apps/mobile/src/components/ComposerAttachmentStrip.tsx b/apps/mobile/src/components/ComposerAttachmentStrip.tsx index 5b4d3d626c9..4ee67e93533 100644 --- a/apps/mobile/src/components/ComposerAttachmentStrip.tsx +++ b/apps/mobile/src/components/ComposerAttachmentStrip.tsx @@ -39,14 +39,14 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) { horizontal showsHorizontalScrollIndicator={false} keyboardShouldPersistTaps="always" - style={{ flexGrow: 0 }} + className="grow-0" > - + {props.attachments.map((image) => ( props.onRemove(image.id)} diff --git a/apps/mobile/src/components/ComposerToolbarTrigger.tsx b/apps/mobile/src/components/ComposerToolbarTrigger.tsx index e054a13f697..7d48e0737c4 100644 --- a/apps/mobile/src/components/ComposerToolbarTrigger.tsx +++ b/apps/mobile/src/components/ComposerToolbarTrigger.tsx @@ -31,11 +31,9 @@ export function ComposerToolbarRow(props: { }) { return ( + - {leftSlot} - - {centerSlot} - - {rightSlot} + {leftSlot} + {centerSlot} + {rightSlot} diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 6ef0819abd7..0e4f98c853a 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -244,9 +244,8 @@ function ProjectGroupLabel(props: { workspaceRoot={props.project.workspaceRoot} /> {props.project.title} @@ -331,10 +330,7 @@ function ArchivedThreadRow(props: { > {props.thread.title} - + {timestamp} @@ -347,9 +343,8 @@ function ArchivedThreadRow(props: { type="monochrome" /> {subtitle.join(" · ")} diff --git a/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx b/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx index 3bc51e397d8..1a7020fd0ed 100644 --- a/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx +++ b/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx @@ -1,13 +1,12 @@ import { useWaitlist } from "@clerk/expo"; -import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native"; +import { ActivityIndicator, Pressable, Text, TextInput, View } from "react-native"; import { useState } from "react"; -import { useThemeColor } from "../../lib/useThemeColor"; +import { cn } from "../../lib/cn"; import { CloudWaitlistJoinRejectedError, joinCloudWaitlist } from "./cloudWaitlistJoin"; export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void }) { const { errors, fetchStatus, waitlist } = useWaitlist(); - const colors = useCloudWaitlistColors(); const [emailAddress, setEmailAddress] = useState(""); const [requestError, setRequestError] = useState(null); const isSubmitting = fetchStatus === "fetching"; @@ -34,7 +33,7 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } if (waitlist.id) { return ( - + You are on the waitlist @@ -47,19 +46,22 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } } return ( - + Enter your email and we will let you know when access is ready. - + Email address { setEmailAddress(value); @@ -67,16 +69,8 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } }} onSubmitEditing={() => void joinWaitlist()} placeholder="Enter your email address" - placeholderTextColor={colors.placeholder} + placeholderTextColorClassName="accent-placeholder" returnKeyType="join" - style={[ - styles.input, - { - backgroundColor: colors.input, - borderColor: - fieldError || requestError ? colors.dangerForeground : colors.inputBorder, - }, - ]} textContentType="emailAddress" value={emailAddress} /> @@ -99,15 +93,11 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } }} disabled={isSubmitting || emailAddress.trim().length === 0} onPress={() => void joinWaitlist()} - style={[ - styles.primaryButton, - { - backgroundColor: colors.primary, - opacity: isSubmitting || emailAddress.trim().length === 0 ? 0.45 : 1, - }, - ]} + className="min-h-[54px] flex-row items-center justify-center gap-2 rounded-full bg-primary px-5 py-3 disabled:opacity-[0.45]" > - {isSubmitting ? : null} + {isSubmitting ? ( + + ) : null} {isSubmitting ? "Joining" : "Join the waitlist"} @@ -120,7 +110,7 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } function SignInAction(props: { readonly onPress: () => void }) { return ( - + Already have access? Sign in @@ -128,48 +118,3 @@ function SignInAction(props: { readonly onPress: () => void }) { ); } - -function useCloudWaitlistColors() { - return { - dangerForeground: String(useThemeColor("--color-danger-foreground")), - input: String(useThemeColor("--color-input")), - inputBorder: String(useThemeColor("--color-input-border")), - placeholder: String(useThemeColor("--color-placeholder")), - primary: String(useThemeColor("--color-primary")), - primaryForeground: String(useThemeColor("--color-primary-foreground")), - }; -} - -const styles = StyleSheet.create({ - content: { - gap: 18, - }, - field: { - gap: 8, - }, - input: { - borderCurve: "continuous", - borderRadius: 16, - borderWidth: 1, - minHeight: 54, - paddingHorizontal: 16, - paddingVertical: 14, - }, - primaryButton: { - alignItems: "center", - borderRadius: 999, - flexDirection: "row", - gap: 8, - justifyContent: "center", - minHeight: 54, - paddingHorizontal: 20, - paddingVertical: 12, - }, - signInRow: { - alignItems: "center", - flexDirection: "row", - gap: 4, - justifyContent: "center", - paddingTop: 4, - }, -}); diff --git a/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx b/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx index 657905c8b65..1f56ea8da17 100644 --- a/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx +++ b/apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx @@ -88,7 +88,7 @@ function ConfiguredConnectOnboardingRouteScreen() { alwaysBounceVertical contentInsetAdjustmentBehavior="automatic" showsVerticalScrollIndicator={false} - style={{ flex: 1 }} + className="flex-1" contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }} contentContainerStyle={{ gap: 16, diff --git a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx index 854ae37d4ce..babec90e45c 100644 --- a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx +++ b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx @@ -297,9 +297,8 @@ function CloudEnvironmentRowShell(props: { {props.connectionError ? ( {measuredErrorText} @@ -321,7 +320,7 @@ function CloudEnvironmentRowShell(props: { { event.stopPropagation(); copyTextWithHaptic(errorTraceId, { target: "connection-trace-id" }); @@ -329,7 +328,6 @@ function CloudEnvironmentRowShell(props: { onPress={(event) => { event.stopPropagation(); }} - style={{ textDecorationStyle: "dotted" }} > {errorTraceId} diff --git a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx index 4b2da3e24ca..9c4ec8cf018 100644 --- a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx +++ b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx @@ -98,7 +98,7 @@ export function ConnectionEnvironmentRow(props: { { event.stopPropagation(); copyTextWithHaptic(statusTraceId, { target: "connection-trace-id" }); @@ -106,7 +106,6 @@ export function ConnectionEnvironmentRow(props: { onPress={(event) => { event.stopPropagation(); }} - style={{ textDecorationStyle: "dotted" }} > {statusTraceId} @@ -140,10 +139,7 @@ export function ConnectionEnvironmentRow(props: { ) : ( <> - + Label - + URL - + Save diff --git a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx index 8a692d80729..52abe49d263 100644 --- a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx +++ b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx @@ -37,33 +37,11 @@ export function ConnectionSheetButton(props: { }) { const tone = props.tone ?? "secondary"; - const primaryBg = useThemeColor("--color-primary"); const primaryFg = useThemeColor("--color-primary-foreground"); - const dangerBg = useThemeColor("--color-danger"); - const dangerBorderColor = useThemeColor("--color-danger-border"); const dangerFg = useThemeColor("--color-danger-foreground"); - const secondaryBg = useThemeColor("--color-secondary"); const secondaryFg = useThemeColor("--color-secondary-foreground"); - const borderColor = useThemeColor("--color-border"); - const colors = - tone === "primary" - ? { - backgroundColor: primaryBg, - borderColor: "transparent", - textColor: primaryFg, - } - : tone === "danger" - ? { - backgroundColor: dangerBg, - borderColor: dangerBorderColor, - textColor: dangerFg, - } - : { - backgroundColor: secondaryBg, - borderColor: borderColor, - textColor: secondaryFg, - }; + const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg; const primaryShadow = tone === "primary" @@ -84,28 +62,32 @@ export function ConnectionSheetButton(props: { props.compact ? "min-h-[42px] flex-row items-center justify-center gap-1.5 rounded-[14px] px-3.5 py-2.5" : "min-h-[48px] flex-row items-center justify-center gap-2 rounded-[16px] px-4 py-3", + "disabled:opacity-50", + tone === "primary" + ? "bg-primary" + : tone === "danger" + ? "border border-danger-border bg-danger" + : "border border-border bg-secondary", )} disabled={props.disabled} onPress={props.onPress} - style={[ - { - backgroundColor: colors.backgroundColor, - borderWidth: tone === "primary" ? 0 : 1, - borderColor: colors.borderColor, - opacity: props.disabled ? 0.5 : 1, - }, - primaryShadow, - ]} + style={primaryShadow} > {props.label} diff --git a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx index af4431c6a66..bdcef23b51f 100644 --- a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx +++ b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx @@ -158,7 +158,7 @@ export function ConnectionsNewRouteScreen({ {showScanner ? ( cameraPermission?.granted ? ( - + ) : ( - + Camera permission is required to scan a QR code. @@ -200,10 +194,7 @@ export function ConnectionsNewRouteScreen({ ) : ( - + Host - + Pairing code item.node.path} contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 487d8271e1b..691cdf439c1 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -224,14 +224,7 @@ function FilesToolbarBottomFade() { pointerEvents="none" accessibilityElementsHidden importantForAccessibility="no-hide-descendants" - style={{ - bottom: 0, - height: 112, - left: 0, - position: "absolute", - right: 0, - zIndex: 1, - }} + className="absolute inset-x-0 bottom-0 z-[1] h-28" > diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index aa4d94ff267..afd438b0ad2 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -412,7 +412,7 @@ export function HomeScreen(props: HomeScreenProps) { {Platform.OS === "ios" ? null : } {shouldShowConnectionStatus && Platform.OS === "ios" ? ( - + ({ - alignItems: "center", - height: "100%", - justifyContent: "center", - opacity: pressed ? 0.72 : 1, - width: "100%", - })} + style={({ pressed }) => ({ opacity: pressed ? 0.72 : 1 })} > - + - + {props.label} @@ -438,14 +406,7 @@ export function ThreadSwipeActions(props: { ); return ( - + - + {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? ( ) : null} - + - + {props.renderInspector?.()} diff --git a/apps/mobile/src/features/layout/workspace-pane-divider.tsx b/apps/mobile/src/features/layout/workspace-pane-divider.tsx index a892ab73655..3be3cdf602d 100644 --- a/apps/mobile/src/features/layout/workspace-pane-divider.tsx +++ b/apps/mobile/src/features/layout/workspace-pane-divider.tsx @@ -69,6 +69,7 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { return ( setHovered(true)} onHoverOut={() => setHovered(false)} - style={styles.hitTarget} > @@ -91,15 +91,6 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { } const styles = StyleSheet.create({ - hitTarget: { - alignSelf: "stretch", - cursor: "pointer", - justifyContent: "center", - marginHorizontal: -22, - position: "relative", - width: 44, - zIndex: 100, - }, line: { alignSelf: "center", backgroundColor: diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index 58fc10529d9..5bcdd7c72ec 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -31,6 +31,7 @@ import * as Arr from "effect/Array"; import * as Cause from "effect/Cause"; import * as Order from "effect/Order"; import { AsyncResult } from "effect/unstable/reactivity"; +import { cn } from "../../lib/cn"; import { useProjects, useServerConfigs } from "../../state/entities"; import { filesystemEnvironment } from "../../state/filesystem"; @@ -99,10 +100,7 @@ function sourceFromParam(value: string | string[] | undefined): AddProjectRemote function SectionTitle(props: { readonly children: string }) { return ( - + {props.children} ); @@ -148,19 +146,17 @@ function ListRow(props: { readonly right?: ReactNode; readonly onPress?: () => void; }) { - const borderColor = useThemeColor("--color-border-subtle"); const chevronColor = useThemeColor("--color-chevron"); return ( - + + diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index a79dfa108bb..dd69cd8fdb4 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -655,7 +655,7 @@ export function ReviewSheet(props: ReviewSheetProps) { {showConnectionNotice ? ( - + {listHeader} {!selectedSection ? ( diff --git a/apps/mobile/src/features/review/reviewDiffRendering.tsx b/apps/mobile/src/features/review/reviewDiffRendering.tsx index d7f5c5ff8a8..d00cf2be4da 100644 --- a/apps/mobile/src/features/review/reviewDiffRendering.tsx +++ b/apps/mobile/src/features/review/reviewDiffRendering.tsx @@ -48,8 +48,8 @@ export function ReviewChangeBar(props: { {Array.from({ length: Math.ceil(height / 2) }, (_, index) => ( - - + + ))} diff --git a/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx index 46b7d210236..21bcad35e20 100644 --- a/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx @@ -13,12 +13,10 @@ export function SettingsAppearanceRouteScreen() { diff --git a/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx index 4b5cd9f8414..9e18d4675fb 100644 --- a/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx @@ -75,13 +75,8 @@ export function SettingsClientStorageRouteScreen() { contentInsetAdjustmentBehavior="automatic" contentInset={{ bottom: Math.max(insets.bottom, 18) }} showsVerticalScrollIndicator={false} - style={{ flex: 1 }} - contentContainerStyle={{ - gap: 24, - paddingBottom: 18, - paddingHorizontal: 20, - paddingTop: 16, - }} + className="flex-1" + contentContainerClassName="gap-6 px-5 pt-4 pb-[18px]" > {AsyncResult.isFailure(summaryResult) ? ( @@ -180,7 +175,6 @@ function CacheEnvironmentRow(props: { readonly onClear: () => void; }) { const iconColor = useThemeColor("--color-icon"); - const dangerForegroundColor = useThemeColor("--color-danger-foreground"); return ( - + Clear {formatBytes(props.environment.payloadBytes)} diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index d5eecb5016f..93b666f7467 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -50,11 +50,10 @@ export function SettingsEnvironmentsRouteScreen() { {hasLocalEnvironments ? ( @@ -63,10 +62,7 @@ export function SettingsEnvironmentsRouteScreen() { @@ -430,12 +428,10 @@ function ConfiguredSettingsRouteScreen() { diff --git a/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx index 5166ed11db7..7b2618079f7 100644 --- a/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx +++ b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx @@ -136,7 +136,7 @@ export function FontSizeSliderRow(props: { }; return ( - + diff --git a/apps/mobile/src/features/settings/components/SettingsSection.tsx b/apps/mobile/src/features/settings/components/SettingsSection.tsx index 7b4f0379161..b3deb7db931 100644 --- a/apps/mobile/src/features/settings/components/SettingsSection.tsx +++ b/apps/mobile/src/features/settings/components/SettingsSection.tsx @@ -7,10 +7,7 @@ export function SettingsSection(props: { readonly title: string; readonly childr return ( {props.title} - + {props.children} diff --git a/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx b/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx index 8620166eff2..d7526521de2 100644 --- a/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx +++ b/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx @@ -20,8 +20,11 @@ export function SettingsSwitchRow(props: { return ( {props.label} diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index e9665a7dab3..68d214193bd 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -82,9 +82,9 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter return ( - + {statusLabel} ) : null} - + {!isEnvironmentReady ? ( ) : ( <> - + diff --git a/apps/mobile/src/features/threads/ComposerCommandPopover.tsx b/apps/mobile/src/features/threads/ComposerCommandPopover.tsx index 87ac2a6f951..6388f12c183 100644 --- a/apps/mobile/src/features/threads/ComposerCommandPopover.tsx +++ b/apps/mobile/src/features/threads/ComposerCommandPopover.tsx @@ -154,23 +154,11 @@ const CommandRow = memo(function CommandRow(props: { ) : iconName ? ( ) : null} - + {props.item.label} {props.item.description ? ( - + {props.item.description} ) : null} @@ -187,18 +175,15 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( return ( {label ? ( - - + + {label} ) : null} {props.items.length > 0 ? ( @@ -212,7 +197,7 @@ export const ComposerCommandPopover = memo(function ComposerCommandPopover( ))} ) : ( - + {emptyText(props.triggerKind, props.isLoading)} diff --git a/apps/mobile/src/features/threads/GitActionProgressOverlay.tsx b/apps/mobile/src/features/threads/GitActionProgressOverlay.tsx index 93d929e5961..56e086ca2dd 100644 --- a/apps/mobile/src/features/threads/GitActionProgressOverlay.tsx +++ b/apps/mobile/src/features/threads/GitActionProgressOverlay.tsx @@ -47,7 +47,8 @@ export function GitActionProgressOverlay(props: { diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 935776eac56..f4f154143d7 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; import { Alert, InteractionManager, View, useColorScheme } from "react-native"; import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useThemeColor } from "../../lib/useThemeColor"; import { EnvironmentId } from "@t3tools/contracts"; import { @@ -112,7 +111,6 @@ export function NewTaskDraftScreen(props: { }; }, [props.pendingTaskId, cancelEditingPendingTask]); - const borderColor = useThemeColor("--color-border"); const headlineText = useScaledTextRole("headline"); const sheetFadeOpaque = colorScheme === "dark" ? "rgba(14,14,14,0.98)" : "rgba(242,242,247,0.98)"; const sheetFadeTransparent = colorScheme === "dark" ? "rgba(14,14,14,0)" : "rgba(242,242,247,0)"; @@ -600,8 +598,8 @@ export function NewTaskDraftScreen(props: { - - + + - + {flow.attachments.length > 0 ? ( - + groupProjectsByRepository({ projects, threads }), [projects, threads], @@ -130,7 +130,7 @@ export function NewTaskRouteScreen() { navigation.navigate("NewTaskSheet", { screen: "NewTaskDraft", @@ -186,16 +185,12 @@ export function NewTaskRouteScreen() { }, }) } - style={{ - paddingHorizontal: 16, - paddingVertical: 14, - borderTopWidth: isFirst ? 0 : 1, - borderTopColor: borderSubtleColor, - borderTopLeftRadius: isFirst ? 24 : 0, - borderTopRightRadius: isFirst ? 24 : 0, - borderBottomLeftRadius: isLast ? 24 : 0, - borderBottomRightRadius: isLast ? 24 : 0, - }} + className={cn( + "bg-card px-4 py-3.5", + !isFirst && "border-t border-border-subtle", + isFirst && "rounded-t-[24px]", + isLast && "rounded-b-[24px]", + )} > diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index d65c670d359..b8227b4996f 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -688,9 +688,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer return ( {composerTrigger && composerMenuItems.length > 0 ? ( - + 0 ? "pb-2.5" : undefined} entering={FadeIn.duration(160)} exiting={FadeOut.duration(120)} - style={{ paddingBottom: props.draftAttachments.length > 0 ? 10 : 0 }} > ) : null} - + {!isExpanded && props.draftAttachments.length > 0 ? ( - + {props.draftAttachments.slice(0, 3).map((image) => ( onPressImage(image.previewUri)}> ))} {props.draftAttachments.length > 3 ? ( - + +{props.draftAttachments.length - 3} @@ -909,7 +886,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer {/* Queue count */} {props.queueCount > 0 ? ( - + {props.queueCount} queued message{props.queueCount === 1 ? "" : "s"} will send automatically. diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index f9f6e878b96..31ad5660983 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -193,10 +193,9 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: { return ( @@ -390,7 +389,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread {showContent ? ( ) : ( - + )} {/* Floating composer — sticks to keyboard via KeyboardStickyView */} @@ -431,10 +430,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread {/* No paddingTop here: the overlay's measured height becomes the list's bottom inset, so any padding above the pill/composer pushes the resting content floor up by the same amount. */} - + {props.activeWorkStartedAt ? ( @@ -442,10 +442,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread {props.activePendingApproval || props.activePendingUserInput ? ( {props.activePendingApproval ? ( void): MarkdownStyleSe const linkHref = presentation.href; return ( { @@ -460,17 +461,14 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe } : undefined } - style={{ - color: markdownLinkColor, - textDecorationLine: "underline", - }} + style={{ color: markdownLinkColor }} > {children} ); }, list: ({ node, Renderer, ordered = false, start = 1 }) => ( - + {node.children?.map((child, index) => { const childKey = `${child.type}:${child.beg ?? "unknown"}:${child.end ?? "unknown"}`; if (child.type === "task_list_item") { @@ -479,14 +477,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe ); } return ( - + void): MarkdownStyleSe > {ordered ? `${start + index}.` : "•"} - + @@ -512,9 +503,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe const value = content ?? ""; return ( void): MarkdownStyleSe : {}), code_block: ({ content, language }) => ( {language ? ( {language} @@ -565,13 +549,13 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe horizontal showsHorizontalScrollIndicator={false} bounces={false} - contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 12 }} + contentContainerClassName="px-3.5 py-3" > - - + + @@ -338,11 +334,8 @@ function ThreadNavigationSidebarPane( const backgroundColor = useThemeColor("--color-drawer"); const borderColor = useThemeColor("--color-border"); - const foregroundColor = useThemeColor("--color-foreground"); const mutedColor = useThemeColor("--color-foreground-muted"); const placeholderColor = useThemeColor("--color-placeholder"); - const searchBackgroundColor = - colorScheme === "dark" ? IOS_SEARCH_FILL_DARK : IOS_SEARCH_FILL_LIGHT; const headerFadeColor = String(backgroundColor); const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); @@ -532,7 +525,7 @@ function ThreadNavigationSidebarPane( [filterIcon, filterMenu, props.onOpenSettings], ); const listEmpty = ( - + {catalogState.isLoadingConnections ? "Loading threads…" : props.searchQuery.trim().length > 0 @@ -566,7 +559,7 @@ function ThreadNavigationSidebarPane( unstable_headerRightItems: () => nativeHeaderItems, }} /> - + + - + @@ -704,12 +687,8 @@ function ThreadNavigationSidebarPane( - - + + Threads @@ -724,14 +703,7 @@ function ThreadNavigationSidebarPane( - + {showsConnectionStatus ? ( - + - + New branch - + New worktree - + Existing branches {branchesLoading ? ( @@ -185,12 +152,11 @@ export function GitBranchesSheet(_props: GitBranchesSheetProps) { return ( { void gitActions.onCheckoutSelectedThreadBranch(branch.name).then(() => { navigation.goBack(); diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index bf2d187af0d..7d023e73488 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -1,10 +1,10 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useState } from "react"; -import { Pressable, ScrollView, View, useColorScheme } from "react-native"; +import { Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useThemeColor } from "../../../lib/useThemeColor"; import { AppText as Text, AppTextInput as TextInput } from "../../../components/AppText"; +import { cn } from "../../../lib/cn"; import { useEnvironmentQuery } from "../../../state/query"; import { useThreadSelection } from "../../../state/use-thread-selection"; import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions"; @@ -21,18 +21,11 @@ type GitCommitSheetProps = StaticScreenProps<{ export function GitCommitSheet(_props: GitCommitSheetProps) { const navigation = useNavigation(); const insets = useSafeAreaInsets(); - const isDarkMode = useColorScheme() === "dark"; const { selectedThread } = useThreadSelection(); const { selectedThreadCwd } = useSelectedThreadWorktree(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); - const borderColor = useThemeColor("--color-border"); - const borderSubtleColor = useThemeColor("--color-border-subtle"); - const inputBorderColor = useThemeColor("--color-input-border"); - const inputBg = useThemeColor("--color-input"); - const foregroundColor = useThemeColor("--color-foreground"); - const gitStatus = useEnvironmentQuery( selectedThread !== null && selectedThreadCwd !== null ? vcsEnvironment.status({ @@ -90,10 +83,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { {isDefaultRef ? ( - + Warning: this is the default branch. ) : null} @@ -138,12 +128,8 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { {file.path} - - +{file.insertions} - - - -{file.deletions} - + +{file.insertions} + -{file.deletions} ))} {selectedFiles.length > selectedFilePreview.length ? ( @@ -159,10 +145,10 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { return ( { setExcludedFiles((current) => { const next = new Set(current); @@ -193,12 +179,10 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { ) : null} - + +{file.insertions} - - -{file.deletions} - + -{file.deletions} @@ -216,14 +200,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { onChangeText={setDialogCommitMessage} placeholder="Leave empty to auto-generate" textAlignVertical="top" - className="min-h-[128px] rounded-[20px] px-4 py-3.5 font-sans text-base" - style={{ - minHeight: 128, - borderWidth: 1, - borderColor: inputBorderColor, - backgroundColor: inputBg, - color: foregroundColor, - }} + className="min-h-[128px] rounded-[20px] px-4 py-3.5" /> diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index b4f78742141..5e759eee776 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -101,13 +101,10 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) { return ( - + - + Confirm @@ -118,10 +115,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) { - + {sheetMenuItems.map(({ item, disabledReason }, index) => ( - {index > 0 ? ( - - ) : null} + {index > 0 ? : null} 0 ? ( <> - + ) : null} - + navigation.navigate("ThreadReview", { environmentId, threadId })} /> - + void gitActions.refreshSelectedThreadGitStatus()} > @@ -378,10 +378,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { weight="medium" /> - + {isInspector ? "Repository" : "Branch"} diff --git a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx index 8045de78228..907f6d9b0d8 100644 --- a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx +++ b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx @@ -3,6 +3,7 @@ import type { ComponentProps } from "react"; import { Pressable, View } from "react-native"; import { useThemeColor } from "../../../lib/useThemeColor"; import { AppText as Text } from "../../../components/AppText"; +import { cn } from "../../../lib/cn"; /* ─── Shared sheet components ──────────────────────────────────────── */ @@ -13,51 +14,36 @@ export function SheetActionButton(props: { readonly tone?: "primary" | "secondary" | "danger"; readonly onPress: () => void; }) { - const primaryBg = useThemeColor("--color-primary"); const primaryFg = useThemeColor("--color-primary-foreground"); - const dangerBg = useThemeColor("--color-danger"); - const dangerBorder = useThemeColor("--color-danger-border"); const dangerFg = useThemeColor("--color-danger-foreground"); - const secondaryBg = useThemeColor("--color-secondary"); - const secondaryBorder = useThemeColor("--color-secondary-border"); const secondaryFg = useThemeColor("--color-secondary-foreground"); const tone = props.tone ?? "secondary"; - const colors = - tone === "primary" - ? { - backgroundColor: primaryBg, - borderColor: "transparent", - textColor: primaryFg, - } - : tone === "danger" - ? { - backgroundColor: dangerBg, - borderColor: dangerBorder, - textColor: dangerFg, - } - : { - backgroundColor: secondaryBg, - borderColor: secondaryBorder, - textColor: secondaryFg, - }; + const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg; return ( - + {props.label} @@ -68,10 +54,7 @@ export function SheetActionButton(props: { export function MetaCard(props: { readonly label: string; readonly value: string }) { return ( - + {props.label} @@ -93,9 +76,8 @@ export function SheetListRow(props: { return ( diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.tsx index 642a4a957bb..9b3a15c906c 100644 --- a/apps/mobile/src/features/threads/sidebar-filter-button.tsx +++ b/apps/mobile/src/features/threads/sidebar-filter-button.tsx @@ -22,16 +22,17 @@ export function SidebarFilterButton(props: { return ( [ - styles.button, props.grouped ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } : { backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, borderColor, + borderWidth: StyleSheet.hairlineWidth, }, ]} > @@ -39,17 +40,3 @@ export function SidebarFilterButton(props: { ); } - -const styles = StyleSheet.create({ - button: { - // Match the native glass UIBarButtonItem group metrics (~50pt slots, - // 44pt bar height, label-colored ~20pt glyphs). - width: 50, - height: 44, - borderRadius: 22, - borderWidth: StyleSheet.hairlineWidth, - alignItems: "center", - justifyContent: "center", - cursor: "pointer", - }, -}); diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.tsx index e193f1f2d9b..ae8242255da 100644 --- a/apps/mobile/src/features/threads/sidebar-header-actions.tsx +++ b/apps/mobile/src/features/threads/sidebar-header-actions.tsx @@ -24,17 +24,18 @@ function FallbackHeaderButton(props: { return ( [ - styles.button, props.grouped ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } : { backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, borderColor, + borderWidth: StyleSheet.hairlineWidth, }, ]} > @@ -45,7 +46,7 @@ function FallbackHeaderButton(props: { export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { return ( - + ); } - -const styles = StyleSheet.create({ - actions: { - flexDirection: "row", - alignItems: "center", - gap: 2, - }, - button: { - // Match the native glass UIBarButtonItem group metrics. - width: 50, - height: 44, - borderRadius: 22, - borderWidth: StyleSheet.hairlineWidth, - alignItems: "center", - justifyContent: "center", - }, -}); diff --git a/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx b/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx index 2c8ec73342d..9b41ed4e362 100644 --- a/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx +++ b/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx @@ -75,7 +75,7 @@ export function ThreadInspectorContentStack(props: { const Route = props.Route; return ( - + {props.title} @@ -236,7 +236,6 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { const compact = props.variant === "compact"; const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); - const foregroundColor = useThemeColor("--color-foreground"); const mutedColor = useThemeColor("--color-foreground-muted"); const pressedBackgroundColor = useThemeColor("--color-subtle"); @@ -254,17 +253,14 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { ); const statusPill = ( - + Pending ); const subtitleRow = subtitleParts.length > 0 ? ( - + {subtitleParts.join(" · ")} @@ -311,12 +310,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { {statusPill} - - {timestamp} - + {timestamp} - + - + {pendingTask.title} {statusPill} - + {timestamp} @@ -418,11 +404,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: { const iconSubtleColor = useThemeColor("--color-icon-subtle"); const screenColor = useThemeColor("--color-screen"); const drawerColor = useThemeColor("--color-drawer"); - const foregroundColor = useThemeColor("--color-foreground"); const mutedColor = useThemeColor("--color-foreground-muted"); const pressedBackgroundColor = useThemeColor("--color-subtle"); const selectedBackgroundColor = useThemeColor("--color-user-bubble"); - const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground"); const selectedMutedColor = useThemeColor("--color-user-bubble-foreground-muted"); const { thread, onSelectThread, onArchiveThread, onDeleteThread } = props; @@ -436,7 +420,6 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ); const backgroundColor = compact ? screenColor : drawerColor; - const effectiveForeground = selected ? selectedForegroundColor : foregroundColor; const effectiveMuted = selected ? selectedMutedColor : mutedColor; const effectivePressedBackground = selected ? "rgba(255,255,255,0.16)" : pressedBackgroundColor; const effectiveStatus = @@ -464,10 +447,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ); const statusPill = effectiveStatus ? ( - + {effectiveStatus.label} @@ -476,7 +456,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { const subtitleRow = subtitleParts.length > 0 || pr !== null ? ( - + {subtitleParts.length > 0 ? ( <> {subtitleParts.join(" · ")} @@ -540,12 +524,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { {statusPill} - - {timestamp} - + {timestamp} - + {thread.title} {statusPill} {timestamp} diff --git a/apps/mobile/src/features/threads/thread-work-log.tsx b/apps/mobile/src/features/threads/thread-work-log.tsx index 24005a898ae..fffadcebf1e 100644 --- a/apps/mobile/src/features/threads/thread-work-log.tsx +++ b/apps/mobile/src/features/threads/thread-work-log.tsx @@ -206,13 +206,12 @@ export function ThreadWorkLog(props: { nestedScrollEnabled directionalLockEnabled showsVerticalScrollIndicator - style={{ maxHeight: 240 }} + className="max-h-60" contentContainerStyle={{ paddingRight: 8 }} > {row.fullDetail} diff --git a/apps/mobile/src/native/T3KeyboardCommands.tsx b/apps/mobile/src/native/T3KeyboardCommands.tsx index 87629e6d676..1f49dd172c5 100644 --- a/apps/mobile/src/native/T3KeyboardCommands.tsx +++ b/apps/mobile/src/native/T3KeyboardCommands.tsx @@ -9,5 +9,5 @@ export function T3KeyboardCommands( readonly onCommand: (command: HardwareKeyboardCommand) => void; }>, ) { - return {props.children}; + return {props.children}; } From 1d1d6db47a0a0a80f4663820ce3b131a4344330d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 13:12:03 +0200 Subject: [PATCH 07/12] refactor(mobile): use one placeholder color source Co-authored-by: codex --- apps/mobile/src/components/AppText.tsx | 14 +++----------- .../connection/ConnectionEnvironmentRow.tsx | 3 --- .../connection/ConnectionsNewRouteScreen.tsx | 3 --- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/apps/mobile/src/components/AppText.tsx b/apps/mobile/src/components/AppText.tsx index 0288e02420c..39517f0e62e 100644 --- a/apps/mobile/src/components/AppText.tsx +++ b/apps/mobile/src/components/AppText.tsx @@ -17,7 +17,7 @@ export function AppText({ className, ...props }: AppTextProps) { return ; } -export type AppTextInputProps = RNTextInputProps & { +export type AppTextInputProps = Omit & { readonly className?: string; readonly ref?: React.Ref; }; @@ -26,12 +26,7 @@ export type AppTextInputProps = RNTextInputProps & { * Thin wrapper around RN TextInput with default input styling. * Uses Uniwind className — no manual style parsing. */ -export function AppTextInput({ - className, - placeholderTextColor, - ref, - ...props -}: AppTextInputProps) { +export function AppTextInput({ className, ref, ...props }: AppTextInputProps) { return ( ); diff --git a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx index 9c4ec8cf018..7074fd0e31b 100644 --- a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx +++ b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx @@ -38,7 +38,6 @@ export function ConnectionEnvironmentRow(props: { const [url, setUrl] = useState(props.environment.displayUrl); const mutedColor = useThemeColor("--color-icon-subtle"); - const placeholderColor = useThemeColor("--color-placeholder"); const primaryFg = useThemeColor("--color-primary-foreground"); const dangerFg = useThemeColor("--color-danger-foreground"); const statusLabel = connectionStatusLabel(props.environment); @@ -146,7 +145,6 @@ export function ConnectionEnvironmentRow(props: { autoCapitalize="words" autoCorrect={false} placeholder="My MacBook" - placeholderTextColor={placeholderColor} value={label} onChangeText={setLabel} className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-base text-foreground" @@ -162,7 +160,6 @@ export function ConnectionEnvironmentRow(props: { autoCorrect={false} keyboardType="url" placeholder="192.168.1.100:8080" - placeholderTextColor={placeholderColor} value={url} onChangeText={setUrl} className="rounded-[14px] border border-input-border bg-input px-4 py-3 text-base text-foreground" diff --git a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx index bdcef23b51f..1f1994ed154 100644 --- a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx +++ b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx @@ -38,7 +38,6 @@ export function ConnectionsNewRouteScreen({ const [scannerLocked, setScannerLocked] = useState(false); const headerIconColor = useThemeColor("--color-icon"); - const placeholderColor = useThemeColor("--color-placeholder"); const connectDisabled = isSubmitting || hostInput.trim().length === 0; @@ -202,7 +201,6 @@ export function ConnectionsNewRouteScreen({ autoCorrect={false} keyboardType="url" placeholder="192.168.1.100:8080" - placeholderTextColor={placeholderColor} value={hostInput} onChangeText={handleHostChange} className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-base text-foreground" @@ -217,7 +215,6 @@ export function ConnectionsNewRouteScreen({ autoCapitalize="none" autoCorrect={false} placeholder="abc-123-xyz" - placeholderTextColor={placeholderColor} value={codeInput} onChangeText={handleCodeChange} className="rounded-[14px] border border-input-border bg-input px-4 py-3.5 text-base text-foreground" From 9eddc64b5999a976255ca59026a088ed71168dda Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 13:17:19 +0200 Subject: [PATCH 08/12] refactor(mobile): narrow glass surface styling API Co-authored-by: codex --- apps/mobile/src/components/GlassSurface.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/components/GlassSurface.tsx b/apps/mobile/src/components/GlassSurface.tsx index 03311e3b7e9..f34bd4e2836 100644 --- a/apps/mobile/src/components/GlassSurface.tsx +++ b/apps/mobile/src/components/GlassSurface.tsx @@ -10,7 +10,7 @@ import { } from "react-native"; import { useThemeColor } from "../lib/useThemeColor"; -export interface GlassSurfaceProps extends ViewProps { +export interface GlassSurfaceProps extends Omit { readonly children: ReactNode; readonly glassEffectStyle?: "clear" | "regular" | "none"; readonly tintColor?: ColorValue; From 81077205c47c4aec878e649b5cd000b581226c67 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 13:28:31 +0200 Subject: [PATCH 09/12] fix(mobile): preserve interactive preview Metro session Co-authored-by: codex --- apps/mobile/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 145a91d4608..17970f826d7 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "expo start --clear", "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear", - "dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear'", + "dev:client:preview": "eas env:exec preview 'script -q /dev/null env EXPO_NO_DOTENV=1 APP_VARIANT=preview ./node_modules/.bin/expo start --dev-client --scheme t3code-preview --clear --lan'", "start": "expo start", "start:dev": "APP_VARIANT=development expo start", "start:preview": "APP_VARIANT=preview expo start", From 38420348caca9dcfd5e5cb1fa5eba6aa8e96650f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 13:33:14 +0200 Subject: [PATCH 10/12] fix(mobile): explicitly dismiss native splash Co-authored-by: codex --- apps/mobile/src/App.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index e9c9c222de1..d6c77dcd27c 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -1,4 +1,6 @@ import * as Linking from "expo-linking"; +import * as SplashScreen from "expo-splash-screen"; +import { useEffect } from "react"; import { StatusBar, useColorScheme } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; @@ -24,6 +26,10 @@ export default function App() { const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); + useEffect(() => { + SplashScreen.hide(); + }, []); + return ( From 6b1f75e8602a949ab98147a3b2da4e112af063fb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 13:41:12 +0200 Subject: [PATCH 11/12] fix(mobile): preserve swipe action geometry Co-authored-by: codex --- .../features/home/thread-swipe-actions.tsx | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index e5b4af341e3..dd412301077 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -350,24 +350,56 @@ function SwipeActionButton(props: { return ( ({ opacity: pressed ? 0.72 : 1 })} + style={({ pressed }) => ({ + alignItems: "center", + height: "100%", + justifyContent: "center", + opacity: pressed ? 0.72 : 1, + width: "100%", + })} > - + - + {props.label} @@ -406,7 +438,14 @@ export function ThreadSwipeActions(props: { ); return ( - + Date: Thu, 9 Jul 2026 13:44:03 +0200 Subject: [PATCH 12/12] fix(mobile): keep preview Metro command portable Co-authored-by: codex --- apps/mobile/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 17970f826d7..269410b2352 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "expo start --clear", "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear", - "dev:client:preview": "eas env:exec preview 'script -q /dev/null env EXPO_NO_DOTENV=1 APP_VARIANT=preview ./node_modules/.bin/expo start --dev-client --scheme t3code-preview --clear --lan'", + "dev:client:preview": "eas env:exec preview 'EXPO_NO_DOTENV=1 APP_VARIANT=preview expo start --dev-client --scheme t3code-preview --clear --lan'", "start": "expo start", "start:dev": "APP_VARIANT=development expo start", "start:preview": "APP_VARIANT=preview expo start",