From 1c5ef36c8952b2ce72481394ac4b3799ec2cc69b Mon Sep 17 00:00:00 2001 From: jensvansteen Date: Tue, 7 Apr 2026 13:40:19 +0700 Subject: [PATCH 1/2] feat: fix app store example --- example/src/App.tsx | 126 ++++++++++++++++-- example/src/components/ExampleLayout.tsx | 61 --------- example/src/components/ExampleMenuScreen.tsx | 4 +- example/src/data.ts | 86 ++---------- example/src/screens/AppStoreScreen.tsx | 84 ++++++++---- example/src/screens/CalendarScreen.tsx | 47 ++++--- example/src/screens/ExampleScreen.tsx | 34 ----- example/src/screens/PrDetailScreen.tsx | 60 ++++++--- example/src/screens/PullRequestsScreen.tsx | 53 +++++--- example/src/screens/SearchBarScreen.tsx | 46 +++++-- example/src/screens/TabAccessoryScreen.tsx | 59 +++++--- example/src/screens/ToolbarScreen.tsx | 41 ++++-- .../src/screens/TransitionShowcaseScreen.tsx | 81 ++++++----- example/src/types.ts | 35 +++-- 14 files changed, 460 insertions(+), 357 deletions(-) delete mode 100644 example/src/components/ExampleLayout.tsx delete mode 100644 example/src/screens/ExampleScreen.tsx diff --git a/example/src/App.tsx b/example/src/App.tsx index f1eff4a..db46ebf 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,8 +6,14 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { enableScreens } from 'react-native-screens'; import { ExampleMenuScreen } from './components/ExampleMenuScreen'; -import { ExampleScreen } from './screens/ExampleScreen'; -import { navigationByKey, titleByKey } from './data'; +import { AppStoreScreen } from './screens/AppStoreScreen'; +import { PullRequestsScreen } from './screens/PullRequestsScreen'; +import { PrDetailScreen } from './screens/PrDetailScreen'; +import { TransitionShowcaseScreen } from './screens/TransitionShowcaseScreen'; +import { ToolbarScreen } from './screens/ToolbarScreen'; +import { SearchBarScreen } from './screens/SearchBarScreen'; +import { TabAccessoryScreen } from './screens/TabAccessoryScreen'; +import { CalendarScreen } from './screens/CalendarScreen'; import type { RootStackParamList } from './types'; enableScreens(); @@ -47,20 +53,124 @@ export default function App() { }} /> ({ - title: titleByKey[route.params.key], + name="AppStore" + component={AppStoreScreen} + options={{ + title: 'App Store Listing', + headerShadowVisible: false, + headerTransparent: true, + headerLargeTitleEnabled: false, + headerBackButtonDisplayMode: 'minimal', + headerTintColor: DynamicColorIOS({ + light: 'black', + dark: 'white', + }) as string, + }} + /> + + + + + + + diff --git a/example/src/components/ExampleLayout.tsx b/example/src/components/ExampleLayout.tsx deleted file mode 100644 index aeabec5..0000000 --- a/example/src/components/ExampleLayout.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { ScrollView, StyleSheet } from 'react-native'; -import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; - -export function ExampleLayout({ - children, - topBar, - bottomBar, - estimatedTopBarHeight, - estimatedBottomBarHeight, -}: { - children: React.ReactNode; - topBar?: React.ReactNode; - bottomBar?: React.ReactNode; - estimatedTopBarHeight?: number; - estimatedBottomBarHeight?: number; -}) { - return ( - - {topBar ? ( - - {topBar} - - ) : null} - - - {children} - - - {bottomBar ? ( - - {bottomBar} - - ) : null} - - ); -} - -export const segmentedStyle = StyleSheet.create({ - segmented: { height: 32 }, -}).segmented; - -const styles = StyleSheet.create({ - scrollEdgeBar: { - flex: 1, - }, - topBar: { - paddingHorizontal: 16, - paddingVertical: 8, - backgroundColor: 'transparent', - }, - bottomBar: { - paddingHorizontal: 16, - paddingVertical: 12, - backgroundColor: 'transparent', - }, -}); diff --git a/example/src/components/ExampleMenuScreen.tsx b/example/src/components/ExampleMenuScreen.tsx index e7e440c..1992897 100644 --- a/example/src/components/ExampleMenuScreen.tsx +++ b/example/src/components/ExampleMenuScreen.tsx @@ -13,12 +13,12 @@ export function ExampleMenuScreen({ navigation }: { navigation: any }) { return ( item.key} + keyExtractor={(item) => item.route} style={styles.menuList} contentContainerStyle={styles.menuContent} renderItem={({ item }) => ( navigation.push('Example', { key: item.key })} + onPress={() => navigation.push(item.route)} style={({ pressed }) => [ styles.menuItem, pressed && styles.menuItemPressed, diff --git a/example/src/data.ts b/example/src/data.ts index 24b30c2..8c9bc17 100644 --- a/example/src/data.ts +++ b/example/src/data.ts @@ -1,127 +1,61 @@ -import type { ExampleKey, ExampleNavigationConfig } from './types'; +import type { ExampleRouteName } from './types'; export const exampleList: Array<{ - key: ExampleKey; + route: ExampleRouteName; title: string; subtitle: string; symbol: string; - navigation?: ExampleNavigationConfig; }> = [ { - key: 'appStore', + route: 'AppStore', title: 'App Store Listing', subtitle: 'Segmented control top bar', symbol: 'bag', - navigation: { - title: 'App Store Listing', - headerTransparent: true, - headerLargeTitleEnabled: false, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'pullRequests', + route: 'PullRequests', title: 'Pull Requests', subtitle: 'Filter chips with large title', symbol: 'arrow.triangle.pull', - navigation: { - title: 'Pull Requests', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'prDetail', + route: 'PrDetail', title: 'PR Detail', subtitle: 'Review banner + action buttons', symbol: 'text.page.badge.magnifyingglass', - navigation: { - title: 'PR Detail', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'transitionShowcase', + route: 'TransitionShowcase', title: 'Transition Showcase', subtitle: 'Top and bottom bars over color blocks', symbol: 'paintpalette', - navigation: { - title: 'Transition Showcase', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'toolbar', + route: 'Toolbar', title: 'Toolbar', subtitle: 'Bottom edge bar emphasis', symbol: 'hammer', - navigation: { - title: 'Toolbar', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'searchBar', + route: 'SearchBar', title: 'Search Bar', subtitle: 'Search-like screen with segmented top bar', symbol: 'magnifyingglass', - navigation: { - title: 'Search Bar', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'tabAccessory', + route: 'TabAccessory', title: 'Tab Accessory', subtitle: 'Large bottom accessory-style bar', symbol: 'music.note.list', - navigation: { - title: 'Tab Accessory', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, { - key: 'calendar', + route: 'Calendar', title: 'Calendar', subtitle: 'Week selector with stronger top bar', symbol: 'calendar', - navigation: { - title: 'Calendar', - headerTransparent: true, - headerLargeTitleEnabled: true, - headerShadowVisible: false, - headerBackButtonDisplayMode: 'minimal', - }, }, ]; -export const titleByKey: Record = Object.fromEntries( - exampleList.map((item) => [item.key, item.title]) -) as Record; - -export const navigationByKey: Record = - Object.fromEntries( - exampleList.map((item) => [item.key, item.navigation ?? {}]) - ) as Record; - export const appColors = [ '#4fd1c5', '#68d391', diff --git a/example/src/screens/AppStoreScreen.tsx b/example/src/screens/AppStoreScreen.tsx index 0fa272c..56ce2b4 100644 --- a/example/src/screens/AppStoreScreen.tsx +++ b/example/src/screens/AppStoreScreen.tsx @@ -1,47 +1,73 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; +import { + DynamicColorIOS, + PlatformColor, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; -import { ExampleLayout, segmentedStyle } from '../components/ExampleLayout'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; import { appColors } from '../data'; export function AppStoreScreen() { return ( - + - } - > - {Array.from({ length: 16 }).map((_, index) => ( - - - - App {index + 1} - - Top downloaded this week - - - - - {index % 4 === 0 ? 'GET' : 'OPEN'} - - + + + + + {Array.from({ length: 16 }).map((_, index) => ( + + + + App {index + 1} + + Top downloaded this week + + + + + {index % 4 === 0 ? 'GET' : 'OPEN'} + + + + ))} - ))} - + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, + segmentedControl: { + height: 32, + }, + screenBackground: { + flex: 1, + backgroundColor: PlatformColor('systemGroupedBackground'), + }, listCard: { height: 92, paddingHorizontal: 16, diff --git a/example/src/screens/CalendarScreen.tsx b/example/src/screens/CalendarScreen.tsx index 7a78dd3..5cb8a1c 100644 --- a/example/src/screens/CalendarScreen.tsx +++ b/example/src/screens/CalendarScreen.tsx @@ -1,11 +1,16 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; -import { ExampleLayout } from '../components/ExampleLayout'; +import { + DynamicColorIOS, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; export function CalendarScreen() { return ( - + {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((day, index) => ( @@ -24,21 +29,31 @@ export function CalendarScreen() { Tuesday - Feb 3, 2026 - } - > - {Array.from({ length: 24 }).map((_, index) => ( - - - {String(index).padStart(2, '0')}:00 - - Event slot {index + 1} - - ))} - + + + + {Array.from({ length: 24 }).map((_, index) => ( + + + {String(index).padStart(2, '0')}:00 + + Event slot {index + 1} + + ))} + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, calendarTopBar: { gap: 10, }, diff --git a/example/src/screens/ExampleScreen.tsx b/example/src/screens/ExampleScreen.tsx deleted file mode 100644 index b57180f..0000000 --- a/example/src/screens/ExampleScreen.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { ExampleKey } from '../types'; -import { AppStoreScreen } from './AppStoreScreen'; -import { PullRequestsScreen } from './PullRequestsScreen'; -import { PrDetailScreen } from './PrDetailScreen'; -import { TransitionShowcaseScreen } from './TransitionShowcaseScreen'; -import { ToolbarScreen } from './ToolbarScreen'; -import { SearchBarScreen } from './SearchBarScreen'; -import { TabAccessoryScreen } from './TabAccessoryScreen'; -import { CalendarScreen } from './CalendarScreen'; - -export function ExampleScreen({ - route, -}: { - route: { params: { key: ExampleKey } }; -}) { - switch (route.params.key) { - case 'appStore': - return ; - case 'pullRequests': - return ; - case 'prDetail': - return ; - case 'transitionShowcase': - return ; - case 'toolbar': - return ; - case 'searchBar': - return ; - case 'tabAccessory': - return ; - case 'calendar': - return ; - } -} diff --git a/example/src/screens/PrDetailScreen.tsx b/example/src/screens/PrDetailScreen.tsx index afc45b9..a0ce4bc 100644 --- a/example/src/screens/PrDetailScreen.tsx +++ b/example/src/screens/PrDetailScreen.tsx @@ -1,13 +1,21 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; -import { ExampleLayout } from '../components/ExampleLayout'; +import { + DynamicColorIOS, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; export function PrDetailScreen() { return ( - + alexjohnson requested your review @@ -16,8 +24,21 @@ export function PrDetailScreen() { Review - } - bottomBar={ + + + + {Array.from({ length: 10 }).map((_, index) => ( + + Section {index + 1} + + This is placeholder content for the PR detail example. The goal is + to verify top and bottom edge bars against long scrolling content. + + + ))} + + + 2 comments @@ -29,22 +50,25 @@ export function PrDetailScreen() { - } - > - {Array.from({ length: 10 }).map((_, index) => ( - - Section {index + 1} - - This is placeholder content for the PR detail example. The goal is - to verify top and bottom edge bars against long scrolling content. - - - ))} - + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, + bottomBar: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: 'transparent', + }, banner: { flexDirection: 'row', alignItems: 'center', diff --git a/example/src/screens/PullRequestsScreen.tsx b/example/src/screens/PullRequestsScreen.tsx index 668e9a2..3d0ca91 100644 --- a/example/src/screens/PullRequestsScreen.tsx +++ b/example/src/screens/PullRequestsScreen.tsx @@ -6,7 +6,7 @@ import { View, } from 'react-native'; import { SFSymbolView, SFSymbolWeight } from 'react-native-nitro-sfsymbols'; -import { ExampleLayout } from '../components/ExampleLayout'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; const prItems = [ { @@ -46,9 +46,8 @@ const filterChips = ['Open', 'Involved', 'Visibility', 'Org']; export function PullRequestsScreen() { return ( - + ))} - } - > - {prItems.map((item, index) => ( - - - - {item.title} - - {item.repo} · {item.age} - + + + + {prItems.map((item, index) => ( + + + + {item.title} + + {item.repo} · {item.age} + + - - ))} - + ))} + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, chipsRow: { gap: 8, paddingRight: 16, diff --git a/example/src/screens/SearchBarScreen.tsx b/example/src/screens/SearchBarScreen.tsx index d9d73d8..aa2cc7f 100644 --- a/example/src/screens/SearchBarScreen.tsx +++ b/example/src/screens/SearchBarScreen.tsx @@ -1,30 +1,48 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; +import { + DynamicColorIOS, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; -import { ExampleLayout, segmentedStyle } from '../components/ExampleLayout'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; export function SearchBarScreen() { return ( - + - } - > - {Array.from({ length: 30 }).map((_, index) => ( - - Result {index + 1} - - ))} - + + + + {Array.from({ length: 30 }).map((_, index) => ( + + Result {index + 1} + + ))} + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, + segmentedControl: { + height: 32, + }, searchResultRow: { paddingHorizontal: 16, paddingVertical: 18, diff --git a/example/src/screens/TabAccessoryScreen.tsx b/example/src/screens/TabAccessoryScreen.tsx index f1fd5a2..1e7692a 100644 --- a/example/src/screens/TabAccessoryScreen.tsx +++ b/example/src/screens/TabAccessoryScreen.tsx @@ -1,21 +1,39 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; +import { + DynamicColorIOS, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; -import { ExampleLayout, segmentedStyle } from '../components/ExampleLayout'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; export function TabAccessoryScreen() { return ( - + - } - bottomBar={ + + + + {Array.from({ length: 24 }).map((_, index) => ( + + Track {index + 1} + Artist name + + ))} + + + Now Playing Neon Skyline - Luna Park @@ -28,19 +46,28 @@ export function TabAccessoryScreen() { - } - > - {Array.from({ length: 24 }).map((_, index) => ( - - Track {index + 1} - Artist name - - ))} - + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, + bottomBar: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: 'transparent', + }, + segmentedControl: { + height: 32, + }, tabAccessoryBar: { gap: 10, }, diff --git a/example/src/screens/ToolbarScreen.tsx b/example/src/screens/ToolbarScreen.tsx index f48f359..adec8aa 100644 --- a/example/src/screens/ToolbarScreen.tsx +++ b/example/src/screens/ToolbarScreen.tsx @@ -1,12 +1,25 @@ -import { DynamicColorIOS, StyleSheet, Text, View } from 'react-native'; -import { ExampleLayout } from '../components/ExampleLayout'; +import { + DynamicColorIOS, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; export function ToolbarScreen() { return ( - + + {Array.from({ length: 24 }).map((_, index) => ( + + Item {index + 1} + + ))} + + + 30 items @@ -14,18 +27,20 @@ export function ToolbarScreen() { Select All - } - > - {Array.from({ length: 24 }).map((_, index) => ( - - Item {index + 1} - - ))} - + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + bottomBar: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: 'transparent', + }, toolbarCard: { marginHorizontal: 16, marginTop: 12, diff --git a/example/src/screens/TransitionShowcaseScreen.tsx b/example/src/screens/TransitionShowcaseScreen.tsx index 405e494..dd906e8 100644 --- a/example/src/screens/TransitionShowcaseScreen.tsx +++ b/example/src/screens/TransitionShowcaseScreen.tsx @@ -1,22 +1,48 @@ -import { StyleSheet, Switch, Text, View } from 'react-native'; +import { ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; -import { ExampleLayout, segmentedStyle } from '../components/ExampleLayout'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; import { sharedStyles } from '../styles/shared'; import { transitionColors } from '../data'; export function TransitionShowcaseScreen() { return ( - + - } - bottomBar={ + + + + {transitionColors.map((color, index) => ( + + + {color.name} + + + ))} + + + Test @@ -25,33 +51,28 @@ export function TransitionShowcaseScreen() { Reset - } - > - {transitionColors.map((color, index) => ( - - - {color.name} - - - ))} - + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, + topBar: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'transparent', + }, + bottomBar: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: 'transparent', + }, + segmentedControl: { + height: 32, + }, colorBlock: { height: 320, alignItems: 'center', diff --git a/example/src/types.ts b/example/src/types.ts index a994036..0648031 100644 --- a/example/src/types.ts +++ b/example/src/types.ts @@ -1,22 +1,21 @@ -export type ExampleKey = - | 'appStore' - | 'pullRequests' - | 'prDetail' - | 'transitionShowcase' - | 'toolbar' - | 'searchBar' - | 'tabAccessory' - | 'calendar'; - -export type ExampleNavigationConfig = { - title?: string; - headerTransparent?: boolean; - headerLargeTitleEnabled?: boolean; - headerShadowVisible?: boolean; - headerBackButtonDisplayMode?: 'default' | 'generic' | 'minimal'; -}; +export type ExampleRouteName = + | 'AppStore' + | 'PullRequests' + | 'PrDetail' + | 'TransitionShowcase' + | 'Toolbar' + | 'SearchBar' + | 'TabAccessory' + | 'Calendar'; export type RootStackParamList = { Home: undefined; - Example: { key: ExampleKey }; + AppStore: undefined; + PullRequests: undefined; + PrDetail: undefined; + TransitionShowcase: undefined; + Toolbar: undefined; + SearchBar: undefined; + TabAccessory: undefined; + Calendar: undefined; }; From 610a96f4787ad0bde88010e3e949451b674b6355 Mon Sep 17 00:00:00 2001 From: jensvansteen Date: Wed, 8 Apr 2026 10:19:43 +0700 Subject: [PATCH 2/2] feat: updated calander example + added hard and soft edge effects --- .../nitro/scrolledgebar/ScrollEdgeBar.kt | 17 +- example/ios/Podfile.lock | 36 +- example/package.json | 1 + example/src/App.tsx | 4 +- example/src/screens/AppStoreScreen.tsx | 2 +- example/src/screens/CalendarScreen.tsx | 273 +++++++--- example/src/screens/PrDetailScreen.tsx | 492 ++++++++++++++++-- ios/HybridScrollEdgeBar.swift | 106 +++- src/RNScrollEdgeBar.nitro.ts | 4 + yarn.lock | 11 + 10 files changed, 818 insertions(+), 128 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/scrolledgebar/ScrollEdgeBar.kt b/android/src/main/java/com/margelo/nitro/scrolledgebar/ScrollEdgeBar.kt index 1a80865..deb23bb 100644 --- a/android/src/main/java/com/margelo/nitro/scrolledgebar/ScrollEdgeBar.kt +++ b/android/src/main/java/com/margelo/nitro/scrolledgebar/ScrollEdgeBar.kt @@ -3,19 +3,16 @@ package com.margelo.nitro.scrolledgebar import android.view.View import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.uimanager.ThemedReactContext -import androidx.core.graphics.toColorInt @DoNotStrip -class HybridScrollEdgeBar(val context: ThemedReactContext) : HybridScrollEdgeBarSpec() { +class HybridScrollEdgeBar(private val context: ThemedReactContext) : HybridRNScrollEdgeBarSpec() { override val view: View = View(context) - private var _color = "#000" - override var color: String - get() = _color - set(value) { - _color = value - val color = value.toColorInt() - view.setBackgroundColor(color) - } + override var estimatedTopBarHeight: Double? = null + override var estimatedBottomBarHeight: Double? = null + override var topBarOffset: Double? = null + override var bottomBarOffset: Double? = null + override var topEdgeEffectStyle: String? = null + override var bottomEdgeEffectStyle: String? = null } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 857970c..e12e15e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,6 +8,34 @@ PODS: - hermes-engine (0.14.0): - hermes-engine/Pre-built (= 0.14.0) - hermes-engine/Pre-built (0.14.0) + - LiquidGlass (0.7.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - NitroModules (0.33.7): - boost - DoubleConversion @@ -2725,6 +2753,7 @@ DEPENDENCIES: - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - "LiquidGlass (from `../node_modules/@callstack/liquid-glass`)" - NitroModules (from `../node_modules/react-native-nitro-modules`) - NitroSfsymbols (from `../node_modules/react-native-nitro-sfsymbols`) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -2825,6 +2854,8 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-v0.14.0 + LiquidGlass: + :path: "../node_modules/@callstack/liquid-glass" NitroModules: :path: "../node_modules/react-native-nitro-modules" NitroSfsymbols: @@ -2988,6 +3019,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 375b17d1732b179ad743a2a0c57569a0de17e767 + LiquidGlass: e177e3bf641a7f4dc5fc596825b5d2b4fada4e9e NitroModules: e8ec707a245a85cf1eb4289f91a9372f9590a897 NitroSfsymbols: 5853b2baaf1a21db8a27d18d66b8fcd7fdafdc76 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f @@ -3065,8 +3097,8 @@ SPEC CHECKSUMS: RNScreens: c77cc7a5d1fbc23f0725b54e96eec81118453382 ScrollEdgeBar: 3823674d457505ae0598b99e99dfc5c70015d062 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 7a9f26c70daf0b08d82ec2f862e9a8872442129e + Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e PODFILE CHECKSUM: 9f12b969a6ade18978b8031561752c8cfbb0aed8 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/package.json b/example/package.json index 89cbfe2..0c8d592 100644 --- a/example/package.json +++ b/example/package.json @@ -10,6 +10,7 @@ "build:ios": "react-native build-ios --mode Debug" }, "dependencies": { + "@callstack/liquid-glass": "^0.7.1", "@react-native-segmented-control/segmented-control": "^2.5.7", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index db46ebf..066a305 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -89,7 +89,6 @@ export default function App() { title: 'PR Detail', headerShadowVisible: false, headerTransparent: true, - headerLargeTitleEnabled: true, headerBackButtonDisplayMode: 'minimal', headerTintColor: DynamicColorIOS({ light: 'black', @@ -161,10 +160,9 @@ export default function App() { name="Calendar" component={CalendarScreen} options={{ - title: 'Calendar', + title: 'February', headerShadowVisible: false, headerTransparent: true, - headerLargeTitleEnabled: true, headerBackButtonDisplayMode: 'minimal', headerTintColor: DynamicColorIOS({ light: 'black', diff --git a/example/src/screens/AppStoreScreen.tsx b/example/src/screens/AppStoreScreen.tsx index 56ce2b4..ab3be2b 100644 --- a/example/src/screens/AppStoreScreen.tsx +++ b/example/src/screens/AppStoreScreen.tsx @@ -16,7 +16,7 @@ export function AppStoreScreen() { diff --git a/example/src/screens/CalendarScreen.tsx b/example/src/screens/CalendarScreen.tsx index 5cb8a1c..7fbd2be 100644 --- a/example/src/screens/CalendarScreen.tsx +++ b/example/src/screens/CalendarScreen.tsx @@ -1,5 +1,5 @@ import { - DynamicColorIOS, + PlatformColor, ScrollView, StyleSheet, Text, @@ -7,39 +7,138 @@ import { } from 'react-native'; import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; +const hours = Array.from( + { length: 24 }, + (_, hour) => `${String(hour).padStart(2, '0')}:00` +); + +const events = [ + { + title: 'Morning Run', + startHour: 7, + durationHours: 1, + color: '#34C759', + }, + { + title: 'Team Standup', + startHour: 9, + durationHours: 1, + color: '#007AFF', + }, + { + title: 'Design Review', + startHour: 11, + durationHours: 2, + color: '#AF52DE', + }, + { + title: 'Lunch', + startHour: 13, + durationHours: 1, + color: '#FF9F0A', + }, + { + title: 'Focus Time', + startHour: 15, + durationHours: 2, + color: '#5AC8FA', + }, + { + title: 'Gym', + startHour: 18, + durationHours: 1, + color: '#FF3B30', + }, +]; + +const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; +const weekDates = [2, 3, 4, 5, 6, 7, 8]; +const todayIndex = 1; + export function CalendarScreen() { return ( - + - {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((day, index) => ( - - {day} - - {index + 2} - - - ))} + {weekDays.map((day, index) => { + const isToday = index === todayIndex; + const isSunday = index === 0; + + return ( + + + {day} + + {isToday ? ( + + + {weekDates[index]} + + + ) : ( + + {weekDates[index]} + + )} + + ); + })} - Tuesday - Feb 3, 2026 + + Tuesday – Feb 3, 2026 + - - {Array.from({ length: 24 }).map((_, index) => ( - - - {String(index).padStart(2, '0')}:00 - - Event slot {index + 1} - - ))} + + + {hours.map((hour, index) => { + const event = events.find((item) => item.startHour === index); + + return ( + + {hour} + + + {event ? ( + + + {event.title} + + + ) : null} + + + ); + })} + ); @@ -50,62 +149,120 @@ const styles = StyleSheet.create({ flex: 1, }, topBar: { - paddingHorizontal: 16, - paddingVertical: 8, + paddingHorizontal: 0, + paddingTop: 4, + paddingBottom: 0, backgroundColor: 'transparent', }, calendarTopBar: { - gap: 10, + backgroundColor: 'transparent', }, weekRow: { flexDirection: 'row', - justifyContent: 'space-between', + alignItems: 'flex-start', + paddingHorizontal: 8, }, weekDay: { - alignItems: 'center', flex: 1, + alignItems: 'center', + gap: 2, }, weekDayLabel: { - fontSize: 12, - color: DynamicColorIOS({ light: '#666666', dark: '#a1a1aa' }), + fontSize: 11, + lineHeight: 14, + fontWeight: '500', + color: PlatformColor('secondaryLabel'), + }, + weekDayLabelSunday: { + color: '#FF3B30', }, weekDayNumber: { - marginTop: 4, - width: 28, - height: 28, - borderRadius: 14, + width: 32, + height: 32, textAlign: 'center', textAlignVertical: 'center', - lineHeight: 28, - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + lineHeight: 32, + fontSize: 17, + color: PlatformColor('label'), + }, + weekDayNumberSunday: { + color: '#FF3B30', }, - weekDayNumberActive: { - backgroundColor: '#007aff', - color: '#ffffff', + todayCircle: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: PlatformColor('label'), + alignItems: 'center', + justifyContent: 'center', + }, + todayDateText: { + fontSize: 17, + fontWeight: '700', + color: PlatformColor('systemBackground'), }, calendarDateLabel: { + marginTop: 6, textAlign: 'center', fontSize: 15, + lineHeight: 20, fontWeight: '600', - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + color: PlatformColor('label'), + }, + separator: { + marginTop: 8, + height: StyleSheet.hairlineWidth, + backgroundColor: PlatformColor('separator'), }, - calendarRow: { + scrollView: { + flex: 1, + backgroundColor: PlatformColor('systemBackground'), + }, + timeline: { + backgroundColor: PlatformColor('systemBackground'), + }, + hourRow: { + minHeight: 44, flexDirection: 'row', - alignItems: 'center', - gap: 16, - minHeight: 72, - paddingHorizontal: 16, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#d9d9d9', - backgroundColor: DynamicColorIOS({ light: '#ffffff', dark: '#1b1b1d' }), - }, - calendarHour: { - width: 56, - fontSize: 13, - color: DynamicColorIOS({ light: '#666666', dark: '#a1a1aa' }), + backgroundColor: PlatformColor('systemBackground'), }, - calendarEvent: { - fontSize: 15, - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + hourLabel: { + width: 60, + paddingTop: 12, + paddingLeft: 16, + fontSize: 12, + lineHeight: 16, + fontVariant: ['tabular-nums'], + color: PlatformColor('secondaryLabel'), + }, + hourTrack: { + flex: 1, + paddingRight: 16, + position: 'relative', + overflow: 'visible', + }, + hourDivider: { + position: 'absolute', + top: 0, + left: 0, + right: 16, + height: StyleSheet.hairlineWidth, + backgroundColor: PlatformColor('separator'), + }, + eventBlock: { + position: 'absolute', + top: 2, + left: 0, + right: 16, + borderRadius: 6, + borderWidth: 2, + paddingTop: 6, + paddingHorizontal: 8, + zIndex: 2, + }, + eventTitle: { + fontSize: 13, + lineHeight: 16, + fontWeight: '600', }, }); diff --git a/example/src/screens/PrDetailScreen.tsx b/example/src/screens/PrDetailScreen.tsx index a0ce4bc..c22d9fb 100644 --- a/example/src/screens/PrDetailScreen.tsx +++ b/example/src/screens/PrDetailScreen.tsx @@ -1,60 +1,288 @@ import { - DynamicColorIOS, + type ColorValue, + PlatformColor, + Pressable, ScrollView, StyleSheet, Text, View, + type StyleProp, + type ViewStyle, } from 'react-native'; +import { SFSymbolView, SFSymbolWeight } from 'react-native-nitro-sfsymbols'; +import { LiquidGlassView } from '@callstack/liquid-glass'; import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; -import { sharedStyles } from '../styles/shared'; + +const conversationItems = Array.from({ length: 5 }, (_, index) => ({ + id: index + 1, + text: `Comment ${index + 1} — Discussion about the implementation details`, +})); export function PrDetailScreen() { return ( - - + + + alexjohnson requested your review - - Review + + Review - + - - {Array.from({ length: 10 }).map((_, index) => ( - - Section {index + 1} - - This is placeholder content for the PR detail example. The goal is - to verify top and bottom edge bars against long scrolling content. - - - ))} + + + + + + + + + + + + + + + + + + {conversationItems.map((item, index) => ( + + + {index < conversationItems.length - 1 ? : null} + + ))} + + + - - 2 comments - - - Info - - - Comment - - + + + ); } +function SectionHeader({ title }: { title: string }) { + return {title}; +} + +function CardGroup({ children }: { children: React.ReactNode }) { + return {children}; +} + +function Divider() { + return ; +} + +function DetailRow({ + icon, + title, + detail, + detailColor, +}: { + icon: string; + title: string; + detail: string; + detailColor: ColorValue; +}) { + return ( + + + {title} + + {detail} + + + ); +} + +function ReviewsBlock() { + return ( + + + + Reviews + + + + + Add Your Review + + + ); +} + +function MergeBlock() { + return ( + + + + + Ready to merge + + This branch has no conflicts with the base branch. + + + + + + + Merge Pull Request + + + + + + + ); +} + +function ConversationRow({ text }: { text: string }) { + return ( + + + {text} + + ); +} + +function GlassButton({ + title, + icon, + tintColor, +}: { + title?: string; + icon: string; + tintColor?: string; +}) { + return ( + + + + {title ? {title} : null} + + + ); +} + +function GlassSurface({ + children, + style, + tintColor, + interactive = false, +}: { + children: React.ReactNode; + style?: StyleProp; + tintColor?: string; + interactive?: boolean; +}) { + if (__DEV__) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + const styles = StyleSheet.create({ container: { flex: 1, @@ -66,41 +294,205 @@ const styles = StyleSheet.create({ }, bottomBar: { paddingHorizontal: 16, - paddingVertical: 12, + paddingTop: 6, + paddingBottom: 4, backgroundColor: 'transparent', }, - banner: { + reviewBanner: { + minHeight: 54, + borderRadius: 24, + paddingHorizontal: 14, + paddingVertical: 10, flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: 10, }, - bannerText: { + reviewBannerText: { flex: 1, + fontSize: 13, + lineHeight: 18, + fontWeight: '500', + color: PlatformColor('label'), + }, + reviewButton: { + minHeight: 30, + paddingHorizontal: 14, + borderRadius: 999, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: PlatformColor('secondarySystemGroupedBackground'), + }, + reviewButtonText: { fontSize: 13, fontWeight: '600', - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + color: '#007AFF', + }, + scrollView: { + flex: 1, + backgroundColor: PlatformColor('systemGroupedBackground'), + }, + scrollContent: { + paddingBottom: 36, }, - actionButtons: { - marginLeft: 'auto', + sectionHeader: { + marginTop: 24, + marginBottom: 12, + marginHorizontal: 16, + fontSize: 22, + lineHeight: 26, + fontWeight: '700', + color: PlatformColor('label'), + }, + cardGroup: { + marginHorizontal: 16, + borderRadius: 14, + overflow: 'hidden', + backgroundColor: PlatformColor('secondarySystemGroupedBackground'), + }, + divider: { + marginLeft: 16, + height: StyleSheet.hairlineWidth, + backgroundColor: PlatformColor('separator'), + }, + row: { + minHeight: 52, + paddingHorizontal: 16, + paddingVertical: 14, flexDirection: 'row', + alignItems: 'center', gap: 10, }, - detailBlock: { - paddingHorizontal: 16, - paddingVertical: 20, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#d9d9d9', - backgroundColor: DynamicColorIOS({ light: '#f7f7f8', dark: '#17171a' }), + rowTitle: { + fontSize: 15, + lineHeight: 20, + color: PlatformColor('label'), }, - sectionTitle: { - fontSize: 18, - fontWeight: '700', - color: DynamicColorIOS({ light: '#111111', dark: '#f5f5f5' }), + rowSpacer: { + flex: 1, }, - sectionBody: { - marginTop: 8, + rowDetail: { fontSize: 15, - lineHeight: 22, - color: DynamicColorIOS({ light: '#444444', dark: '#d4d4d8' }), + lineHeight: 20, + }, + reviewsBlock: { + paddingHorizontal: 16, + paddingVertical: 14, + }, + reviewsDot: { + width: 16, + height: 16, + borderRadius: 8, + backgroundColor: PlatformColor('systemGray4'), + }, + addReviewButton: { + marginTop: 12, + height: 44, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: PlatformColor('secondarySystemFill'), + }, + addReviewButtonText: { + fontSize: 16, + fontWeight: '500', + color: '#007AFF', + }, + mergeBlock: { + paddingHorizontal: 16, + paddingVertical: 16, + backgroundColor: 'rgba(52,199,89,0.08)', + }, + mergeHeader: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 10, + }, + mergeTextColumn: { + flex: 1, + }, + mergeTitle: { + fontSize: 16, + lineHeight: 20, + fontWeight: '600', + color: PlatformColor('label'), + }, + mergeSubtitle: { + marginTop: 4, + fontSize: 13, + lineHeight: 18, + color: PlatformColor('secondaryLabel'), + }, + mergeActions: { + marginTop: 16, + flexDirection: 'row', + gap: 8, + }, + mergeButton: { + flex: 1, + height: 48, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#34C759', + }, + mergeButtonText: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + }, + settingsButton: { + width: 48, + height: 48, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: PlatformColor('secondarySystemFill'), + }, + conversationRow: { + minHeight: 52, + paddingHorizontal: 16, + paddingVertical: 12, + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + avatar: { + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: PlatformColor('systemGray3'), + }, + conversationText: { + flex: 1, + fontSize: 14, + lineHeight: 19, + color: PlatformColor('secondaryLabel'), + }, + bottomSpacer: { + height: 200, + }, + bottomActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center', + }, + glassButton: { + minHeight: 36, + paddingHorizontal: 14, + borderRadius: 18, + flexDirection: 'row', + alignItems: 'center', + gap: 6, + backgroundColor: 'transparent', + }, + glassButtonText: { + fontSize: 14, + fontWeight: '500', + color: PlatformColor('label'), + }, + devGlassFallback: { + backgroundColor: 'rgba(255,255,255,0.72)', + borderWidth: StyleSheet.hairlineWidth, + borderColor: 'rgba(60,60,67,0.18)', }, }); diff --git a/ios/HybridScrollEdgeBar.swift b/ios/HybridScrollEdgeBar.swift index 5f3fc97..a0ad584 100644 --- a/ios/HybridScrollEdgeBar.swift +++ b/ios/HybridScrollEdgeBar.swift @@ -52,6 +52,19 @@ class HybridScrollEdgeBar: HybridRNScrollEdgeBarSpec { containerView.bottomBarOffset = CGFloat(bottomBarOffset ?? 0) } } + + var topEdgeEffectStyle: String? = "automatic" { + didSet { + containerView.topEdgeEffectStyle = topEdgeEffectStyle ?? "automatic" + } + } + + var bottomEdgeEffectStyle: String? = "automatic" { + didSet { + containerView.bottomEdgeEffectStyle = bottomEdgeEffectStyle ?? "automatic" + } + } + } // MARK: - Container View @@ -59,6 +72,8 @@ class HybridScrollEdgeBar: HybridRNScrollEdgeBarSpec { @objcMembers class ScrollEdgeBarContainerView: UIView { + private static let bridgeWillReloadNotification = Notification.Name("RCTBridgeWillReloadNotification") + var estimatedTopBarHeight: CGFloat = 60 var estimatedBottomBarHeight: CGFloat = 60 var topBarOffset: CGFloat = 0 { @@ -67,7 +82,12 @@ class ScrollEdgeBarContainerView: UIView { var bottomBarOffset: CGFloat = 0 { didSet { offsetsDidChange() } } - + var topEdgeEffectStyle: String = "automatic" { + didSet { edgeEffectStylesDidChange() } + } + var bottomEdgeEffectStyle: String = "automatic" { + didSet { edgeEffectStylesDidChange() } + } private var edgeBarController: AnyObject? private weak var parentViewController: UIViewController? private var isSetup = false @@ -77,6 +97,31 @@ class ScrollEdgeBarContainerView: UIView { private var detectedScrollView: UIScrollView? private var didAttachControllerView = false private var didNotifyOffsets: Bool = false + private var isBridgeReloading = false + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleBridgeWillReload), + name: Self.bridgeWillReloadNotification, + object: nil + ) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleBridgeWillReload), + name: Self.bridgeWillReloadNotification, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return false @@ -99,7 +144,12 @@ class ScrollEdgeBarContainerView: UIView { override func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) // Restore reparented views before Fabric unmounts them. + #if DEBUG if newWindow == nil && window != nil { + return + } + #endif + if newWindow == nil && window != nil && !isBridgeReloading { cleanupController() } } @@ -107,6 +157,7 @@ class ScrollEdgeBarContainerView: UIView { override func didMoveToWindow() { super.didMoveToWindow() if window != nil { + isBridgeReloading = false parentViewController = findViewController() trySetup() } @@ -159,12 +210,22 @@ class ScrollEdgeBarContainerView: UIView { // Explicit wiring from the generated component view. func setTopBarView(_ view: UIView?) { - detectedTopBarView = view as? ScrollEdgeBarTopBarView + if let view { + let marker = findView(ofType: ScrollEdgeBarTopBarView.self, in: view) + detectedTopBarView = marker?.superview ?? marker + } else { + detectedTopBarView = nil + } trySetup() } func setBottomBarView(_ view: UIView?) { - detectedBottomBarView = view as? ScrollEdgeBarBottomBarView + if let view { + let marker = findView(ofType: ScrollEdgeBarBottomBarView.self, in: view) + detectedBottomBarView = marker?.superview ?? marker + } else { + detectedBottomBarView = nil + } trySetup() } @@ -291,6 +352,7 @@ class ScrollEdgeBarContainerView: UIView { controller.estimatedTopBarHeight = estimatedTopBarHeight controller.estimatedBottomBarHeight = estimatedBottomBarHeight controller.setOffsets(top: topBarOffset, bottom: bottomBarOffset) + controller.setEdgeEffectStyles(top: topEdgeEffectStyle, bottom: bottomEdgeEffectStyle) if let topBarView = detectedTopBarView { controller.setTopBar(topBarView) @@ -320,6 +382,10 @@ class ScrollEdgeBarContainerView: UIView { cleanupController() } + @objc private func handleBridgeWillReload() { + isBridgeReloading = true + } + private func cleanupController() { if #available(iOS 16.0, *), let controller = edgeBarController as? ScrollEdgeBarController { @@ -343,6 +409,12 @@ class ScrollEdgeBarContainerView: UIView { controller.setOffsets(top: topBarOffset, bottom: bottomBarOffset) } } + + private func edgeEffectStylesDidChange() { + guard #available(iOS 16.0, *), + let controller = edgeBarController as? ScrollEdgeBarController else { return } + controller.setEdgeEffectStyles(top: topEdgeEffectStyle, bottom: bottomEdgeEffectStyle) + } } // MARK: - ScrollEdgeBarController @@ -355,7 +427,8 @@ final class ScrollEdgeBarController: UIViewController { var estimatedBottomBarHeight: CGFloat = 60 var topBarOffset: CGFloat = 0 var bottomBarOffset: CGFloat = 0 - + var topEdgeEffectStyle: String = "automatic" + var bottomEdgeEffectStyle: String = "automatic" private var topBarView: UIView? private var bottomBarView: UIView? private var hostingController: UIHostingController? @@ -466,6 +539,7 @@ final class ScrollEdgeBarController: UIViewController { hosting.didMove(toParent: self) hostingController = hosting hosting.view.layoutIfNeeded() + applyEdgeEffectStyles() let estimatedInsets = UIEdgeInsets( top: estimatedTopBarHeight + topBarOffset, left: 0, @@ -509,6 +583,30 @@ final class ScrollEdgeBarController: UIViewController { updateHostingControllerIfNeeded() } + func setEdgeEffectStyles(top: String, bottom: String) { + topEdgeEffectStyle = top + bottomEdgeEffectStyle = bottom + applyEdgeEffectStyles() + } + + private func applyEdgeEffectStyles() { + guard #available(iOS 26.0, *) else { return } + scrollView.topEdgeEffect.style = mapEdgeEffectStyle(topEdgeEffectStyle) + scrollView.bottomEdgeEffect.style = mapEdgeEffectStyle(bottomEdgeEffectStyle) + } + + @available(iOS 26.0, *) + private func mapEdgeEffectStyle(_ style: String) -> UIScrollEdgeEffect.Style { + switch style.lowercased() { + case "hard": + return .hard + case "soft": + return .soft + default: + return .automatic + } + } + private func applyInsets() { let (topInset, bottomInset) = findEdgeBarInsets() let adjustedTop = topInset + topBarOffset diff --git a/src/RNScrollEdgeBar.nitro.ts b/src/RNScrollEdgeBar.nitro.ts index 8a04b41..c4e4f84 100644 --- a/src/RNScrollEdgeBar.nitro.ts +++ b/src/RNScrollEdgeBar.nitro.ts @@ -14,6 +14,10 @@ export interface RNScrollEdgeBarProps extends HybridViewProps { topBarOffset?: number; /** Extra offset to lift the bottom bar above a tab bar */ bottomBarOffset?: number; + /** iOS 26+: edge effect style for the top edge (`automatic`, `soft`, `hard`) */ + topEdgeEffectStyle?: string; + /** iOS 26+: edge effect style for the bottom edge (`automatic`, `soft`, `hard`) */ + bottomEdgeEffectStyle?: string; } export interface RNScrollEdgeBarMethods extends HybridViewMethods {} diff --git a/yarn.lock b/yarn.lock index 4a1909e..d57fdf6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,6 +1523,16 @@ __metadata: languageName: node linkType: hard +"@callstack/liquid-glass@npm:^0.7.1": + version: 0.7.1 + resolution: "@callstack/liquid-glass@npm:0.7.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/222550d4edabc4794fbd31378515bbdcf674116f9f5599a5a300a77228ccbd8d041ad766050b0194d18ed1043c78d08b0fcbeb7559bbac93e6e4116722dc1d86 + languageName: node + linkType: hard + "@commitlint/cli@npm:^19.8.1": version: 19.8.1 resolution: "@commitlint/cli@npm:19.8.1" @@ -10391,6 +10401,7 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@callstack/liquid-glass": "npm:^0.7.1" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0"