| 1 | import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; |
| 2 | import type { ReactNode, Ref } from "react"; |
| 3 | import { |
| 4 | Platform, |
| 5 | View, |
| 6 | type ColorValue, |
| 7 | type ViewInstance, |
| 8 | type ViewProps, |
| 9 | type ViewStyle, |
| 10 | } from "react-native"; |
| 11 | import { withUniwind } from "uniwind"; |
| 12 | |
| 13 | import { cn } from "../lib/cn"; |
| 14 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 15 | import { GlassBackdrop } from "./GlassBackdrop"; |
| 16 | |
| 17 | // Explicit mappings keep the native glassEffectStyle enum out of style-array conversion. |
| 18 | const ThemedGlassView = withUniwind(GlassView, { |
| 19 | style: { fromClassName: "className" }, |
| 20 | tintColor: { fromClassName: "tintColorClassName", styleProperty: "accentColor" }, |
| 21 | }); |
| 22 | |
| 23 | interface GlassSurfaceProps extends ViewProps { |
| 24 | readonly ref?: Ref<ViewInstance>; |
| 25 | readonly children: ReactNode; |
| 26 | readonly glassEffectStyle?: "clear" | "regular" | "none"; |
| 27 | readonly tintColor?: ColorValue; |
| 28 | readonly tintColorClassName?: string; |
| 29 | readonly chrome?: "default" | "none"; |
| 30 | /** Base color for the frosted tint, or solid fill when blur is unavailable. */ |
| 31 | readonly fallbackColor?: ColorValue; |
| 32 | /** Uniwind styling used only when native Liquid Glass is unavailable. */ |
| 33 | readonly fallbackClassName?: string; |
| 34 | } |
| 35 | |
| 36 | export function GlassSurface({ |
| 37 | ref, |
| 38 | children, |
| 39 | glassEffectStyle = "regular", |
| 40 | chrome = "default", |
| 41 | tintColor, |
| 42 | tintColorClassName, |
| 43 | fallbackColor, |
| 44 | fallbackClassName, |
| 45 | className, |
| 46 | style, |
| 47 | ...props |
| 48 | }: GlassSurfaceProps) { |
| 49 | const { themeAppearance } = useAppearancePreferences(); |
| 50 | const isDarkMode = themeAppearance === "dark"; |
| 51 | const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable(); |
| 52 | const hasShadow = chrome !== "none" && Platform.OS !== "android"; |
| 53 | const surfaceStyle: ViewStyle = { |
| 54 | borderRadius: 32, |
| 55 | overflow: "hidden", |
| 56 | shadowColor: hasShadow ? "#000000" : "transparent", |
| 57 | shadowOpacity: hasShadow ? (isDarkMode ? 0.22 : 0.08) : 0, |
| 58 | shadowRadius: hasShadow ? 28 : 0, |
| 59 | shadowOffset: { width: 0, height: hasShadow ? 14 : 0 }, |
| 60 | elevation: hasShadow ? 12 : 0, |
| 61 | }; |
| 62 | |
| 63 | if (supportsGlass) { |
| 64 | return ( |
| 65 | <ThemedGlassView |
| 66 | {...props} |
| 67 | ref={ref} |
| 68 | className={cn( |
| 69 | chrome === "none" |
| 70 | ? "border-0 border-transparent bg-transparent" |
| 71 | : "border border-border bg-glass-surface", |
| 72 | className, |
| 73 | )} |
| 74 | glassEffectStyle={glassEffectStyle} |
| 75 | tintColor={tintColor === undefined ? undefined : String(tintColor)} |
| 76 | tintColorClassName={ |
| 77 | tintColorClassName ?? (tintColor === undefined ? "accent-glass-tint" : undefined) |
| 78 | } |
| 79 | colorScheme={isDarkMode ? "dark" : "light"} |
| 80 | style={[surfaceStyle, style]} |
| 81 | > |
| 82 | {children} |
| 83 | </ThemedGlassView> |
| 84 | ); |
| 85 | } |
| 86 | |
| 87 | return ( |
| 88 | <View |
| 89 | {...props} |
| 90 | ref={ref} |
| 91 | className={cn( |
| 92 | chrome === "none" ? "border-0 border-transparent" : "border border-border", |
| 93 | fallbackClassName, |
| 94 | className, |
| 95 | )} |
| 96 | style={[surfaceStyle, style]} |
| 97 | > |
| 98 | <GlassBackdrop fallbackColor={fallbackColor} /> |
| 99 | {children} |
| 100 | </View> |
| 101 | ); |
| 102 | } |