authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-20 12:17:30+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-20 10:17:30+00:00
logb6d9ce325c4586ea3ab2d2ea5cfd5e8f8c167aeb
treebf520fc3d368ac9aacd65bb3ecd1995d5db1f7e6
parent8e3467fe60c49ee739b278229156108032b72e25
signature Signed by PGP key B5690EEEBB952194

Gate iOS glass layout on native support (#4032)

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 {
5959 EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
6060 transitionIncomingSharePresentation,
6161} from "./features/sharing/incoming-share-presentation";
62import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
6263import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
6364import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
6465
......@@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & {
7879// Shared header presets. Screens only override genuinely dynamic values (titles,
7980// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions.
8081//
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.
8385const GLASS_HEADER_OPTIONS: AppScreenOptions = {
8486 headerBackButtonDisplayMode: "minimal",
8587 headerBackTitle: "",
8688 headerLargeTitle: false,
8789 headerShadowVisible: false,
8890 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,
9096 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,
94100};
95101
96102// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally
apps/mobile/src/features/files/FileTreeBrowser.tsx+7-11
......@@ -1,20 +1,14 @@
11import type { ProjectEntry } from "@t3tools/contracts";
22import { SymbolView } from "../../components/AppSymbol";
33import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4import {
5 ActivityIndicator,
6 FlatList,
7 Platform,
8 Pressable,
9 RefreshControl,
10 View,
11} from "react-native";
4import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native";
125import { useSafeAreaInsets } from "react-native-safe-area-context";
136
147import { AppText as Text } from "../../components/AppText";
158import { PierreEntryIcon } from "../../components/PierreEntryIcon";
169import { cn } from "../../lib/cn";
1710import { useThemeColor } from "../../lib/useThemeColor";
11import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1812import {
1913 buildFileTree,
2014 defaultExpandedTreePaths,
......@@ -129,7 +123,7 @@ export function FileTreeBrowser(props: {
129123 const insets = useSafeAreaInsets();
130124 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the
131125 // 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;
133127 const iconColor = String(useThemeColor("--color-icon-muted"));
134128 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;
135129 const controlledSelectedPathRef = useRef(controlledSelectedPath);
......@@ -249,9 +243,11 @@ export function FileTreeBrowser(props: {
249243 className="flex-1"
250244 data={visibleNodes}
251245 keyExtractor={(item) => item.node.path}
252 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}
246 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
253247 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
255251 }
256252 keyboardDismissMode="on-drag"
257253 keyboardShouldPersistTaps="handled"
apps/mobile/src/features/home/HomeScreen.tsx+4-3
......@@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState";
2424import type { WorkspaceState } from "../../state/workspaceModel";
2525import type { SavedRemoteConnection } from "../../lib/connection";
2626import { scopedProjectKey } from "../../lib/scopedEntities";
27import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2728import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
2829import type { PendingNewTask } from "../../state/use-pending-new-tasks";
2930import {
......@@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) {
388389 className="flex-1 items-center justify-center bg-screen px-8"
389390 style={{
390391 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,
392393 }}
393394 >
394395 <View className="w-full max-w-[430px]">
......@@ -469,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) {
469470 ListHeaderComponent={listHeader}
470471 ListEmptyComponent={listEmpty}
471472 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"}
474475 showsVerticalScrollIndicator={false}
475476 keyboardDismissMode="on-drag"
476477 keyboardShouldPersistTaps="handled"
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+8-1
......@@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill";
2828import type { DraftComposerImageAttachment } from "../../lib/composerImages";
2929import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout";
3030import { scopedThreadKey } from "../../lib/scopedEntities";
31import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
3132import type {
3233 PendingApproval,
3334 PendingUserInput,
......@@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: {
197198 entering={FadeInDown.duration(200)}
198199 exiting={FadeOut.duration(140)}
199200 >
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 >
201208 <View className="flex-row items-center gap-2">
202209 <View className="flex-row items-center gap-1">
203210 <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";
1717import { AppText as Text } from "../../components/AppText";
1818import { ControlPillMenu } from "../../components/ControlPill";
1919import { SymbolView } from "../../components/AppSymbol";
20import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2021import { NativeStackScreenOptions } from "../../native/StackHeader";
2122import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
2223import { useThemeColor } from "../../lib/useThemeColor";
......@@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane(
571572 itemsAreEqual={homeListItemsAreEqual}
572573 keyExtractor={(item) => item.key}
573574 renderItem={renderListItem}
574 automaticallyAdjustsScrollIndicatorInsets
575 contentInsetAdjustmentBehavior="automatic"
575 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
576 contentInsetAdjustmentBehavior={
577 NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"
578 }
576579 contentContainerStyle={[
577580 styles.threadListContent,
578581 {
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+2-1
......@@ -23,6 +23,7 @@ import {
2323} from "../../components/AndroidScreenHeader";
2424import { LoadingScreen } from "../../components/LoadingScreen";
2525import { scopedThreadKey } from "../../lib/scopedEntities";
26import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2627import { connectionTone } from "../connection/connectionTone";
2728
2829import {
......@@ -278,7 +279,7 @@ function ThreadRouteContent(
278279 );
279280
280281 /* ─── Native header theming ──────────────────────────────────────── */
281 const usesNativeHeaderGlass = Platform.OS === "ios";
282 const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED;
282283 const headerSubtitle = [
283284 selectedThreadProject?.title ?? null,
284285 selectedEnvironmentConnection?.environmentLabel ?? null,
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+5-4
......@@ -11,6 +11,7 @@ import {
1111import type { ReactNode } from "react";
1212import { Platform, useColorScheme } from "react-native";
1313
14import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1415import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
1516
1617const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
......@@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = {
3334 headerLargeTitle: false,
3435 headerShadowVisible: false,
3536 headerShown: true,
36 headerStyle: { backgroundColor: "transparent" },
37 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
3738 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,
4041 title: "Threads",
41 unstable_navigationItemStyle: "editor",
42 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
4243};
4344
4445const SidebarStack = createNativeStackNavigator();
apps/mobile/src/lib/native-glass-capability.test.ts created+18
......@@ -0,0 +1,18 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { supportsNativeLiquidGlass } from "./native-glass-capability";
4
5describe("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 @@
1export 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 @@
1import { isLiquidGlassSupported } from "@callstack/liquid-glass";
2import { Platform } from "react-native";
3
4import { supportsNativeLiquidGlass } from "../lib/native-glass-capability";
5
6export const NATIVE_LIQUID_GLASS_SUPPORTED = supportsNativeLiquidGlass(
7 Platform.OS,
8 isLiquidGlassSupported,
9);