| author | |
| committer | |
| log | 2d2efff28c4fc47745538570fcb7dd07272e3e4b |
| tree | 3476de409e230919ecd0d972a77f0bac6801c671 |
| parent | 9eba1252c1f8264b6951068d2e2396363412299c |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 73 insertions(+), 175 deletions(-)
apps/mobile/src/components/CompactBrandTitle.tsx+4-1| ... | @@ -33,6 +33,7 @@ export function brandTitleOffset(nativeLeadingItem: boolean): number { | ... | @@ -33,6 +33,7 @@ export function brandTitleOffset(nativeLeadingItem: boolean): number { |
| 33 | */ | 33 | */ |
| 34 | export function CompactBrandTitle( | 34 | export function CompactBrandTitle( |
| 35 | props: { | 35 | props: { |
| 36 | readonly allowFontScaling?: boolean; | ||
| 36 | readonly nativeLeadingItem?: boolean; | 37 | readonly nativeLeadingItem?: boolean; |
| 37 | } = {}, | 38 | } = {}, |
| 38 | ) { | 39 | ) { |
| ... | @@ -57,6 +58,7 @@ export function CompactBrandTitle( | ... | @@ -57,6 +58,7 @@ export function CompactBrandTitle( |
| 57 | > | 58 | > |
| 58 | <T3Wordmark color={iconColor} height={15} /> | 59 | <T3Wordmark color={iconColor} height={15} /> |
| 59 | <Text | 60 | <Text |
| 61 | allowFontScaling={props.allowFontScaling} | ||
| 60 | style={{ | 62 | style={{ |
| 61 | color: mutedColor, | 63 | color: mutedColor, |
| 62 | fontFamily: "DMSans-Medium", | 64 | fontFamily: "DMSans-Medium", |
| ... | @@ -75,6 +77,7 @@ export function CompactBrandTitle( | ... | @@ -75,6 +77,7 @@ export function CompactBrandTitle( |
| 75 | }} | 77 | }} |
| 76 | > | 78 | > |
| 77 | <Text | 79 | <Text |
| 80 | allowFontScaling={props.allowFontScaling} | ||
| 78 | style={{ | 81 | style={{ |
| 79 | color: mutedColor, | 82 | color: mutedColor, |
| 80 | fontFamily: "DMSans-Bold", | 83 | fontFamily: "DMSans-Bold", |
| ... | @@ -91,7 +94,7 @@ export function CompactBrandTitle( | ... | @@ -91,7 +94,7 @@ export function CompactBrandTitle( |
| 91 | } | 94 | } |
| 92 | 95 | ||
| 93 | export function renderCompactBrandTitle() { | 96 | export function renderCompactBrandTitle() { |
| 94 | return <CompactBrandTitle />; | 97 | return <CompactBrandTitle allowFontScaling={Platform.OS === "ios"} />; |
| 95 | } | 98 | } |
| 96 | 99 | ||
| 97 | export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { | 100 | export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { |
apps/mobile/src/features/home/AndroidHomeFab.tsx+2-2| ... | @@ -6,8 +6,8 @@ import { SymbolView } from "../../components/AppSymbol"; | ... | @@ -6,8 +6,8 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 6 | import { useThemeColor } from "../../lib/useThemeColor"; | 6 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 7 | 7 | ||
| 8 | /** | 8 | /** |
| 9 | * Android-only wrapper that overlays a bottom-right new-task FAB on the home | 9 | * Android-only wrapper that overlays a bottom-right new-task FAB on a thread |
| 10 | * screen. Other platforms render children unchanged. | 10 | * list. Other platforms render children unchanged. |
| 11 | */ | 11 | */ |
| 12 | export function AndroidHomeFabLayout(props: { | 12 | export function AndroidHomeFabLayout(props: { |
| 13 | readonly onStartNewTask: () => void; | 13 | readonly onStartNewTask: () => void; |
apps/mobile/src/features/home/HomeRouteScreen.tsx+19-12| ... | @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; | ... | @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; |
| 2 | import * as Order from "effect/Order"; | 2 | import * as Order from "effect/Order"; |
| 3 | import { useNavigation } from "@react-navigation/native"; | 3 | import { useNavigation } from "@react-navigation/native"; |
| 4 | import { useEffect, useMemo, useState } from "react"; | 4 | import { useEffect, useMemo, useState } from "react"; |
| 5 | import { Platform } from "react-native"; | ||
| 5 | 6 | ||
| 6 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; | 7 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 7 | import { useProjects, useThreadShells } from "../../state/entities"; | 8 | import { useProjects, useThreadShells } from "../../state/entities"; |
| ... | @@ -106,7 +107,11 @@ export function HomeRouteScreen() { | ... | @@ -106,7 +107,11 @@ export function HomeRouteScreen() { |
| 106 | return ( | 107 | return ( |
| 107 | <> | 108 | <> |
| 108 | <NativeStackScreenOptions | 109 | <NativeStackScreenOptions |
| 109 | options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }} | 110 | options={ |
| 111 | Platform.OS === "android" | ||
| 112 | ? { headerShown: false } | ||
| 113 | : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] } | ||
| 114 | } | ||
| 110 | /> | 115 | /> |
| 111 | <WorkspaceSidebarToolbar | 116 | <WorkspaceSidebarToolbar |
| 112 | afterSidebarButton={ | 117 | afterSidebarButton={ |
| ... | @@ -129,18 +134,20 @@ export function HomeRouteScreen() { | ... | @@ -129,18 +134,20 @@ export function HomeRouteScreen() { |
| 129 | onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} | 134 | onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} |
| 130 | > | 135 | > |
| 131 | <> | 136 | <> |
| 132 | {/* Restore the compact title after the split branch blanks the detail | 137 | {/* Restore the header after leaving split view; screen options are |
| 133 | header. The brand slot doubles as the connection status surface: | 138 | shallow-merged. The brand slot also doubles as the connection |
| 134 | while an environment reconnects, the lockup fades to a status label | 139 | status surface while an environment reconnects. */} |
| 135 | in place (no layout shift in the list below). */} | ||
| 136 | <NativeStackScreenOptions | 140 | <NativeStackScreenOptions |
| 137 | options={getConnectionAwareBrandHeaderOptions({ | 141 | options={{ |
| 138 | onOpenEnvironments: () => | 142 | ...getConnectionAwareBrandHeaderOptions({ |
| 139 | navigation.navigate("SettingsSheet", { | 143 | onOpenEnvironments: () => |
| 140 | screen: "SettingsContent", | 144 | navigation.navigate("SettingsSheet", { |
| 141 | params: { screen: "SettingsEnvironments" }, | 145 | screen: "SettingsContent", |
| 142 | }), | 146 | params: { screen: "SettingsEnvironments" }, |
| 143 | })} | 147 | }), |
| 148 | }), | ||
| 149 | headerShown: true, | ||
| 150 | }} | ||
| 144 | /> | 151 | /> |
| 145 | <HomeHeader | 152 | <HomeHeader |
| 146 | environments={environments} | 153 | environments={environments} |
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+21-12| ... | @@ -45,6 +45,7 @@ import { | ... | @@ -45,6 +45,7 @@ import { |
| 45 | parseActiveThreadPath, | 45 | parseActiveThreadPath, |
| 46 | useHardwareKeyboardCommand, | 46 | useHardwareKeyboardCommand, |
| 47 | } from "../keyboard/hardwareKeyboardCommands"; | 47 | } from "../keyboard/hardwareKeyboardCommands"; |
| 48 | import { AndroidHomeFabLayout } from "../home/AndroidHomeFab"; | ||
| 48 | import { HomeListOptionsProvider } from "../home/home-list-options"; | 49 | import { HomeListOptionsProvider } from "../home/home-list-options"; |
| 49 | import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; | 50 | import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; |
| 50 | import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; | 51 | import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; |
| ... | @@ -435,6 +436,10 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -435,6 +436,10 @@ function AdaptiveWorkspaceLayoutContent( |
| 435 | }); | 436 | }); |
| 436 | }, [navigation]); | 437 | }, [navigation]); |
| 437 | 438 | ||
| 439 | const handleStartNewTask = useCallback(() => { | ||
| 440 | navigation.navigate("NewTaskSheet", { screen: "NewTask" }); | ||
| 441 | }, [navigation]); | ||
| 442 | |||
| 438 | // Minted here (root stack navigation) so the sidebar pane stays free of | 443 | // Minted here (root stack navigation) so the sidebar pane stays free of |
| 439 | // navigation hooks — on iOS it renders inside an independent nav tree. | 444 | // navigation hooks — on iOS it renders inside an independent nav tree. |
| 440 | const handleOpenEnvironmentSettings = useCallback(() => { | 445 | const handleOpenEnvironmentSettings = useCallback(() => { |
| ... | @@ -528,18 +533,22 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -528,18 +533,22 @@ function AdaptiveWorkspaceLayoutContent( |
| 528 | pointerEvents={panes.primarySidebarVisible ? "auto" : "none"} | 533 | pointerEvents={panes.primarySidebarVisible ? "auto" : "none"} |
| 529 | style={sidebarAnimatedStyle} | 534 | style={sidebarAnimatedStyle} |
| 530 | > | 535 | > |
| 531 | <ThreadNavigationSidebar | 536 | <View className="flex-1" style={{ width: layout.listPaneWidth }}> |
| 532 | width={layout.listPaneWidth} | 537 | <AndroidHomeFabLayout onStartNewTask={handleStartNewTask}> |
| 533 | visible={panes.primarySidebarVisible} | 538 | <ThreadNavigationSidebar |
| 534 | onRequestVisibility={revealPrimarySidebar} | 539 | width={layout.listPaneWidth} |
| 535 | selectedThreadKey={selectedThreadKey} | 540 | visible={panes.primarySidebarVisible} |
| 536 | onOpenSettings={handleOpenSettings} | 541 | onRequestVisibility={revealPrimarySidebar} |
| 537 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} | 542 | selectedThreadKey={selectedThreadKey} |
| 538 | onNewThreadInProject={handleNewThreadInProject} | 543 | onOpenSettings={handleOpenSettings} |
| 539 | onSelectThread={handleSelectThread} | 544 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} |
| 540 | onSearchQueryChange={setPrimarySidebarSearchQuery} | 545 | onNewThreadInProject={handleNewThreadInProject} |
| 541 | searchQuery={primarySidebarSearchQuery} | 546 | onSelectThread={handleSelectThread} |
| 542 | /> | 547 | onSearchQueryChange={setPrimarySidebarSearchQuery} |
| 548 | searchQuery={primarySidebarSearchQuery} | ||
| 549 | /> | ||
| 550 | </AndroidHomeFabLayout> | ||
| 551 | </View> | ||
| 543 | </Animated.View> | 552 | </Animated.View> |
| 544 | ) : null} | 553 | ) : null} |
| 545 | <View className="flex-1 overflow-hidden bg-screen" collapsable={false}> | 554 | <View className="flex-1 overflow-hidden bg-screen" collapsable={false}> |
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+21-112| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; | ||
| 2 | import type { | 1 | import type { |
| 3 | EnvironmentProject, | 2 | EnvironmentProject, |
| 4 | EnvironmentThreadShell, | 3 | EnvironmentThreadShell, |
| ... | @@ -14,16 +13,16 @@ import { AsyncResult } from "effect/unstable/reactivity"; | ... | @@ -14,16 +13,16 @@ import { AsyncResult } from "effect/unstable/reactivity"; |
| 14 | import type { EnvironmentId } from "@t3tools/contracts"; | 13 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 15 | import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; | 14 | import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; |
| 16 | import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled"; | 15 | import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled"; |
| 17 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; | 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 18 | import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; | 17 | import type { LayoutChangeEvent } from "react-native"; |
| 19 | import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native"; | 18 | import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native"; |
| 20 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; | 19 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; |
| 21 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; | 20 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 22 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 21 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 23 | import type { SearchBarCommands } from "react-native-screens"; | 22 | import type { SearchBarCommands } from "react-native-screens"; |
| 24 | import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; | ||
| 25 | 23 | ||
| 26 | import { AppText as Text } from "../../components/AppText"; | 24 | import { AppText as Text } from "../../components/AppText"; |
| 25 | import { CompactBrandTitle } from "../../components/CompactBrandTitle"; | ||
| 27 | import { ControlPillMenu } from "../../components/ControlPill"; | 26 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 28 | import { SymbolView } from "../../components/AppSymbol"; | 27 | import { SymbolView } from "../../components/AppSymbol"; |
| 29 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | 28 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| ... | @@ -40,7 +39,6 @@ import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; | ... | @@ -40,7 +39,6 @@ import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; |
| 40 | import { useWorkspaceState } from "../../state/workspace"; | 39 | import { useWorkspaceState } from "../../state/workspace"; |
| 41 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; | 40 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 42 | import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; | 41 | import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; |
| 43 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | ||
| 44 | import { | 42 | import { |
| 45 | hasCustomHomeListOptions, | 43 | hasCustomHomeListOptions, |
| 46 | PROJECT_SORT_OPTIONS, | 44 | PROJECT_SORT_OPTIONS, |
| ... | @@ -97,48 +95,7 @@ type SidebarListItem = | ... | @@ -97,48 +95,7 @@ type SidebarListItem = |
| 97 | | ThreadListV2ListItem | 95 | | ThreadListV2ListItem |
| 98 | | { readonly type: "v2-show-more"; readonly key: string; readonly hiddenCount: number }; | 96 | | { readonly type: "v2-show-more"; readonly key: string; readonly hiddenCount: number }; |
| 99 | 97 | ||
| 100 | /** | ||
| 101 | * Shared capsule behind the sidebar header buttons — a native liquid-glass | ||
| 102 | * surface on iOS 26+, a tinted pill everywhere else. | ||
| 103 | */ | ||
| 104 | function SidebarHeaderButtonGroup(props: { | ||
| 105 | readonly children: ReactNode; | ||
| 106 | readonly colorScheme: "light" | "dark"; | ||
| 107 | }) { | ||
| 108 | const fallbackBackground = useThemeColor("--color-glass-surface"); | ||
| 109 | const fallbackBorder = useThemeColor("--color-header-border"); | ||
| 110 | if (isLiquidGlassSupported) { | ||
| 111 | return ( | ||
| 112 | <LiquidGlassView | ||
| 113 | colorScheme={props.colorScheme} | ||
| 114 | effect="regular" | ||
| 115 | interactive | ||
| 116 | style={styles.headerButtonGroup} | ||
| 117 | > | ||
| 118 | {props.children} | ||
| 119 | </LiquidGlassView> | ||
| 120 | ); | ||
| 121 | } | ||
| 122 | |||
| 123 | return ( | ||
| 124 | <View | ||
| 125 | style={[ | ||
| 126 | styles.headerButtonGroup, | ||
| 127 | { backgroundColor: fallbackBackground, borderColor: fallbackBorder }, | ||
| 128 | { borderWidth: StyleSheet.hairlineWidth }, | ||
| 129 | ]} | ||
| 130 | > | ||
| 131 | {props.children} | ||
| 132 | </View> | ||
| 133 | ); | ||
| 134 | } | ||
| 135 | |||
| 136 | const SIDEBAR_STICKY_HEADER_HEIGHT = 106; | 98 | const SIDEBAR_STICKY_HEADER_HEIGHT = 106; |
| 137 | const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44; | ||
| 138 | const SIDEBAR_HEADER_WASH_OPACITY = { | ||
| 139 | dark: [0.22, 0.14, 0.04], | ||
| 140 | light: [0.46, 0.3, 0.08], | ||
| 141 | } as const; | ||
| 142 | 99 | ||
| 143 | interface ThreadNavigationSidebarProps { | 100 | interface ThreadNavigationSidebarProps { |
| 144 | readonly width: number; | 101 | readonly width: number; |
| ... | @@ -195,16 +152,13 @@ function ThreadNavigationSidebarPane( | ... | @@ -195,16 +152,13 @@ function ThreadNavigationSidebarPane( |
| 195 | props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean }, | 152 | props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean }, |
| 196 | ) { | 153 | ) { |
| 197 | const insets = useSafeAreaInsets(); | 154 | const insets = useSafeAreaInsets(); |
| 198 | const { themeAppearance: colorScheme } = useAppearancePreferences(); | ||
| 199 | const projects = useProjects(); | 155 | const projects = useProjects(); |
| 200 | const threads = useThreadShells(); | 156 | const threads = useThreadShells(); |
| 201 | const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); | 157 | const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); |
| 202 | const { savedConnectionsById } = useSavedRemoteConnections(); | 158 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| 203 | const [headerIsOverContent, setHeaderIsOverContent] = useState(false); | ||
| 204 | const searchInputRef = useRef<TextInput>(null); | 159 | const searchInputRef = useRef<TextInput>(null); |
| 205 | const searchBarRef = useRef<SearchBarCommands>(null); | 160 | const searchBarRef = useRef<SearchBarCommands>(null); |
| 206 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); | 161 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); |
| 207 | const headerIsOverContentRef = useRef(false); | ||
| 208 | const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); | 162 | const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); |
| 209 | const { | 163 | const { |
| 210 | archiveThread, | 164 | archiveThread, |
| ... | @@ -780,8 +734,6 @@ function ThreadNavigationSidebarPane( | ... | @@ -780,8 +734,6 @@ function ThreadNavigationSidebarPane( |
| 780 | const borderColor = useThemeColor("--color-border"); | 734 | const borderColor = useThemeColor("--color-border"); |
| 781 | const mutedColor = useThemeColor("--color-foreground-muted"); | 735 | const mutedColor = useThemeColor("--color-foreground-muted"); |
| 782 | const placeholderColor = useThemeColor("--color-placeholder"); | 736 | const placeholderColor = useThemeColor("--color-placeholder"); |
| 783 | const headerFadeColor = String(backgroundColor); | ||
| 784 | const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; | ||
| 785 | const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null); | 737 | const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null); |
| 786 | // The sticky header (title row, search field, optional connection status) | 738 | // The sticky header (title row, search field, optional connection status) |
| 787 | // is measured so the list inset always matches its real height — no | 739 | // is measured so the list inset always matches its real height — no |
| ... | @@ -810,19 +762,10 @@ function ThreadNavigationSidebarPane( | ... | @@ -810,19 +762,10 @@ function ThreadNavigationSidebarPane( |
| 810 | }, | 762 | }, |
| 811 | [props.onSelectThread], | 763 | [props.onSelectThread], |
| 812 | ); | 764 | ); |
| 813 | const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => { | ||
| 814 | const next = event.nativeEvent.contentOffset.y > 6; | ||
| 815 | if (headerIsOverContentRef.current === next) { | ||
| 816 | return; | ||
| 817 | } | ||
| 818 | headerIsOverContentRef.current = next; | ||
| 819 | setHeaderIsOverContent(next); | ||
| 820 | }, []); | ||
| 821 | const handleScrollBeginDrag = useCallback(() => { | 765 | const handleScrollBeginDrag = useCallback(() => { |
| 822 | openSwipeableRef.current?.close(); | 766 | openSwipeableRef.current?.close(); |
| 823 | }, []); | 767 | }, []); |
| 824 | const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ | 768 | const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ |
| 825 | onScroll: handleScroll, | ||
| 826 | onScrollBeginDrag: handleScrollBeginDrag, | 769 | onScrollBeginDrag: handleScrollBeginDrag, |
| 827 | }); | 770 | }); |
| 828 | // Project shells load after the first rows draw, so the maps they feed have | 771 | // Project shells load after the first rows draw, so the maps they feed have |
| ... | @@ -1321,7 +1264,10 @@ function ThreadNavigationSidebarPane( | ... | @@ -1321,7 +1264,10 @@ function ThreadNavigationSidebarPane( |
| 1321 | contentContainerStyle={[ | 1264 | contentContainerStyle={[ |
| 1322 | styles.threadListContent, | 1265 | styles.threadListContent, |
| 1323 | { | 1266 | { |
| 1324 | paddingBottom: 16 + insets.bottom, | 1267 | paddingBottom: |
| 1268 | Platform.OS === "android" | ||
| 1269 | ? Math.max(insets.bottom, 16) + 88 - insets.bottom | ||
| 1270 | : 16 + insets.bottom, | ||
| 1325 | paddingTop: topListInset, | 1271 | paddingTop: topListInset, |
| 1326 | }, | 1272 | }, |
| 1327 | ]} | 1273 | ]} |
| ... | @@ -1340,65 +1286,34 @@ function ThreadNavigationSidebarPane( | ... | @@ -1340,65 +1286,34 @@ function ThreadNavigationSidebarPane( |
| 1340 | 1286 | ||
| 1341 | <View | 1287 | <View |
| 1342 | className="absolute inset-x-0 top-0 z-[4]" | 1288 | className="absolute inset-x-0 top-0 z-[4]" |
| 1289 | collapsable={false} | ||
| 1343 | onLayout={handleStickyHeaderLayout} | 1290 | onLayout={handleStickyHeaderLayout} |
| 1344 | pointerEvents="box-none" | 1291 | pointerEvents="auto" |
| 1345 | style={{ paddingTop: insets.top }} | 1292 | style={{ |
| 1293 | paddingTop: insets.top, | ||
| 1294 | backgroundColor, | ||
| 1295 | }} | ||
| 1346 | > | 1296 | > |
| 1347 | <View | ||
| 1348 | className="absolute inset-x-0 top-0" | ||
| 1349 | pointerEvents="none" | ||
| 1350 | accessibilityElementsHidden | ||
| 1351 | importantForAccessibility="no-hide-descendants" | ||
| 1352 | style={{ height: stickyHeaderHeight + SIDEBAR_STICKY_HEADER_FADE_HEIGHT }} | ||
| 1353 | > | ||
| 1354 | <Svg width="100%" height="100%"> | ||
| 1355 | <Defs> | ||
| 1356 | <LinearGradient id="sidebar-header-wash" x1="0%" x2="0%" y1="0%" y2="100%"> | ||
| 1357 | <Stop | ||
| 1358 | offset="0%" | ||
| 1359 | stopColor={headerFadeColor} | ||
| 1360 | stopOpacity={headerIsOverContent ? headerWashOpacity[0] : 0} | ||
| 1361 | /> | ||
| 1362 | <Stop | ||
| 1363 | offset="58%" | ||
| 1364 | stopColor={headerFadeColor} | ||
| 1365 | stopOpacity={headerIsOverContent ? headerWashOpacity[1] : 0} | ||
| 1366 | /> | ||
| 1367 | <Stop | ||
| 1368 | offset="88%" | ||
| 1369 | stopColor={headerFadeColor} | ||
| 1370 | stopOpacity={headerIsOverContent ? headerWashOpacity[2] : 0} | ||
| 1371 | /> | ||
| 1372 | <Stop offset="100%" stopColor={headerFadeColor} stopOpacity={0} /> | ||
| 1373 | </LinearGradient> | ||
| 1374 | </Defs> | ||
| 1375 | <Rect width="100%" height="100%" fill="url(#sidebar-header-wash)" /> | ||
| 1376 | </Svg> | ||
| 1377 | </View> | ||
| 1378 | <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5"> | 1297 | <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5"> |
| 1379 | {/* Title slot doubles as the connection status surface: while an | 1298 | {/* Title slot doubles as the connection status surface: while an |
| 1380 | environment reconnects, "Threads" fades to a status label in | 1299 | environment reconnects, the brand fades to a status label in |
| 1381 | place (no layout shift in the list below). */} | 1300 | place (no layout shift in the list below). */} |
| 1382 | <WorkspaceConnectionTitle | 1301 | <WorkspaceConnectionTitle |
| 1383 | grow | 1302 | grow |
| 1384 | onPress={props.onOpenEnvironmentSettings} | 1303 | onPress={props.onOpenEnvironmentSettings} |
| 1385 | size="pageTitle" | 1304 | size="pageTitle" |
| 1386 | brand={ | 1305 | brand={ |
| 1387 | <Text className="flex-1 text-[34px] font-t3-bold text-foreground" numberOfLines={1}> | 1306 | <View className="h-11 flex-1 justify-center"> |
| 1388 | Threads | 1307 | <CompactBrandTitle allowFontScaling={false} /> |
| 1389 | </Text> | 1308 | </View> |
| 1390 | } | 1309 | } |
| 1391 | /> | 1310 | /> |
| 1392 | <SidebarHeaderButtonGroup colorScheme={colorScheme}> | 1311 | <View className="flex-row items-center gap-2.5"> |
| 1393 | <ControlPillMenu actions={listMenuActions} onPressAction={handleListMenuAction}> | 1312 | <ControlPillMenu actions={listMenuActions} onPressAction={handleListMenuAction}> |
| 1394 | <SidebarFilterButton | 1313 | <SidebarFilterButton accessibilityLabel="Filter and sort threads" icon={filterIcon} /> |
| 1395 | grouped | ||
| 1396 | accessibilityLabel="Filter and sort threads" | ||
| 1397 | icon={filterIcon} | ||
| 1398 | /> | ||
| 1399 | </ControlPillMenu> | 1314 | </ControlPillMenu> |
| 1400 | <SidebarHeaderActions grouped onOpenSettings={props.onOpenSettings} /> | 1315 | <SidebarHeaderActions onOpenSettings={props.onOpenSettings} /> |
| 1401 | </SidebarHeaderButtonGroup> | 1316 | </View> |
| 1402 | </View> | 1317 | </View> |
| 1403 | 1318 | ||
| 1404 | <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]"> | 1319 | <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]"> |
| ... | @@ -1423,12 +1338,6 @@ function ThreadNavigationSidebarPane( | ... | @@ -1423,12 +1338,6 @@ function ThreadNavigationSidebarPane( |
| 1423 | } | 1338 | } |
| 1424 | 1339 | ||
| 1425 | const styles = StyleSheet.create({ | 1340 | const styles = StyleSheet.create({ |
| 1426 | headerButtonGroup: { | ||
| 1427 | alignItems: "center", | ||
| 1428 | borderRadius: 22, | ||
| 1429 | flexDirection: "row", | ||
| 1430 | overflow: "hidden", | ||
| 1431 | }, | ||
| 1432 | threadList: { | 1341 | threadList: { |
| 1433 | flex: 1, | 1342 | flex: 1, |
| 1434 | }, | 1343 | }, |
apps/mobile/src/features/threads/sidebar-filter-button.tsx+3-17| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { SymbolView } from "../../components/AppSymbol"; | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | import { Pressable, StyleSheet } from "react-native"; | 2 | import { Pressable } from "react-native"; |
| 3 | 3 | ||
| 4 | import { useThemeColor } from "../../lib/useThemeColor"; | 4 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 5 | 5 | ||
| ... | @@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon = | ... | @@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon = |
| 10 | export function SidebarFilterButton(props: { | 10 | export function SidebarFilterButton(props: { |
| 11 | readonly accessibilityLabel: string; | 11 | readonly accessibilityLabel: string; |
| 12 | readonly icon: SidebarFilterButtonIcon; | 12 | readonly icon: SidebarFilterButtonIcon; |
| 13 | /** Rendered inside a shared capsule group — no own background/border. */ | ||
| 14 | readonly grouped?: boolean; | ||
| 15 | }) { | 13 | }) { |
| 16 | const iconColor = useThemeColor("--color-foreground"); | 14 | const iconColor = useThemeColor("--color-foreground"); |
| 17 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | ||
| 18 | const idleBackgroundColor = useThemeColor("--color-glass-surface"); | ||
| 19 | const borderColor = useThemeColor("--color-header-border"); | ||
| 20 | 15 | ||
| 21 | return ( | 16 | return ( |
| 22 | <Pressable | 17 | <Pressable |
| 23 | className="h-11 w-[50px] cursor-pointer items-center justify-center rounded-[22px]" | 18 | className="size-11 cursor-pointer items-center justify-center rounded-full bg-subtle active:opacity-70" |
| 24 | accessibilityLabel={props.accessibilityLabel} | 19 | accessibilityLabel={props.accessibilityLabel} |
| 25 | accessibilityRole="button" | 20 | accessibilityRole="button" |
| 26 | hitSlop={4} | 21 | hitSlop={4} |
| 27 | style={({ pressed }) => [ | ||
| 28 | props.grouped | ||
| 29 | ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } | ||
| 30 | : { | ||
| 31 | backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, | ||
| 32 | borderColor, | ||
| 33 | borderWidth: StyleSheet.hairlineWidth, | ||
| 34 | }, | ||
| 35 | ]} | ||
| 36 | > | 22 | > |
| 37 | <SymbolView name={props.icon} size={20} tintColor={iconColor} type="monochrome" /> | 23 | <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" /> |
| 38 | </Pressable> | 24 | </Pressable> |
| 39 | ); | 25 | ); |
| 40 | } | 26 | } |
apps/mobile/src/features/threads/sidebar-header-actions.tsx+3-19| ... | @@ -1,43 +1,28 @@ | ... | @@ -1,43 +1,28 @@ |
| 1 | import { SymbolView } from "../../components/AppSymbol"; | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | import { Pressable, StyleSheet, View } from "react-native"; | 2 | import { Pressable, View } from "react-native"; |
| 3 | 3 | ||
| 4 | import { useThemeColor } from "../../lib/useThemeColor"; | 4 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 5 | 5 | ||
| 6 | export interface SidebarHeaderActionsProps { | 6 | export interface SidebarHeaderActionsProps { |
| 7 | readonly onOpenSettings: () => void; | 7 | readonly onOpenSettings: () => void; |
| 8 | /** Rendered inside a shared capsule group — buttons drop their own chrome. */ | ||
| 9 | readonly grouped?: boolean; | ||
| 10 | } | 8 | } |
| 11 | 9 | ||
| 12 | function FallbackHeaderButton(props: { | 10 | function FallbackHeaderButton(props: { |
| 13 | readonly accessibilityLabel: string; | 11 | readonly accessibilityLabel: string; |
| 14 | readonly icon: "gearshape" | "square.and.pencil"; | 12 | readonly icon: "gearshape" | "square.and.pencil"; |
| 15 | readonly grouped?: boolean; | ||
| 16 | readonly onPress: () => void; | 13 | readonly onPress: () => void; |
| 17 | }) { | 14 | }) { |
| 18 | const iconColor = useThemeColor("--color-foreground"); | 15 | const iconColor = useThemeColor("--color-foreground"); |
| 19 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | ||
| 20 | const idleBackgroundColor = useThemeColor("--color-glass-surface"); | ||
| 21 | const borderColor = useThemeColor("--color-header-border"); | ||
| 22 | 16 | ||
| 23 | return ( | 17 | return ( |
| 24 | <Pressable | 18 | <Pressable |
| 25 | className="h-11 w-[50px] items-center justify-center rounded-[22px]" | 19 | className="size-11 items-center justify-center rounded-full bg-subtle active:opacity-70" |
| 26 | accessibilityLabel={props.accessibilityLabel} | 20 | accessibilityLabel={props.accessibilityLabel} |
| 27 | accessibilityRole="button" | 21 | accessibilityRole="button" |
| 28 | hitSlop={4} | 22 | hitSlop={4} |
| 29 | onPress={props.onPress} | 23 | onPress={props.onPress} |
| 30 | style={({ pressed }) => [ | ||
| 31 | props.grouped | ||
| 32 | ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } | ||
| 33 | : { | ||
| 34 | backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, | ||
| 35 | borderColor, | ||
| 36 | borderWidth: StyleSheet.hairlineWidth, | ||
| 37 | }, | ||
| 38 | ]} | ||
| 39 | > | 24 | > |
| 40 | <SymbolView name={props.icon} size={20} tintColor={iconColor} type="monochrome" /> | 25 | <SymbolView name={props.icon} size={18} tintColor={iconColor} type="monochrome" /> |
| 41 | </Pressable> | 26 | </Pressable> |
| 42 | ); | 27 | ); |
| 43 | } | 28 | } |
| ... | @@ -47,7 +32,6 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { | ... | @@ -47,7 +32,6 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { |
| 47 | <View className="flex-row items-center gap-0.5"> | 32 | <View className="flex-row items-center gap-0.5"> |
| 48 | <FallbackHeaderButton | 33 | <FallbackHeaderButton |
| 49 | accessibilityLabel="Open settings" | 34 | accessibilityLabel="Open settings" |
| 50 | grouped={props.grouped} | ||
| 51 | icon="gearshape" | 35 | icon="gearshape" |
| 52 | onPress={props.onOpenSettings} | 36 | onPress={props.onOpenSettings} |
| 53 | /> | 37 | /> |