import React, { memo } from 'react'; import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native'; import Ionicons from '@react-native-vector-icons/ionicons'; import { useTheme } from '../../theme'; type InfoBannerTone = 'info' | 'warning' | 'danger' | 'success' | 'brand'; type IoniconName = React.ComponentProps['name']; interface InfoBannerProps { /** Optional — a one-line banner reads better without one. */ title?: string; message: string; tone?: InfoBannerTone; /** Overrides the tone's default icon. */ icon?: IoniconName; /** Renders a text action beneath the message. */ actionLabel?: string; onActionPress?: () => void; style?: StyleProp; } /** * An inline notice — the thing to reach for instead of a toast when the message * has to stay on screen while the customer fixes something. * * The tone is carried by an icon as well as a colour, because a red border * alone tells a colour-blind customer nothing, and tells an agent holding the * phone in direct sunlight even less. */ const DEFAULT_ICON: Record = { info: 'information-circle', warning: 'warning', danger: 'alert-circle', success: 'checkmark-circle', brand: 'shield-checkmark', }; const InfoBannerComponent = ({ title, message, tone = 'info', icon, actionLabel, onActionPress, style, }: InfoBannerProps) => { const theme = useTheme(); const palette = ((): { fg: string; bg: string; border: string } => { switch (tone) { case 'warning': return { fg: theme.colors.warning, bg: theme.colors.warningSurface, border: theme.colors.warningBorder, }; case 'danger': return { fg: theme.colors.danger, bg: theme.colors.dangerSurface, border: theme.colors.dangerBorder, }; case 'success': return { fg: theme.colors.success, bg: theme.colors.successSurface, border: theme.colors.successBorder, }; case 'brand': return { fg: theme.colors.primary, bg: theme.colors.primarySurface, border: theme.colors.primaryBorder, }; case 'info': default: return { fg: theme.colors.info, bg: theme.colors.infoSurface, border: theme.colors.infoBorder, }; } })(); return ( {title ? ( {title} ) : null} {/* The message stays in secondary ink rather than the tone colour. Tinting a whole paragraph is what makes these banners shout — the icon and the tinted surface already carry the tone. */} {message} {actionLabel && onActionPress ? ( {actionLabel} ) : null} ); }; const styles = StyleSheet.create({ banner: { flexDirection: 'row', borderWidth: 1, }, icon: { // Optically aligns the icon with the first line of text rather than with // the centre of a paragraph that may run to four lines. marginTop: 1, }, body: { flex: 1, gap: 2, }, action: { flexDirection: 'row', alignItems: 'center', gap: 2, marginTop: 6, minHeight: 24, }, }); export const InfoBanner = memo(InfoBannerComponent);