Have something to say?

Tell us how we could make the product more useful to you.

Floating Action Button and App Bar code

I have messing around with Hero UI Native for a bit and was able to come up with a couple of components that you guys don’t have yet, But I am not sure to how contributions work or anything. But i have included the components code below hopefully this helps: FAB: import { Button, type ButtonRootProps } from 'heroui-native'; import React, { type ReactNode } from 'react'; import { ViewStyle } from 'react-native'; import Animated, { FadeInDown, FadeOutDown, LinearTransition, type BaseAnimationBuilder, type EntryExitAnimationFunction, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface UniversalFABProps { /** Function called when the FAB is pressed */ onPress: () => void; /** Icon component to render */ icon: ReactNode; /** Optional text label for Extended FAB pattern */ label?: string; /** HeroUI variant */ variant?: ButtonRootProps['variant']; /** HeroUI size */ size?: ButtonRootProps['size']; /** Additional styling for the button itself */ className?: string; /** Base Tailwind classes for positioning (excluding bottom offset) */ containerClassName?: string; /** * Extra vertical offset. * Useful if you have a TabBar height to account for. * Default is 16. */ bottomOffset?: number; entering?: BaseAnimationBuilder | EntryExitAnimationFunction; exiting?: BaseAnimationBuilder | EntryExitAnimationFunction; } /** * FloatingActionButton - Adapts dynamically to Safe Areas and Tab Bars. */ export const FloatingActionButton = ({ onPress, icon, label, variant = 'secondary', size = 'lg', className = 'rounded-xl shadow-lg', containerClassName = 'absolute right-5', // Removed bottom-8 bottomOffset = 16, entering = FadeInDown.delay(200).springify(), exiting = FadeOutDown.duration(200), }: UniversalFABProps) => { const insets = useSafeAreaInsets(); /** * The 'bottom' value is the safe area inset (which includes TabBar height * if wrapped in a safe provider) plus our custom padding. */ const animatedContainerStyle: ViewStyle = { bottom: Math.max(insets.bottom, 16) + bottomOffset, }; return ( entering={entering} exiting={exiting} layout={LinearTransition.springify()} className={containerClassName} style={animatedContainerStyle} className={className} variant={variant} size={size} onPress={onPress} isIconOnly={!label} pressableFeedbackVariant="ripple" {icon} {label && ( {label} )} ); }; App-Bar: import { clsx, type ClassValue } from 'clsx'; import { BlurView } from 'expo-blur'; import * as Haptics from 'expo-haptics'; import * as React from 'react'; import { Platform, Pressable, Text, View, useColorScheme } from 'react-native'; import Animated, { FadeInDown, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } type AppBarAction = { id: string | number; // Stable identifier for use as React key icon: React.ReactNode; // Flexible: pass any icon component here onPress: () => void; badge?: number; }; interface AppBarProps { children?: React.ReactNode; title?: string; subtitle?: string; actions?: AppBarAction[]; leading?: React.ReactNode; trailing?: React.ReactNode; elevation?: 0 | 1 | 2; variant?: 'surface' | 'primary' | 'transparent'; onLeadingPress?: () => void; className?: string; } const AppBar = React.forwardRef ( ( { title, subtitle, actions = [], leading, trailing, elevation = 0, variant = 'surface', onLeadingPress, className, ...props }, ref, ) => { const insets = useSafeAreaInsets(); const isDark = useColorScheme() === 'dark'; const height = 64; const totalHeight = height + insets.top; return ( ref={ref} entering={FadeInDown.duration(400)} style={{ paddingTop: insets.top, height: totalHeight }} className={cn( 'w-full z-50', variant === 'surface' && 'bg-background', variant === 'primary' && 'bg-primary', elevation === 1 && 'border-b border-border', elevation === 2 && 'shadow-lg bg-background', className, )} {...props} {variant === 'transparent' && ( intensity={Platform.OS === 'ios' ? 80 : 100} tint={isDark ? 'dark' : 'light'} className="absolute inset-0" /> )} {/* Leading Section */} {leading || (onLeadingPress ? ( {/* Fallback back icon or pass one in leading */} {props.children} ) : null)} {/* Center Title Section */} {title && ( numberOfLines={1} className={cn( 'text-lg font-semibold', variant === 'primary' ? 'text-primary-foreground' : 'text-foreground', )} {title} )} {subtitle && ( {subtitle} )} {/* Trailing Actions */} {trailing || actions.map((action) => ( key={action.id} onPress={action.onPress} badge={action.badge} {action.icon} ))} ); }, ); // For the AppBar component AppBar.displayName = 'AppBar'; const ActionButton = ({ onPress, children, badge, }: { onPress?: () => void; children: React.ReactNode; badge?: number; }) => { const scale = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }], })); const handlePress = () => { if (Platform.OS !== 'web') Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onPress?.(); }; return ( onPressIn={() => (scale.value = withSpring(0.9))} onPressOut={() => (scale.value = withSpring(1))} onPress={handlePress} hitSlop={12} className="h-10 w-10 items-center justify-center rounded-full active:bg-muted/20" {children} {!!badge && ( {badge > 99 ? '99+' : badge} )} ); }; // If you exported ActionButton as a separate component ActionButton.displayName = 'ActionButton'; export default AppBar;

Rumaiz Ahmed 19 days ago

1