import React, { createContext, FC, ReactNode, useContext } from 'react' import { Pressable, StyleSheet, View, ViewStyle } from 'react-native' import { useCreateAndroidRippleColor, useFontScale } from '../../hooks' import { MAX_FONT_SIZE_MULTIPLIER, platformFontWeightMedium, platformPressedOpacityStyle, } from '../../utils' import { tokens } from '../../vendor/tapestry/tokens' import { Heading } from '../display/heading' import { Icon, IconString } from '../display/icon' import { Text } from '../display/text' import { TextInlineButton } from '../display/text_inline_button' import { useStatusColorAppearanceMap, type StatusAppearanceUnion, } from '../display/utils/status_colors' // ======================================== // ====== Exports ========================= // ======================================== const Banner = { Root: BannerRoot, StaticLayout: BannerStaticLayout, CollapsibleLayout: BannerCollapsibleLayout, Content: BannerContent, StatusIcon: BannerStatusIcon, Heading: BannerHeading, Text: BannerText, Link: BannerLink, } as const type BannerComponents = { Root: FC StaticLayout: FC CollapsibleLayout: FC Content: FC StatusIcon: FC Heading: FC Text: FC Link: FC } export default Banner as BannerComponents export type { BannerRootProps, BannerStaticLayoutProps, BannerCollapsibleLayoutProps, BannerContentProps, BannerStatusIconProps, BannerHeadingProps, BannerTextProps, BannerLinkProps, } // ======================================== // ====== Context ========================= // ======================================== interface BannerContextType { appearance?: StatusAppearanceUnion } const BannerContext = createContext(null) function useBannerContext() { const context = useContext(BannerContext) if (!context) { throw new Error('Banner components must be used within Banner.Root') } return context } // ======================================== // ====== BannerRoot ====================== // ======================================== interface BannerRootProps { children: ReactNode appearance?: StatusAppearanceUnion } function BannerRoot({ children, appearance = 'neutral' }: BannerRootProps) { return {children} } BannerRoot.displayName = 'Banner.Root' // ======================================== // ====== BannerStaticLayout ============== // ======================================== interface BannerStaticLayoutProps { children: ReactNode style?: ViewStyle } function BannerStaticLayout({ children, style }: BannerStaticLayoutProps) { const { appearance } = useBannerContext() const styles = useStyles({ appearance }) return {children} } BannerStaticLayout.displayName = 'Banner.StaticLayout' // ======================================== // ====== BannerCollapsibleLayout ========= // ======================================== interface BannerCollapsibleLayoutProps { children: ReactNode expanded?: boolean onPress?: () => void style?: ViewStyle } function BannerCollapsibleLayout({ children, expanded, onPress, style, }: BannerCollapsibleLayoutProps) { const { appearance = 'neutral' } = useBannerContext() const styles = useStyles({ appearance }) const iconName = expanded ? 'general.upCaret' : 'general.downCaret' const statusColorMap = useStatusColorAppearanceMap() const androidRippleColor = useCreateAndroidRippleColor({ color: statusColorMap[appearance].background, }) return ( [styles.staticLayout, pressed && platformPressedOpacityStyle, style]} android_ripple={{ color: androidRippleColor, borderless: false, foreground: true }} accessibilityState={{ expanded }} accessibilityRole="button" > {children} ) } BannerCollapsibleLayout.displayName = 'Banner.CollapsibleLayout' // ======================================== // ====== BannerContent =================== // ======================================== interface BannerContentProps { children: ReactNode } function BannerContent({ children }: BannerContentProps) { const styles = useStyles() return {children} } BannerContent.displayName = 'Banner.Content' // ======================================== // ====== BannerStatusIcon ================ // ======================================== interface BannerStatusIconProps { iconName?: IconString } function BannerStatusIcon({ iconName }: BannerStatusIconProps) { const { appearance = 'neutral' } = useBannerContext() const styles = useStyles({ appearance }) const iconNameMap = { error: 'general.exclamationTriangle', info: 'general.outlinedInfoCircle', neutral: 'general.outlinedInfoCircle', success: 'general.check', warning: 'general.exclamationTriangle', } as const return ( ) } BannerStatusIcon.displayName = 'Banner.StatusIcon' // ======================================== // ====== BannerHeading =================== // ======================================== interface BannerHeadingProps { children: ReactNode } function BannerHeading({ children }: BannerHeadingProps) { const { appearance = 'neutral' } = useBannerContext() const styles = useStyles({ appearance }) return ( {children} ) } BannerHeading.displayName = 'Banner.Heading' // ======================================== // ====== BannerText ====================== // ======================================== interface BannerTextProps { children: ReactNode } function BannerText({ children }: BannerTextProps) { const { appearance = 'neutral' } = useBannerContext() const styles = useStyles({ appearance }) return ( {children} ) } BannerText.displayName = 'Banner.Text' // ======================================== // ====== BannerLink ====================== // ======================================== interface BannerLinkProps { children: ReactNode onPress?: () => void } function BannerLink({ children, onPress }: BannerLinkProps) { const { appearance = 'neutral' } = useBannerContext() const styles = useStyles({ appearance }) return ( {children} ) } BannerLink.displayName = 'Banner.Link' // ======================================== // ====== Styles ========================== // ======================================== interface Styles { appearance?: StatusAppearanceUnion } const useStyles = ({ appearance = 'neutral' }: Styles = {}) => { const statusColorMap = useStatusColorAppearanceMap() const fontScale = useFontScale() const LINE_HEIGHT_OFFSET = fontScale > 1 ? 0 : 1 return StyleSheet.create({ staticLayout: { flexDirection: 'row', backgroundColor: statusColorMap[appearance].background, padding: 12, gap: 8, borderRadius: tokens.borderRadiusMd, }, content: { gap: 4, flex: 1, }, icon: { color: statusColorMap[appearance].icon, fontSize: tokens.fontSizeMd, marginTop: (4 - LINE_HEIGHT_OFFSET) * fontScale, }, heading: { color: statusColorMap[appearance].text, fontWeight: platformFontWeightMedium, fontSize: tokens.fontSizeMd, }, text: { color: statusColorMap[appearance].text, }, }) }