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