authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-08 09:27:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-08 09:27:29-07:00
log4664c572a78231611491f63f677f0e007ebaab03
tree0311f651532fc164acfd4b419fed1d6de3b12763
parent579266caa89238611ddb974ffebe9968c6a6e5d5
signature Signed by PGP key B5690EEEBB952194

feat(mobile): add optional Material You layout (#10692)

Co-authored-by: Pixel Perfect <me@pixp.cc>

26 files changed, 688 insertions(+), 172 deletions(-)

apps/mobile/generated-uniwind-default-theme-variables.json+14
......@@ -6,6 +6,13 @@
66 "--color-card": "#ffffff",
77 "--color-card-alt": "#f5f5f5",
88 "--color-card-translucent": "rgba(255, 255, 255, 0.8)",
9 "--color-thread-canvas": "#f5f5f5",
10 "--color-thread-selected": "#ffffff",
11 "--color-thread-selected-foreground": "#262626",
12 "--color-thread-selected-foreground-muted": "#737373",
13 "--color-composer-panel": "rgba(245, 245, 245, 0.88)",
14 "--color-composer-surface": "rgba(255, 255, 255, 0.94)",
15 "--color-composer-border": "rgba(0, 0, 0, 0.1)",
916 "--color-foreground": "#262626",
1017 "--color-foreground-secondary": "#525252",
1118 "--color-foreground-muted": "#737373",
......@@ -76,6 +83,13 @@
7683 "--color-card": "#171717",
7784 "--color-card-alt": "#1c1c1c",
7885 "--color-card-translucent": "rgba(17, 17, 17, 0.8)",
86 "--color-thread-canvas": "#1c1c1c",
87 "--color-thread-selected": "#171717",
88 "--color-thread-selected-foreground": "#f5f5f5",
89 "--color-thread-selected-foreground-muted": "#8e8e93",
90 "--color-composer-panel": "rgba(28, 28, 28, 0.92)",
91 "--color-composer-surface": "rgba(23, 23, 23, 0.9)",
92 "--color-composer-border": "rgba(255, 255, 255, 0.08)",
7993 "--color-foreground": "#f5f5f5",
8094 "--color-foreground-secondary": "#a3a3a3",
8195 "--color-foreground-muted": "#8e8e93",
apps/mobile/generated-uniwind-themes.css+70
......@@ -128,6 +128,13 @@
128128 --color-card: #fdfafd;
129129 --color-card-alt: #faf3fb;
130130 --color-card-translucent: rgba(253, 250, 253, 0.8);
131 --color-thread-canvas: #faf3fb;
132 --color-thread-selected: #fdfafd;
133 --color-thread-selected-foreground: #501854;
134 --color-thread-selected-foreground-muted: #ac1668;
135 --color-composer-panel: rgba(250, 243, 251, 0.88);
136 --color-composer-surface: rgba(253, 250, 253, 0.94);
137 --color-composer-border: rgba(238, 225, 237, 0.54);
131138 --color-foreground: #501854;
132139 --color-foreground-secondary: #ac1668;
133140 --color-foreground-muted: #8d1255;
......@@ -256,6 +263,13 @@
256263 --color-card: #2c2631;
257264 --color-card-alt: #29232d;
258265 --color-card-translucent: rgba(44, 38, 49, 0.8);
266 --color-thread-canvas: #29232d;
267 --color-thread-selected: #2c2631;
268 --color-thread-selected-foreground: #f9f8fb;
269 --color-thread-selected-foreground-muted: #e7d0dd;
270 --color-composer-panel: rgba(41, 35, 45, 0.92);
271 --color-composer-surface: rgba(44, 38, 49, 0.9);
272 --color-composer-border: rgba(39, 36, 44, 0.46);
259273 --color-foreground: #f9f8fb;
260274 --color-foreground-secondary: #e7d0dd;
261275 --color-foreground-muted: #e7d0dd;
......@@ -384,6 +398,13 @@
384398 --color-card: #ecefed;
385399 --color-card-alt: #f3f7f4;
386400 --color-card-translucent: rgba(236, 239, 237, 0.8);
401 --color-thread-canvas: #f3f7f4;
402 --color-thread-selected: #ecefed;
403 --color-thread-selected-foreground: #241523;
404 --color-thread-selected-foreground-muted: #746c73;
405 --color-composer-panel: rgba(243, 247, 244, 0.88);
406 --color-composer-surface: rgba(236, 239, 237, 0.94);
407 --color-composer-border: rgba(203, 213, 209, 0.54);
387408 --color-foreground: #241523;
388409 --color-foreground-secondary: #746c73;
389410 --color-foreground-muted: #6e696f;
......@@ -512,6 +533,13 @@
512533 --color-card: #36413c;
513534 --color-card-alt: #1b2821;
514535 --color-card-translucent: rgba(54, 65, 60, 0.8);
536 --color-thread-canvas: #1b2821;
537 --color-thread-selected: #36413c;
538 --color-thread-selected-foreground: #fffaff;
539 --color-thread-selected-foreground-muted: #919595;
540 --color-composer-panel: rgba(27, 40, 33, 0.92);
541 --color-composer-surface: rgba(54, 65, 60, 0.9);
542 --color-composer-border: rgba(65, 95, 79, 0.46);
515543 --color-foreground: #fffaff;
516544 --color-foreground-secondary: #919595;
517545 --color-foreground-muted: #9da5a2;
......@@ -640,6 +668,13 @@
640668 --color-card: #edeff1;
641669 --color-card-alt: #f5f7f8;
642670 --color-card-translucent: rgba(237, 239, 241, 0.8);
671 --color-thread-canvas: #f5f7f8;
672 --color-thread-selected: #edeff1;
673 --color-thread-selected-foreground: #241523;
674 --color-thread-selected-foreground-muted: #746c75;
675 --color-composer-panel: rgba(245, 247, 248, 0.88);
676 --color-composer-surface: rgba(237, 239, 241, 0.94);
677 --color-composer-border: rgba(205, 212, 220, 0.54);
643678 --color-foreground: #241523;
644679 --color-foreground-secondary: #746c75;
645680 --color-foreground-muted: #6f6873;
......@@ -768,6 +803,13 @@
768803 --color-card: #333b45;
769804 --color-card-alt: #17212b;
770805 --color-card-translucent: rgba(51, 59, 69, 0.8);
806 --color-thread-canvas: #17212b;
807 --color-thread-selected: #333b45;
808 --color-thread-selected-foreground: #fffaff;
809 --color-thread-selected-foreground-muted: #8d8f97;
810 --color-composer-panel: rgba(23, 33, 43, 0.92);
811 --color-composer-surface: rgba(51, 59, 69, 0.9);
812 --color-composer-border: rgba(64, 85, 103, 0.46);
771813 --color-foreground: #fffaff;
772814 --color-foreground-secondary: #8d8f97;
773815 --color-foreground-muted: #969ca6;
......@@ -896,6 +938,13 @@
896938 --color-card: #f1efee;
897939 --color-card-alt: #f9f7f5;
898940 --color-card-translucent: rgba(241, 239, 238, 0.8);
941 --color-thread-canvas: #f9f7f5;
942 --color-thread-selected: #f1efee;
943 --color-thread-selected-foreground: #241523;
944 --color-thread-selected-foreground-muted: #766c74;
945 --color-composer-panel: rgba(249, 247, 245, 0.88);
946 --color-composer-surface: rgba(241, 239, 238, 0.94);
947 --color-composer-border: rgba(221, 210, 206, 0.54);
899948 --color-foreground: #241523;
900949 --color-foreground-secondary: #766c74;
901950 --color-foreground-muted: #74686f;
......@@ -1024,6 +1073,13 @@
10241073 --color-card: #433835;
10251074 --color-card-alt: #291e1a;
10261075 --color-card-translucent: rgba(67, 56, 53, 0.8);
1076 --color-thread-canvas: #291e1a;
1077 --color-thread-selected: #433835;
1078 --color-thread-selected-foreground: #fffaff;
1079 --color-thread-selected-foreground-muted: #968e8f;
1080 --color-composer-panel: rgba(41, 30, 26, 0.92);
1081 --color-composer-surface: rgba(67, 56, 53, 0.9);
1082 --color-composer-border: rgba(102, 76, 63, 0.46);
10271083 --color-foreground: #fffaff;
10281084 --color-foreground-secondary: #968e8f;
10291085 --color-foreground-muted: #a59996;
......@@ -1152,6 +1208,13 @@
11521208 --color-card: #f0eff2;
11531209 --color-card-alt: #f8f7f9;
11541210 --color-card-translucent: rgba(240, 239, 242, 0.8);
1211 --color-thread-canvas: #f8f7f9;
1212 --color-thread-selected: #f0eff2;
1213 --color-thread-selected-foreground: #241523;
1214 --color-thread-selected-foreground-muted: #766c76;
1215 --color-composer-panel: rgba(248, 247, 249, 0.88);
1216 --color-composer-surface: rgba(240, 239, 242, 0.94);
1217 --color-composer-border: rgba(214, 209, 222, 0.54);
11551218 --color-foreground: #241523;
11561219 --color-foreground-secondary: #766c76;
11571220 --color-foreground-muted: #726874;
......@@ -1280,6 +1343,13 @@
12801343 --color-card: #383443;
12811344 --color-card-alt: #1d1929;
12821345 --color-card-translucent: rgba(56, 52, 67, 0.8);
1346 --color-thread-canvas: #1d1929;
1347 --color-thread-selected: #383443;
1348 --color-thread-selected-foreground: #fffaff;
1349 --color-thread-selected-foreground-muted: #8e8a95;
1350 --color-composer-panel: rgba(29, 25, 41, 0.92);
1351 --color-composer-surface: rgba(56, 52, 67, 0.9);
1352 --color-composer-border: rgba(77, 67, 102, 0.46);
12831353 --color-foreground: #fffaff;
12841354 --color-foreground-secondary: #8e8a95;
12851355 --color-foreground-muted: #9690a1;
apps/mobile/global.css+14
......@@ -19,6 +19,13 @@
1919 --color-card: #ffffff;
2020 --color-card-alt: #f5f5f5;
2121 --color-card-translucent: rgba(255, 255, 255, 0.8);
22 --color-thread-canvas: #f5f5f5;
23 --color-thread-selected: #ffffff;
24 --color-thread-selected-foreground: #262626;
25 --color-thread-selected-foreground-muted: #737373;
26 --color-composer-panel: rgba(245, 245, 245, 0.88);
27 --color-composer-surface: rgba(255, 255, 255, 0.94);
28 --color-composer-border: rgba(0, 0, 0, 0.1);
2229
2330 /* Text */
2431 --color-foreground: #262626;
......@@ -123,6 +130,13 @@
123130 --color-card: #171717;
124131 --color-card-alt: #1c1c1c;
125132 --color-card-translucent: rgba(17, 17, 17, 0.8);
133 --color-thread-canvas: #1c1c1c;
134 --color-thread-selected: #171717;
135 --color-thread-selected-foreground: #f5f5f5;
136 --color-thread-selected-foreground-muted: #8e8e93;
137 --color-composer-panel: rgba(28, 28, 28, 0.92);
138 --color-composer-surface: rgba(23, 23, 23, 0.9);
139 --color-composer-border: rgba(255, 255, 255, 0.08);
126140
127141 /* Text */
128142 --color-foreground: #f5f5f5;
apps/mobile/src/components/AndroidScreenHeader.tsx+2
......@@ -48,6 +48,7 @@ export function AndroidScreenHeader(props: {
4848 readonly trailing?: ReactNode;
4949 readonly onBack?: () => void;
5050 readonly embedded?: boolean;
51 readonly hideBottomBorder?: boolean;
5152}) {
5253 const insets = useSafeAreaInsets();
5354
......@@ -56,6 +57,7 @@ export function AndroidScreenHeader(props: {
5657 className="border-b border-header-border bg-header px-3 pb-2.5"
5758 style={{
5859 paddingTop: props.embedded ? 8 : Math.max(insets.top, 12),
60 borderBottomWidth: props.hideBottomBorder ? 0 : undefined,
5961 }}
6062 >
6163 <View className="min-h-12 flex-row items-center gap-2">
apps/mobile/src/components/ThemedSwitch.tsx+47-1
......@@ -1,6 +1,52 @@
1import { Platform, Switch, type SwitchProps } from "react-native";
1import { Platform, Pressable, Switch, View, type SwitchProps } from "react-native";
2
3import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
4import { SymbolView } from "./AppSymbol";
25
36export function ThemedSwitch(props: SwitchProps) {
7 const { materialYouStyleLayoutActive } = useAppearancePreferences();
8 if (materialYouStyleLayoutActive) {
9 return (
10 <Pressable
11 accessibilityHint={props.accessibilityHint}
12 accessibilityLabel={props.accessibilityLabel}
13 accessibilityRole="switch"
14 accessibilityState={{ checked: Boolean(props.value), disabled: props.disabled }}
15 disabled={props.disabled}
16 hitSlop={8}
17 onPress={() => props.onValueChange?.(!props.value)}
18 style={props.style}
19 testID={props.testID}
20 className={props.disabled ? "opacity-40" : "active:opacity-70"}
21 >
22 <View
23 className={
24 props.value
25 ? "h-8 w-[52px] items-end justify-center rounded-full border-2 border-switch-active-track bg-switch-active-track px-0.5"
26 : "h-8 w-[52px] items-start justify-center rounded-full border-2 border-switch-inactive-thumb bg-switch-inactive-track px-0.5"
27 }
28 >
29 <View
30 className={
31 props.value
32 ? "size-6 items-center justify-center rounded-full bg-switch-active-thumb"
33 : "size-6 items-center justify-center rounded-full bg-switch-inactive-thumb"
34 }
35 >
36 <SymbolView
37 name={props.value ? "checkmark" : "xmark"}
38 size={13}
39 tintColorClassName={
40 props.value ? "accent-switch-active-track" : "accent-switch-inactive-track"
41 }
42 type="monochrome"
43 />
44 </View>
45 </View>
46 </Pressable>
47 );
48 }
49
450 return (
551 <Switch
652 {...props}
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+28-5
......@@ -306,8 +306,10 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
306306 useAdaptiveWorkspaceLayout();
307307 const [searchQuery, setSearchQuery] = useState("");
308308 const isAndroid = Platform.OS === "android";
309 const { themeAppearance: highlightTheme } = useAppearancePreferences();
309 const { themeAppearance: highlightTheme, materialYouStyleLayoutActive } =
310 useAppearancePreferences();
310311 const theme = useUniwindTheme();
312 const screenColor = theme["--color-screen"];
311313 const sheetSurfaceColor = theme["--color-sheet-solid"];
312314 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
313315 props.route.params,
......@@ -423,14 +425,16 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
423425 const usesCompactMailToolbar =
424426 Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
425427
426 return (
428 const content = (
427429 <>
428430 {/* Static header config (glass preset and title) lives in Stack.tsx. The
429431 live sheet color stays dynamic here so the FlatList can remain the
430432 direct scene child for native scroll-edge sampling. */}
431433 <NativeStackScreenOptions
432434 options={{
433 contentStyle: { backgroundColor: sheetSurfaceColor },
435 contentStyle: {
436 backgroundColor: materialYouStyleLayoutActive ? screenColor : sheetSurfaceColor,
437 },
434438 headerShown: !isAndroid,
435439 unstable_headerSubtitle:
436440 Platform.OS === "ios" && projectName.length > 0 ? projectName : undefined,
......@@ -466,6 +470,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
466470 title="Files"
467471 subtitle={projectName}
468472 onBack={handleReturnToThread}
473 hideBottomBorder={materialYouStyleLayoutActive}
469474 actions={[
470475 {
471476 accessibilityLabel: "Refresh files",
......@@ -474,7 +479,13 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
474479 },
475480 ]}
476481 />
477 <View className="flex-row items-center gap-2 border-b border-border px-3 py-2">
482 <View
483 className={
484 materialYouStyleLayoutActive
485 ? "mx-4 my-2 min-h-12 flex-row items-center gap-2 rounded-full border border-input-border bg-input px-3.5"
486 : "flex-row items-center gap-2 border-b border-border px-3 py-2"
487 }
488 >
478489 <SymbolView
479490 name="magnifyingglass"
480491 size={17}
......@@ -485,7 +496,11 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
485496 accessibilityLabel="Search files"
486497 autoCapitalize="none"
487498 autoCorrect={false}
488 className="min-h-10 flex-1 rounded-xl py-2 text-sm"
499 className={
500 materialYouStyleLayoutActive
501 ? "min-h-10 flex-1 py-2 text-sm text-foreground"
502 : "min-h-10 flex-1 rounded-xl py-2 text-sm"
503 }
489504 placeholder="Search files"
490505 value={searchQuery}
491506 onChangeText={setSearchQuery}
......@@ -528,6 +543,14 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
528543 <FilesToolbarBottomFade />
529544 </>
530545 );
546
547 return materialYouStyleLayoutActive ? (
548 <View className="flex-1" style={{ backgroundColor: screenColor }}>
549 {content}
550 </View>
551 ) : (
552 content
553 );
531554}
532555
533556export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
apps/mobile/src/features/home/HomeHeader.tsx+14-2
......@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
23import type { MenuAction } from "@react-native-menu/menu";
34import Constants from "expo-constants";
......@@ -66,6 +67,7 @@ function checkedMenuState(checked: boolean) {
6667}
6768
6869function AndroidHomeHeader(props: HomeHeaderProps) {
70 const { materialYouStyleLayoutActive } = useAppearancePreferences();
6971 const insets = useSafeAreaInsets();
7072 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
7173 // Thread List v2 lays the list out in fixed creation order, so the
......@@ -199,7 +201,11 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
199201 <>
200202 <NativeStackScreenOptions options={{ headerShown: false }} />
201203 <View
202 className="border-b border-header-border bg-header pb-3"
204 className={
205 materialYouStyleLayoutActive
206 ? "bg-header pb-3"
207 : "border-b border-header-border bg-header pb-3"
208 }
203209 style={{
204210 paddingHorizontal: HOME_HORIZONTAL_INSET,
205211 paddingTop: Math.max(insets.top, 12),
......@@ -269,7 +275,13 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
269275 </Pressable>
270276 </View>
271277
272 <View className="min-h-12 flex-row items-center gap-2.5 rounded-2xl border border-input-border bg-input px-3.5">
278 <View
279 className={
280 materialYouStyleLayoutActive
281 ? "min-h-12 flex-row items-center gap-2.5 rounded-full border border-input-border bg-input px-3.5"
282 : "min-h-12 flex-row items-center gap-2.5 rounded-2xl border border-input-border bg-input px-3.5"
283 }
284 >
273285 <SymbolView
274286 name="magnifyingglass"
275287 size={17}
apps/mobile/src/features/home/HomeScreen.tsx+123-99
......@@ -26,6 +26,7 @@ import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-na
2626import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
2727import { useSafeAreaInsets } from "react-native-safe-area-context";
2828
29import { cn } from "../../lib/cn";
2930import { AppText as Text } from "../../components/AppText";
3031import { EmptyState } from "../../components/EmptyState";
3132import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspaceModel";
......@@ -34,6 +35,7 @@ import { scopedProjectKey } from "../../lib/scopedEntities";
3435import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
3536import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
3637import { useThreadSearch } from "../../state/queries";
38import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
3739import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
3840import { usePendingThreadOrder } from "../../state/thread-order";
3941import { environmentServerConfigsAtom } from "../../state/server";
......@@ -210,6 +212,7 @@ function HomeTopContentSpacer() {
210212/* ─── Main screen ────────────────────────────────────────────────────── */
211213
212214export function HomeScreen(props: HomeScreenProps) {
215 const { materialYouStyleLayoutActive } = useAppearancePreferences();
213216 const [groupDisplayStates, setGroupDisplayStates] = useState<
214217 ReadonlyMap<string, HomeGroupDisplayState>
215218 >(() => new Map());
......@@ -1081,26 +1084,31 @@ export function HomeScreen(props: HomeScreenProps) {
10811084
10821085 if (!hasAnyThreads) {
10831086 return (
1084 <View
1085 className="flex-1 items-center justify-center bg-screen px-8"
1086 style={{
1087 paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance,
1088 paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
1089 }}
1090 >
1091 <View className="w-full max-w-[430px]">
1092 <EmptyState
1093 title={emptyState.title}
1094 detail={emptyState.detail}
1095 actionLabel={!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined}
1096 onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
1097 variant="plain"
1098 />
1099 {emptyState.loading ? (
1100 <View className="mt-4 items-center">
1101 <ActivityIndicator colorClassName={"accent-icon-muted"} />
1102 </View>
1103 ) : null}
1087 <View className={materialYouStyleLayoutActive ? "flex-1 bg-header" : "flex-1 bg-screen"}>
1088 <View
1089 className={cn(
1090 "flex-1 items-center justify-center bg-screen px-8",
1091 materialYouStyleLayoutActive && "overflow-hidden rounded-t-[28px]",
1092 )}
1093 style={{
1094 paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance,
1095 paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
1096 }}
1097 >
1098 <View className="w-full max-w-[430px]">
1099 <EmptyState
1100 title={emptyState.title}
1101 detail={emptyState.detail}
1102 actionLabel={!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined}
1103 onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
1104 variant="plain"
1105 />
1106 {emptyState.loading ? (
1107 <View className="mt-4 items-center">
1108 <ActivityIndicator colorClassName={"accent-icon-muted"} />
1109 </View>
1110 ) : null}
1111 </View>
11041112 </View>
11051113 </View>
11061114 );
......@@ -1145,100 +1153,116 @@ export function HomeScreen(props: HomeScreenProps) {
11451153
11461154 if (threadListV2Enabled) {
11471155 return (
1148 <View className="flex-1 bg-screen">
1156 <View className={materialYouStyleLayoutActive ? "flex-1 bg-header" : "flex-1 bg-screen"}>
1157 <View
1158 className={
1159 materialYouStyleLayoutActive
1160 ? "flex-1 overflow-hidden rounded-t-[28px] bg-screen"
1161 : "flex-1 bg-screen"
1162 }
1163 >
1164 <SwipeableScrollGateProvider enabled={swipeEnabled}>
1165 <FlatList
1166 data={threadListV2Items}
1167 renderItem={renderV2Item}
1168 keyExtractor={v2KeyExtractor}
1169 extraData={v2ExtraData}
1170 ListHeaderComponent={v2ListHeader}
1171 ListFooterComponent={
1172 settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
1173 <Pressable
1174 accessibilityRole="button"
1175 accessibilityLabel={`Show ${Math.min(threadListV2Layout.hiddenSettledCount, THREAD_LIST_V2_SETTLED_PAGE_COUNT)} more settled threads`}
1176 onPress={showMoreSettled}
1177 className="mx-4 mt-2 items-center rounded-lg border border-dashed border-border py-2.5"
1178 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
1179 >
1180 <Text className="text-xs font-t3-medium text-foreground-muted">
1181 Show more ({threadListV2Layout.hiddenSettledCount} settled hidden)
1182 </Text>
1183 </Pressable>
1184 ) : null
1185 }
1186 ListEmptyComponent={v2ListEmpty}
1187 style={{ flex: 1 }}
1188 automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
1189 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
1190 showsVerticalScrollIndicator={false}
1191 keyboardDismissMode="on-drag"
1192 keyboardShouldPersistTaps="handled"
1193 {...scrollGateHandlers}
1194 scrollEventThrottle={16}
1195 contentContainerStyle={{
1196 paddingBottom:
1197 Platform.OS === "ios"
1198 ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance
1199 : Math.max(insets.bottom, 16) + 88,
1200 }}
1201 />
1202 </SwipeableScrollGateProvider>
1203 </View>
1204 </View>
1205 );
1206 }
1207
1208 return (
1209 <View className={materialYouStyleLayoutActive ? "flex-1 bg-header" : "flex-1 bg-screen"}>
1210 <View
1211 className={
1212 materialYouStyleLayoutActive
1213 ? "flex-1 overflow-hidden rounded-t-[28px] bg-screen"
1214 : "flex-1 bg-screen"
1215 }
1216 >
1217 {/* Sticky headers are deliberately not wired up: LegendList's JS sticky
1218 implementation mispositions pinned headers at mount under iOS
1219 automatic content insets (headers render one nav-inset too low until
1220 the first scroll event) and blanks non-pinned headers after
1221 collapse/expand data changes. The flattened layout still exposes
1222 `stickyHeaderIndices` if this gets revisited. */}
11491223 <SwipeableScrollGateProvider enabled={swipeEnabled}>
1150 <FlatList
1151 data={threadListV2Items}
1152 renderItem={renderV2Item}
1153 keyExtractor={v2KeyExtractor}
1154 extraData={v2ExtraData}
1155 ListHeaderComponent={v2ListHeader}
1156 ListFooterComponent={
1157 settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
1158 <Pressable
1159 accessibilityRole="button"
1160 accessibilityLabel={`Show ${Math.min(threadListV2Layout.hiddenSettledCount, THREAD_LIST_V2_SETTLED_PAGE_COUNT)} more settled threads`}
1161 onPress={showMoreSettled}
1162 className="mx-4 mt-2 items-center rounded-lg border border-dashed border-border py-2.5"
1163 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
1164 >
1165 <Text className="text-xs font-t3-medium text-foreground-muted">
1166 Show more ({threadListV2Layout.hiddenSettledCount} settled hidden)
1167 </Text>
1168 </Pressable>
1169 ) : null
1170 }
1171 ListEmptyComponent={v2ListEmpty}
1224 <LegendList
1225 ref={listRef}
1226 data={listLayout.items}
1227 renderItem={renderItem}
1228 keyExtractor={keyExtractor}
1229 itemsAreEqual={homeListItemsAreEqual}
1230 drawDistance={500}
1231 estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT}
1232 extraData={extraData}
1233 ListHeaderComponent={listHeader}
1234 ListEmptyComponent={listEmpty}
11721235 style={{ flex: 1 }}
1173 automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}
1174 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
1236 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
1237 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
11751238 showsVerticalScrollIndicator={false}
11761239 keyboardDismissMode="on-drag"
11771240 keyboardShouldPersistTaps="handled"
11781241 {...scrollGateHandlers}
1242 recycleItems
11791243 scrollEventThrottle={16}
11801244 contentContainerStyle={{
1245 // Android reserves room for the floating new-task FAB
1246 // (56 button + 16 gap + bottom inset). Pre-glass iOS shows a
1247 // standard 44pt bottom toolbar that overlays the list and is not
1248 // reflected in insets while contentInsetAdjustmentBehavior is
1249 // "never".
11811250 paddingBottom:
11821251 Platform.OS === "ios"
1183 ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance
1252 ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance
11841253 : Math.max(insets.bottom, 16) + 88,
11851254 }}
1255 scrollIndicatorInsets={
1256 Platform.OS === "ios"
1257 ? {
1258 bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance,
1259 top: 0,
1260 }
1261 : undefined
1262 }
11861263 />
11871264 </SwipeableScrollGateProvider>
11881265 </View>
1189 );
1190 }
1191
1192 return (
1193 <View className="flex-1 bg-screen">
1194 {/* Sticky headers are deliberately not wired up: LegendList's JS sticky
1195 implementation mispositions pinned headers at mount under iOS
1196 automatic content insets (headers render one nav-inset too low until
1197 the first scroll event) and blanks non-pinned headers after
1198 collapse/expand data changes. The flattened layout still exposes
1199 `stickyHeaderIndices` if this gets revisited. */}
1200 <SwipeableScrollGateProvider enabled={swipeEnabled}>
1201 <LegendList
1202 ref={listRef}
1203 data={listLayout.items}
1204 renderItem={renderItem}
1205 keyExtractor={keyExtractor}
1206 itemsAreEqual={homeListItemsAreEqual}
1207 drawDistance={500}
1208 estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT}
1209 extraData={extraData}
1210 ListHeaderComponent={listHeader}
1211 ListEmptyComponent={listEmpty}
1212 style={{ flex: 1 }}
1213 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
1214 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
1215 showsVerticalScrollIndicator={false}
1216 keyboardDismissMode="on-drag"
1217 keyboardShouldPersistTaps="handled"
1218 {...scrollGateHandlers}
1219 recycleItems
1220 scrollEventThrottle={16}
1221 contentContainerStyle={{
1222 // Android reserves room for the floating new-task FAB
1223 // (56 button + 16 gap + bottom inset). Pre-glass iOS shows a
1224 // standard 44pt bottom toolbar that overlays the list and is not
1225 // reflected in insets while contentInsetAdjustmentBehavior is
1226 // "never".
1227 paddingBottom:
1228 Platform.OS === "ios"
1229 ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance
1230 : Math.max(insets.bottom, 16) + 88,
1231 }}
1232 scrollIndicatorInsets={
1233 Platform.OS === "ios"
1234 ? {
1235 bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance,
1236 top: 0,
1237 }
1238 : undefined
1239 }
1240 />
1241 </SwipeableScrollGateProvider>
12421266 </View>
12431267 );
12441268}
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+10-1
......@@ -47,6 +47,7 @@ import {
4747} from "../keyboard/hardwareKeyboardCommands";
4848import { AndroidHomeFabLayout } from "../home/AndroidHomeFab";
4949import { HomeListOptionsProvider } from "../home/home-list-options";
50import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
5051import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
5152import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
5253import { WorkspaceInspectorPane } from "./workspace-inspector-pane";
......@@ -219,6 +220,7 @@ function AdaptiveWorkspaceLayoutContent(
219220 },
220221) {
221222 const projectGroupingMode = props.projectGroupingMode;
223 const { materialYouStyleLayoutActive } = useAppearancePreferences();
222224 const { width, height } = useWindowDimensions();
223225 const pathname = props.pathname;
224226 const navigation = useNavigation();
......@@ -567,7 +569,14 @@ function AdaptiveWorkspaceLayoutContent(
567569 </View>
568570 </Animated.View>
569571 ) : null}
570 <View className="flex-1 overflow-hidden bg-screen" collapsable={false}>
572 <View
573 className={
574 materialYouStyleLayoutActive
575 ? "flex-1 overflow-hidden bg-header"
576 : "flex-1 overflow-hidden bg-screen"
577 }
578 collapsable={false}
579 >
571580 <View
572581 collapsable={false}
573582 style={
apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx+9-1
......@@ -1,11 +1,19 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import { SymbolView } from "../../components/AppSymbol";
23import { Pressable, View } from "react-native";
34
45import { AppText as Text } from "../../components/AppText";
56
67export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {
8 const { materialYouStyleLayoutActive } = useAppearancePreferences();
79 return (
8 <View className="flex-1 items-center justify-center bg-screen px-10">
10 <View
11 className={
12 materialYouStyleLayoutActive
13 ? "flex-1 items-center justify-center px-10"
14 : "flex-1 items-center justify-center bg-screen px-10"
15 }
16 >
917 <View className="max-w-[360px] items-center gap-3">
1018 <SymbolView
1119 name="sidebar.left"
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+19-1
......@@ -10,7 +10,7 @@ import {
1010 useRef,
1111 type ReactNode,
1212} from "react";
13import { AppState, Appearance, useColorScheme } from "react-native";
13import { AppState, Appearance, Platform, useColorScheme } from "react-native";
1414
1515import { useAtomSet, useAtomValue } from "@effect/atom-react";
1616import { AsyncResult } from "effect/unstable/reactivity";
......@@ -51,6 +51,9 @@ interface AppearancePreferencesContextValue {
5151 readonly themeIds: MobileThemeIds;
5252 readonly themeMode: MobileThemeMode;
5353 readonly themeAppearance: MobileThemeAppearance;
54 readonly materialYouStyleLayoutEnabled: boolean;
55 readonly materialYouStyleLayoutActive: boolean;
56 readonly setMaterialYouStyleLayoutEnabled: (value: boolean) => void;
5457 readonly systemColorsAvailable: boolean;
5558 readonly systemColorsActive: boolean;
5659 readonly themeVariables: MobileThemeVariables;
......@@ -94,6 +97,8 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
9497 [resolvedThemeIds.dark, resolvedThemeIds.light],
9598 );
9699 const themeId = themeIds[themeAppearance];
100 const materialYouStyleLayoutEnabled = storedPreferences?.materialYouStyleLayoutEnabled ?? false;
101 const materialYouStyleLayoutActive = Platform.OS === "android" && materialYouStyleLayoutEnabled;
97102 const systemColorsActive = themeId === "material-you" && isSystemColorsAvailable;
98103 const [systemColorPalettes, setSystemColorPalettes] = useState(readSystemColorPalettes);
99104 useEffect(() => {
......@@ -242,6 +247,13 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
242247 [runtimeState, syncThemeRuntime, updateThemePreferences],
243248 );
244249
250 const setMaterialYouStyleLayoutEnabled = useCallback(
251 (value: boolean) => {
252 updatePreferences({ materialYouStyleLayoutEnabled: value });
253 },
254 [updatePreferences],
255 );
256
245257 const setBaseFontSize = useCallback(
246258 (value: number) => {
247259 const current = appliedRuntimeStateRef.current ?? runtimeState;
......@@ -281,6 +293,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
281293 themeAppearance,
282294 systemColorsAvailable: isSystemColorsAvailable,
283295 systemColorsActive,
296 materialYouStyleLayoutEnabled,
297 materialYouStyleLayoutActive,
298 setMaterialYouStyleLayoutEnabled,
284299 themeVariables,
285300 themeVariablesByAppearance,
286301 systemColorPalettes,
......@@ -300,6 +315,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
300315 themeMode,
301316 themeAppearance,
302317 systemColorsActive,
318 materialYouStyleLayoutEnabled,
319 materialYouStyleLayoutActive,
320 setMaterialYouStyleLayoutEnabled,
303321 themeVariables,
304322 themeVariablesByAppearance,
305323 systemColorPalettes,
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+18-1
......@@ -1,5 +1,5 @@
11import { memo, useId } from "react";
2import { Pressable, View } from "react-native";
2import { Platform, Pressable, View } from "react-native";
33import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
44import { ScopedTheme, ScopedVariables } from "uniwind";
55
......@@ -19,6 +19,9 @@ import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime";
1919import { cn } from "../../../../lib/cn";
2020import { useAppearancePreferences } from "../AppearancePreferencesProvider";
2121
22import { SettingsSection } from "../../components/SettingsSection";
23import { SettingsSwitchRow } from "../../components/SettingsSwitchRow";
24
2225const APPEARANCE_MODES: ReadonlyArray<{
2326 readonly id: MobileThemeMode;
2427 readonly label: string;
......@@ -290,11 +293,25 @@ export function ThemeAppearanceSection() {
290293 setThemeMode,
291294 themeIds,
292295 themeMode,
296 materialYouStyleLayoutEnabled,
297 setMaterialYouStyleLayoutEnabled,
293298 systemColorsAvailable,
294299 } = useAppearancePreferences();
295300
296301 return (
297302 <View className="gap-6">
303 {Platform.OS === "android" ? (
304 <SettingsSection card title="Android">
305 <SettingsSwitchRow
306 disabled={!isReady}
307 icon="square.grid.2x2"
308 label="Material You Layout"
309 onValueChange={setMaterialYouStyleLayoutEnabled}
310 subtitle="Use Material You surfaces, shapes, and component styling."
311 value={materialYouStyleLayoutEnabled}
312 />
313 </SettingsSection>
314 ) : null}
298315 <View className="gap-2">
299316 <SectionLabel>Color scheme</SectionLabel>
300317 <View accessibilityRole="radiogroup" className="flex-row gap-2">
apps/mobile/src/features/threads/ThreadComposer.tsx+21-3
......@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import { useAtomValue } from "@effect/atom-react";
23import type {
34 EnvironmentId,
......@@ -170,6 +171,7 @@ export function ComposerSurface(props: {
170171 /** Morphs between the compact and expanded composer layouts. */
171172 readonly animateLayout?: boolean;
172173}) {
174 const { materialYouStyleLayoutActive } = useAppearancePreferences();
173175 const targetBorderRadius =
174176 typeof props.style.borderRadius === "number" ? props.style.borderRadius : 0;
175177 const animatedBorderRadius = useSharedValue(targetBorderRadius);
......@@ -191,7 +193,11 @@ export function ComposerSurface(props: {
191193 // clip leaves the glass and content at their final height on the first frame.
192194 return (
193195 <Animated.View
194 className="shadow-[0_6px_28px] shadow-adaptive-black-a15-a35"
196 className={
197 materialYouStyleLayoutActive
198 ? undefined
199 : "shadow-[0_6px_28px] shadow-adaptive-black-a15-a35"
200 }
195201 layout={layoutTransition}
196202 style={[
197203 animatedShapeStyle,
......@@ -204,7 +210,11 @@ export function ComposerSurface(props: {
204210 >
205211 <AnimatedGlassSurface
206212 chrome="none"
207 fallbackClassName="border border-border bg-card-translucent"
213 fallbackClassName={
214 materialYouStyleLayoutActive
215 ? "border border-composer-border bg-composer-surface"
216 : "border border-border bg-card-translucent"
217 }
208218 glassEffectStyle="regular"
209219 // The composer is a passive material containing interactive controls.
210220 // Keep native glass out of the interactive content's layout path.
......@@ -227,6 +237,9 @@ export function ComposerSurface(props: {
227237}
228238
229239export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
240 const { materialYouStyleLayoutActive, themeVariables: materialTheme } =
241 useAppearancePreferences();
242 const composerPanel = materialTheme["--color-composer-panel"];
230243 const navigation = useNavigation();
231244 const foregroundColor = useUniwindTheme()["--color-foreground"];
232245 const bodyText = useScaledTextRole("body");
......@@ -543,13 +556,18 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
543556 style={{
544557 paddingTop: isExpanded ? 8 : 6,
545558 paddingBottom: (props.bottomInset ?? 0) + (isExpanded ? 8 : 6),
559 backgroundColor: materialYouStyleLayoutActive ? composerPanel : undefined,
546560 }}
547561 >
548562 {/* The backdrop gradient lives on a plain View: Reanimated's Animated.View
549563 silently drops experimental_backgroundImage on Android, which left this
550564 strip fully transparent and the feed text legible through the composer. */}
551565 <View
552 className="absolute inset-0 bg-linear-to-b from-screen/0 via-screen/60 to-screen/90"
566 className={
567 materialYouStyleLayoutActive
568 ? "hidden"
569 : "absolute inset-0 bg-linear-to-b from-screen/0 via-screen/60 to-screen/90"
570 }
553571 pointerEvents="none"
554572 />
555573 <Animated.View
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+58-11
......@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import { createThreadMovePlanner } from "./threadOrder";
23import type {
34 EnvironmentProject,
......@@ -145,6 +146,11 @@ function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {
145146function ThreadNavigationSidebarPane(
146147 props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean },
147148) {
149 const { materialYouStyleLayoutActive, themeVariables: materialTheme } =
150 useAppearancePreferences();
151 const screenColor = materialTheme["--color-screen"];
152 const mutedColor = materialTheme["--color-foreground-muted"];
153
148154 const insets = useSafeAreaInsets();
149155 const projects = useProjects();
150156 const threads = useThreadShells();
......@@ -916,7 +922,7 @@ function ThreadNavigationSidebarPane(
916922 }),
917923 )}
918924 searchQuery={props.searchQuery}
919 pane="sidebar"
925 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
920926 selected={
921927 scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey
922928 }
......@@ -956,7 +962,7 @@ function ThreadNavigationSidebarPane(
956962 disabled={!shelfPreferencesLoaded}
957963 expanded={item.expanded}
958964 onToggle={toggleSnoozedShelf}
959 pane="sidebar"
965 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
960966 />
961967 );
962968 case "v2-settled-shelf":
......@@ -966,7 +972,7 @@ function ThreadNavigationSidebarPane(
966972 disabled={!shelfPreferencesLoaded}
967973 expanded={item.expanded}
968974 onToggle={toggleSettledShelf}
969 pane="sidebar"
975 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
970976 />
971977 );
972978 case "v2-show-more":
......@@ -986,7 +992,7 @@ function ThreadNavigationSidebarPane(
986992 case "header":
987993 return (
988994 <ThreadListGroupHeader
989 variant="sidebar"
995 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
990996 collapsed={item.collapsed}
991997 isFirst={item.isFirst}
992998 groupKey={item.group.key}
......@@ -1004,7 +1010,7 @@ function ThreadNavigationSidebarPane(
10041010 case "pending-task":
10051011 return (
10061012 <PendingTaskListRow
1007 variant="sidebar"
1013 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
10081014 pendingTask={item.pendingTask}
10091015 environmentLabel={
10101016 savedConnectionsById[item.pendingTask.environmentId]?.environmentLabel ?? null
......@@ -1053,7 +1059,7 @@ function ThreadNavigationSidebarPane(
10531059 case "show-more":
10541060 return (
10551061 <ThreadListShowMoreRow
1056 variant="sidebar"
1062 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
10571063 hiddenCount={item.hiddenCount}
10581064 canShowLess={item.canShowLess}
10591065 groupKey={item.groupKey}
......@@ -1063,6 +1069,7 @@ function ThreadNavigationSidebarPane(
10631069 }
10641070 },
10651071 [
1072 materialYouStyleLayoutActive,
10661073 archiveThread,
10671074 activeReorderEnvironmentIds,
10681075 threadMovePlanners,
......@@ -1219,6 +1226,7 @@ function ThreadNavigationSidebarPane(
12191226 }
12201227 contentContainerStyle={[
12211228 styles.threadListContent,
1229 materialYouStyleLayoutActive ? { paddingHorizontal: 0 } : null,
12221230 {
12231231 paddingBottom: Math.max(insets.bottom, 16) + 16,
12241232 paddingTop: 6,
......@@ -1246,7 +1254,21 @@ function ThreadNavigationSidebarPane(
12461254 className="flex-1 border-r border-border bg-drawer"
12471255 style={{ width: props.width }}
12481256 >
1249 <View className="flex-1" style={{ paddingBottom: insets.bottom }}>
1257 <View
1258 className="flex-1"
1259 style={
1260 materialYouStyleLayoutActive
1261 ? {
1262 marginTop: stickyHeaderHeight,
1263 paddingBottom: insets.bottom,
1264 backgroundColor: screenColor,
1265 borderTopLeftRadius: 28,
1266 borderTopRightRadius: 28,
1267 overflow: "hidden",
1268 }
1269 : { paddingBottom: insets.bottom }
1270 }
1271 >
12501272 <SwipeableScrollGateProvider enabled={swipeEnabled}>
12511273 <GestureDetector gesture={sidebarScrollGesture}>
12521274 <LegendList
......@@ -1260,12 +1282,13 @@ function ThreadNavigationSidebarPane(
12601282 renderItem={renderListItem}
12611283 contentContainerStyle={[
12621284 styles.threadListContent,
1285 materialYouStyleLayoutActive ? { paddingHorizontal: 0 } : null,
12631286 {
12641287 paddingBottom:
12651288 Platform.OS === "android"
12661289 ? Math.max(insets.bottom, 16) + 88 - insets.bottom
12671290 : 16 + insets.bottom,
1268 paddingTop: topListInset,
1291 paddingTop: materialYouStyleLayoutActive ? 6 : topListInset,
12691292 },
12701293 ]}
12711294 keyboardDismissMode="on-drag"
......@@ -1310,7 +1333,13 @@ function ThreadNavigationSidebarPane(
13101333 </View>
13111334 </View>
13121335
1313 <View className="mx-4 mt-[9px] h-[38px] flex-row items-center gap-1.5 rounded-xl bg-sidebar-search pr-2.5 pl-[11px]">
1336 <View
1337 className={
1338 materialYouStyleLayoutActive
1339 ? "mx-4 mt-[9px] min-h-12 flex-row items-center gap-2.5 rounded-full border border-input-border bg-input px-3.5"
1340 : "mx-4 mt-[9px] h-[38px] flex-row items-center gap-1.5 rounded-xl bg-sidebar-search pr-2.5 pl-[11px]"
1341 }
1342 >
13141343 <SymbolView
13151344 name="magnifyingglass"
13161345 size={15}
......@@ -1322,14 +1351,32 @@ function ThreadNavigationSidebarPane(
13221351 accessibilityLabel="Search threads"
13231352 autoCapitalize="none"
13241353 autoCorrect={false}
1325 clearButtonMode="while-editing"
1354 clearButtonMode={materialYouStyleLayoutActive ? "never" : "while-editing"}
13261355 onChangeText={props.onSearchQueryChange}
13271356 placeholder="Search"
13281357 placeholderTextColorClassName={"accent-placeholder"}
13291358 returnKeyType="search"
1330 className="h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground"
1359 className={
1360 materialYouStyleLayoutActive
1361 ? "flex-1 px-0 py-2.5 font-sans text-base text-foreground"
1362 : "h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground"
1363 }
13311364 value={props.searchQuery}
13321365 />
1366 {materialYouStyleLayoutActive && props.searchQuery.length > 0 ? (
1367 <Pressable
1368 accessibilityLabel="Clear search"
1369 hitSlop={10}
1370 onPress={() => props.onSearchQueryChange("")}
1371 >
1372 <SymbolView
1373 name="xmark.circle.fill"
1374 size={17}
1375 tintColor={mutedColor}
1376 type="monochrome"
1377 />
1378 </Pressable>
1379 ) : null}
13331380 </View>
13341381 </View>
13351382 </View>
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+21-1
......@@ -76,6 +76,7 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt
7676import { useThreadComposerState } from "../../state/use-thread-composer-state";
7777import { threadEnvironment } from "../../state/threads";
7878import { projectThreadContentPresentation } from "./threadContentPresentation";
79import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
7980import {
8081 useAdaptiveWorkspaceLayout,
8182 useAdaptiveWorkspacePaneRole,
......@@ -193,6 +194,8 @@ function ThreadRouteContent(
193194 readonly selectedThreadDetailState: ReturnType<typeof useSelectedThreadDetailState>;
194195 },
195196) {
197 const { materialYouStyleLayoutActive, themeVariables } = useAppearancePreferences();
198 const headerColor = themeVariables["--color-header"];
196199 const {
197200 fileInspector,
198201 layout,
......@@ -839,7 +842,19 @@ function ThreadRouteContent(
839842
840843 <GitActionProgressOverlay progress={gitActionProgress} onDismiss={dismissGitActionResult} />
841844
842 <View className="flex-1 bg-screen">
845 <View
846 className={materialYouStyleLayoutActive ? "flex-1 bg-thread-canvas" : "flex-1 bg-screen"}
847 style={
848 materialYouStyleLayoutActive
849 ? {
850 borderTopLeftRadius: 28,
851 borderTopRightRadius: 28,
852 marginRight: layout.usesSplitView ? 8 : 0,
853 overflow: "hidden",
854 }
855 : undefined
856 }
857 >
843858 <ThreadDetailScreen
844859 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}
845860 contentPresentation={contentPresentation}
......@@ -931,6 +946,10 @@ function ThreadRouteContent(
931946 ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems)
932947 : undefined,
933948 unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined,
949 contentStyle:
950 Platform.OS === "android" && materialYouStyleLayoutActive
951 ? { backgroundColor: headerColor }
952 : undefined,
934953 }}
935954 />
936955
......@@ -940,6 +959,7 @@ function ThreadRouteContent(
940959 subtitle={headerSubtitle}
941960 onBack={layout.usesSplitView ? undefined : () => navigation.goBack()}
942961 actions={androidHeaderActions}
962 hideBottomBorder={materialYouStyleLayoutActive}
943963 />
944964 ) : null}
945965
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+4
......@@ -52,6 +52,7 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
5252import { serverEnvironment } from "../../state/server";
5353import { useAtomCommand } from "../../state/use-atom-command";
5454import { useNewTaskFlow } from "./new-task-flow-provider";
55import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
5556import {
5657 createProviderCatalogRefreshRunner,
5758 providerCatalogRefreshError,
......@@ -104,6 +105,8 @@ function ModelRow(props: {
104105 readonly isFirst: boolean;
105106 readonly isLast: boolean;
106107}) {
108 const { materialYouStyleLayoutActive } = useAppearancePreferences();
109 const selectedMaterialRow = materialYouStyleLayoutActive && props.selected;
107110 return (
108111 <Pressable
109112 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
......@@ -116,6 +119,7 @@ function ModelRow(props: {
116119 onPress={props.onPress}
117120 className={cn(
118121 "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4 py-2 active:bg-subtle",
122 selectedMaterialRow && "bg-thread-selected",
119123 props.isFirst && "rounded-t-2xl",
120124 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
121125 )}
apps/mobile/src/features/threads/thread-list-items.tsx+90-22
......@@ -463,9 +463,10 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
463463 >["simultaneousWithExternalGesture"];
464464}) {
465465 const { width: windowWidth } = useWindowDimensions();
466 const { themeAppearance: colorScheme } = useAppearancePreferences();
466 const { themeAppearance: colorScheme, materialYouStyleLayoutActive } = useAppearancePreferences();
467467 const compact = props.variant === "compact";
468468 const selected = props.selected === true;
469 const visuallySelected = selected && (!compact || materialYouStyleLayoutActive);
469470 // Recycling-safe: resets when the list container is reused for another
470471 // thread, so a hover highlight can't leak across rows.
471472 const [hovered, setHovered] = useRecyclingState(false);
......@@ -475,6 +476,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
475476 const drawerColor = theme["--color-drawer"];
476477 const pressedBackgroundColor = theme["--color-subtle"];
477478 const selectedBackgroundColor = theme["--color-user-bubble"];
479 const materialSelectedBackgroundColor = theme["--color-thread-selected"];
480 const materialSelectedForegroundColor = theme["--color-thread-selected-foreground"];
478481 const selectedForegroundColor = theme["--color-user-bubble-foreground"];
479482
480483 const {
......@@ -502,15 +505,25 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
502505 );
503506
504507 const backgroundColor = compact ? screenColor : drawerColor;
505 const effectivePressedBackground = selected
506 ? themeColorWithAlpha(String(selectedForegroundColor), 0.16)
508 const effectiveSelectedBackground = materialYouStyleLayoutActive
509 ? materialSelectedBackgroundColor
510 : selectedBackgroundColor;
511 const effectiveSelectedForeground = materialYouStyleLayoutActive
512 ? materialSelectedForegroundColor
513 : selectedForegroundColor;
514 const effectivePressedBackground = visuallySelected
515 ? themeColorWithAlpha(String(effectiveSelectedForeground), 0.16)
507516 : pressedBackgroundColor;
508517 const effectiveStatus =
509 selected && status
518 visuallySelected && status
510519 ? {
511520 ...status,
512 pillClassName: "bg-user-bubble-foreground/20",
513 textClassName: "text-user-bubble-foreground",
521 pillClassName: materialYouStyleLayoutActive
522 ? "bg-thread-selected-foreground/20"
523 : "bg-user-bubble-foreground/20",
524 textClassName: materialYouStyleLayoutActive
525 ? "text-thread-selected-foreground"
526 : "text-user-bubble-foreground",
514527 }
515528 : status;
516529
......@@ -581,7 +594,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
581594 compact
582595 ? "accent-icon-subtle"
583596 : selected
584 ? "accent-user-bubble-foreground-muted"
597 ? materialYouStyleLayoutActive
598 ? "accent-thread-selected-foreground-muted"
599 : "accent-user-bubble-foreground-muted"
585600 : "accent-foreground-muted"
586601 }
587602 />
......@@ -589,9 +604,12 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
589604 <Text
590605 className={cn(
591606 "shrink",
592 compact ? "text-sm text-foreground-muted" : "text-xs",
593 !compact &&
594 (selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted"),
607 compact ? "text-sm" : "text-xs",
608 visuallySelected
609 ? materialYouStyleLayoutActive
610 ? "text-thread-selected-foreground-muted"
611 : "text-user-bubble-foreground-muted"
612 : "text-foreground-muted",
595613 )}
596614 numberOfLines={1}
597615 >
......@@ -604,12 +622,18 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
604622 <PullRequestIcon
605623 size={compact ? 13 : 11}
606624 color={
607 selected ? String(selectedForegroundColor) : pullRequestTintColor(pr, colorScheme)
625 visuallySelected
626 ? String(effectiveSelectedForeground)
627 : pullRequestTintColor(pr, colorScheme)
608628 }
609629 />
610630 <Text
611631 className={`${compact ? "text-sm" : "text-xs"} font-t3-medium ${
612 selected ? "text-user-bubble-foreground" : pr.textClassName
632 visuallySelected
633 ? materialYouStyleLayoutActive
634 ? "text-thread-selected-foreground"
635 : "text-user-bubble-foreground"
636 : pr.textClassName
613637 }`}
614638 >
615639 {pr.label}
......@@ -626,6 +650,14 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
626650 accessibilityLabel={threadAccessibilityLabel}
627651 accessibilityRole="button"
628652 className="bg-screen active:opacity-70"
653 style={
654 materialYouStyleLayoutActive
655 ? {
656 backgroundColor: visuallySelected ? effectiveSelectedBackground : backgroundColor,
657 borderRadius: SIDEBAR_ROW_RADIUS,
658 }
659 : undefined
660 }
629661 onPress={() => {
630662 close();
631663 onSelectThread(thread);
......@@ -634,13 +666,36 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
634666 <View className="pr-[18px] pt-[10px]" style={{ paddingLeft: THREAD_LIST_COMPACT_INSET }}>
635667 <View className={cn("gap-[3px] pb-[10px]", !props.isLast && "border-b border-separator")}>
636668 <View className="flex-row items-center justify-between gap-2">
637 <Text className="flex-1 text-lg font-t3-bold text-foreground" numberOfLines={1}>
669 <Text
670 className={cn(
671 "flex-1 text-lg font-t3-bold",
672 visuallySelected
673 ? materialYouStyleLayoutActive
674 ? "text-thread-selected-foreground"
675 : "text-user-bubble-foreground"
676 : "text-foreground",
677 )}
678 numberOfLines={1}
679 >
638680 {thread.title}
639681 </Text>
640682 <View className="flex-row items-center gap-2">
641 {props.hasQueuedMessages ? <QueuedMessageIcon selected={selected} /> : null}
683 {props.hasQueuedMessages ? (
684 <QueuedMessageIcon selected={visuallySelected && !materialYouStyleLayoutActive} />
685 ) : null}
642686 {statusPill}
643 <Text className="text-base tabular-nums text-foreground-tertiary">{timestamp}</Text>
687 <Text
688 className={cn(
689 "text-base tabular-nums",
690 visuallySelected
691 ? materialYouStyleLayoutActive
692 ? "text-thread-selected-foreground-muted"
693 : "text-user-bubble-foreground-muted"
694 : "text-foreground-tertiary",
695 )}
696 >
697 {timestamp}
698 </Text>
644699 <SymbolView
645700 name="chevron.right"
646701 size={13}
......@@ -654,6 +709,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
654709 compact
655710 match={props.searchMatch}
656711 query={props.searchQuery ?? ""}
712 selected={visuallySelected}
657713 />
658714 ) : null}
659715 {subtitleRow}
......@@ -673,8 +729,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
673729 onSelectThread(thread);
674730 }}
675731 style={({ pressed }) => ({
676 backgroundColor: selected
677 ? selectedBackgroundColor
732 backgroundColor: visuallySelected
733 ? effectiveSelectedBackground
678734 : pressed || hovered
679735 ? effectivePressedBackground
680736 : backgroundColor,
......@@ -691,19 +747,29 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
691747 <Text
692748 className={cn(
693749 "flex-1 text-base font-t3-medium",
694 selected ? "text-user-bubble-foreground" : "text-foreground",
750 visuallySelected
751 ? materialYouStyleLayoutActive
752 ? "text-thread-selected-foreground"
753 : "text-user-bubble-foreground"
754 : "text-foreground",
695755 )}
696756 numberOfLines={1}
697757 >
698758 {thread.title}
699759 </Text>
700760 <View className="flex-row items-center gap-2">
701 {props.hasQueuedMessages ? <QueuedMessageIcon selected={selected} /> : null}
761 {props.hasQueuedMessages ? (
762 <QueuedMessageIcon selected={visuallySelected && !materialYouStyleLayoutActive} />
763 ) : null}
702764 {statusPill}
703765 <Text
704766 className={cn(
705767 "text-xs tabular-nums",
706 selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted",
768 visuallySelected
769 ? materialYouStyleLayoutActive
770 ? "text-thread-selected-foreground-muted"
771 : "text-user-bubble-foreground-muted"
772 : "text-foreground-muted",
707773 )}
708774 numberOfLines={1}
709775 >
......@@ -715,7 +781,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
715781 <ThreadSearchMatchExcerpt
716782 match={props.searchMatch}
717783 query={props.searchQuery ?? ""}
718 selected={selected}
784 selected={visuallySelected}
719785 />
720786 ) : null}
721787 {subtitleRow}
......@@ -728,7 +794,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
728794 threadKey={`${thread.environmentId}:${thread.id}`}
729795 backgroundColor={backgroundColor}
730796 containerStyle={
731 compact ? undefined : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" }
797 compact && !materialYouStyleLayoutActive
798 ? undefined
799 : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" }
732800 }
733801 enableTrackpadSwipe
734802 fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32}
apps/mobile/src/features/threads/thread-list-v2-items.tsx+69-20
......@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
12import type {
23 EnvironmentProject,
34 EnvironmentThreadShell,
......@@ -429,11 +430,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
429430
430431 const pr = useThreadPr(thread);
431432
433 const { materialYouStyleLayoutActive } = useAppearancePreferences();
432434 const theme = useUniwindTheme();
433435 const screenColor = theme["--color-screen"];
434436 const drawerColor = theme["--color-drawer"];
435437 const pressedBackgroundColor = theme["--color-subtle"];
436 const selectedBackgroundColor = theme["--color-user-bubble"];
438 const selectedBackgroundColor =
439 theme[materialYouStyleLayoutActive ? "--color-thread-selected" : "--color-user-bubble"];
437440 const sidebarPane = props.pane === "sidebar";
438441 const selected = props.selected === true;
439442 // The provider badge's border blends into the row's own surface, which
......@@ -720,7 +723,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
720723 <Text
721724 className={cn(
722725 "flex-1 text-sm font-t3-medium",
723 selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted",
726 selected
727 ? materialYouStyleLayoutActive
728 ? "text-thread-selected-foreground-muted"
729 : "text-user-bubble-foreground-muted"
730 : "text-foreground-muted",
724731 )}
725732 numberOfLines={1}
726733 >
......@@ -739,7 +746,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
739746 className={cn(
740747 "text-xs tabular-nums",
741748 selected
742 ? "text-user-bubble-foreground"
749 ? materialYouStyleLayoutActive
750 ? "text-thread-selected-foreground"
751 : "text-user-bubble-foreground"
743752 : (statusLabel?.className ?? "text-foreground-tertiary"),
744753 )}
745754 >
......@@ -749,7 +758,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
749758 <Text
750759 className={cn(
751760 "mt-1 text-base font-t3-medium",
752 selected ? "text-user-bubble-foreground" : "text-foreground",
761 selected
762 ? materialYouStyleLayoutActive
763 ? "text-thread-selected-foreground"
764 : "text-user-bubble-foreground"
765 : "text-foreground",
753766 )}
754767 numberOfLines={2}
755768 >
......@@ -769,7 +782,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
769782 <Text
770783 className={cn(
771784 "flex-1 text-xs",
772 selected ? "text-user-bubble-foreground-muted" : "text-danger-foreground",
785 selected
786 ? materialYouStyleLayoutActive
787 ? "text-thread-selected-foreground-muted"
788 : "text-user-bubble-foreground-muted"
789 : "text-danger-foreground",
773790 )}
774791 numberOfLines={1}
775792 >
......@@ -786,7 +803,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
786803 <Text
787804 className={cn(
788805 "shrink text-xs",
789 selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted",
806 selected
807 ? materialYouStyleLayoutActive
808 ? "text-thread-selected-foreground-muted"
809 : "text-user-bubble-foreground-muted"
810 : "text-foreground-muted",
790811 )}
791812 numberOfLines={1}
792813 >
......@@ -794,7 +815,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
794815 <Text
795816 className={cn(
796817 "text-xs",
797 selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted",
818 selected
819 ? materialYouStyleLayoutActive
820 ? "text-thread-selected-foreground-muted"
821 : "text-user-bubble-foreground-muted"
822 : "text-foreground-muted",
798823 )}
799824 style={{ fontFamily: MONO_FONT }}
800825 >
......@@ -806,7 +831,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
806831 <Text
807832 className={cn(
808833 "text-xs",
809 selected ? "text-user-bubble-foreground-muted" : "text-foreground-tertiary",
834 selected
835 ? materialYouStyleLayoutActive
836 ? "text-thread-selected-foreground-muted"
837 : "text-user-bubble-foreground-muted"
838 : "text-foreground-tertiary",
810839 )}
811840 >
812841 {props.environmentLabel}
......@@ -818,7 +847,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
818847 kind={props.environmentMachine}
819848 size={11}
820849 tintColorClassName={
821 selected ? "accent-user-bubble-foreground-muted" : "accent-foreground-tertiary"
850 selected
851 ? materialYouStyleLayoutActive
852 ? "accent-thread-selected-foreground-muted"
853 : "accent-user-bubble-foreground-muted"
854 : "accent-foreground-tertiary"
822855 }
823856 />
824857 ) : null}
......@@ -829,7 +862,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
829862 {pr ? (
830863 <Text
831864 accessibilityLabel={pr.accessibilityLabel}
832 className={cn("text-xs", selected ? "text-user-bubble-foreground" : pr.textClassName)}
865 className={cn(
866 "text-xs",
867 selected
868 ? materialYouStyleLayoutActive
869 ? "text-thread-selected-foreground"
870 : "text-user-bubble-foreground"
871 : pr.textClassName,
872 )}
833873 style={{ fontFamily: MONO_FONT }}
834874 >
835875 #{pr.label}
......@@ -863,16 +903,17 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
863903 onSelectThread(thread);
864904 }}
865905 style={
866 sidebarPane
906 sidebarPane || materialYouStyleLayoutActive
867907 ? ({ pressed }) => ({
868908 backgroundColor: selected
869909 ? selectedBackgroundColor
870910 : pressed
871911 ? pressedBackgroundColor
872 : drawerColor,
912 : sidebarPane
913 ? drawerColor
914 : screenColor,
873915 borderRadius: SIDEBAR_V2_ROW_RADIUS,
874 paddingHorizontal: 12,
875 paddingVertical: 10,
916 ...(sidebarPane ? { paddingHorizontal: 12, paddingVertical: 10 } : null),
876917 })
877918 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })
878919 }
......@@ -884,7 +925,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
884925 labels and text hierarchy carry state, an inset hairline
885926 separates rows. The opaque screen background stays so swipe
886927 actions reveal behind the row. */
887 <View className="bg-screen">
928 <View className={materialYouStyleLayoutActive ? undefined : "bg-screen"}>
888929 <View className="px-5 py-2.5">{cardContent}</View>
889930 {props.showTrailingDivider !== false ? (
890931 <View className="ml-5 h-px bg-border-subtle" />
......@@ -900,19 +941,21 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
900941 }
901942 accessibilityRole="button"
902943 accessibilityState={{ selected }}
903 className={sidebarPane ? undefined : "bg-screen"}
944 className={sidebarPane || materialYouStyleLayoutActive ? undefined : "bg-screen"}
904945 onPress={() => {
905946 close();
906947 onSelectThread(thread);
907948 }}
908949 style={
909 sidebarPane
950 sidebarPane || materialYouStyleLayoutActive
910951 ? ({ pressed }) => ({
911952 backgroundColor: selected
912953 ? selectedBackgroundColor
913954 : pressed
914955 ? pressedBackgroundColor
915 : drawerColor,
956 : sidebarPane
957 ? drawerColor
958 : screenColor,
916959 borderRadius: SIDEBAR_V2_ROW_RADIUS,
917960 })
918961 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })
......@@ -940,7 +983,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
940983 <Text
941984 className={cn(
942985 "text-base",
943 selected ? "text-user-bubble-foreground" : "text-foreground-muted",
986 selected
987 ? materialYouStyleLayoutActive
988 ? "text-thread-selected-foreground"
989 : "text-user-bubble-foreground"
990 : "text-foreground-muted",
944991 )}
945992 numberOfLines={1}
946993 >
......@@ -959,7 +1006,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
9591006 className={cn(
9601007 "text-sm tabular-nums",
9611008 selected
962 ? "text-user-bubble-foreground-muted"
1009 ? materialYouStyleLayoutActive
1010 ? "text-thread-selected-foreground-muted"
1011 : "text-user-bubble-foreground-muted"
9631012 : snoozedRow
9641013 ? "text-foreground-secondary"
9651014 : "text-foreground-tertiary",
apps/mobile/src/features/threads/thread-search-match.tsx+13-3
......@@ -2,6 +2,7 @@ import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state
22
33import { AppText as Text } from "../../components/AppText";
44import { cn } from "../../lib/cn";
5import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
56
67function foldAsciiCase(value: string): string {
78 return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
......@@ -50,12 +51,17 @@ export function ThreadSearchMatchExcerpt(props: {
5051 readonly compact?: boolean;
5152}) {
5253 const isUser = props.match.source === "user";
54 const { materialYouStyleLayoutActive } = useAppearancePreferences();
5355 const parts = splitHighlightParts(props.match.snippet, props.query);
5456 return (
5557 <Text
5658 className={cn(
5759 props.compact ? "text-sm" : "text-xs",
58 props.selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted",
60 props.selected
61 ? materialYouStyleLayoutActive
62 ? "text-thread-selected-foreground-muted"
63 : "text-user-bubble-foreground-muted"
64 : "text-foreground-muted",
5965 )}
6066 numberOfLines={1}
6167 >
......@@ -63,7 +69,9 @@ export function ThreadSearchMatchExcerpt(props: {
6369 className={cn(
6470 props.compact ? "text-sm font-t3-medium" : "text-xs font-t3-medium",
6571 props.selected
66 ? "text-user-bubble-foreground"
72 ? materialYouStyleLayoutActive
73 ? "text-thread-selected-foreground"
74 : "text-user-bubble-foreground"
6775 : isUser
6876 ? "text-foreground-secondary"
6977 : "text-adaptive-emerald-600-400",
......@@ -77,7 +85,9 @@ export function ThreadSearchMatchExcerpt(props: {
7785 props.compact ? "text-sm" : "text-xs",
7886 part.highlighted && "font-t3-bold",
7987 props.selected
80 ? "text-user-bubble-foreground"
88 ? materialYouStyleLayoutActive
89 ? "text-thread-selected-foreground"
90 : "text-user-bubble-foreground"
8191 : part.highlighted
8292 ? "text-foreground"
8393 : "text-foreground-muted",
apps/mobile/src/lib/materialYouTheme.test.ts+2
......@@ -39,6 +39,8 @@ describe("Material You system colors", () => {
3939
4040 expect(base).toEqual(snapshot);
4141 expect(variables["--color-screen"]).toBe(palette.surface);
42 expect(variables["--color-thread-canvas"]).toBe(palette.surfaceContainerLow);
43 expect(variables["--color-thread-selected"]).toBe(palette.surfaceContainer);
4244 expect(variables["--color-header"]).toBe(palette.surfaceContainerHigh);
4345 expect(variables["--color-primary"]).toBe(palette.primary);
4446 expect(variables["--color-placeholder"]).toBe("#1C1B1F9E");
apps/mobile/src/lib/materialYouTheme.ts+7
......@@ -38,6 +38,13 @@ export function materialYouPaletteToMobileThemeVariables(
3838 "--color-card": palette.surfaceContainer,
3939 "--color-card-alt": palette.surfaceContainerHigh,
4040 "--color-card-translucent": withAlpha(palette.surfaceContainer, 0.8),
41 "--color-thread-canvas": palette.surfaceContainerLow,
42 "--color-thread-selected": palette.surfaceContainer,
43 "--color-thread-selected-foreground": palette.onSurface,
44 "--color-thread-selected-foreground-muted": palette.onSurfaceVariant,
45 "--color-composer-panel": withAlpha(palette.surfaceContainerLow, dark ? 0.92 : 0.88),
46 "--color-composer-surface": withAlpha(palette.surfaceContainerHigh, dark ? 0.9 : 0.94),
47 "--color-composer-border": withAlpha(palette.outlineVariant, dark ? 0.34 : 0.46),
4148 "--color-foreground": palette.onSurface,
4249 "--color-foreground-secondary": palette.onSurfaceVariant,
4350 "--color-foreground-muted": palette.onSurfaceVariant,
apps/mobile/src/lib/mobileTheme.test.ts+1-1
......@@ -153,7 +153,7 @@ describe("mobile themes", () => {
153153
154154 it("maps semantic palette roles onto every mobile color variable", () => {
155155 const variables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
156 expect(Object.keys(variables)).toHaveLength(68);
156 expect(Object.keys(variables)).toHaveLength(75);
157157 expect(variables["--color-sheet-solid"]).toBe(
158158 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.chrome),
159159 );
apps/mobile/src/lib/mobileTheme.ts+10
......@@ -218,6 +218,16 @@ export function createMobileThemeVariables(
218218 "--color-card": c.surfaceRaised,
219219 "--color-card-alt": c.surface,
220220 "--color-card-translucent": withAlpha(c.surfaceRaised, 0.8),
221 "--color-thread-canvas": c.surface,
222 "--color-thread-selected": c.surfaceRaised,
223 "--color-thread-selected-foreground": c.text,
224 "--color-thread-selected-foreground-muted": c.textMuted,
225 "--color-composer-panel": themeColorWithAlpha(c.surface, appearance === "dark" ? 0.92 : 0.88),
226 "--color-composer-surface": themeColorWithAlpha(
227 c.surfaceRaised,
228 appearance === "dark" ? 0.9 : 0.94,
229 ),
230 "--color-composer-border": themeColorWithAlpha(c.border, appearance === "dark" ? 0.46 : 0.54),
221231 "--color-foreground": c.text,
222232 "--color-foreground-secondary": c.textMuted,
223233 "--color-foreground-muted": c.mutedForeground,
apps/mobile/src/lib/storage.test.ts+17
......@@ -204,6 +204,23 @@ describe("mobile connection storage", () => {
204204 await expect(loadPreferences()).resolves.toEqual({ ...themes, lightThemeId: "t3-chat" });
205205 });
206206
207 it("persists the Material You layout independently of the selected theme", async () => {
208 await savePreferencesPatch({
209 lightThemeId: "material-you",
210 materialYouStyleLayoutEnabled: true,
211 });
212 await savePreferencesPatch({ lightThemeId: "t3-chat" });
213 await expect(loadPreferences()).resolves.toEqual({
214 lightThemeId: "t3-chat",
215 materialYouStyleLayoutEnabled: true,
216 });
217 await savePreferencesPatch({ materialYouStyleLayoutEnabled: false });
218 await expect(loadPreferences()).resolves.toEqual({
219 lightThemeId: "t3-chat",
220 materialYouStyleLayoutEnabled: false,
221 });
222 });
223
207224 it("drops the removed theme transition preference", async () => {
208225 mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10);
209226
apps/mobile/src/persistence/mobile-preferences.ts+5
......@@ -21,6 +21,7 @@ export interface Preferences {
2121 readonly lightThemeId?: MobileThemeId;
2222 readonly darkThemeId?: MobileThemeId;
2323 readonly themeMode?: MobileThemeMode;
24 readonly materialYouStyleLayoutEnabled?: boolean;
2425 readonly baseFontSize?: number;
2526 readonly terminalFontSize?: number | null;
2627 readonly markdownFontSize?: number;
......@@ -90,6 +91,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
9091 lightThemeId?: MobileThemeId;
9192 darkThemeId?: MobileThemeId;
9293 themeMode?: MobileThemeMode;
94 materialYouStyleLayoutEnabled?: boolean;
9395 baseFontSize?: number;
9496 terminalFontSize?: number | null;
9597 markdownFontSize?: number;
......@@ -133,6 +135,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
133135 ) {
134136 preferences.themeMode = parsed.themeMode;
135137 }
138 if (typeof parsed.materialYouStyleLayoutEnabled === "boolean") {
139 preferences.materialYouStyleLayoutEnabled = parsed.materialYouStyleLayoutEnabled;
140 }
136141 if (typeof parsed.baseFontSize === "number") preferences.baseFontSize = parsed.baseFontSize;
137142 if (typeof parsed.terminalFontSize === "number" || parsed.terminalFontSize === null) {
138143 preferences.terminalFontSize = parsed.terminalFontSize;
docs/user/appearance.md+2
......@@ -10,6 +10,8 @@ themes or defaults.
1010On Android 12 or newer, choose the **Material You** theme in Appearance to use colors from
1111your wallpaper. Selecting another theme replaces those colors. Like other themes, Material You
1212can be selected separately for light and dark appearances.
13Android also offers **Material You Layout**, which changes shapes and spacing independently
14of the selected theme.
1315
1416## Motion
1517