History

Gate iOS glass layout on native support (#4032)

Julius Marminge committed 3 months ago

Commit b6d9ce31 parent 8e3467f10 files · +76 −28Browse files

Unified · Split

10 files
  1. apps/mobile/src/Stack.tsx +12 −6
  2. apps/mobile/src/features/files/FileTreeBrowser.tsx +7 −11
  3. apps/mobile/src/features/home/HomeScreen.tsx +4 −3
  4. apps/mobile/src/features/threads/ThreadDetailScreen.tsx +8 −1
  5. apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +5 −2
  6. apps/mobile/src/features/threads/ThreadRouteScreen.tsx +2 −1
  7. apps/mobile/src/features/threads/sidebar-navigation-shell.tsx +5 −4
  8. apps/mobile/src/lib/native-glass-capability.test.ts +18 −0
  9. apps/mobile/src/lib/native-glass-capability.ts +6 −0
  10. apps/mobile/src/native/native-glass.ts +9 −0
apps/mobile/src/Stack.tsx+12−6
@@ -59,6 +59,7 @@ import { · 58 unmodified lines
5959 EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
6060 transitionIncomingSharePresentation,
6161 } from "./features/sharing/incoming-share-presentation";
62+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
6263 import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
6364 import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
6465
@@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & { · 13 unmodified lines
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.
8385 const 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 @@
11 import type { ProjectEntry } from "@t3tools/contracts";
22 import { SymbolView } from "../../components/AppSymbol";
33 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";
125 import { useSafeAreaInsets } from "react-native-safe-area-context";
136
147 import { AppText as Text } from "../../components/AppText";
158 import { PierreEntryIcon } from "../../components/PierreEntryIcon";
169 import { cn } from "../../lib/cn";
1710 import { useThemeColor } from "../../lib/useThemeColor";
11+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1812 import {
1913 buildFileTree,
2014 defaultExpandedTreePaths,
@@ -129,7 +123,7 @@ export function FileTreeBrowser(props: { · 108 unmodified lines
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: { · 113 unmodified lines
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"; · 23 unmodified lines
2424 import type { WorkspaceState } from "../../state/workspaceModel";
2525 import type { SavedRemoteConnection } from "../../lib/connection";
2626 import { scopedProjectKey } from "../../lib/scopedEntities";
27+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2728 import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
2829 import type { PendingNewTask } from "../../state/use-pending-new-tasks";
2930 import {
@@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) { · 358 unmodified lines
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) { · 74 unmodified lines
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"; · 27 unmodified lines
2828 import type { DraftComposerImageAttachment } from "../../lib/composerImages";
2929 import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout";
3030 import { scopedThreadKey } from "../../lib/scopedEntities";
31+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
3132 import type {
3233 PendingApproval,
3334 PendingUserInput,
@@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: { · 163 unmodified lines
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"; · 16 unmodified lines
1717 import { AppText as Text } from "../../components/AppText";
1818 import { ControlPillMenu } from "../../components/ControlPill";
1919 import { SymbolView } from "../../components/AppSymbol";
20+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2021 import { NativeStackScreenOptions } from "../../native/StackHeader";
2122 import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
2223 import { useThemeColor } from "../../lib/useThemeColor";
@@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane( · 548 unmodified lines
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 { · 22 unmodified lines
2323 } from "../../components/AndroidScreenHeader";
2424 import { LoadingScreen } from "../../components/LoadingScreen";
2525 import { scopedThreadKey } from "../../lib/scopedEntities";
26+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2627 import { connectionTone } from "../connection/connectionTone";
2728
2829 import {
@@ -278,7 +279,7 @@ function ThreadRouteContent( · 249 unmodified lines
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 { · 10 unmodified lines
1111 import type { ReactNode } from "react";
1212 import { Platform, useColorScheme } from "react-native";
1313
14+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1415 import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
1516
1617 const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
@@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { · 16 unmodified lines
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
4445 const SidebarStack = createNativeStackNavigator();
apps/mobile/src/lib/native-glass-capability.test.ts+18−0
@@ -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+6−0
@@ -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+9−0
@@ -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+);