Update CBS Mobile App codebase
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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<typeof Ionicons>['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<ViewStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<InfoBannerTone, IoniconName> = {
|
||||
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 (
|
||||
<View
|
||||
// Announced when it appears, which is the point of an inline notice that
|
||||
// replaces a toast.
|
||||
accessibilityRole="alert"
|
||||
accessibilityLabel={title ? `${title}. ${message}` : message}
|
||||
style={[
|
||||
styles.banner,
|
||||
{
|
||||
backgroundColor: palette.bg,
|
||||
borderColor: palette.border,
|
||||
borderRadius: theme.radius.md,
|
||||
padding: theme.spacing.sm,
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={icon ?? DEFAULT_ICON[tone]}
|
||||
size={18}
|
||||
color={palette.fg}
|
||||
style={styles.icon}
|
||||
/>
|
||||
|
||||
<View style={styles.body}>
|
||||
{title ? (
|
||||
<Text style={[theme.text.bodyStrong, { color: theme.colors.textPrimary }]}>{title}</Text>
|
||||
) : 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. */}
|
||||
<Text style={[theme.text.bodySmall, { color: theme.colors.textSecondary }]}>{message}</Text>
|
||||
|
||||
{actionLabel && onActionPress ? (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={actionLabel}
|
||||
onPress={onActionPress}
|
||||
// A text link inside a banner is the easiest place to ship a 20pt
|
||||
// target by accident; the hitSlop is what brings it to 44.
|
||||
hitSlop={12}
|
||||
style={styles.action}
|
||||
>
|
||||
<Text style={[theme.text.button, { color: palette.fg }]}>{actionLabel}</Text>
|
||||
<Ionicons name="chevron-forward" size={15} color={palette.fg} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user