authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-08-24 02:52:26-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-24 02:52:26-07:00
log2d2efff28c4fc47745538570fcb7dd07272e3e4b
tree3476de409e230919ecd0d972a77f0bac6801c671
parent9eba1252c1f8264b6951068d2e2396363412299c
signature Signed by PGP key B5690EEEBB952194

fix(mobile): restore Android tablet thread controls, clean up header (#5385)


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 */
34export function CompactBrandTitle(34export 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 <Text60 <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 <Text79 <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}
9295
93export function renderCompactBrandTitle() {96export function renderCompactBrandTitle() {
94 return <CompactBrandTitle />;97 return <CompactBrandTitle allowFontScaling={Platform.OS === "ios"} />;
95}98}
9699
97export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] {100export 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";
6import { useThemeColor } from "../../lib/useThemeColor";6import { useThemeColor } from "../../lib/useThemeColor";
77
8/**8/**
9 * Android-only wrapper that overlays a bottom-right new-task FAB on the home9 * 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 */
12export function AndroidHomeFabLayout(props: {12export 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";
2import * as Order from "effect/Order";2import * as Order from "effect/Order";
3import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
4import { useEffect, useMemo, useState } from "react";4import { useEffect, useMemo, useState } from "react";
5import { Platform } from "react-native";
56
6import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";7import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
7import { useProjects, useThreadShells } from "../../state/entities";8import { 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 <NativeStackScreenOptions109 <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 <WorkspaceSidebarToolbar116 <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 detail137 {/* 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 label139 status surface while an environment reconnects. */}
135 in place (no layout shift in the list below). */}
136 <NativeStackScreenOptions140 <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 <HomeHeader152 <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";
48import { AndroidHomeFabLayout } from "../home/AndroidHomeFab";
48import { HomeListOptionsProvider } from "../home/home-list-options";49import { HomeListOptionsProvider } from "../home/home-list-options";
49import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";50import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
50import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";51import { 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]);
437438
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 of443 // 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 <ThreadNavigationSidebar536 <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 @@
1import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass";
2import type {1import 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";
14import type { EnvironmentId } from "@t3tools/contracts";13import type { EnvironmentId } from "@t3tools/contracts";
15import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";14import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort";
16import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled";15import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled";
17import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";16import { useCallback, useEffect, useMemo, useRef, useState } from "react";
18import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native";17import type { LayoutChangeEvent } from "react-native";
19import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native";18import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native";
20import { Gesture, GestureDetector } from "react-native-gesture-handler";19import { Gesture, GestureDetector } from "react-native-gesture-handler";
21import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";20import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
22import { useSafeAreaInsets } from "react-native-safe-area-context";21import { useSafeAreaInsets } from "react-native-safe-area-context";
23import type { SearchBarCommands } from "react-native-screens";22import type { SearchBarCommands } from "react-native-screens";
24import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
2523
26import { AppText as Text } from "../../components/AppText";24import { AppText as Text } from "../../components/AppText";
25import { CompactBrandTitle } from "../../components/CompactBrandTitle";
27import { ControlPillMenu } from "../../components/ControlPill";26import { ControlPillMenu } from "../../components/ControlPill";
28import { SymbolView } from "../../components/AppSymbol";27import { SymbolView } from "../../components/AppSymbol";
29import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";28import { 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";
40import { useWorkspaceState } from "../../state/workspace";39import { useWorkspaceState } from "../../state/workspace";
41import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";40import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
42import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";41import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
43import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
44import {42import {
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 | ThreadListV2ListItem95 | 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 };
9997
100/**
101 * Shared capsule behind the sidebar header buttons — a native liquid-glass
102 * surface on iOS 26+, a tinted pill everywhere else.
103 */
104function 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
136const SIDEBAR_STICKY_HEADER_HEIGHT = 106;98const SIDEBAR_STICKY_HEADER_HEIGHT = 106;
137const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44;
138const SIDEBAR_HEADER_WASH_OPACITY = {
139 dark: [0.22, 0.14, 0.04],
140 light: [0.46, 0.3, 0.08],
141} as const;
14299
143interface ThreadNavigationSidebarProps {100interface 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 — no739 // 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 have771 // 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(
13401286
1341 <View1287 <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 an1298 {/* Title slot doubles as the connection status surface: while an
1380 environment reconnects, "Threads" fades to a status label in1299 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 <WorkspaceConnectionTitle1301 <WorkspaceConnectionTitle
1383 grow1302 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 Threads1307 <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 <SidebarFilterButton1313 <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>
14031318
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}
14241339
1425const styles = StyleSheet.create({1340const 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 @@
1import { SymbolView } from "../../components/AppSymbol";1import { SymbolView } from "../../components/AppSymbol";
2import { Pressable, StyleSheet } from "react-native";2import { Pressable } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";4import { useThemeColor } from "../../lib/useThemeColor";
55
...@@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon =...@@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon =
10export function SidebarFilterButton(props: {10export 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");
2015
21 return (16 return (
22 <Pressable17 <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 @@
1import { SymbolView } from "../../components/AppSymbol";1import { SymbolView } from "../../components/AppSymbol";
2import { Pressable, StyleSheet, View } from "react-native";2import { Pressable, View } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";4import { useThemeColor } from "../../lib/useThemeColor";
55
6export interface SidebarHeaderActionsProps {6export 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}
119
12function FallbackHeaderButton(props: {10function 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");
2216
23 return (17 return (
24 <Pressable18 <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 <FallbackHeaderButton33 <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 />