| author | |
| committer | |
| log | b6d9ce325c4586ea3ab2d2ea5cfd5e8f8c167aeb |
| tree | bf520fc3d368ac9aacd65bb3ecd1995d5db1f7e6 |
| parent | 8e3467fe60c49ee739b278229156108032b72e25 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>10 files changed, 76 insertions(+), 28 deletions(-)
apps/mobile/src/Stack.tsx+12-6| ... | ... | @@ -59,6 +59,7 @@ import { |
| 59 | 59 | EMPTY_INCOMING_SHARE_PRESENTATION_STATE, |
| 60 | 60 | transitionIncomingSharePresentation, |
| 61 | 61 | } from "./features/sharing/incoming-share-presentation"; |
| 62 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; | |
| 62 | 63 | import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; |
| 63 | 64 | import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; |
| 64 | 65 | |
| ... | ... | @@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & { |
| 78 | 79 | // Shared header presets. Screens only override genuinely dynamic values (titles, |
| 79 | 80 | // subtitles, toolbar items, search callbacks) via NativeStackScreenOptions. |
| 80 | 81 | // |
| 81 | // GLASS: transparent header over the screen's primary scroll view, with the iOS 26 | |
| 82 | // scroll-edge blur sampling the content (Home, Thread, Files tree, settings sheet). | |
| 82 | // GLASS: transparent header over the screen's primary scroll view on supported | |
| 83 | // iOS versions. Pre-glass iOS gets the same solid material as internal-scroll | |
| 84 | // surfaces so content is laid out below the bar instead of underlapping it. | |
| 83 | 85 | const GLASS_HEADER_OPTIONS: AppScreenOptions = { |
| 84 | 86 | headerBackButtonDisplayMode: "minimal", |
| 85 | 87 | headerBackTitle: "", |
| 86 | 88 | headerLargeTitle: false, |
| 87 | 89 | headerShadowVisible: false, |
| 88 | 90 | headerShown: true, |
| 89 | headerStyle: Platform.OS === "ios" ? { backgroundColor: "transparent" } : undefined, | |
| 91 | headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED | |
| 92 | ? { backgroundColor: "transparent" } | |
| 93 | : SHEET_BACKGROUND_COLOR !== undefined | |
| 94 | ? { backgroundColor: SHEET_BACKGROUND_COLOR as unknown as string } | |
| 95 | : undefined, | |
| 90 | 96 | headerTitleStyle: { fontSize: 18, fontWeight: "800" }, |
| 91 | headerTransparent: Platform.OS === "ios", | |
| 92 | scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined, | |
| 93 | unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, | |
| 97 | headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, | |
| 98 | scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined, | |
| 99 | unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, | |
| 94 | 100 | }; |
| 95 | 101 | |
| 96 | 102 | // SOLID: opaque sheet-colored header for surfaces whose content scrolls internally |
apps/mobile/src/features/files/FileTreeBrowser.tsx+7-11| ... | ... | @@ -1,20 +1,14 @@ |
| 1 | 1 | import type { ProjectEntry } from "@t3tools/contracts"; |
| 2 | 2 | import { SymbolView } from "../../components/AppSymbol"; |
| 3 | 3 | import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 4 | import { | |
| 5 | ActivityIndicator, | |
| 6 | FlatList, | |
| 7 | Platform, | |
| 8 | Pressable, | |
| 9 | RefreshControl, | |
| 10 | View, | |
| 11 | } from "react-native"; | |
| 4 | import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native"; | |
| 12 | 5 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 13 | 6 | |
| 14 | 7 | import { AppText as Text } from "../../components/AppText"; |
| 15 | 8 | import { PierreEntryIcon } from "../../components/PierreEntryIcon"; |
| 16 | 9 | import { cn } from "../../lib/cn"; |
| 17 | 10 | import { useThemeColor } from "../../lib/useThemeColor"; |
| 11 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 18 | 12 | import { |
| 19 | 13 | buildFileTree, |
| 20 | 14 | defaultExpandedTreePaths, |
| ... | ... | @@ -129,7 +123,7 @@ export function FileTreeBrowser(props: { |
| 129 | 123 | const insets = useSafeAreaInsets(); |
| 130 | 124 | // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the |
| 131 | 125 | // observed adjustedContentInset bottom (~102) seen in the native trace. |
| 132 | const headerInset = Platform.OS === "ios" ? insets.top + 44 : 0; | |
| 126 | const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 44 : 0; | |
| 133 | 127 | const iconColor = String(useThemeColor("--color-icon-muted")); |
| 134 | 128 | const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; |
| 135 | 129 | const controlledSelectedPathRef = useRef(controlledSelectedPath); |
| ... | ... | @@ -249,9 +243,11 @@ export function FileTreeBrowser(props: { |
| 249 | 243 | className="flex-1" |
| 250 | 244 | data={visibleNodes} |
| 251 | 245 | keyExtractor={(item) => item.node.path} |
| 252 | contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} | |
| 246 | contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"} | |
| 253 | 247 | scrollIndicatorInsets={ |
| 254 | Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined | |
| 248 | NATIVE_LIQUID_GLASS_SUPPORTED | |
| 249 | ? { top: headerInset, left: 0, right: 0, bottom: 0 } | |
| 250 | : undefined | |
| 255 | 251 | } |
| 256 | 252 | keyboardDismissMode="on-drag" |
| 257 | 253 | keyboardShouldPersistTaps="handled" |
apps/mobile/src/features/home/HomeScreen.tsx+4-3| ... | ... | @@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState"; |
| 24 | 24 | import type { WorkspaceState } from "../../state/workspaceModel"; |
| 25 | 25 | import type { SavedRemoteConnection } from "../../lib/connection"; |
| 26 | 26 | import { scopedProjectKey } from "../../lib/scopedEntities"; |
| 27 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 27 | 28 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; |
| 28 | 29 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; |
| 29 | 30 | import { |
| ... | ... | @@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 388 | 389 | className="flex-1 items-center justify-center bg-screen px-8" |
| 389 | 390 | style={{ |
| 390 | 391 | paddingBottom: Math.max(insets.bottom, 24), |
| 391 | paddingTop: Platform.OS === "ios" ? insets.top + 72 : 0, | |
| 392 | paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0, | |
| 392 | 393 | }} |
| 393 | 394 | > |
| 394 | 395 | <View className="w-full max-w-[430px]"> |
| ... | ... | @@ -469,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) { |
| 469 | 470 | ListHeaderComponent={listHeader} |
| 470 | 471 | ListEmptyComponent={listEmpty} |
| 471 | 472 | style={{ flex: 1 }} |
| 472 | automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"} | |
| 473 | contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} | |
| 473 | automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED} | |
| 474 | contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"} | |
| 474 | 475 | showsVerticalScrollIndicator={false} |
| 475 | 476 | keyboardDismissMode="on-drag" |
| 476 | 477 | keyboardShouldPersistTaps="handled" |
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+8-1| ... | ... | @@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill"; |
| 28 | 28 | import type { DraftComposerImageAttachment } from "../../lib/composerImages"; |
| 29 | 29 | import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout"; |
| 30 | 30 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 31 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 31 | 32 | import type { |
| 32 | 33 | PendingApproval, |
| 33 | 34 | PendingUserInput, |
| ... | ... | @@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: { |
| 197 | 198 | entering={FadeInDown.duration(200)} |
| 198 | 199 | exiting={FadeOut.duration(140)} |
| 199 | 200 | > |
| 200 | <View className="self-start rounded-full border border-neutral-200/80 bg-neutral-50/90 px-3 py-2 dark:border-white/[0.08] dark:bg-white/[0.04]"> | |
| 201 | <View | |
| 202 | className={ | |
| 203 | Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED | |
| 204 | ? "self-start rounded-full border border-border bg-card px-3 py-2" | |
| 205 | : "self-start rounded-full border border-neutral-200/80 bg-neutral-50/90 px-3 py-2 dark:border-white/[0.08] dark:bg-white/[0.04]" | |
| 206 | } | |
| 207 | > | |
| 201 | 208 | <View className="flex-row items-center gap-2"> |
| 202 | 209 | <View className="flex-row items-center gap-1"> |
| 203 | 210 | <View className="h-1.5 w-1.5 rounded-full bg-neutral-400 dark:bg-neutral-500" /> |
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+5-2| ... | ... | @@ -17,6 +17,7 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; |
| 17 | 17 | import { AppText as Text } from "../../components/AppText"; |
| 18 | 18 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 19 | 19 | import { SymbolView } from "../../components/AppSymbol"; |
| 20 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 20 | 21 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 21 | 22 | import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; |
| 22 | 23 | import { useThemeColor } from "../../lib/useThemeColor"; |
| ... | ... | @@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane( |
| 571 | 572 | itemsAreEqual={homeListItemsAreEqual} |
| 572 | 573 | keyExtractor={(item) => item.key} |
| 573 | 574 | renderItem={renderListItem} |
| 574 | automaticallyAdjustsScrollIndicatorInsets | |
| 575 | contentInsetAdjustmentBehavior="automatic" | |
| 575 | automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED} | |
| 576 | contentInsetAdjustmentBehavior={ | |
| 577 | NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never" | |
| 578 | } | |
| 576 | 579 | contentContainerStyle={[ |
| 577 | 580 | styles.threadListContent, |
| 578 | 581 | { |
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+2-1| ... | ... | @@ -23,6 +23,7 @@ import { |
| 23 | 23 | } from "../../components/AndroidScreenHeader"; |
| 24 | 24 | import { LoadingScreen } from "../../components/LoadingScreen"; |
| 25 | 25 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 26 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 26 | 27 | import { connectionTone } from "../connection/connectionTone"; |
| 27 | 28 | |
| 28 | 29 | import { |
| ... | ... | @@ -278,7 +279,7 @@ function ThreadRouteContent( |
| 278 | 279 | ); |
| 279 | 280 | |
| 280 | 281 | /* ─── Native header theming ──────────────────────────────────────── */ |
| 281 | const usesNativeHeaderGlass = Platform.OS === "ios"; | |
| 282 | const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED; | |
| 282 | 283 | const headerSubtitle = [ |
| 283 | 284 | selectedThreadProject?.title ?? null, |
| 284 | 285 | selectedEnvironmentConnection?.environmentLabel ?? null, |
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+5-4| ... | ... | @@ -11,6 +11,7 @@ import { |
| 11 | 11 | import type { ReactNode } from "react"; |
| 12 | 12 | import { Platform, useColorScheme } from "react-native"; |
| 13 | 13 | |
| 14 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 14 | 15 | import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; |
| 15 | 16 | |
| 16 | 17 | const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); |
| ... | ... | @@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { |
| 33 | 34 | headerLargeTitle: false, |
| 34 | 35 | headerShadowVisible: false, |
| 35 | 36 | headerShown: true, |
| 36 | headerStyle: { backgroundColor: "transparent" }, | |
| 37 | headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined, | |
| 37 | 38 | headerTitleStyle: { fontSize: 18, fontWeight: "800" }, |
| 38 | headerTransparent: true, | |
| 39 | scrollEdgeEffects: SCROLL_EDGE_EFFECTS, | |
| 39 | headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, | |
| 40 | scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined, | |
| 40 | 41 | title: "Threads", |
| 41 | unstable_navigationItemStyle: "editor", | |
| 42 | unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, | |
| 42 | 43 | }; |
| 43 | 44 | |
| 44 | 45 | const SidebarStack = createNativeStackNavigator(); |
apps/mobile/src/lib/native-glass-capability.test.ts created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { supportsNativeLiquidGlass } from "./native-glass-capability"; | |
| 4 | ||
| 5 | describe("supportsNativeLiquidGlass", () => { | |
| 6 | it("uses native liquid glass when iOS reports the capability", () => { | |
| 7 | expect(supportsNativeLiquidGlass("ios", true)).toBe(true); | |
| 8 | }); | |
| 9 | ||
| 10 | it("keeps pre-glass iOS on the solid fallback", () => { | |
| 11 | expect(supportsNativeLiquidGlass("ios", false)).toBe(false); | |
| 12 | }); | |
| 13 | ||
| 14 | it("does not enable iOS liquid-glass layout behavior on other platforms", () => { | |
| 15 | expect(supportsNativeLiquidGlass("android", true)).toBe(false); | |
| 16 | expect(supportsNativeLiquidGlass("web", true)).toBe(false); | |
| 17 | }); | |
| 18 | }); |
apps/mobile/src/lib/native-glass-capability.ts created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | export function supportsNativeLiquidGlass( | |
| 2 | platform: string, | |
| 3 | nativeCapabilityAvailable: boolean, | |
| 4 | ): boolean { | |
| 5 | return platform === "ios" && nativeCapabilityAvailable; | |
| 6 | } |
apps/mobile/src/native/native-glass.ts created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | import { isLiquidGlassSupported } from "@callstack/liquid-glass"; | |
| 2 | import { Platform } from "react-native"; | |
| 3 | ||
| 4 | import { supportsNativeLiquidGlass } from "../lib/native-glass-capability"; | |
| 5 | ||
| 6 | export const NATIVE_LIQUID_GLASS_SUPPORTED = supportsNativeLiquidGlass( | |
| 7 | Platform.OS, | |
| 8 | isLiquidGlassSupported, | |
| 9 | ); |