| 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 | 33 | */ |
| 34 | 34 | export function CompactBrandTitle( |
| 35 | 35 | props: { |
| 36 | readonly allowFontScaling?: boolean; | |
| 36 | 37 | readonly nativeLeadingItem?: boolean; |
| 37 | 38 | } = {}, |
| 38 | 39 | ) { |
| ... | ... | @@ -57,6 +58,7 @@ export function CompactBrandTitle( |
| 57 | 58 | > |
| 58 | 59 | <T3Wordmark color={iconColor} height={15} /> |
| 59 | 60 | <Text |
| 61 | allowFontScaling={props.allowFontScaling} | |
| 60 | 62 | style={{ |
| 61 | 63 | color: mutedColor, |
| 62 | 64 | fontFamily: "DMSans-Medium", |
| ... | ... | @@ -75,6 +77,7 @@ export function CompactBrandTitle( |
| 75 | 77 | }} |
| 76 | 78 | > |
| 77 | 79 | <Text |
| 80 | allowFontScaling={props.allowFontScaling} | |
| 78 | 81 | style={{ |
| 79 | 82 | color: mutedColor, |
| 80 | 83 | fontFamily: "DMSans-Bold", |
| ... | ... | @@ -91,7 +94,7 @@ export function CompactBrandTitle( |
| 91 | 94 | } |
| 92 | 95 | |
| 93 | 96 | export function renderCompactBrandTitle() { |
| 94 | return <CompactBrandTitle />; | |
| 97 | return <CompactBrandTitle allowFontScaling={Platform.OS === "ios"} />; | |
| 95 | 98 | } |
| 96 | 99 | |
| 97 | 100 | export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { |
apps/mobile/src/features/home/AndroidHomeFab.tsx+2-2| ... | ... | @@ -6,8 +6,8 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 6 | 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 | |
| 10 | * screen. Other platforms render children unchanged. | |
| 9 | * Android-only wrapper that overlays a bottom-right new-task FAB on a thread | |
| 10 | * list. Other platforms render children unchanged. | |
| 11 | 11 | */ |
| 12 | 12 | export function AndroidHomeFabLayout(props: { |
| 13 | 13 | readonly onStartNewTask: () => void; |
apps/mobile/src/features/home/HomeRouteScreen.tsx+19-12| ... | ... | @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; |
| 2 | 2 | import * as Order from "effect/Order"; |
| 3 | 3 | import { useNavigation } from "@react-navigation/native"; |
| 4 | 4 | import { useEffect, useMemo, useState } from "react"; |
| 5 | import { Platform } from "react-native"; | |
| 5 | 6 | |
| 6 | 7 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 7 | 8 | import { useProjects, useThreadShells } from "../../state/entities"; |
| ... | ... | @@ -106,7 +107,11 @@ export function HomeRouteScreen() { |
| 106 | 107 | return ( |
| 107 | 108 | <> |
| 108 | 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 | 116 | <WorkspaceSidebarToolbar |
| 112 | 117 | afterSidebarButton={ |
| ... | ... | @@ -129,18 +134,20 @@ export function HomeRouteScreen() { |
| 129 | 134 | onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} |
| 130 | 135 | > |
| 131 | 136 | <> |
| 132 | {/* Restore the compact title after the split branch blanks the detail | |
| 133 | header. The brand slot doubles as the connection status surface: | |
| 134 | while an environment reconnects, the lockup fades to a status label | |
| 135 | in place (no layout shift in the list below). */} | |
| 137 | {/* Restore the header after leaving split view; screen options are | |
| 138 | shallow-merged. The brand slot also doubles as the connection | |
| 139 | status surface while an environment reconnects. */} | |
| 136 | 140 | <NativeStackScreenOptions |
| 137 | options={getConnectionAwareBrandHeaderOptions({ | |
| 138 | onOpenEnvironments: () => | |
| 139 | navigation.navigate("SettingsSheet", { | |
| 140 | screen: "SettingsContent", | |
| 141 | params: { screen: "SettingsEnvironments" }, | |
| 142 | }), | |
| 143 | })} | |
| 141 | options={{ | |
| 142 | ...getConnectionAwareBrandHeaderOptions({ | |
| 143 | onOpenEnvironments: () => | |
| 144 | navigation.navigate("SettingsSheet", { | |
| 145 | screen: "SettingsContent", | |
| 146 | params: { screen: "SettingsEnvironments" }, | |
| 147 | }), | |
| 148 | }), | |
| 149 | headerShown: true, | |
| 150 | }} | |
| 144 | 151 | /> |
| 145 | 152 | <HomeHeader |
| 146 | 153 | environments={environments} |
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+21-12| ... | ... | @@ -45,6 +45,7 @@ import { |
| 45 | 45 | parseActiveThreadPath, |
| 46 | 46 | useHardwareKeyboardCommand, |
| 47 | 47 | } from "../keyboard/hardwareKeyboardCommands"; |
| 48 | import { AndroidHomeFabLayout } from "../home/AndroidHomeFab"; | |
| 48 | 49 | import { HomeListOptionsProvider } from "../home/home-list-options"; |
| 49 | 50 | import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; |
| 50 | 51 | import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; |
| ... | ... | @@ -435,6 +436,10 @@ function AdaptiveWorkspaceLayoutContent( |
| 435 | 436 | }); |
| 436 | 437 | }, [navigation]); |
| 437 | 438 | |
| 439 | const handleStartNewTask = useCallback(() => { | |
| 440 | navigation.navigate("NewTaskSheet", { screen: "NewTask" }); | |
| 441 | }, [navigation]); | |
| 442 | ||
| 438 | 443 | // Minted here (root stack navigation) so the sidebar pane stays free of |
| 439 | 444 | // navigation hooks — on iOS it renders inside an independent nav tree. |
| 440 | 445 | const handleOpenEnvironmentSettings = useCallback(() => { |
| ... | ... | @@ -528,18 +533,22 @@ function AdaptiveWorkspaceLayoutContent( |
| 528 | 533 | pointerEvents={panes.primarySidebarVisible ? "auto" : "none"} |
| 529 | 534 | style={sidebarAnimatedStyle} |
| 530 | 535 | > |
| 531 | <ThreadNavigationSidebar | |
| 532 | width={layout.listPaneWidth} | |
| 533 | visible={panes.primarySidebarVisible} | |
| 534 | onRequestVisibility={revealPrimarySidebar} | |
| 535 | selectedThreadKey={selectedThreadKey} | |
| 536 | onOpenSettings={handleOpenSettings} | |
| 537 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} | |
| 538 | onNewThreadInProject={handleNewThreadInProject} | |
| 539 | onSelectThread={handleSelectThread} | |
| 540 | onSearchQueryChange={setPrimarySidebarSearchQuery} | |
| 541 | searchQuery={primarySidebarSearchQuery} | |
| 542 | /> | |
| 536 | <View className="flex-1" style={{ width: layout.listPaneWidth }}> | |
| 537 | <AndroidHomeFabLayout onStartNewTask={handleStartNewTask}> | |
| 538 | <ThreadNavigationSidebar | |
| 539 | width={layout.listPaneWidth} | |
| 540 | visible={panes.primarySidebarVisible} | |
| 541 | onRequestVisibility={revealPrimarySidebar} | |
| 542 | selectedThreadKey={selectedThreadKey} | |
| 543 | onOpenSettings={handleOpenSettings} | |
| 544 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} | |
| 545 | onNewThreadInProject={handleNewThreadInProject} | |
| 546 | onSelectThread={handleSelectThread} | |
| 547 | onSearchQueryChange={setPrimarySidebarSearchQuery} | |
| 548 | searchQuery={primarySidebarSearchQuery} | |
| 549 | /> | |
| 550 | </AndroidHomeFabLayout> | |
| 551 | </View> | |
| 543 | 552 | </Animated.View> |
| 544 | 553 | ) : null} |
| 545 | 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 | import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; | |
| 2 | 1 | import type { |
| 3 | 2 | EnvironmentProject, |
| 4 | 3 | EnvironmentThreadShell, |
| ... | ... | @@ -14,16 +13,16 @@ import { AsyncResult } from "effect/unstable/reactivity"; |
| 14 | 13 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 15 | 14 | import { sortPinnedThreadsByOrderKey } from "@t3tools/client-runtime/state/thread-sort"; |
| 16 | 15 | import type { ChangeRequestSettleSource } from "@t3tools/client-runtime/state/thread-settled"; |
| 17 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; | |
| 18 | import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; | |
| 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 17 | import type { LayoutChangeEvent } from "react-native"; | |
| 19 | 18 | import { Platform, Pressable, StyleSheet, TextInput, View } from "react-native"; |
| 20 | 19 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; |
| 21 | 20 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 22 | 21 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 23 | 22 | import type { SearchBarCommands } from "react-native-screens"; |
| 24 | import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; | |
| 25 | 23 | |
| 26 | 24 | import { AppText as Text } from "../../components/AppText"; |
| 25 | import { CompactBrandTitle } from "../../components/CompactBrandTitle"; | |
| 27 | 26 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 28 | 27 | import { SymbolView } from "../../components/AppSymbol"; |
| 29 | 28 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| ... | ... | @@ -40,7 +39,6 @@ import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; |
| 40 | 39 | import { useWorkspaceState } from "../../state/workspace"; |
| 41 | 40 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 42 | 41 | import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; |
| 43 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 44 | 42 | import { |
| 45 | 43 | hasCustomHomeListOptions, |
| 46 | 44 | PROJECT_SORT_OPTIONS, |
| ... | ... | @@ -97,48 +95,7 @@ type SidebarListItem = |
| 97 | 95 | | ThreadListV2ListItem |
| 98 | 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 | 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 | 100 | interface ThreadNavigationSidebarProps { |
| 144 | 101 | readonly width: number; |
| ... | ... | @@ -195,16 +152,13 @@ function ThreadNavigationSidebarPane( |
| 195 | 152 | props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean }, |
| 196 | 153 | ) { |
| 197 | 154 | const insets = useSafeAreaInsets(); |
| 198 | const { themeAppearance: colorScheme } = useAppearancePreferences(); | |
| 199 | 155 | const projects = useProjects(); |
| 200 | 156 | const threads = useThreadShells(); |
| 201 | 157 | const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); |
| 202 | 158 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| 203 | const [headerIsOverContent, setHeaderIsOverContent] = useState(false); | |
| 204 | 159 | const searchInputRef = useRef<TextInput>(null); |
| 205 | 160 | const searchBarRef = useRef<SearchBarCommands>(null); |
| 206 | 161 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); |
| 207 | const headerIsOverContentRef = useRef(false); | |
| 208 | 162 | const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); |
| 209 | 163 | const { |
| 210 | 164 | archiveThread, |
| ... | ... | @@ -780,8 +734,6 @@ function ThreadNavigationSidebarPane( |
| 780 | 734 | const borderColor = useThemeColor("--color-border"); |
| 781 | 735 | const mutedColor = useThemeColor("--color-foreground-muted"); |
| 782 | 736 | const placeholderColor = useThemeColor("--color-placeholder"); |
| 783 | const headerFadeColor = String(backgroundColor); | |
| 784 | const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; | |
| 785 | 737 | const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null); |
| 786 | 738 | // The sticky header (title row, search field, optional connection status) |
| 787 | 739 | // is measured so the list inset always matches its real height — no |
| ... | ... | @@ -810,19 +762,10 @@ function ThreadNavigationSidebarPane( |
| 810 | 762 | }, |
| 811 | 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 | 765 | const handleScrollBeginDrag = useCallback(() => { |
| 822 | 766 | openSwipeableRef.current?.close(); |
| 823 | 767 | }, []); |
| 824 | 768 | const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ |
| 825 | onScroll: handleScroll, | |
| 826 | 769 | onScrollBeginDrag: handleScrollBeginDrag, |
| 827 | 770 | }); |
| 828 | 771 | // Project shells load after the first rows draw, so the maps they feed have |
| ... | ... | @@ -1321,7 +1264,10 @@ function ThreadNavigationSidebarPane( |
| 1321 | 1264 | contentContainerStyle={[ |
| 1322 | 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 | 1271 | paddingTop: topListInset, |
| 1326 | 1272 | }, |
| 1327 | 1273 | ]} |
| ... | ... | @@ -1340,65 +1286,34 @@ function ThreadNavigationSidebarPane( |
| 1340 | 1286 | |
| 1341 | 1287 | <View |
| 1342 | 1288 | className="absolute inset-x-0 top-0 z-[4]" |
| 1289 | collapsable={false} | |
| 1343 | 1290 | onLayout={handleStickyHeaderLayout} |
| 1344 | pointerEvents="box-none" | |
| 1345 | style={{ paddingTop: insets.top }} | |
| 1291 | pointerEvents="auto" | |
| 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 | 1297 | <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5"> |
| 1379 | 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 | 1300 | place (no layout shift in the list below). */} |
| 1382 | 1301 | <WorkspaceConnectionTitle |
| 1383 | 1302 | grow |
| 1384 | 1303 | onPress={props.onOpenEnvironmentSettings} |
| 1385 | 1304 | size="pageTitle" |
| 1386 | 1305 | brand={ |
| 1387 | <Text className="flex-1 text-[34px] font-t3-bold text-foreground" numberOfLines={1}> | |
| 1388 | Threads | |
| 1389 | </Text> | |
| 1306 | <View className="h-11 flex-1 justify-center"> | |
| 1307 | <CompactBrandTitle allowFontScaling={false} /> | |
| 1308 | </View> | |
| 1390 | 1309 | } |
| 1391 | 1310 | /> |
| 1392 | <SidebarHeaderButtonGroup colorScheme={colorScheme}> | |
| 1311 | <View className="flex-row items-center gap-2.5"> | |
| 1393 | 1312 | <ControlPillMenu actions={listMenuActions} onPressAction={handleListMenuAction}> |
| 1394 | <SidebarFilterButton | |
| 1395 | grouped | |
| 1396 | accessibilityLabel="Filter and sort threads" | |
| 1397 | icon={filterIcon} | |
| 1398 | /> | |
| 1313 | <SidebarFilterButton accessibilityLabel="Filter and sort threads" icon={filterIcon} /> | |
| 1399 | 1314 | </ControlPillMenu> |
| 1400 | <SidebarHeaderActions grouped onOpenSettings={props.onOpenSettings} /> | |
| 1401 | </SidebarHeaderButtonGroup> | |
| 1315 | <SidebarHeaderActions onOpenSettings={props.onOpenSettings} /> | |
| 1316 | </View> | |
| 1402 | 1317 | </View> |
| 1403 | 1318 | |
| 1404 | 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 | 1338 | } |
| 1424 | 1339 | |
| 1425 | 1340 | const styles = StyleSheet.create({ |
| 1426 | headerButtonGroup: { | |
| 1427 | alignItems: "center", | |
| 1428 | borderRadius: 22, | |
| 1429 | flexDirection: "row", | |
| 1430 | overflow: "hidden", | |
| 1431 | }, | |
| 1432 | 1341 | threadList: { |
| 1433 | 1342 | flex: 1, |
| 1434 | 1343 | }, |
apps/mobile/src/features/threads/sidebar-filter-button.tsx+3-17| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | import { Pressable, StyleSheet } from "react-native"; | |
| 2 | import { Pressable } from "react-native"; | |
| 3 | 3 | |
| 4 | 4 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 5 | 5 | |
| ... | ... | @@ -10,31 +10,17 @@ export type SidebarFilterButtonIcon = |
| 10 | 10 | export function SidebarFilterButton(props: { |
| 11 | 11 | readonly accessibilityLabel: string; |
| 12 | 12 | readonly icon: SidebarFilterButtonIcon; |
| 13 | /** Rendered inside a shared capsule group — no own background/border. */ | |
| 14 | readonly grouped?: boolean; | |
| 15 | 13 | }) { |
| 16 | 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 | 16 | return ( |
| 22 | 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 | 19 | accessibilityLabel={props.accessibilityLabel} |
| 25 | 20 | accessibilityRole="button" |
| 26 | 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 | 24 | </Pressable> |
| 39 | 25 | ); |
| 40 | 26 | } |
apps/mobile/src/features/threads/sidebar-header-actions.tsx+3-19| ... | ... | @@ -1,43 +1,28 @@ |
| 1 | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | import { Pressable, StyleSheet, View } from "react-native"; | |
| 2 | import { Pressable, View } from "react-native"; | |
| 3 | 3 | |
| 4 | 4 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 5 | 5 | |
| 6 | 6 | export interface SidebarHeaderActionsProps { |
| 7 | 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 | 10 | function FallbackHeaderButton(props: { |
| 13 | 11 | readonly accessibilityLabel: string; |
| 14 | 12 | readonly icon: "gearshape" | "square.and.pencil"; |
| 15 | readonly grouped?: boolean; | |
| 16 | 13 | readonly onPress: () => void; |
| 17 | 14 | }) { |
| 18 | 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 | 17 | return ( |
| 24 | 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 | 20 | accessibilityLabel={props.accessibilityLabel} |
| 27 | 21 | accessibilityRole="button" |
| 28 | 22 | hitSlop={4} |
| 29 | 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 | 26 | </Pressable> |
| 42 | 27 | ); |
| 43 | 28 | } |
| ... | ... | @@ -47,7 +32,6 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { |
| 47 | 32 | <View className="flex-row items-center gap-0.5"> |
| 48 | 33 | <FallbackHeaderButton |
| 49 | 34 | accessibilityLabel="Open settings" |
| 50 | grouped={props.grouped} | |
| 51 | 35 | icon="gearshape" |
| 52 | 36 | onPress={props.onOpenSettings} |
| 53 | 37 | /> |