/* ------------------------------------------------------------------ * * BahlaTabBar — barre d'onglets floutée, partagée par (tabs) et (org). * * Une seule implémentation pour les deux parcours : le verre dépoli, * le calcul de safe area, le rendu des items et l'accessibilité sont * identiques. Seul le bouton central change, et il est optionnel. * * Usage : * }> // utilisateur * }> * * Dépendances : expo-blur · expo-linear-gradient · * react-native-safe-area-context · react-native-svg * ------------------------------------------------------------------ */ import { LinearGradient } from 'expo-linear-gradient'; import { Tabs } from 'expo-router'; import { useCallback, type ComponentProps } from 'react'; import { Pressable, StyleSheet, Text, View, type ViewStyle } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { colors, gradientDirection, gradients } from '@/theme/colors'; import { fontFamily, typography } from '@/theme/typography'; import { Plus } from 'lucide-react-native'; /** Hauteur de la barre hors safe area. La hauteur réelle y ajoute l'inset bas. */ export const TAB_BAR_BASE_HEIGHT = 52; const FAB_SIZE = 72; const FAB_RADIUS = 24; const FAB_LIFT = 40; type ExpoRouterTabBarProps = Parameters< NonNullable['tabBar']> >[0]; type Props = ExpoRouterTabBarProps & { /** * Nom de la route factice qui occupe l'emplacement central. * Absent côté utilisateur — il n'y a pas de bouton flottant. */ fabRouteName?: string; /** Appelé à la place d'une navigation quand on presse le bouton central. */ onFabPress?: () => void; /** Libellé lu par les lecteurs d'écran sur le bouton central. */ fabAccessibilityLabel?: string; }; export function BahlaTabBar({ state, descriptors, navigation, fabRouteName, onFabPress, fabAccessibilityLabel = 'Créer', }: Props) { const insets = useSafeAreaInsets(); const handlePress = useCallback( (routeKey: string, routeName: string, isFocused: boolean) => { const event = navigation.emit({ type: 'tabPress', target: routeKey, canPreventDefault: true, }); if (isFocused || event.defaultPrevented) return; navigation.navigate(routeName as never); }, [navigation], ); const handleLongPress = useCallback( (routeKey: string) => { navigation.emit({ type: 'tabLongPress', target: routeKey }); }, [navigation], ); return ( {state.routes.map((route: (typeof state.routes)[number], index: number) => { const { options } = descriptors[route.key]; const isFocused = state.index === index; const tint = isFocused ? colors.primary : colors.textSecondary; /* --- Emplacement central : bouton flottant, pas un onglet ---- */ if (fabRouteName && route.name === fabRouteName) { return ( {({ pressed }) => ( )} ); } /* --- Onglet ordinaire ---------------------------------------- */ const label = typeof options.tabBarLabel === 'string' ? options.tabBarLabel : (options.title ?? route.name); return ( handlePress(route.key, route.name, isFocused)} onLongPress={() => handleLongPress(route.key)} style={styles.item} > {options.tabBarIcon?.({ focused: isFocused, color: tint, size: 23, })} {options.tabBarBadge != null && ( {options.tabBarBadge} )} {label} ); })} ); } const styles = StyleSheet.create({ bar: { position: 'absolute', left: 0, right: 0, bottom: 0, flexDirection: 'row', alignItems: 'flex-start', paddingTop: 8, borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: colors.border, overflow: 'visible', } as ViewStyle, item: { flex: 1, alignItems: 'center', gap: 5, // La zone tactile couvre toute la hauteur de la barre. alignSelf: 'stretch', paddingTop: 2, }, label: { ...typography.tabLabel, fontFamily: undefined, // remplacé plus haut selon l'état }, badge: { position: 'absolute', top: -4, right: -8, minWidth: 16, height: 16, paddingHorizontal: 4, borderRadius: 999, backgroundColor: colors.primary, alignItems: 'center', justifyContent: 'center', }, badgeText: { ...typography.overlineSm, fontSize: 9, lineHeight: 16, letterSpacing: 0, color: colors.onPrimary, }, /* Le créneau central est plus étroit que les onglets : 84 px fixes, comme la colonne du même nom dans la maquette. */ fabSlot: { width: 96, alignItems: 'center', overflow: 'visible', zIndex: 20, }, fabShell: { width: FAB_SIZE, height: FAB_SIZE, aspectRatio: 1, borderRadius: FAB_RADIUS, // Remonte au-dessus de la barre et découpe un anneau dans le fond. marginTop: -FAB_LIFT, overflow: 'hidden', alignSelf: 'center', zIndex: 20, borderWidth: 3, borderColor: colors.deepBackground, shadowColor: colors.primary, shadowOffset: { width: 0, height: 16 }, shadowOpacity: 0.9, shadowRadius: 20, elevation: 18, }, fabPressable: { width: '100%', height: '100%', borderRadius: FAB_RADIUS, overflow: 'hidden', }, fabGradient: { width: '100%', height: '100%', aspectRatio: 1, borderRadius: FAB_RADIUS, alignItems: 'center', justifyContent: 'center', }, fabGradientPressed: { transform: [{ scale: 0.96 }], opacity: 0.92, }, }); export default BahlaTabBar;