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 {...@@ -59,6 +59,7 @@ import {
59 EMPTY_INCOMING_SHARE_PRESENTATION_STATE,59 EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
60 transitionIncomingSharePresentation,60 transitionIncomingSharePresentation,
61} from "./features/sharing/incoming-share-presentation";61} from "./features/sharing/incoming-share-presentation";
62import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
62import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";63import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
63import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";64import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
6465
...@@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & {...@@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & {
78// Shared header presets. Screens only override genuinely dynamic values (titles,79// Shared header presets. Screens only override genuinely dynamic values (titles,
79// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions.80// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions.
80//81//
81// GLASS: transparent header over the screen's primary scroll view, with the iOS 2682// GLASS: transparent header over the screen's primary scroll view on supported
82// scroll-edge blur sampling the content (Home, Thread, Files tree, settings sheet).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.
83const GLASS_HEADER_OPTIONS: AppScreenOptions = {85const GLASS_HEADER_OPTIONS: AppScreenOptions = {
84 headerBackButtonDisplayMode: "minimal",86 headerBackButtonDisplayMode: "minimal",
85 headerBackTitle: "",87 headerBackTitle: "",
86 headerLargeTitle: false,88 headerLargeTitle: false,
87 headerShadowVisible: false,89 headerShadowVisible: false,
88 headerShown: true,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 headerTitleStyle: { fontSize: 18, fontWeight: "800" },96 headerTitleStyle: { fontSize: 18, fontWeight: "800" },
91 headerTransparent: Platform.OS === "ios",97 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
92 scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined,98 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
93 unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined,99 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
94};100};
95101
96// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally102// 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,20 +1,14 @@
1import type { ProjectEntry } from "@t3tools/contracts";1import type { ProjectEntry } from "@t3tools/contracts";
2import { SymbolView } from "../../components/AppSymbol";2import { SymbolView } from "../../components/AppSymbol";
3import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";3import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4import {4import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native";
5 ActivityIndicator,
6 FlatList,
7 Platform,
8 Pressable,
9 RefreshControl,
10 View,
11} from "react-native";
12import { useSafeAreaInsets } from "react-native-safe-area-context";5import { useSafeAreaInsets } from "react-native-safe-area-context";
136
14import { AppText as Text } from "../../components/AppText";7import { AppText as Text } from "../../components/AppText";
15import { PierreEntryIcon } from "../../components/PierreEntryIcon";8import { PierreEntryIcon } from "../../components/PierreEntryIcon";
16import { cn } from "../../lib/cn";9import { cn } from "../../lib/cn";
17import { useThemeColor } from "../../lib/useThemeColor";10import { useThemeColor } from "../../lib/useThemeColor";
11import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
18import {12import {
19 buildFileTree,13 buildFileTree,
20 defaultExpandedTreePaths,14 defaultExpandedTreePaths,
...@@ -129,7 +123,7 @@ export function FileTreeBrowser(props: {...@@ -129,7 +123,7 @@ export function FileTreeBrowser(props: {
129 const insets = useSafeAreaInsets();123 const insets = useSafeAreaInsets();
130 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the124 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the
131 // observed adjustedContentInset bottom (~102) seen in the native trace.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 const iconColor = String(useThemeColor("--color-icon-muted"));127 const iconColor = String(useThemeColor("--color-icon-muted"));
134 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;128 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;
135 const controlledSelectedPathRef = useRef(controlledSelectedPath);129 const controlledSelectedPathRef = useRef(controlledSelectedPath);
...@@ -249,9 +243,11 @@ export function FileTreeBrowser(props: {...@@ -249,9 +243,11 @@ export function FileTreeBrowser(props: {
249 className="flex-1"243 className="flex-1"
250 data={visibleNodes}244 data={visibleNodes}
251 keyExtractor={(item) => item.node.path}245 keyExtractor={(item) => item.node.path}
252 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}246 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
253 scrollIndicatorInsets={247 scrollIndicatorInsets={
254 Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined248 NATIVE_LIQUID_GLASS_SUPPORTED
249 ? { top: headerInset, left: 0, right: 0, bottom: 0 }
250 : undefined
255 }251 }
256 keyboardDismissMode="on-drag"252 keyboardDismissMode="on-drag"
257 keyboardShouldPersistTaps="handled"253 keyboardShouldPersistTaps="handled"
apps/mobile/src/features/home/HomeScreen.tsx+4-3
...@@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState";...@@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState";
24import type { WorkspaceState } from "../../state/workspaceModel";24import type { WorkspaceState } from "../../state/workspaceModel";
25import type { SavedRemoteConnection } from "../../lib/connection";25import type { SavedRemoteConnection } from "../../lib/connection";
26import { scopedProjectKey } from "../../lib/scopedEntities";26import { scopedProjectKey } from "../../lib/scopedEntities";
27import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
27import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";28import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
28import type { PendingNewTask } from "../../state/use-pending-new-tasks";29import type { PendingNewTask } from "../../state/use-pending-new-tasks";
29import {30import {
...@@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) {
388 className="flex-1 items-center justify-center bg-screen px-8"389 className="flex-1 items-center justify-center bg-screen px-8"
389 style={{390 style={{
390 paddingBottom: Math.max(insets.bottom, 24),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 <View className="w-full max-w-[430px]">395 <View className="w-full max-w-[430px]">
...@@ -469,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -469,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) {
469 ListHeaderComponent={listHeader}470 ListHeaderComponent={listHeader}
470 ListEmptyComponent={listEmpty}471 ListEmptyComponent={listEmpty}
471 style={{ flex: 1 }}472 style={{ flex: 1 }}
472 automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"}473 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
473 contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"}474 contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"}
474 showsVerticalScrollIndicator={false}475 showsVerticalScrollIndicator={false}
475 keyboardDismissMode="on-drag"476 keyboardDismissMode="on-drag"
476 keyboardShouldPersistTaps="handled"477 keyboardShouldPersistTaps="handled"
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+8-1
...@@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill";...@@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill";
28import type { DraftComposerImageAttachment } from "../../lib/composerImages";28import type { DraftComposerImageAttachment } from "../../lib/composerImages";
29import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout";29import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout";
30import { scopedThreadKey } from "../../lib/scopedEntities";30import { scopedThreadKey } from "../../lib/scopedEntities";
31import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
31import type {32import type {
32 PendingApproval,33 PendingApproval,
33 PendingUserInput,34 PendingUserInput,
...@@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: {...@@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: {
197 entering={FadeInDown.duration(200)}198 entering={FadeInDown.duration(200)}
198 exiting={FadeOut.duration(140)}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 <View className="flex-row items-center gap-2">208 <View className="flex-row items-center gap-2">
202 <View className="flex-row items-center gap-1">209 <View className="flex-row items-center gap-1">
203 <View className="h-1.5 w-1.5 rounded-full bg-neutral-400 dark:bg-neutral-500" />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,6 +17,7 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
17import { AppText as Text } from "../../components/AppText";17import { AppText as Text } from "../../components/AppText";
18import { ControlPillMenu } from "../../components/ControlPill";18import { ControlPillMenu } from "../../components/ControlPill";
19import { SymbolView } from "../../components/AppSymbol";19import { SymbolView } from "../../components/AppSymbol";
20import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
20import { NativeStackScreenOptions } from "../../native/StackHeader";21import { NativeStackScreenOptions } from "../../native/StackHeader";
21import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";22import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
22import { useThemeColor } from "../../lib/useThemeColor";23import { useThemeColor } from "../../lib/useThemeColor";
...@@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane(...@@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane(
571 itemsAreEqual={homeListItemsAreEqual}572 itemsAreEqual={homeListItemsAreEqual}
572 keyExtractor={(item) => item.key}573 keyExtractor={(item) => item.key}
573 renderItem={renderListItem}574 renderItem={renderListItem}
574 automaticallyAdjustsScrollIndicatorInsets575 automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED}
575 contentInsetAdjustmentBehavior="automatic"576 contentInsetAdjustmentBehavior={
577 NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"
578 }
576 contentContainerStyle={[579 contentContainerStyle={[
577 styles.threadListContent,580 styles.threadListContent,
578 {581 {
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+2-1
...@@ -23,6 +23,7 @@ import {...@@ -23,6 +23,7 @@ import {
23} from "../../components/AndroidScreenHeader";23} from "../../components/AndroidScreenHeader";
24import { LoadingScreen } from "../../components/LoadingScreen";24import { LoadingScreen } from "../../components/LoadingScreen";
25import { scopedThreadKey } from "../../lib/scopedEntities";25import { scopedThreadKey } from "../../lib/scopedEntities";
26import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
26import { connectionTone } from "../connection/connectionTone";27import { connectionTone } from "../connection/connectionTone";
2728
28import {29import {
...@@ -278,7 +279,7 @@ function ThreadRouteContent(...@@ -278,7 +279,7 @@ function ThreadRouteContent(
278 );279 );
279280
280 /* ─── Native header theming ──────────────────────────────────────── */281 /* ─── Native header theming ──────────────────────────────────────── */
281 const usesNativeHeaderGlass = Platform.OS === "ios";282 const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED;
282 const headerSubtitle = [283 const headerSubtitle = [
283 selectedThreadProject?.title ?? null,284 selectedThreadProject?.title ?? null,
284 selectedEnvironmentConnection?.environmentLabel ?? null,285 selectedEnvironmentConnection?.environmentLabel ?? null,
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+5-4
...@@ -11,6 +11,7 @@ import {...@@ -11,6 +11,7 @@ import {
11import type { ReactNode } from "react";11import type { ReactNode } from "react";
12import { Platform, useColorScheme } from "react-native";12import { Platform, useColorScheme } from "react-native";
1313
14import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
14import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";15import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
1516
16const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);17const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
...@@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = {...@@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = {
33 headerLargeTitle: false,34 headerLargeTitle: false,
34 headerShadowVisible: false,35 headerShadowVisible: false,
35 headerShown: true,36 headerShown: true,
36 headerStyle: { backgroundColor: "transparent" },37 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
37 headerTitleStyle: { fontSize: 18, fontWeight: "800" },38 headerTitleStyle: { fontSize: 18, fontWeight: "800" },
38 headerTransparent: true,39 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
39 scrollEdgeEffects: SCROLL_EDGE_EFFECTS,40 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined,
40 title: "Threads",41 title: "Threads",
41 unstable_navigationItemStyle: "editor",42 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
42};43};
4344
44const SidebarStack = createNativeStackNavigator();45const 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);