Unified · Split
| @@ -59,6 +59,7 @@ import { · 58 unmodified lines | ||
| 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 & { · 13 unmodified lines | ||
| 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 |
| @@ -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: { · 108 unmodified lines | ||
| 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: { · 113 unmodified lines | ||
| 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" |
| @@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState"; · 23 unmodified lines | ||
| 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) { · 358 unmodified lines | ||
| 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) { · 74 unmodified lines | ||
| 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" |
| @@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill"; · 27 unmodified lines | ||
| 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: { · 163 unmodified lines | ||
| 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" /> |
| @@ -17,6 +17,7 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; · 16 unmodified lines | ||
| 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( · 548 unmodified lines | ||
| 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 | { |
| @@ -23,6 +23,7 @@ import { · 22 unmodified lines | ||
| 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( · 249 unmodified lines | ||
| 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, |
| @@ -11,6 +11,7 @@ import { · 10 unmodified lines | ||
| 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 = { · 16 unmodified lines | ||
| 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(); |
| @@ -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 | +}); | |
| @@ -0,0 +1,6 @@ | ||
| 1 | +export function supportsNativeLiquidGlass( | |
| 2 | + platform: string, | |
| 3 | + nativeCapabilityAvailable: boolean, | |
| 4 | +): boolean { | |
| 5 | + return platform === "ios" && nativeCapabilityAvailable; | |
| 6 | +} | |
| @@ -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 | +); | |