| author | |
| committer | |
| log | 383cc40f4d5d9f61d47b3caa28fac839a8b9c433 |
| tree | 484db112fb69caf5e68249180cb4b3f6ca1de4ab |
| parent | 75e4ceb96489a2fe92c710c79dbea59778cccfd1 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 58 insertions(+), 41 deletions(-)
apps/mobile/src/components/AndroidAnchoredMenu.tsx+2-14| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | import type { MenuAction, MenuComponentProps } from "@react-native-menu/menu"; |
| 2 | import { BlurView } from "expo-blur"; | |
| 3 | 2 | import type { ReactNode } from "react"; |
| 4 | 3 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 5 | 4 | import type { StyleProp, ViewStyle } from "react-native"; |
| ... | ... | @@ -8,11 +7,11 @@ import { useKeyboardState } from "react-native-keyboard-controller"; |
| 8 | 7 | import Animated, { FadeIn } from "react-native-reanimated"; |
| 9 | 8 | |
| 10 | 9 | import { appBlurTargetRef } from "../lib/appBlurTarget"; |
| 11 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 12 | 10 | import { cn } from "../lib/cn"; |
| 13 | 11 | import { type AppSymbolName, SymbolView } from "./AppSymbol"; |
| 14 | 12 | import { AppText as Text } from "./AppText"; |
| 15 | 13 | import { OverlayPortal } from "./OverlayPortal"; |
| 14 | import { GlassBackdrop } from "./GlassBackdrop"; | |
| 16 | 15 | |
| 17 | 16 | const MENU_WIDTH = 250; |
| 18 | 17 | const SCREEN_MARGIN = 12; |
| ... | ... | @@ -79,8 +78,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 79 | 78 | const anchorRef = useRef<View>(null); |
| 80 | 79 | const overlayRef = useRef<View>(null); |
| 81 | 80 | |
| 82 | const { themeAppearance } = useAppearancePreferences(); | |
| 83 | const isDarkMode = themeAppearance === "dark"; | |
| 84 | 81 | const keyboardVisible = useKeyboardState((state) => state.isVisible); |
| 85 | 82 | const keyboardHeight = useKeyboardState((state) => state.height); |
| 86 | 83 | const close = useCallback(() => { |
| ... | ... | @@ -227,16 +224,7 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 227 | 224 | : { bottom: (rootHeight ?? 0) - local.y + ANCHOR_GAP }), |
| 228 | 225 | }} |
| 229 | 226 | > |
| 230 | {/* Frosted backdrop: blur of the app content behind the menu, | |
| 231 | washed with the translucent card tone so rows keep contrast. */} | |
| 232 | <BlurView | |
| 233 | blurMethod="dimezisBlurView" | |
| 234 | blurTarget={appBlurTargetRef} | |
| 235 | intensity={40} | |
| 236 | tint={isDarkMode ? "dark" : "light"} | |
| 237 | className="absolute inset-0" | |
| 238 | /> | |
| 239 | <View className="absolute inset-0 bg-card-translucent" /> | |
| 227 | <GlassBackdrop blurTarget={appBlurTargetRef} /> | |
| 240 | 228 | {/* keyboardShouldPersistTaps: the menu often opens over an |
| 241 | 229 | active editor; the first item tap must act, not just |
| 242 | 230 | dismiss the keyboard. */} |
apps/mobile/src/components/GlassBackdrop.tsx created+53| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | import { BlurView } from "expo-blur"; | |
| 2 | import { useContext, type RefObject } from "react"; | |
| 3 | import { Platform, StyleSheet, View, type ColorValue } from "react-native"; | |
| 4 | ||
| 5 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 6 | import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; | |
| 7 | import { themeColorWithAlpha } from "../lib/mobileTheme"; | |
| 8 | ||
| 9 | /** Frosted backdrop for containers that clip their children to their shape. */ | |
| 10 | export function GlassBackdrop(props: { | |
| 11 | readonly fallbackColor?: ColorValue; | |
| 12 | readonly blurTarget?: RefObject<View | null>; | |
| 13 | }) { | |
| 14 | const { themeAppearance } = useAppearancePreferences(); | |
| 15 | const inheritedBlurTarget = useContext(GlassBlurTargetContext); | |
| 16 | const target = props.blurTarget ?? inheritedBlurTarget; | |
| 17 | const supportsBlur = | |
| 18 | Platform.OS === "ios" || | |
| 19 | (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined); | |
| 20 | const colorStyle = | |
| 21 | props.fallbackColor === undefined | |
| 22 | ? undefined | |
| 23 | : { backgroundColor: themeColorWithAlpha(String(props.fallbackColor), 1) }; | |
| 24 | ||
| 25 | return ( | |
| 26 | <> | |
| 27 | {/* Android samples a separate target. An opaque backing prevents any | |
| 28 | transparent pixels in that sample from exposing the unblurred feed. | |
| 29 | iOS samples its actual backdrop, so a backing there would hide it. */} | |
| 30 | {Platform.OS === "android" ? ( | |
| 31 | <View pointerEvents="none" className="absolute inset-0 bg-card" style={colorStyle} /> | |
| 32 | ) : null} | |
| 33 | {supportsBlur ? ( | |
| 34 | <BlurView | |
| 35 | pointerEvents="none" | |
| 36 | blurTarget={target} | |
| 37 | blurMethod="dimezisBlurViewSdk31Plus" | |
| 38 | intensity={80} | |
| 39 | tint={themeAppearance === "dark" ? "dark" : "default"} | |
| 40 | style={StyleSheet.absoluteFill} | |
| 41 | /> | |
| 42 | ) : null} | |
| 43 | <View | |
| 44 | pointerEvents="none" | |
| 45 | className="absolute inset-0 bg-card" | |
| 46 | style={[ | |
| 47 | colorStyle, | |
| 48 | { opacity: supportsBlur ? (themeAppearance === "dark" ? 0.25 : 0.55) : 1 }, | |
| 49 | ]} | |
| 50 | /> | |
| 51 | </> | |
| 52 | ); | |
| 53 | } |
apps/mobile/src/components/GlassSurface.tsx+3-27| ... | ... | @@ -1,9 +1,7 @@ |
| 1 | import { BlurView } from "expo-blur"; | |
| 2 | 1 | import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; |
| 3 | import { useContext, type ReactNode, type Ref, type RefObject } from "react"; | |
| 2 | import type { ReactNode, Ref, RefObject } from "react"; | |
| 4 | 3 | import { |
| 5 | 4 | Platform, |
| 6 | StyleSheet, | |
| 7 | 5 | useColorScheme, |
| 8 | 6 | View, |
| 9 | 7 | type ColorValue, |
| ... | ... | @@ -13,8 +11,7 @@ import { |
| 13 | 11 | import { withUniwind } from "uniwind"; |
| 14 | 12 | |
| 15 | 13 | import { cn } from "../lib/cn"; |
| 16 | import { GlassBlurTargetContext } from "../lib/glassBlurTarget"; | |
| 17 | import { themeColorWithAlpha } from "../lib/mobileTheme"; | |
| 14 | import { GlassBackdrop } from "./GlassBackdrop"; | |
| 18 | 15 | |
| 19 | 16 | // Explicit mappings keep the native glassEffectStyle enum out of style-array conversion. |
| 20 | 17 | const ThemedGlassView = withUniwind(GlassView, { |
| ... | ... | @@ -51,13 +48,6 @@ export function GlassSurface({ |
| 51 | 48 | ...props |
| 52 | 49 | }: GlassSurfaceProps) { |
| 53 | 50 | const isDarkMode = useColorScheme() === "dark"; |
| 54 | const inheritedBlurTarget = useContext(GlassBlurTargetContext); | |
| 55 | const target = blurTarget ?? inheritedBlurTarget; | |
| 56 | const supportsBlur = | |
| 57 | Platform.OS === "ios" || | |
| 58 | (Platform.OS === "android" && Platform.Version >= 31 && target !== undefined); | |
| 59 | const backgroundColor = | |
| 60 | fallbackColor === undefined ? undefined : themeColorWithAlpha(String(fallbackColor), 1); | |
| 61 | 51 | const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable(); |
| 62 | 52 | const surfaceStyle: ViewStyle = { |
| 63 | 53 | borderRadius: 32, |
| ... | ... | @@ -113,21 +103,7 @@ export function GlassSurface({ |
| 113 | 103 | )} |
| 114 | 104 | style={[surfaceStyle, style]} |
| 115 | 105 | > |
| 116 | {supportsBlur ? ( | |
| 117 | <BlurView | |
| 118 | pointerEvents="none" | |
| 119 | blurTarget={target} | |
| 120 | blurMethod="dimezisBlurViewSdk31Plus" | |
| 121 | intensity={80} | |
| 122 | tint={isDarkMode ? "dark" : "default"} | |
| 123 | style={StyleSheet.absoluteFill} | |
| 124 | /> | |
| 125 | ) : null} | |
| 126 | <View | |
| 127 | pointerEvents="none" | |
| 128 | className="absolute inset-0 bg-card" | |
| 129 | style={{ backgroundColor, opacity: supportsBlur ? (isDarkMode ? 0.25 : 0.55) : 1 }} | |
| 130 | /> | |
| 106 | <GlassBackdrop blurTarget={blurTarget} fallbackColor={fallbackColor} /> | |
| 131 | 107 | {children} |
| 132 | 108 | </View> |
| 133 | 109 | ); |