Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/mobile/src/components/core/Screen/GlassCapsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type GlassCapsuleProps = {
* time under a page-colored cover that fades out.
*/
floatProgress: SharedValue<number>
/**
* Tints the iOS 26 glass, for pills that sit over busy content where the
* clear material leaves text hard to read.
*/
tintColor?: string
}

/**
Expand All @@ -41,7 +46,7 @@ type GlassCapsuleProps = {
* `GlassSurface` clipped to the same shape with a soft shadow.
*/
export const GlassCapsule = (props: GlassCapsuleProps) => {
const { children, height, style, floatProgress } = props
const { children, height, style, floatProgress, tintColor } = props
const isDarkMode = isDarkTheme(useThemeVariant())
const { background, borderDefault } = useThemeColors()
const shape = { height, borderRadius: height / 2 }
Expand Down Expand Up @@ -86,6 +91,7 @@ export const GlassCapsule = (props: GlassCapsuleProps) => {
<Animated.View style={[frameStyle, style]}>
<LiquidGlassView
effect='regular'
tintColor={tintColor}
colorScheme={isDarkMode ? 'dark' : 'light'}
style={[shape, styles.clip]}
>
Expand Down
24 changes: 20 additions & 4 deletions packages/mobile/src/components/top-tab-bar/TopTabNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { ComponentType, ReactNode } from 'react'

import type { MaterialTopTabNavigationOptions } from '@react-navigation/material-top-tabs'
import type {
MaterialTopTabBarProps,
MaterialTopTabNavigationOptions
} from '@react-navigation/material-top-tabs'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import type { SvgProps } from 'react-native-svg'

Expand All @@ -12,14 +15,25 @@ type TabNavigatorProps = {
initialScreenName?: string
children: ReactNode
screenOptions?: MaterialTopTabNavigationOptions
/** Replaces the default tab bar, e.g. to float it over the content. */
tabBar?: (props: MaterialTopTabBarProps) => ReactNode
}

const renderDefaultTabBar = (props: MaterialTopTabBarProps) => (
<TopTabBar {...props} />
)

export const TabNavigator = (props: TabNavigatorProps) => {
const { initialScreenName, children, screenOptions } = props
const {
initialScreenName,
children,
screenOptions,
tabBar = renderDefaultTabBar
} = props
return (
<Tab.Navigator
initialRouteName={initialScreenName}
tabBar={(props) => <TopTabBar {...props} />}
tabBar={tabBar}
screenOptions={screenOptions}
>
{children}
Expand Down Expand Up @@ -59,14 +73,16 @@ type TopTabsProps = {
initialScreenName?: string
screens?: ScreenConfig[]
screenOptions?: MaterialTopTabNavigationOptions
tabBar?: (props: MaterialTopTabBarProps) => ReactNode
}

export const TopTabNavigator = (props: TopTabsProps) => {
const { initialScreenName, screens, screenOptions } = props
const { initialScreenName, screens, screenOptions, tabBar } = props
return (
<TabNavigator
initialScreenName={initialScreenName}
screenOptions={screenOptions}
tabBar={tabBar}
>
{screens?.map((screen) => tabScreen({ key: screen.name, ...screen }))}
</TabNavigator>
Expand Down
163 changes: 163 additions & 0 deletions packages/mobile/src/screens/app-screen/CompactHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type { ReactNode } from 'react'

import { StyleSheet, Text, View } from 'react-native'
import type { SharedValue } from 'react-native-reanimated'
import Animated, {
useAnimatedStyle,
useSharedValue
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

import { GlassCapsule } from 'app/components/core/Screen/GlassCapsule'
import { makeStyles } from 'app/styles'
import { useThemeColors } from 'app/utils/theme'
import { zIndex } from 'app/utils/zIndex'

/** Height of the compact title pill. */
export const COMPACT_TITLE_HEIGHT = 36
/** Height of the glass bar holding a screen's compact controls. */
const COMPACT_CONTROLS_HEIGHT = 36
/** Gap between the status bar and the title pill. */
const COMPACT_TOP_GAP = 4
/** Gap between the title pill and the controls under it. */
const COMPACT_STACK_GAP = 6

/** Page-colored tint so pill text stays readable over the list. */
const useCompactTint = () => `${useThemeColors().background.slice(0, 7)}B3`

/**
* Glass can't fade in from transparent, so compact chrome slides down from
* above the screen as it forms instead of fading.
*/
const useSlideInStyle = (progress: SharedValue<number>, distance: number) =>
useAnimatedStyle(() => ({
transform: [
{ translateY: -distance * (1 - progress.value) },
{ scale: 0.9 + 0.1 * progress.value }
]
}))

const useStyles = makeStyles(({ palette, spacing, typography }) => ({
titleContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: spacing(2),
paddingHorizontal: spacing(4)
},
title: {
fontSize: 15,
lineHeight: 20,
fontFamily: typography.fontByWeight.bold,
color: palette.neutral
},
controlsContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing(1.5)
}
}))

type CompactTitlePillProps = {
title: string
progress: SharedValue<number>
/** Shown before the title, e.g. a search icon. */
leading?: ReactNode
}

/**
* The small glass pill a root screen's header contracts to once its title row
* has scrolled away. Pinned just under the status bar.
*/
export const CompactTitlePill = (props: CompactTitlePillProps) => {
const { title, progress, leading } = props
const styles = useStyles()
const insets = useSafeAreaInsets()
const floating = useSharedValue(1)
const tint = useCompactTint()
const top = insets.top + COMPACT_TOP_GAP
const slideStyle = useSlideInStyle(
progress,
top + COMPACT_TITLE_HEIGHT + COMPACT_TOP_GAP
)

return (
<Animated.View
pointerEvents='none'
accessibilityElementsHidden
importantForAccessibility='no-hide-descendants'
style={[layout.root, { top }, slideStyle]}
>
<GlassCapsule
height={COMPACT_TITLE_HEIGHT}
floatProgress={floating}
tintColor={tint}
style={layout.capsule}
>
<View style={styles.titleContent}>
{leading}
<Text numberOfLines={1} style={styles.title}>
{title}
</Text>
</View>
</GlassCapsule>
</Animated.View>
)
}

type CompactControlsProps = {
children: ReactNode
progress: SharedValue<number>
}

/**
* A screen's controls (tab pills, filter) in a small glass bar under the
* compact title pill. Rendered by the screen so tab state changes don't
* rebuild the navigation header.
*/
export const CompactControls = (props: CompactControlsProps) => {
const { children, progress } = props
const styles = useStyles()
const insets = useSafeAreaInsets()
const floating = useSharedValue(1)
const tint = useCompactTint()
const top =
insets.top + COMPACT_TOP_GAP + COMPACT_TITLE_HEIGHT + COMPACT_STACK_GAP
const slideStyle = useSlideInStyle(
progress,
top + COMPACT_CONTROLS_HEIGHT + COMPACT_TOP_GAP
)

return (
<Animated.View
pointerEvents='box-none'
style={[layout.root, { top }, slideStyle]}
>
<GlassCapsule
height={COMPACT_CONTROLS_HEIGHT}
floatProgress={floating}
tintColor={tint}
style={layout.capsule}
>
<View style={styles.controlsContent}>{children}</View>
</GlassCapsule>
</Animated.View>
)
}

const layout = StyleSheet.create({
root: {
position: 'absolute',
left: 0,
right: 0,
alignItems: 'center',
zIndex: zIndex.HEADER_SHADOW
},
capsule: {
alignSelf: 'center',
// GlassCapsule insets itself 16px from each edge while floating; centered
// pills size to their content instead.
marginHorizontal: 0
}
})
Loading
Loading