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 @@...@@ -6,6 +6,13 @@
6 "--color-card": "#ffffff",6 "--color-card": "#ffffff",
7 "--color-card-alt": "#f5f5f5",7 "--color-card-alt": "#f5f5f5",
8 "--color-card-translucent": "rgba(255, 255, 255, 0.8)",8 "--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)",
9 "--color-foreground": "#262626",16 "--color-foreground": "#262626",
10 "--color-foreground-secondary": "#525252",17 "--color-foreground-secondary": "#525252",
11 "--color-foreground-muted": "#737373",18 "--color-foreground-muted": "#737373",
...@@ -76,6 +83,13 @@...@@ -76,6 +83,13 @@
76 "--color-card": "#171717",83 "--color-card": "#171717",
77 "--color-card-alt": "#1c1c1c",84 "--color-card-alt": "#1c1c1c",
78 "--color-card-translucent": "rgba(17, 17, 17, 0.8)",85 "--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)",
79 "--color-foreground": "#f5f5f5",93 "--color-foreground": "#f5f5f5",
80 "--color-foreground-secondary": "#a3a3a3",94 "--color-foreground-secondary": "#a3a3a3",
81 "--color-foreground-muted": "#8e8e93",95 "--color-foreground-muted": "#8e8e93",
apps/mobile/generated-uniwind-themes.css+70
...@@ -128,6 +128,13 @@...@@ -128,6 +128,13 @@
128 --color-card: #fdfafd;128 --color-card: #fdfafd;
129 --color-card-alt: #faf3fb;129 --color-card-alt: #faf3fb;
130 --color-card-translucent: rgba(253, 250, 253, 0.8);130 --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);
131 --color-foreground: #501854;138 --color-foreground: #501854;
132 --color-foreground-secondary: #ac1668;139 --color-foreground-secondary: #ac1668;
133 --color-foreground-muted: #8d1255;140 --color-foreground-muted: #8d1255;
...@@ -256,6 +263,13 @@...@@ -256,6 +263,13 @@
256 --color-card: #2c2631;263 --color-card: #2c2631;
257 --color-card-alt: #29232d;264 --color-card-alt: #29232d;
258 --color-card-translucent: rgba(44, 38, 49, 0.8);265 --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);
259 --color-foreground: #f9f8fb;273 --color-foreground: #f9f8fb;
260 --color-foreground-secondary: #e7d0dd;274 --color-foreground-secondary: #e7d0dd;
261 --color-foreground-muted: #e7d0dd;275 --color-foreground-muted: #e7d0dd;
...@@ -384,6 +398,13 @@...@@ -384,6 +398,13 @@
384 --color-card: #ecefed;398 --color-card: #ecefed;
385 --color-card-alt: #f3f7f4;399 --color-card-alt: #f3f7f4;
386 --color-card-translucent: rgba(236, 239, 237, 0.8);400 --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);
387 --color-foreground: #241523;408 --color-foreground: #241523;
388 --color-foreground-secondary: #746c73;409 --color-foreground-secondary: #746c73;
389 --color-foreground-muted: #6e696f;410 --color-foreground-muted: #6e696f;
...@@ -512,6 +533,13 @@...@@ -512,6 +533,13 @@
512 --color-card: #36413c;533 --color-card: #36413c;
513 --color-card-alt: #1b2821;534 --color-card-alt: #1b2821;
514 --color-card-translucent: rgba(54, 65, 60, 0.8);535 --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);
515 --color-foreground: #fffaff;543 --color-foreground: #fffaff;
516 --color-foreground-secondary: #919595;544 --color-foreground-secondary: #919595;
517 --color-foreground-muted: #9da5a2;545 --color-foreground-muted: #9da5a2;
...@@ -640,6 +668,13 @@...@@ -640,6 +668,13 @@
640 --color-card: #edeff1;668 --color-card: #edeff1;
641 --color-card-alt: #f5f7f8;669 --color-card-alt: #f5f7f8;
642 --color-card-translucent: rgba(237, 239, 241, 0.8);670 --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);
643 --color-foreground: #241523;678 --color-foreground: #241523;
644 --color-foreground-secondary: #746c75;679 --color-foreground-secondary: #746c75;
645 --color-foreground-muted: #6f6873;680 --color-foreground-muted: #6f6873;
...@@ -768,6 +803,13 @@...@@ -768,6 +803,13 @@
768 --color-card: #333b45;803 --color-card: #333b45;
769 --color-card-alt: #17212b;804 --color-card-alt: #17212b;
770 --color-card-translucent: rgba(51, 59, 69, 0.8);805 --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);
771 --color-foreground: #fffaff;813 --color-foreground: #fffaff;
772 --color-foreground-secondary: #8d8f97;814 --color-foreground-secondary: #8d8f97;
773 --color-foreground-muted: #969ca6;815 --color-foreground-muted: #969ca6;
...@@ -896,6 +938,13 @@...@@ -896,6 +938,13 @@
896 --color-card: #f1efee;938 --color-card: #f1efee;
897 --color-card-alt: #f9f7f5;939 --color-card-alt: #f9f7f5;
898 --color-card-translucent: rgba(241, 239, 238, 0.8);940 --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);
899 --color-foreground: #241523;948 --color-foreground: #241523;
900 --color-foreground-secondary: #766c74;949 --color-foreground-secondary: #766c74;
901 --color-foreground-muted: #74686f;950 --color-foreground-muted: #74686f;
...@@ -1024,6 +1073,13 @@...@@ -1024,6 +1073,13 @@
1024 --color-card: #433835;1073 --color-card: #433835;
1025 --color-card-alt: #291e1a;1074 --color-card-alt: #291e1a;
1026 --color-card-translucent: rgba(67, 56, 53, 0.8);1075 --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);
1027 --color-foreground: #fffaff;1083 --color-foreground: #fffaff;
1028 --color-foreground-secondary: #968e8f;1084 --color-foreground-secondary: #968e8f;
1029 --color-foreground-muted: #a59996;1085 --color-foreground-muted: #a59996;
...@@ -1152,6 +1208,13 @@...@@ -1152,6 +1208,13 @@
1152 --color-card: #f0eff2;1208 --color-card: #f0eff2;
1153 --color-card-alt: #f8f7f9;1209 --color-card-alt: #f8f7f9;
1154 --color-card-translucent: rgba(240, 239, 242, 0.8);1210 --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);
1155 --color-foreground: #241523;1218 --color-foreground: #241523;
1156 --color-foreground-secondary: #766c76;1219 --color-foreground-secondary: #766c76;
1157 --color-foreground-muted: #726874;1220 --color-foreground-muted: #726874;
...@@ -1280,6 +1343,13 @@...@@ -1280,6 +1343,13 @@
1280 --color-card: #383443;1343 --color-card: #383443;
1281 --color-card-alt: #1d1929;1344 --color-card-alt: #1d1929;
1282 --color-card-translucent: rgba(56, 52, 67, 0.8);1345 --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);
1283 --color-foreground: #fffaff;1353 --color-foreground: #fffaff;
1284 --color-foreground-secondary: #8e8a95;1354 --color-foreground-secondary: #8e8a95;
1285 --color-foreground-muted: #9690a1;1355 --color-foreground-muted: #9690a1;
apps/mobile/global.css+14
...@@ -19,6 +19,13 @@...@@ -19,6 +19,13 @@
19 --color-card: #ffffff;19 --color-card: #ffffff;
20 --color-card-alt: #f5f5f5;20 --color-card-alt: #f5f5f5;
21 --color-card-translucent: rgba(255, 255, 255, 0.8);21 --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
23 /* Text */30 /* Text */
24 --color-foreground: #262626;31 --color-foreground: #262626;
...@@ -123,6 +130,13 @@...@@ -123,6 +130,13 @@
123 --color-card: #171717;130 --color-card: #171717;
124 --color-card-alt: #1c1c1c;131 --color-card-alt: #1c1c1c;
125 --color-card-translucent: rgba(17, 17, 17, 0.8);132 --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
127 /* Text */141 /* Text */
128 --color-foreground: #f5f5f5;142 --color-foreground: #f5f5f5;
apps/mobile/src/components/AndroidScreenHeader.tsx+2
...@@ -48,6 +48,7 @@ export function AndroidScreenHeader(props: {...@@ -48,6 +48,7 @@ export function AndroidScreenHeader(props: {
48 readonly trailing?: ReactNode;48 readonly trailing?: ReactNode;
49 readonly onBack?: () => void;49 readonly onBack?: () => void;
50 readonly embedded?: boolean;50 readonly embedded?: boolean;
51 readonly hideBottomBorder?: boolean;
51}) {52}) {
52 const insets = useSafeAreaInsets();53 const insets = useSafeAreaInsets();
5354
...@@ -56,6 +57,7 @@ export function AndroidScreenHeader(props: {...@@ -56,6 +57,7 @@ export function AndroidScreenHeader(props: {
56 className="border-b border-header-border bg-header px-3 pb-2.5"57 className="border-b border-header-border bg-header px-3 pb-2.5"
57 style={{58 style={{
58 paddingTop: props.embedded ? 8 : Math.max(insets.top, 12),59 paddingTop: props.embedded ? 8 : Math.max(insets.top, 12),
60 borderBottomWidth: props.hideBottomBorder ? 0 : undefined,
59 }}61 }}
60 >62 >
61 <View className="min-h-12 flex-row items-center gap-2">63 <View className="min-h-12 flex-row items-center gap-2">
apps/mobile/src/components/ThemedSwitch.tsx+47-1
...@@ -1,6 +1,52 @@...@@ -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
3export function ThemedSwitch(props: SwitchProps) {6export 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
4 return (50 return (
5 <Switch51 <Switch
6 {...props}52 {...props}
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+28-5
...@@ -306,8 +306,10 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -306,8 +306,10 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
306 useAdaptiveWorkspaceLayout();306 useAdaptiveWorkspaceLayout();
307 const [searchQuery, setSearchQuery] = useState("");307 const [searchQuery, setSearchQuery] = useState("");
308 const isAndroid = Platform.OS === "android";308 const isAndroid = Platform.OS === "android";
309 const { themeAppearance: highlightTheme } = useAppearancePreferences();309 const { themeAppearance: highlightTheme, materialYouStyleLayoutActive } =
310 useAppearancePreferences();
310 const theme = useUniwindTheme();311 const theme = useUniwindTheme();
312 const screenColor = theme["--color-screen"];
311 const sheetSurfaceColor = theme["--color-sheet-solid"];313 const sheetSurfaceColor = theme["--color-sheet-solid"];
312 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(314 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
313 props.route.params,315 props.route.params,
...@@ -423,14 +425,16 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -423,14 +425,16 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
423 const usesCompactMailToolbar =425 const usesCompactMailToolbar =
424 Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;426 Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
425427
426 return (428 const content = (
427 <>429 <>
428 {/* Static header config (glass preset and title) lives in Stack.tsx. The430 {/* Static header config (glass preset and title) lives in Stack.tsx. The
429 live sheet color stays dynamic here so the FlatList can remain the431 live sheet color stays dynamic here so the FlatList can remain the
430 direct scene child for native scroll-edge sampling. */}432 direct scene child for native scroll-edge sampling. */}
431 <NativeStackScreenOptions433 <NativeStackScreenOptions
432 options={{434 options={{
433 contentStyle: { backgroundColor: sheetSurfaceColor },435 contentStyle: {
436 backgroundColor: materialYouStyleLayoutActive ? screenColor : sheetSurfaceColor,
437 },
434 headerShown: !isAndroid,438 headerShown: !isAndroid,
435 unstable_headerSubtitle:439 unstable_headerSubtitle:
436 Platform.OS === "ios" && projectName.length > 0 ? projectName : undefined,440 Platform.OS === "ios" && projectName.length > 0 ? projectName : undefined,
...@@ -466,6 +470,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -466,6 +470,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
466 title="Files"470 title="Files"
467 subtitle={projectName}471 subtitle={projectName}
468 onBack={handleReturnToThread}472 onBack={handleReturnToThread}
473 hideBottomBorder={materialYouStyleLayoutActive}
469 actions={[474 actions={[
470 {475 {
471 accessibilityLabel: "Refresh files",476 accessibilityLabel: "Refresh files",
...@@ -474,7 +479,13 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -474,7 +479,13 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
474 },479 },
475 ]}480 ]}
476 />481 />
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 >
478 <SymbolView489 <SymbolView
479 name="magnifyingglass"490 name="magnifyingglass"
480 size={17}491 size={17}
...@@ -485,7 +496,11 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -485,7 +496,11 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
485 accessibilityLabel="Search files"496 accessibilityLabel="Search files"
486 autoCapitalize="none"497 autoCapitalize="none"
487 autoCorrect={false}498 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 }
489 placeholder="Search files"504 placeholder="Search files"
490 value={searchQuery}505 value={searchQuery}
491 onChangeText={setSearchQuery}506 onChangeText={setSearchQuery}
...@@ -528,6 +543,14 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -528,6 +543,14 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
528 <FilesToolbarBottomFade />543 <FilesToolbarBottomFade />
529 </>544 </>
530 );545 );
546
547 return materialYouStyleLayoutActive ? (
548 <View className="flex-1" style={{ backgroundColor: screenColor }}>
549 {content}
550 </View>
551 ) : (
552 content
553 );
531}554}
532555
533export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {556export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
apps/mobile/src/features/home/HomeHeader.tsx+14-2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";2import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
2import type { MenuAction } from "@react-native-menu/menu";3import type { MenuAction } from "@react-native-menu/menu";
3import Constants from "expo-constants";4import Constants from "expo-constants";
...@@ -66,6 +67,7 @@ function checkedMenuState(checked: boolean) {...@@ -66,6 +67,7 @@ function checkedMenuState(checked: boolean) {
66}67}
6768
68function AndroidHomeHeader(props: HomeHeaderProps) {69function AndroidHomeHeader(props: HomeHeaderProps) {
70 const { materialYouStyleLayoutActive } = useAppearancePreferences();
69 const insets = useSafeAreaInsets();71 const insets = useSafeAreaInsets();
70 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);72 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
71 // Thread List v2 lays the list out in fixed creation order, so the73 // Thread List v2 lays the list out in fixed creation order, so the
...@@ -199,7 +201,11 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -199,7 +201,11 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
199 <>201 <>
200 <NativeStackScreenOptions options={{ headerShown: false }} />202 <NativeStackScreenOptions options={{ headerShown: false }} />
201 <View203 <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 }
203 style={{209 style={{
204 paddingHorizontal: HOME_HORIZONTAL_INSET,210 paddingHorizontal: HOME_HORIZONTAL_INSET,
205 paddingTop: Math.max(insets.top, 12),211 paddingTop: Math.max(insets.top, 12),
...@@ -269,7 +275,13 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -269,7 +275,13 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
269 </Pressable>275 </Pressable>
270 </View>276 </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 >
273 <SymbolView285 <SymbolView
274 name="magnifyingglass"286 name="magnifyingglass"
275 size={17}287 size={17}
apps/mobile/src/features/home/HomeScreen.tsx+123-99
...@@ -26,6 +26,7 @@ import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-na...@@ -26,6 +26,7 @@ import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-na
26import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";26import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
27import { useSafeAreaInsets } from "react-native-safe-area-context";27import { useSafeAreaInsets } from "react-native-safe-area-context";
2828
29import { cn } from "../../lib/cn";
29import { AppText as Text } from "../../components/AppText";30import { AppText as Text } from "../../components/AppText";
30import { EmptyState } from "../../components/EmptyState";31import { EmptyState } from "../../components/EmptyState";
31import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspaceModel";32import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspaceModel";
...@@ -34,6 +35,7 @@ import { scopedProjectKey } from "../../lib/scopedEntities";...@@ -34,6 +35,7 @@ import { scopedProjectKey } from "../../lib/scopedEntities";
34import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";35import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
35import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";36import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
36import { useThreadSearch } from "../../state/queries";37import { useThreadSearch } from "../../state/queries";
38import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
37import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";39import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
38import { usePendingThreadOrder } from "../../state/thread-order";40import { usePendingThreadOrder } from "../../state/thread-order";
39import { environmentServerConfigsAtom } from "../../state/server";41import { environmentServerConfigsAtom } from "../../state/server";
...@@ -210,6 +212,7 @@ function HomeTopContentSpacer() {...@@ -210,6 +212,7 @@ function HomeTopContentSpacer() {
210/* ─── Main screen ────────────────────────────────────────────────────── */212/* ─── Main screen ────────────────────────────────────────────────────── */
211213
212export function HomeScreen(props: HomeScreenProps) {214export function HomeScreen(props: HomeScreenProps) {
215 const { materialYouStyleLayoutActive } = useAppearancePreferences();
213 const [groupDisplayStates, setGroupDisplayStates] = useState<216 const [groupDisplayStates, setGroupDisplayStates] = useState<
214 ReadonlyMap<string, HomeGroupDisplayState>217 ReadonlyMap<string, HomeGroupDisplayState>
215 >(() => new Map());218 >(() => new Map());
...@@ -1081,26 +1084,31 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -1081,26 +1084,31 @@ export function HomeScreen(props: HomeScreenProps) {
10811084
1082 if (!hasAnyThreads) {1085 if (!hasAnyThreads) {
1083 return (1086 return (
1084 <View1087 <View className={materialYouStyleLayoutActive ? "flex-1 bg-header" : "flex-1 bg-screen"}>
1085 className="flex-1 items-center justify-center bg-screen px-8"1088 <View
1086 style={{1089 className={cn(
1087 paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance,1090 "flex-1 items-center justify-center bg-screen px-8",
1088 paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,1091 materialYouStyleLayoutActive && "overflow-hidden rounded-t-[28px]",
1089 }}1092 )}
1090 >1093 style={{
1091 <View className="w-full max-w-[430px]">1094 paddingBottom: Math.max(insets.bottom, 24) + iosBottomToolbarClearance,
1092 <EmptyState1095 paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0,
1093 title={emptyState.title}1096 }}
1094 detail={emptyState.detail}1097 >
1095 actionLabel={!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined}1098 <View className="w-full max-w-[430px]">
1096 onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}1099 <EmptyState
1097 variant="plain"1100 title={emptyState.title}
1098 />1101 detail={emptyState.detail}
1099 {emptyState.loading ? (1102 actionLabel={!props.catalogState.hasReadyEnvironment ? "Add environment" : undefined}
1100 <View className="mt-4 items-center">1103 onAction={!props.catalogState.hasReadyEnvironment ? props.onAddConnection : undefined}
1101 <ActivityIndicator colorClassName={"accent-icon-muted"} />1104 variant="plain"
1102 </View>1105 />
1103 ) : null}1106 {emptyState.loading ? (
1107 <View className="mt-4 items-center">
1108 <ActivityIndicator colorClassName={"accent-icon-muted"} />
1109 </View>
1110 ) : null}
1111 </View>
1104 </View>1112 </View>
1105 </View>1113 </View>
1106 );1114 );
...@@ -1145,100 +1153,116 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -1145,100 +1153,116 @@ export function HomeScreen(props: HomeScreenProps) {
11451153
1146 if (threadListV2Enabled) {1154 if (threadListV2Enabled) {
1147 return (1155 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. */}
1149 <SwipeableScrollGateProvider enabled={swipeEnabled}>1223 <SwipeableScrollGateProvider enabled={swipeEnabled}>
1150 <FlatList1224 <LegendList
1151 data={threadListV2Items}1225 ref={listRef}
1152 renderItem={renderV2Item}1226 data={listLayout.items}
1153 keyExtractor={v2KeyExtractor}1227 renderItem={renderItem}
1154 extraData={v2ExtraData}1228 keyExtractor={keyExtractor}
1155 ListHeaderComponent={v2ListHeader}1229 itemsAreEqual={homeListItemsAreEqual}
1156 ListFooterComponent={1230 drawDistance={500}
1157 settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (1231 estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT}
1158 <Pressable1232 extraData={extraData}
1159 accessibilityRole="button"1233 ListHeaderComponent={listHeader}
1160 accessibilityLabel={`Show ${Math.min(threadListV2Layout.hiddenSettledCount, THREAD_LIST_V2_SETTLED_PAGE_COUNT)} more settled threads`}1234 ListEmptyComponent={listEmpty}
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}
1172 style={{ flex: 1 }}1235 style={{ flex: 1 }}
1173 automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}1236 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
1174 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}1237 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
1175 showsVerticalScrollIndicator={false}1238 showsVerticalScrollIndicator={false}
1176 keyboardDismissMode="on-drag"1239 keyboardDismissMode="on-drag"
1177 keyboardShouldPersistTaps="handled"1240 keyboardShouldPersistTaps="handled"
1178 {...scrollGateHandlers}1241 {...scrollGateHandlers}
1242 recycleItems
1179 scrollEventThrottle={16}1243 scrollEventThrottle={16}
1180 contentContainerStyle={{1244 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".
1181 paddingBottom:1250 paddingBottom:
1182 Platform.OS === "ios"1251 Platform.OS === "ios"
1183 ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance1252 ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance
1184 : Math.max(insets.bottom, 16) + 88,1253 : Math.max(insets.bottom, 16) + 88,
1185 }}1254 }}
1255 scrollIndicatorInsets={
1256 Platform.OS === "ios"
1257 ? {
1258 bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance,
1259 top: 0,
1260 }
1261 : undefined
1262 }
1186 />1263 />
1187 </SwipeableScrollGateProvider>1264 </SwipeableScrollGateProvider>
1188 </View>1265 </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>
1242 </View>1266 </View>
1243 );1267 );
1244}1268}
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+10-1
...@@ -47,6 +47,7 @@ import {...@@ -47,6 +47,7 @@ import {
47} from "../keyboard/hardwareKeyboardCommands";47} from "../keyboard/hardwareKeyboardCommands";
48import { AndroidHomeFabLayout } from "../home/AndroidHomeFab";48import { AndroidHomeFabLayout } from "../home/AndroidHomeFab";
49import { HomeListOptionsProvider } from "../home/home-list-options";49import { HomeListOptionsProvider } from "../home/home-list-options";
50import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
50import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";51import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
51import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";52import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
52import { WorkspaceInspectorPane } from "./workspace-inspector-pane";53import { WorkspaceInspectorPane } from "./workspace-inspector-pane";
...@@ -219,6 +220,7 @@ function AdaptiveWorkspaceLayoutContent(...@@ -219,6 +220,7 @@ function AdaptiveWorkspaceLayoutContent(
219 },220 },
220) {221) {
221 const projectGroupingMode = props.projectGroupingMode;222 const projectGroupingMode = props.projectGroupingMode;
223 const { materialYouStyleLayoutActive } = useAppearancePreferences();
222 const { width, height } = useWindowDimensions();224 const { width, height } = useWindowDimensions();
223 const pathname = props.pathname;225 const pathname = props.pathname;
224 const navigation = useNavigation();226 const navigation = useNavigation();
...@@ -567,7 +569,14 @@ function AdaptiveWorkspaceLayoutContent(...@@ -567,7 +569,14 @@ function AdaptiveWorkspaceLayoutContent(
567 </View>569 </View>
568 </Animated.View>570 </Animated.View>
569 ) : null}571 ) : 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 >
571 <View580 <View
572 collapsable={false}581 collapsable={false}
573 style={582 style={
apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx+9-1
...@@ -1,11 +1,19 @@...@@ -1,11 +1,19 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1import { SymbolView } from "../../components/AppSymbol";2import { SymbolView } from "../../components/AppSymbol";
2import { Pressable, View } from "react-native";3import { Pressable, View } from "react-native";
34
4import { AppText as Text } from "../../components/AppText";5import { AppText as Text } from "../../components/AppText";
56
6export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {7export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {
8 const { materialYouStyleLayoutActive } = useAppearancePreferences();
7 return (9 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 >
9 <View className="max-w-[360px] items-center gap-3">17 <View className="max-w-[360px] items-center gap-3">
10 <SymbolView18 <SymbolView
11 name="sidebar.left"19 name="sidebar.left"
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+19-1
...@@ -10,7 +10,7 @@ import {...@@ -10,7 +10,7 @@ import {
10 useRef,10 useRef,
11 type ReactNode,11 type ReactNode,
12} from "react";12} from "react";
13import { AppState, Appearance, useColorScheme } from "react-native";13import { AppState, Appearance, Platform, useColorScheme } from "react-native";
1414
15import { useAtomSet, useAtomValue } from "@effect/atom-react";15import { useAtomSet, useAtomValue } from "@effect/atom-react";
16import { AsyncResult } from "effect/unstable/reactivity";16import { AsyncResult } from "effect/unstable/reactivity";
...@@ -51,6 +51,9 @@ interface AppearancePreferencesContextValue {...@@ -51,6 +51,9 @@ interface AppearancePreferencesContextValue {
51 readonly themeIds: MobileThemeIds;51 readonly themeIds: MobileThemeIds;
52 readonly themeMode: MobileThemeMode;52 readonly themeMode: MobileThemeMode;
53 readonly themeAppearance: MobileThemeAppearance;53 readonly themeAppearance: MobileThemeAppearance;
54 readonly materialYouStyleLayoutEnabled: boolean;
55 readonly materialYouStyleLayoutActive: boolean;
56 readonly setMaterialYouStyleLayoutEnabled: (value: boolean) => void;
54 readonly systemColorsAvailable: boolean;57 readonly systemColorsAvailable: boolean;
55 readonly systemColorsActive: boolean;58 readonly systemColorsActive: boolean;
56 readonly themeVariables: MobileThemeVariables;59 readonly themeVariables: MobileThemeVariables;
...@@ -94,6 +97,8 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -94,6 +97,8 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
94 [resolvedThemeIds.dark, resolvedThemeIds.light],97 [resolvedThemeIds.dark, resolvedThemeIds.light],
95 );98 );
96 const themeId = themeIds[themeAppearance];99 const themeId = themeIds[themeAppearance];
100 const materialYouStyleLayoutEnabled = storedPreferences?.materialYouStyleLayoutEnabled ?? false;
101 const materialYouStyleLayoutActive = Platform.OS === "android" && materialYouStyleLayoutEnabled;
97 const systemColorsActive = themeId === "material-you" && isSystemColorsAvailable;102 const systemColorsActive = themeId === "material-you" && isSystemColorsAvailable;
98 const [systemColorPalettes, setSystemColorPalettes] = useState(readSystemColorPalettes);103 const [systemColorPalettes, setSystemColorPalettes] = useState(readSystemColorPalettes);
99 useEffect(() => {104 useEffect(() => {
...@@ -242,6 +247,13 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -242,6 +247,13 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
242 [runtimeState, syncThemeRuntime, updateThemePreferences],247 [runtimeState, syncThemeRuntime, updateThemePreferences],
243 );248 );
244249
250 const setMaterialYouStyleLayoutEnabled = useCallback(
251 (value: boolean) => {
252 updatePreferences({ materialYouStyleLayoutEnabled: value });
253 },
254 [updatePreferences],
255 );
256
245 const setBaseFontSize = useCallback(257 const setBaseFontSize = useCallback(
246 (value: number) => {258 (value: number) => {
247 const current = appliedRuntimeStateRef.current ?? runtimeState;259 const current = appliedRuntimeStateRef.current ?? runtimeState;
...@@ -281,6 +293,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -281,6 +293,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
281 themeAppearance,293 themeAppearance,
282 systemColorsAvailable: isSystemColorsAvailable,294 systemColorsAvailable: isSystemColorsAvailable,
283 systemColorsActive,295 systemColorsActive,
296 materialYouStyleLayoutEnabled,
297 materialYouStyleLayoutActive,
298 setMaterialYouStyleLayoutEnabled,
284 themeVariables,299 themeVariables,
285 themeVariablesByAppearance,300 themeVariablesByAppearance,
286 systemColorPalettes,301 systemColorPalettes,
...@@ -300,6 +315,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -300,6 +315,9 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
300 themeMode,315 themeMode,
301 themeAppearance,316 themeAppearance,
302 systemColorsActive,317 systemColorsActive,
318 materialYouStyleLayoutEnabled,
319 materialYouStyleLayoutActive,
320 setMaterialYouStyleLayoutEnabled,
303 themeVariables,321 themeVariables,
304 themeVariablesByAppearance,322 themeVariablesByAppearance,
305 systemColorPalettes,323 systemColorPalettes,
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+18-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { memo, useId } from "react";1import { memo, useId } from "react";
2import { Pressable, View } from "react-native";2import { Platform, Pressable, View } from "react-native";
3import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";3import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
4import { ScopedTheme, ScopedVariables } from "uniwind";4import { ScopedTheme, ScopedVariables } from "uniwind";
55
...@@ -19,6 +19,9 @@ import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime";...@@ -19,6 +19,9 @@ import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime";
19import { cn } from "../../../../lib/cn";19import { cn } from "../../../../lib/cn";
20import { useAppearancePreferences } from "../AppearancePreferencesProvider";20import { useAppearancePreferences } from "../AppearancePreferencesProvider";
2121
22import { SettingsSection } from "../../components/SettingsSection";
23import { SettingsSwitchRow } from "../../components/SettingsSwitchRow";
24
22const APPEARANCE_MODES: ReadonlyArray<{25const APPEARANCE_MODES: ReadonlyArray<{
23 readonly id: MobileThemeMode;26 readonly id: MobileThemeMode;
24 readonly label: string;27 readonly label: string;
...@@ -290,11 +293,25 @@ export function ThemeAppearanceSection() {...@@ -290,11 +293,25 @@ export function ThemeAppearanceSection() {
290 setThemeMode,293 setThemeMode,
291 themeIds,294 themeIds,
292 themeMode,295 themeMode,
296 materialYouStyleLayoutEnabled,
297 setMaterialYouStyleLayoutEnabled,
293 systemColorsAvailable,298 systemColorsAvailable,
294 } = useAppearancePreferences();299 } = useAppearancePreferences();
295300
296 return (301 return (
297 <View className="gap-6">302 <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}
298 <View className="gap-2">315 <View className="gap-2">
299 <SectionLabel>Color scheme</SectionLabel>316 <SectionLabel>Color scheme</SectionLabel>
300 <View accessibilityRole="radiogroup" className="flex-row gap-2">317 <View accessibilityRole="radiogroup" className="flex-row gap-2">
apps/mobile/src/features/threads/ThreadComposer.tsx+21-3
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1import { useAtomValue } from "@effect/atom-react";2import { useAtomValue } from "@effect/atom-react";
2import type {3import type {
3 EnvironmentId,4 EnvironmentId,
...@@ -170,6 +171,7 @@ export function ComposerSurface(props: {...@@ -170,6 +171,7 @@ export function ComposerSurface(props: {
170 /** Morphs between the compact and expanded composer layouts. */171 /** Morphs between the compact and expanded composer layouts. */
171 readonly animateLayout?: boolean;172 readonly animateLayout?: boolean;
172}) {173}) {
174 const { materialYouStyleLayoutActive } = useAppearancePreferences();
173 const targetBorderRadius =175 const targetBorderRadius =
174 typeof props.style.borderRadius === "number" ? props.style.borderRadius : 0;176 typeof props.style.borderRadius === "number" ? props.style.borderRadius : 0;
175 const animatedBorderRadius = useSharedValue(targetBorderRadius);177 const animatedBorderRadius = useSharedValue(targetBorderRadius);
...@@ -191,7 +193,11 @@ export function ComposerSurface(props: {...@@ -191,7 +193,11 @@ export function ComposerSurface(props: {
191 // clip leaves the glass and content at their final height on the first frame.193 // clip leaves the glass and content at their final height on the first frame.
192 return (194 return (
193 <Animated.View195 <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 }
195 layout={layoutTransition}201 layout={layoutTransition}
196 style={[202 style={[
197 animatedShapeStyle,203 animatedShapeStyle,
...@@ -204,7 +210,11 @@ export function ComposerSurface(props: {...@@ -204,7 +210,11 @@ export function ComposerSurface(props: {
204 >210 >
205 <AnimatedGlassSurface211 <AnimatedGlassSurface
206 chrome="none"212 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 }
208 glassEffectStyle="regular"218 glassEffectStyle="regular"
209 // The composer is a passive material containing interactive controls.219 // The composer is a passive material containing interactive controls.
210 // Keep native glass out of the interactive content's layout path.220 // Keep native glass out of the interactive content's layout path.
...@@ -227,6 +237,9 @@ export function ComposerSurface(props: {...@@ -227,6 +237,9 @@ export function ComposerSurface(props: {
227}237}
228238
229export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {239export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
240 const { materialYouStyleLayoutActive, themeVariables: materialTheme } =
241 useAppearancePreferences();
242 const composerPanel = materialTheme["--color-composer-panel"];
230 const navigation = useNavigation();243 const navigation = useNavigation();
231 const foregroundColor = useUniwindTheme()["--color-foreground"];244 const foregroundColor = useUniwindTheme()["--color-foreground"];
232 const bodyText = useScaledTextRole("body");245 const bodyText = useScaledTextRole("body");
...@@ -543,13 +556,18 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -543,13 +556,18 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
543 style={{556 style={{
544 paddingTop: isExpanded ? 8 : 6,557 paddingTop: isExpanded ? 8 : 6,
545 paddingBottom: (props.bottomInset ?? 0) + (isExpanded ? 8 : 6),558 paddingBottom: (props.bottomInset ?? 0) + (isExpanded ? 8 : 6),
559 backgroundColor: materialYouStyleLayoutActive ? composerPanel : undefined,
546 }}560 }}
547 >561 >
548 {/* The backdrop gradient lives on a plain View: Reanimated's Animated.View562 {/* The backdrop gradient lives on a plain View: Reanimated's Animated.View
549 silently drops experimental_backgroundImage on Android, which left this563 silently drops experimental_backgroundImage on Android, which left this
550 strip fully transparent and the feed text legible through the composer. */}564 strip fully transparent and the feed text legible through the composer. */}
551 <View565 <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 }
553 pointerEvents="none"571 pointerEvents="none"
554 />572 />
555 <Animated.View573 <Animated.View
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+58-11
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1import { createThreadMovePlanner } from "./threadOrder";2import { createThreadMovePlanner } from "./threadOrder";
2import type {3import type {
3 EnvironmentProject,4 EnvironmentProject,
...@@ -145,6 +146,11 @@ function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {...@@ -145,6 +146,11 @@ function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {
145function ThreadNavigationSidebarPane(146function ThreadNavigationSidebarPane(
146 props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean },147 props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean },
147) {148) {
149 const { materialYouStyleLayoutActive, themeVariables: materialTheme } =
150 useAppearancePreferences();
151 const screenColor = materialTheme["--color-screen"];
152 const mutedColor = materialTheme["--color-foreground-muted"];
153
148 const insets = useSafeAreaInsets();154 const insets = useSafeAreaInsets();
149 const projects = useProjects();155 const projects = useProjects();
150 const threads = useThreadShells();156 const threads = useThreadShells();
...@@ -916,7 +922,7 @@ function ThreadNavigationSidebarPane(...@@ -916,7 +922,7 @@ function ThreadNavigationSidebarPane(
916 }),922 }),
917 )}923 )}
918 searchQuery={props.searchQuery}924 searchQuery={props.searchQuery}
919 pane="sidebar"925 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
920 selected={926 selected={
921 scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey927 scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey
922 }928 }
...@@ -956,7 +962,7 @@ function ThreadNavigationSidebarPane(...@@ -956,7 +962,7 @@ function ThreadNavigationSidebarPane(
956 disabled={!shelfPreferencesLoaded}962 disabled={!shelfPreferencesLoaded}
957 expanded={item.expanded}963 expanded={item.expanded}
958 onToggle={toggleSnoozedShelf}964 onToggle={toggleSnoozedShelf}
959 pane="sidebar"965 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
960 />966 />
961 );967 );
962 case "v2-settled-shelf":968 case "v2-settled-shelf":
...@@ -966,7 +972,7 @@ function ThreadNavigationSidebarPane(...@@ -966,7 +972,7 @@ function ThreadNavigationSidebarPane(
966 disabled={!shelfPreferencesLoaded}972 disabled={!shelfPreferencesLoaded}
967 expanded={item.expanded}973 expanded={item.expanded}
968 onToggle={toggleSettledShelf}974 onToggle={toggleSettledShelf}
969 pane="sidebar"975 pane={materialYouStyleLayoutActive ? "screen" : "sidebar"}
970 />976 />
971 );977 );
972 case "v2-show-more":978 case "v2-show-more":
...@@ -986,7 +992,7 @@ function ThreadNavigationSidebarPane(...@@ -986,7 +992,7 @@ function ThreadNavigationSidebarPane(
986 case "header":992 case "header":
987 return (993 return (
988 <ThreadListGroupHeader994 <ThreadListGroupHeader
989 variant="sidebar"995 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
990 collapsed={item.collapsed}996 collapsed={item.collapsed}
991 isFirst={item.isFirst}997 isFirst={item.isFirst}
992 groupKey={item.group.key}998 groupKey={item.group.key}
...@@ -1004,7 +1010,7 @@ function ThreadNavigationSidebarPane(...@@ -1004,7 +1010,7 @@ function ThreadNavigationSidebarPane(
1004 case "pending-task":1010 case "pending-task":
1005 return (1011 return (
1006 <PendingTaskListRow1012 <PendingTaskListRow
1007 variant="sidebar"1013 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
1008 pendingTask={item.pendingTask}1014 pendingTask={item.pendingTask}
1009 environmentLabel={1015 environmentLabel={
1010 savedConnectionsById[item.pendingTask.environmentId]?.environmentLabel ?? null1016 savedConnectionsById[item.pendingTask.environmentId]?.environmentLabel ?? null
...@@ -1053,7 +1059,7 @@ function ThreadNavigationSidebarPane(...@@ -1053,7 +1059,7 @@ function ThreadNavigationSidebarPane(
1053 case "show-more":1059 case "show-more":
1054 return (1060 return (
1055 <ThreadListShowMoreRow1061 <ThreadListShowMoreRow
1056 variant="sidebar"1062 variant={materialYouStyleLayoutActive ? "compact" : "sidebar"}
1057 hiddenCount={item.hiddenCount}1063 hiddenCount={item.hiddenCount}
1058 canShowLess={item.canShowLess}1064 canShowLess={item.canShowLess}
1059 groupKey={item.groupKey}1065 groupKey={item.groupKey}
...@@ -1063,6 +1069,7 @@ function ThreadNavigationSidebarPane(...@@ -1063,6 +1069,7 @@ function ThreadNavigationSidebarPane(
1063 }1069 }
1064 },1070 },
1065 [1071 [
1072 materialYouStyleLayoutActive,
1066 archiveThread,1073 archiveThread,
1067 activeReorderEnvironmentIds,1074 activeReorderEnvironmentIds,
1068 threadMovePlanners,1075 threadMovePlanners,
...@@ -1219,6 +1226,7 @@ function ThreadNavigationSidebarPane(...@@ -1219,6 +1226,7 @@ function ThreadNavigationSidebarPane(
1219 }1226 }
1220 contentContainerStyle={[1227 contentContainerStyle={[
1221 styles.threadListContent,1228 styles.threadListContent,
1229 materialYouStyleLayoutActive ? { paddingHorizontal: 0 } : null,
1222 {1230 {
1223 paddingBottom: Math.max(insets.bottom, 16) + 16,1231 paddingBottom: Math.max(insets.bottom, 16) + 16,
1224 paddingTop: 6,1232 paddingTop: 6,
...@@ -1246,7 +1254,21 @@ function ThreadNavigationSidebarPane(...@@ -1246,7 +1254,21 @@ function ThreadNavigationSidebarPane(
1246 className="flex-1 border-r border-border bg-drawer"1254 className="flex-1 border-r border-border bg-drawer"
1247 style={{ width: props.width }}1255 style={{ width: props.width }}
1248 >1256 >
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 >
1250 <SwipeableScrollGateProvider enabled={swipeEnabled}>1272 <SwipeableScrollGateProvider enabled={swipeEnabled}>
1251 <GestureDetector gesture={sidebarScrollGesture}>1273 <GestureDetector gesture={sidebarScrollGesture}>
1252 <LegendList1274 <LegendList
...@@ -1260,12 +1282,13 @@ function ThreadNavigationSidebarPane(...@@ -1260,12 +1282,13 @@ function ThreadNavigationSidebarPane(
1260 renderItem={renderListItem}1282 renderItem={renderListItem}
1261 contentContainerStyle={[1283 contentContainerStyle={[
1262 styles.threadListContent,1284 styles.threadListContent,
1285 materialYouStyleLayoutActive ? { paddingHorizontal: 0 } : null,
1263 {1286 {
1264 paddingBottom:1287 paddingBottom:
1265 Platform.OS === "android"1288 Platform.OS === "android"
1266 ? Math.max(insets.bottom, 16) + 88 - insets.bottom1289 ? Math.max(insets.bottom, 16) + 88 - insets.bottom
1267 : 16 + insets.bottom,1290 : 16 + insets.bottom,
1268 paddingTop: topListInset,1291 paddingTop: materialYouStyleLayoutActive ? 6 : topListInset,
1269 },1292 },
1270 ]}1293 ]}
1271 keyboardDismissMode="on-drag"1294 keyboardDismissMode="on-drag"
...@@ -1310,7 +1333,13 @@ function ThreadNavigationSidebarPane(...@@ -1310,7 +1333,13 @@ function ThreadNavigationSidebarPane(
1310 </View>1333 </View>
1311 </View>1334 </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 >
1314 <SymbolView1343 <SymbolView
1315 name="magnifyingglass"1344 name="magnifyingglass"
1316 size={15}1345 size={15}
...@@ -1322,14 +1351,32 @@ function ThreadNavigationSidebarPane(...@@ -1322,14 +1351,32 @@ function ThreadNavigationSidebarPane(
1322 accessibilityLabel="Search threads"1351 accessibilityLabel="Search threads"
1323 autoCapitalize="none"1352 autoCapitalize="none"
1324 autoCorrect={false}1353 autoCorrect={false}
1325 clearButtonMode="while-editing"1354 clearButtonMode={materialYouStyleLayoutActive ? "never" : "while-editing"}
1326 onChangeText={props.onSearchQueryChange}1355 onChangeText={props.onSearchQueryChange}
1327 placeholder="Search"1356 placeholder="Search"
1328 placeholderTextColorClassName={"accent-placeholder"}1357 placeholderTextColorClassName={"accent-placeholder"}
1329 returnKeyType="search"1358 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 }
1331 value={props.searchQuery}1364 value={props.searchQuery}
1332 />1365 />
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}
1333 </View>1380 </View>
1334 </View>1381 </View>
1335 </View>1382 </View>
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+21-1
...@@ -76,6 +76,7 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt...@@ -76,6 +76,7 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt
76import { useThreadComposerState } from "../../state/use-thread-composer-state";76import { useThreadComposerState } from "../../state/use-thread-composer-state";
77import { threadEnvironment } from "../../state/threads";77import { threadEnvironment } from "../../state/threads";
78import { projectThreadContentPresentation } from "./threadContentPresentation";78import { projectThreadContentPresentation } from "./threadContentPresentation";
79import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
79import {80import {
80 useAdaptiveWorkspaceLayout,81 useAdaptiveWorkspaceLayout,
81 useAdaptiveWorkspacePaneRole,82 useAdaptiveWorkspacePaneRole,
...@@ -193,6 +194,8 @@ function ThreadRouteContent(...@@ -193,6 +194,8 @@ function ThreadRouteContent(
193 readonly selectedThreadDetailState: ReturnType<typeof useSelectedThreadDetailState>;194 readonly selectedThreadDetailState: ReturnType<typeof useSelectedThreadDetailState>;
194 },195 },
195) {196) {
197 const { materialYouStyleLayoutActive, themeVariables } = useAppearancePreferences();
198 const headerColor = themeVariables["--color-header"];
196 const {199 const {
197 fileInspector,200 fileInspector,
198 layout,201 layout,
...@@ -839,7 +842,19 @@ function ThreadRouteContent(...@@ -839,7 +842,19 @@ function ThreadRouteContent(
839842
840 <GitActionProgressOverlay progress={gitActionProgress} onDismiss={dismissGitActionResult} />843 <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 >
843 <ThreadDetailScreen858 <ThreadDetailScreen
844 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}859 selectedThread={selectedThreadWithDraftSettings ?? selectedThread}
845 contentPresentation={contentPresentation}860 contentPresentation={contentPresentation}
...@@ -931,6 +946,10 @@ function ThreadRouteContent(...@@ -931,6 +946,10 @@ function ThreadRouteContent(
931 ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems)946 ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems)
932 : undefined,947 : undefined,
933 unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined,948 unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined,
949 contentStyle:
950 Platform.OS === "android" && materialYouStyleLayoutActive
951 ? { backgroundColor: headerColor }
952 : undefined,
934 }}953 }}
935 />954 />
936955
...@@ -940,6 +959,7 @@ function ThreadRouteContent(...@@ -940,6 +959,7 @@ function ThreadRouteContent(
940 subtitle={headerSubtitle}959 subtitle={headerSubtitle}
941 onBack={layout.usesSplitView ? undefined : () => navigation.goBack()}960 onBack={layout.usesSplitView ? undefined : () => navigation.goBack()}
942 actions={androidHeaderActions}961 actions={androidHeaderActions}
962 hideBottomBorder={materialYouStyleLayoutActive}
943 />963 />
944 ) : null}964 ) : null}
945965
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+4
...@@ -52,6 +52,7 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";...@@ -52,6 +52,7 @@ import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
52import { serverEnvironment } from "../../state/server";52import { serverEnvironment } from "../../state/server";
53import { useAtomCommand } from "../../state/use-atom-command";53import { useAtomCommand } from "../../state/use-atom-command";
54import { useNewTaskFlow } from "./new-task-flow-provider";54import { useNewTaskFlow } from "./new-task-flow-provider";
55import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
55import {56import {
56 createProviderCatalogRefreshRunner,57 createProviderCatalogRefreshRunner,
57 providerCatalogRefreshError,58 providerCatalogRefreshError,
...@@ -104,6 +105,8 @@ function ModelRow(props: {...@@ -104,6 +105,8 @@ function ModelRow(props: {
104 readonly isFirst: boolean;105 readonly isFirst: boolean;
105 readonly isLast: boolean;106 readonly isLast: boolean;
106}) {107}) {
108 const { materialYouStyleLayoutActive } = useAppearancePreferences();
109 const selectedMaterialRow = materialYouStyleLayoutActive && props.selected;
107 return (110 return (
108 <Pressable111 <Pressable
109 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}112 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
...@@ -116,6 +119,7 @@ function ModelRow(props: {...@@ -116,6 +119,7 @@ function ModelRow(props: {
116 onPress={props.onPress}119 onPress={props.onPress}
117 className={cn(120 className={cn(
118 "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4 py-2 active:bg-subtle",121 "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",
119 props.isFirst && "rounded-t-2xl",123 props.isFirst && "rounded-t-2xl",
120 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",124 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
121 )}125 )}
apps/mobile/src/features/threads/thread-list-items.tsx+90-22
...@@ -463,9 +463,10 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -463,9 +463,10 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
463 >["simultaneousWithExternalGesture"];463 >["simultaneousWithExternalGesture"];
464}) {464}) {
465 const { width: windowWidth } = useWindowDimensions();465 const { width: windowWidth } = useWindowDimensions();
466 const { themeAppearance: colorScheme } = useAppearancePreferences();466 const { themeAppearance: colorScheme, materialYouStyleLayoutActive } = useAppearancePreferences();
467 const compact = props.variant === "compact";467 const compact = props.variant === "compact";
468 const selected = props.selected === true;468 const selected = props.selected === true;
469 const visuallySelected = selected && (!compact || materialYouStyleLayoutActive);
469 // Recycling-safe: resets when the list container is reused for another470 // Recycling-safe: resets when the list container is reused for another
470 // thread, so a hover highlight can't leak across rows.471 // thread, so a hover highlight can't leak across rows.
471 const [hovered, setHovered] = useRecyclingState(false);472 const [hovered, setHovered] = useRecyclingState(false);
...@@ -475,6 +476,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -475,6 +476,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
475 const drawerColor = theme["--color-drawer"];476 const drawerColor = theme["--color-drawer"];
476 const pressedBackgroundColor = theme["--color-subtle"];477 const pressedBackgroundColor = theme["--color-subtle"];
477 const selectedBackgroundColor = theme["--color-user-bubble"];478 const selectedBackgroundColor = theme["--color-user-bubble"];
479 const materialSelectedBackgroundColor = theme["--color-thread-selected"];
480 const materialSelectedForegroundColor = theme["--color-thread-selected-foreground"];
478 const selectedForegroundColor = theme["--color-user-bubble-foreground"];481 const selectedForegroundColor = theme["--color-user-bubble-foreground"];
479482
480 const {483 const {
...@@ -502,15 +505,25 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -502,15 +505,25 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
502 );505 );
503506
504 const backgroundColor = compact ? screenColor : drawerColor;507 const backgroundColor = compact ? screenColor : drawerColor;
505 const effectivePressedBackground = selected508 const effectiveSelectedBackground = materialYouStyleLayoutActive
506 ? themeColorWithAlpha(String(selectedForegroundColor), 0.16)509 ? materialSelectedBackgroundColor
510 : selectedBackgroundColor;
511 const effectiveSelectedForeground = materialYouStyleLayoutActive
512 ? materialSelectedForegroundColor
513 : selectedForegroundColor;
514 const effectivePressedBackground = visuallySelected
515 ? themeColorWithAlpha(String(effectiveSelectedForeground), 0.16)
507 : pressedBackgroundColor;516 : pressedBackgroundColor;
508 const effectiveStatus =517 const effectiveStatus =
509 selected && status518 visuallySelected && status
510 ? {519 ? {
511 ...status,520 ...status,
512 pillClassName: "bg-user-bubble-foreground/20",521 pillClassName: materialYouStyleLayoutActive
513 textClassName: "text-user-bubble-foreground",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",
514 }527 }
515 : status;528 : status;
516529
...@@ -581,7 +594,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -581,7 +594,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
581 compact594 compact
582 ? "accent-icon-subtle"595 ? "accent-icon-subtle"
583 : selected596 : selected
584 ? "accent-user-bubble-foreground-muted"597 ? materialYouStyleLayoutActive
598 ? "accent-thread-selected-foreground-muted"
599 : "accent-user-bubble-foreground-muted"
585 : "accent-foreground-muted"600 : "accent-foreground-muted"
586 }601 }
587 />602 />
...@@ -589,9 +604,12 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -589,9 +604,12 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
589 <Text604 <Text
590 className={cn(605 className={cn(
591 "shrink",606 "shrink",
592 compact ? "text-sm text-foreground-muted" : "text-xs",607 compact ? "text-sm" : "text-xs",
593 !compact &&608 visuallySelected
594 (selected ? "text-user-bubble-foreground-muted" : "text-foreground-muted"),609 ? materialYouStyleLayoutActive
610 ? "text-thread-selected-foreground-muted"
611 : "text-user-bubble-foreground-muted"
612 : "text-foreground-muted",
595 )}613 )}
596 numberOfLines={1}614 numberOfLines={1}
597 >615 >
...@@ -604,12 +622,18 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -604,12 +622,18 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
604 <PullRequestIcon622 <PullRequestIcon
605 size={compact ? 13 : 11}623 size={compact ? 13 : 11}
606 color={624 color={
607 selected ? String(selectedForegroundColor) : pullRequestTintColor(pr, colorScheme)625 visuallySelected
626 ? String(effectiveSelectedForeground)
627 : pullRequestTintColor(pr, colorScheme)
608 }628 }
609 />629 />
610 <Text630 <Text
611 className={`${compact ? "text-sm" : "text-xs"} font-t3-medium ${631 className={`${compact ? "text-sm" : "text-xs"} font-t3-medium ${
612 selected ? "text-user-bubble-foreground" : pr.textClassName632 visuallySelected
633 ? materialYouStyleLayoutActive
634 ? "text-thread-selected-foreground"
635 : "text-user-bubble-foreground"
636 : pr.textClassName
613 }`}637 }`}
614 >638 >
615 {pr.label}639 {pr.label}
...@@ -626,6 +650,14 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -626,6 +650,14 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
626 accessibilityLabel={threadAccessibilityLabel}650 accessibilityLabel={threadAccessibilityLabel}
627 accessibilityRole="button"651 accessibilityRole="button"
628 className="bg-screen active:opacity-70"652 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 }
629 onPress={() => {661 onPress={() => {
630 close();662 close();
631 onSelectThread(thread);663 onSelectThread(thread);
...@@ -634,13 +666,36 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -634,13 +666,36 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
634 <View className="pr-[18px] pt-[10px]" style={{ paddingLeft: THREAD_LIST_COMPACT_INSET }}>666 <View className="pr-[18px] pt-[10px]" style={{ paddingLeft: THREAD_LIST_COMPACT_INSET }}>
635 <View className={cn("gap-[3px] pb-[10px]", !props.isLast && "border-b border-separator")}>667 <View className={cn("gap-[3px] pb-[10px]", !props.isLast && "border-b border-separator")}>
636 <View className="flex-row items-center justify-between gap-2">668 <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 >
638 {thread.title}680 {thread.title}
639 </Text>681 </Text>
640 <View className="flex-row items-center gap-2">682 <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}
642 {statusPill}686 {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>
644 <SymbolView699 <SymbolView
645 name="chevron.right"700 name="chevron.right"
646 size={13}701 size={13}
...@@ -654,6 +709,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -654,6 +709,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
654 compact709 compact
655 match={props.searchMatch}710 match={props.searchMatch}
656 query={props.searchQuery ?? ""}711 query={props.searchQuery ?? ""}
712 selected={visuallySelected}
657 />713 />
658 ) : null}714 ) : null}
659 {subtitleRow}715 {subtitleRow}
...@@ -673,8 +729,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -673,8 +729,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
673 onSelectThread(thread);729 onSelectThread(thread);
674 }}730 }}
675 style={({ pressed }) => ({731 style={({ pressed }) => ({
676 backgroundColor: selected732 backgroundColor: visuallySelected
677 ? selectedBackgroundColor733 ? effectiveSelectedBackground
678 : pressed || hovered734 : pressed || hovered
679 ? effectivePressedBackground735 ? effectivePressedBackground
680 : backgroundColor,736 : backgroundColor,
...@@ -691,19 +747,29 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -691,19 +747,29 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
691 <Text747 <Text
692 className={cn(748 className={cn(
693 "flex-1 text-base font-t3-medium",749 "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",
695 )}755 )}
696 numberOfLines={1}756 numberOfLines={1}
697 >757 >
698 {thread.title}758 {thread.title}
699 </Text>759 </Text>
700 <View className="flex-row items-center gap-2">760 <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}
702 {statusPill}764 {statusPill}
703 <Text765 <Text
704 className={cn(766 className={cn(
705 "text-xs tabular-nums",767 "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",
707 )}773 )}
708 numberOfLines={1}774 numberOfLines={1}
709 >775 >
...@@ -715,7 +781,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -715,7 +781,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
715 <ThreadSearchMatchExcerpt781 <ThreadSearchMatchExcerpt
716 match={props.searchMatch}782 match={props.searchMatch}
717 query={props.searchQuery ?? ""}783 query={props.searchQuery ?? ""}
718 selected={selected}784 selected={visuallySelected}
719 />785 />
720 ) : null}786 ) : null}
721 {subtitleRow}787 {subtitleRow}
...@@ -728,7 +794,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -728,7 +794,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
728 threadKey={`${thread.environmentId}:${thread.id}`}794 threadKey={`${thread.environmentId}:${thread.id}`}
729 backgroundColor={backgroundColor}795 backgroundColor={backgroundColor}
730 containerStyle={796 containerStyle={
731 compact ? undefined : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" }797 compact && !materialYouStyleLayoutActive
798 ? undefined
799 : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" }
732 }800 }
733 enableTrackpadSwipe801 enableTrackpadSwipe
734 fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32}802 fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32}
apps/mobile/src/features/threads/thread-list-v2-items.tsx+69-20
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1import type {2import type {
2 EnvironmentProject,3 EnvironmentProject,
3 EnvironmentThreadShell,4 EnvironmentThreadShell,
...@@ -429,11 +430,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -429,11 +430,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
429430
430 const pr = useThreadPr(thread);431 const pr = useThreadPr(thread);
431432
433 const { materialYouStyleLayoutActive } = useAppearancePreferences();
432 const theme = useUniwindTheme();434 const theme = useUniwindTheme();
433 const screenColor = theme["--color-screen"];435 const screenColor = theme["--color-screen"];
434 const drawerColor = theme["--color-drawer"];436 const drawerColor = theme["--color-drawer"];
435 const pressedBackgroundColor = theme["--color-subtle"];437 const pressedBackgroundColor = theme["--color-subtle"];
436 const selectedBackgroundColor = theme["--color-user-bubble"];438 const selectedBackgroundColor =
439 theme[materialYouStyleLayoutActive ? "--color-thread-selected" : "--color-user-bubble"];
437 const sidebarPane = props.pane === "sidebar";440 const sidebarPane = props.pane === "sidebar";
438 const selected = props.selected === true;441 const selected = props.selected === true;
439 // The provider badge's border blends into the row's own surface, which442 // The provider badge's border blends into the row's own surface, which
...@@ -720,7 +723,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -720,7 +723,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
720 <Text723 <Text
721 className={cn(724 className={cn(
722 "flex-1 text-sm font-t3-medium",725 "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",
724 )}731 )}
725 numberOfLines={1}732 numberOfLines={1}
726 >733 >
...@@ -739,7 +746,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -739,7 +746,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
739 className={cn(746 className={cn(
740 "text-xs tabular-nums",747 "text-xs tabular-nums",
741 selected748 selected
742 ? "text-user-bubble-foreground"749 ? materialYouStyleLayoutActive
750 ? "text-thread-selected-foreground"
751 : "text-user-bubble-foreground"
743 : (statusLabel?.className ?? "text-foreground-tertiary"),752 : (statusLabel?.className ?? "text-foreground-tertiary"),
744 )}753 )}
745 >754 >
...@@ -749,7 +758,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -749,7 +758,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
749 <Text758 <Text
750 className={cn(759 className={cn(
751 "mt-1 text-base font-t3-medium",760 "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",
753 )}766 )}
754 numberOfLines={2}767 numberOfLines={2}
755 >768 >
...@@ -769,7 +782,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -769,7 +782,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
769 <Text782 <Text
770 className={cn(783 className={cn(
771 "flex-1 text-xs",784 "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",
773 )}790 )}
774 numberOfLines={1}791 numberOfLines={1}
775 >792 >
...@@ -786,7 +803,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -786,7 +803,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
786 <Text803 <Text
787 className={cn(804 className={cn(
788 "shrink text-xs",805 "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",
790 )}811 )}
791 numberOfLines={1}812 numberOfLines={1}
792 >813 >
...@@ -794,7 +815,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -794,7 +815,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
794 <Text815 <Text
795 className={cn(816 className={cn(
796 "text-xs",817 "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",
798 )}823 )}
799 style={{ fontFamily: MONO_FONT }}824 style={{ fontFamily: MONO_FONT }}
800 >825 >
...@@ -806,7 +831,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -806,7 +831,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
806 <Text831 <Text
807 className={cn(832 className={cn(
808 "text-xs",833 "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",
810 )}839 )}
811 >840 >
812 {props.environmentLabel}841 {props.environmentLabel}
...@@ -818,7 +847,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -818,7 +847,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
818 kind={props.environmentMachine}847 kind={props.environmentMachine}
819 size={11}848 size={11}
820 tintColorClassName={849 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"
822 }855 }
823 />856 />
824 ) : null}857 ) : null}
...@@ -829,7 +862,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -829,7 +862,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
829 {pr ? (862 {pr ? (
830 <Text863 <Text
831 accessibilityLabel={pr.accessibilityLabel}864 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 )}
833 style={{ fontFamily: MONO_FONT }}873 style={{ fontFamily: MONO_FONT }}
834 >874 >
835 #{pr.label}875 #{pr.label}
...@@ -863,16 +903,17 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -863,16 +903,17 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
863 onSelectThread(thread);903 onSelectThread(thread);
864 }}904 }}
865 style={905 style={
866 sidebarPane906 sidebarPane || materialYouStyleLayoutActive
867 ? ({ pressed }) => ({907 ? ({ pressed }) => ({
868 backgroundColor: selected908 backgroundColor: selected
869 ? selectedBackgroundColor909 ? selectedBackgroundColor
870 : pressed910 : pressed
871 ? pressedBackgroundColor911 ? pressedBackgroundColor
872 : drawerColor,912 : sidebarPane
913 ? drawerColor
914 : screenColor,
873 borderRadius: SIDEBAR_V2_ROW_RADIUS,915 borderRadius: SIDEBAR_V2_ROW_RADIUS,
874 paddingHorizontal: 12,916 ...(sidebarPane ? { paddingHorizontal: 12, paddingVertical: 10 } : null),
875 paddingVertical: 10,
876 })917 })
877 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })918 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })
878 }919 }
...@@ -884,7 +925,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -884,7 +925,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
884 labels and text hierarchy carry state, an inset hairline925 labels and text hierarchy carry state, an inset hairline
885 separates rows. The opaque screen background stays so swipe926 separates rows. The opaque screen background stays so swipe
886 actions reveal behind the row. */927 actions reveal behind the row. */
887 <View className="bg-screen">928 <View className={materialYouStyleLayoutActive ? undefined : "bg-screen"}>
888 <View className="px-5 py-2.5">{cardContent}</View>929 <View className="px-5 py-2.5">{cardContent}</View>
889 {props.showTrailingDivider !== false ? (930 {props.showTrailingDivider !== false ? (
890 <View className="ml-5 h-px bg-border-subtle" />931 <View className="ml-5 h-px bg-border-subtle" />
...@@ -900,19 +941,21 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -900,19 +941,21 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
900 }941 }
901 accessibilityRole="button"942 accessibilityRole="button"
902 accessibilityState={{ selected }}943 accessibilityState={{ selected }}
903 className={sidebarPane ? undefined : "bg-screen"}944 className={sidebarPane || materialYouStyleLayoutActive ? undefined : "bg-screen"}
904 onPress={() => {945 onPress={() => {
905 close();946 close();
906 onSelectThread(thread);947 onSelectThread(thread);
907 }}948 }}
908 style={949 style={
909 sidebarPane950 sidebarPane || materialYouStyleLayoutActive
910 ? ({ pressed }) => ({951 ? ({ pressed }) => ({
911 backgroundColor: selected952 backgroundColor: selected
912 ? selectedBackgroundColor953 ? selectedBackgroundColor
913 : pressed954 : pressed
914 ? pressedBackgroundColor955 ? pressedBackgroundColor
915 : drawerColor,956 : sidebarPane
957 ? drawerColor
958 : screenColor,
916 borderRadius: SIDEBAR_V2_ROW_RADIUS,959 borderRadius: SIDEBAR_V2_ROW_RADIUS,
917 })960 })
918 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })961 : ({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })
...@@ -940,7 +983,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -940,7 +983,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
940 <Text983 <Text
941 className={cn(984 className={cn(
942 "text-base",985 "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",
944 )}991 )}
945 numberOfLines={1}992 numberOfLines={1}
946 >993 >
...@@ -959,7 +1006,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -959,7 +1006,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
959 className={cn(1006 className={cn(
960 "text-sm tabular-nums",1007 "text-sm tabular-nums",
961 selected1008 selected
962 ? "text-user-bubble-foreground-muted"1009 ? materialYouStyleLayoutActive
1010 ? "text-thread-selected-foreground-muted"
1011 : "text-user-bubble-foreground-muted"
963 : snoozedRow1012 : snoozedRow
964 ? "text-foreground-secondary"1013 ? "text-foreground-secondary"
965 : "text-foreground-tertiary",1014 : "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...@@ -2,6 +2,7 @@ import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state
22
3import { AppText as Text } from "../../components/AppText";3import { AppText as Text } from "../../components/AppText";
4import { cn } from "../../lib/cn";4import { cn } from "../../lib/cn";
5import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
56
6function foldAsciiCase(value: string): string {7function foldAsciiCase(value: string): string {
7 return value.replace(/[A-Z]/g, (character) => character.toLowerCase());8 return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
...@@ -50,12 +51,17 @@ export function ThreadSearchMatchExcerpt(props: {...@@ -50,12 +51,17 @@ export function ThreadSearchMatchExcerpt(props: {
50 readonly compact?: boolean;51 readonly compact?: boolean;
51}) {52}) {
52 const isUser = props.match.source === "user";53 const isUser = props.match.source === "user";
54 const { materialYouStyleLayoutActive } = useAppearancePreferences();
53 const parts = splitHighlightParts(props.match.snippet, props.query);55 const parts = splitHighlightParts(props.match.snippet, props.query);
54 return (56 return (
55 <Text57 <Text
56 className={cn(58 className={cn(
57 props.compact ? "text-sm" : "text-xs",59 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",
59 )}65 )}
60 numberOfLines={1}66 numberOfLines={1}
61 >67 >
...@@ -63,7 +69,9 @@ export function ThreadSearchMatchExcerpt(props: {...@@ -63,7 +69,9 @@ export function ThreadSearchMatchExcerpt(props: {
63 className={cn(69 className={cn(
64 props.compact ? "text-sm font-t3-medium" : "text-xs font-t3-medium",70 props.compact ? "text-sm font-t3-medium" : "text-xs font-t3-medium",
65 props.selected71 props.selected
66 ? "text-user-bubble-foreground"72 ? materialYouStyleLayoutActive
73 ? "text-thread-selected-foreground"
74 : "text-user-bubble-foreground"
67 : isUser75 : isUser
68 ? "text-foreground-secondary"76 ? "text-foreground-secondary"
69 : "text-adaptive-emerald-600-400",77 : "text-adaptive-emerald-600-400",
...@@ -77,7 +85,9 @@ export function ThreadSearchMatchExcerpt(props: {...@@ -77,7 +85,9 @@ export function ThreadSearchMatchExcerpt(props: {
77 props.compact ? "text-sm" : "text-xs",85 props.compact ? "text-sm" : "text-xs",
78 part.highlighted && "font-t3-bold",86 part.highlighted && "font-t3-bold",
79 props.selected87 props.selected
80 ? "text-user-bubble-foreground"88 ? materialYouStyleLayoutActive
89 ? "text-thread-selected-foreground"
90 : "text-user-bubble-foreground"
81 : part.highlighted91 : part.highlighted
82 ? "text-foreground"92 ? "text-foreground"
83 : "text-foreground-muted",93 : "text-foreground-muted",
apps/mobile/src/lib/materialYouTheme.test.ts+2
...@@ -39,6 +39,8 @@ describe("Material You system colors", () => {...@@ -39,6 +39,8 @@ describe("Material You system colors", () => {
3939
40 expect(base).toEqual(snapshot);40 expect(base).toEqual(snapshot);
41 expect(variables["--color-screen"]).toBe(palette.surface);41 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);
42 expect(variables["--color-header"]).toBe(palette.surfaceContainerHigh);44 expect(variables["--color-header"]).toBe(palette.surfaceContainerHigh);
43 expect(variables["--color-primary"]).toBe(palette.primary);45 expect(variables["--color-primary"]).toBe(palette.primary);
44 expect(variables["--color-placeholder"]).toBe("#1C1B1F9E");46 expect(variables["--color-placeholder"]).toBe("#1C1B1F9E");
apps/mobile/src/lib/materialYouTheme.ts+7
...@@ -38,6 +38,13 @@ export function materialYouPaletteToMobileThemeVariables(...@@ -38,6 +38,13 @@ export function materialYouPaletteToMobileThemeVariables(
38 "--color-card": palette.surfaceContainer,38 "--color-card": palette.surfaceContainer,
39 "--color-card-alt": palette.surfaceContainerHigh,39 "--color-card-alt": palette.surfaceContainerHigh,
40 "--color-card-translucent": withAlpha(palette.surfaceContainer, 0.8),40 "--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),
41 "--color-foreground": palette.onSurface,48 "--color-foreground": palette.onSurface,
42 "--color-foreground-secondary": palette.onSurfaceVariant,49 "--color-foreground-secondary": palette.onSurfaceVariant,
43 "--color-foreground-muted": palette.onSurfaceVariant,50 "--color-foreground-muted": palette.onSurfaceVariant,
apps/mobile/src/lib/mobileTheme.test.ts+1-1
...@@ -153,7 +153,7 @@ describe("mobile themes", () => {...@@ -153,7 +153,7 @@ describe("mobile themes", () => {
153153
154 it("maps semantic palette roles onto every mobile color variable", () => {154 it("maps semantic palette roles onto every mobile color variable", () => {
155 const variables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");155 const variables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
156 expect(Object.keys(variables)).toHaveLength(68);156 expect(Object.keys(variables)).toHaveLength(75);
157 expect(variables["--color-sheet-solid"]).toBe(157 expect(variables["--color-sheet-solid"]).toBe(
158 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.chrome),158 themeColorToNativeColor(BUILT_IN_THEMES[0].colors.chrome),
159 );159 );
apps/mobile/src/lib/mobileTheme.ts+10
...@@ -218,6 +218,16 @@ export function createMobileThemeVariables(...@@ -218,6 +218,16 @@ export function createMobileThemeVariables(
218 "--color-card": c.surfaceRaised,218 "--color-card": c.surfaceRaised,
219 "--color-card-alt": c.surface,219 "--color-card-alt": c.surface,
220 "--color-card-translucent": withAlpha(c.surfaceRaised, 0.8),220 "--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),
221 "--color-foreground": c.text,231 "--color-foreground": c.text,
222 "--color-foreground-secondary": c.textMuted,232 "--color-foreground-secondary": c.textMuted,
223 "--color-foreground-muted": c.mutedForeground,233 "--color-foreground-muted": c.mutedForeground,
apps/mobile/src/lib/storage.test.ts+17
...@@ -204,6 +204,23 @@ describe("mobile connection storage", () => {...@@ -204,6 +204,23 @@ describe("mobile connection storage", () => {
204 await expect(loadPreferences()).resolves.toEqual({ ...themes, lightThemeId: "t3-chat" });204 await expect(loadPreferences()).resolves.toEqual({ ...themes, lightThemeId: "t3-chat" });
205 });205 });
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
207 it("drops the removed theme transition preference", async () => {224 it("drops the removed theme transition preference", async () => {
208 mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10);225 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 {...@@ -21,6 +21,7 @@ export interface Preferences {
21 readonly lightThemeId?: MobileThemeId;21 readonly lightThemeId?: MobileThemeId;
22 readonly darkThemeId?: MobileThemeId;22 readonly darkThemeId?: MobileThemeId;
23 readonly themeMode?: MobileThemeMode;23 readonly themeMode?: MobileThemeMode;
24 readonly materialYouStyleLayoutEnabled?: boolean;
24 readonly baseFontSize?: number;25 readonly baseFontSize?: number;
25 readonly terminalFontSize?: number | null;26 readonly terminalFontSize?: number | null;
26 readonly markdownFontSize?: number;27 readonly markdownFontSize?: number;
...@@ -90,6 +91,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {...@@ -90,6 +91,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
90 lightThemeId?: MobileThemeId;91 lightThemeId?: MobileThemeId;
91 darkThemeId?: MobileThemeId;92 darkThemeId?: MobileThemeId;
92 themeMode?: MobileThemeMode;93 themeMode?: MobileThemeMode;
94 materialYouStyleLayoutEnabled?: boolean;
93 baseFontSize?: number;95 baseFontSize?: number;
94 terminalFontSize?: number | null;96 terminalFontSize?: number | null;
95 markdownFontSize?: number;97 markdownFontSize?: number;
...@@ -133,6 +135,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {...@@ -133,6 +135,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
133 ) {135 ) {
134 preferences.themeMode = parsed.themeMode;136 preferences.themeMode = parsed.themeMode;
135 }137 }
138 if (typeof parsed.materialYouStyleLayoutEnabled === "boolean") {
139 preferences.materialYouStyleLayoutEnabled = parsed.materialYouStyleLayoutEnabled;
140 }
136 if (typeof parsed.baseFontSize === "number") preferences.baseFontSize = parsed.baseFontSize;141 if (typeof parsed.baseFontSize === "number") preferences.baseFontSize = parsed.baseFontSize;
137 if (typeof parsed.terminalFontSize === "number" || parsed.terminalFontSize === null) {142 if (typeof parsed.terminalFontSize === "number" || parsed.terminalFontSize === null) {
138 preferences.terminalFontSize = parsed.terminalFontSize;143 preferences.terminalFontSize = parsed.terminalFontSize;
docs/user/appearance.md+2
...@@ -10,6 +10,8 @@ themes or defaults....@@ -10,6 +10,8 @@ themes or defaults.
10On Android 12 or newer, choose the **Material You** theme in Appearance to use colors from10On Android 12 or newer, choose the **Material You** theme in Appearance to use colors from
11your wallpaper. Selecting another theme replaces those colors. Like other themes, Material You11your wallpaper. Selecting another theme replaces those colors. Like other themes, Material You
12can be selected separately for light and dark appearances.12can 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
14## Motion16## Motion
1517