diff options
-rw-r--r-- | src/view/com/util/Toast.tsx | 78 | ||||
-rw-r--r-- | src/view/com/util/Toast.web.tsx | 100 |
2 files changed, 158 insertions, 20 deletions
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 56c6780ad..fc9bdf672 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -21,20 +21,36 @@ import { FontAwesomeIcon, type Props as FontAwesomeProps, } from '@fortawesome/react-native-fontawesome' - import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {isWeb} from '#/platform/detection' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' const TIMEOUT = 2e3 +export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' + +const TOAST_TYPE_TO_ICON: Record<ToastType, FontAwesomeProps['icon']> = { + default: 'check', + success: 'check', + error: 'exclamation', + warning: 'circle-exclamation', + info: 'info', +} + export function show( message: string, - icon: FontAwesomeProps['icon'] = 'check', + type: ToastType | FontAwesomeProps['icon'] = 'default', ) { if (process.env.NODE_ENV === 'test') { return } + + const icon = + typeof type === 'string' && type in TOAST_TYPE_TO_ICON + ? TOAST_TYPE_TO_ICON[type as ToastType] + : (type as FontAwesomeProps['icon']) + AccessibilityInfo.announceForAccessibility(message) const item = new RootSiblings( <Toast message={message} icon={icon} destroy={() => item.destroy()} />, @@ -153,9 +169,41 @@ function Toast({ } }) + // Web-specific styles for better compatibility + const webContainerStyle = isWeb + ? { + position: 'absolute' as const, + top: topOffset, + left: 16, + right: 16, + zIndex: 9999, + pointerEvents: 'auto' as const, + } + : {} + + const webToastStyle = isWeb + ? { + backgroundColor: + t.name === 'dark' ? t.palette.contrast_25 : t.palette.white, + shadowColor: '#000', + shadowOffset: {width: 0, height: 10}, + shadowOpacity: 0.1, + shadowRadius: 15, + elevation: 10, + borderColor: t.palette.contrast_300, + borderWidth: 1, + borderRadius: 8, + minHeight: 60, + } + : {} + return ( <GestureHandlerRootView - style={[a.absolute, {top: topOffset, left: 16, right: 16}]} + style={[ + a.absolute, + {top: topOffset, left: 16, right: 16}, + isWeb && webContainerStyle, + ]} pointerEvents="box-none"> {alive && ( <Animated.View @@ -171,12 +219,16 @@ function Toast({ onAccessibilityEscape={hideAndDestroyImmediately} style={[ a.flex_1, - t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg, - a.shadow_lg, - t.atoms.border_contrast_medium, - a.rounded_sm, - a.border, - animatedStyle, + isWeb + ? webToastStyle + : [ + t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg, + a.shadow_lg, + t.atoms.border_contrast_medium, + a.rounded_sm, + a.border, + ], + !isWeb && animatedStyle, ]}> <GestureDetector gesture={panGesture}> <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> @@ -200,7 +252,13 @@ function Toast({ style={t.atoms.text_contrast_medium} /> </View> - <View style={[a.h_full, a.justify_center, a.flex_1]}> + <View + style={[ + a.h_full, + a.justify_center, + a.flex_1, + a.justify_center, + ]}> <Text style={a.text_md} emoji> {message} </Text> diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx index d3b7bda33..949dce7ef 100644 --- a/src/view/com/util/Toast.web.tsx +++ b/src/view/com/util/Toast.web.tsx @@ -9,12 +9,24 @@ import { type FontAwesomeIconStyle, type Props as FontAwesomeProps, } from '@fortawesome/react-native-fontawesome' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' -const DURATION = 3500 +const DURATION = 60000 + +export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' + +const TOAST_TYPE_TO_ICON: Record<ToastType, FontAwesomeProps['icon']> = { + default: 'check', + success: 'check', + error: 'exclamation', + warning: 'circle-exclamation', + info: 'info', +} interface ActiveToast { text: string icon: FontAwesomeProps['icon'] + type: ToastType } type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void @@ -33,16 +45,76 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => { setActiveToast(t) } }) + + const t = useTheme() + + const TOAST_TYPE_TO_STYLES = { + default: { + backgroundColor: t.atoms.text_contrast_low.color, + borderColor: t.atoms.border_contrast_medium.borderColor, + iconColor: '#fff', + textColor: '#fff', + }, + success: { + backgroundColor: '#059669', + borderColor: '#047857', + iconColor: '#fff', + textColor: '#fff', + }, + error: { + backgroundColor: t.palette.negative_100, + borderColor: t.palette.negative_400, + iconColor: t.palette.negative_600, + textColor: t.palette.negative_600, + }, + warning: { + backgroundColor: t.palette.negative_500, + borderColor: t.palette.negative_600, + iconColor: '#fff', + textColor: '#fff', + }, + info: { + backgroundColor: t.atoms.text_contrast_low.color, + borderColor: t.atoms.border_contrast_medium.borderColor, + iconColor: '#fff', + textColor: '#fff', + }, + } + + const toastStyles = activeToast + ? TOAST_TYPE_TO_STYLES[activeToast.type] + : TOAST_TYPE_TO_STYLES.default + return ( <> {activeToast && ( - <View style={styles.container}> + <View + style={[ + styles.container, + { + backgroundColor: toastStyles.backgroundColor, + borderColor: toastStyles.borderColor, + }, + ]}> <FontAwesomeIcon icon={activeToast.icon} size={20} - style={styles.icon as FontAwesomeIconStyle} + style={ + [ + styles.icon, + {color: toastStyles.iconColor}, + ] as FontAwesomeIconStyle + } /> - <Text style={styles.text}>{activeToast.text}</Text> + <Text + style={[ + styles.text, + a.text_sm, + a.font_bold, + {color: toastStyles.textColor}, + ]}> + {activeToast.text} + </Text> <Pressable style={styles.dismissBackdrop} accessibilityLabel="Dismiss" @@ -60,11 +132,22 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => { // methods // = -export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') { +export function show( + text: string, + type: ToastType | FontAwesomeProps['icon'] = 'default', +) { if (toastTimeout) { clearTimeout(toastTimeout) } - globalSetActiveToast?.({text, icon}) + + // Determine if type is a semantic type or direct icon + const isSemanticType = typeof type === 'string' && type in TOAST_TYPE_TO_ICON + const icon = isSemanticType + ? TOAST_TYPE_TO_ICON[type as ToastType] + : (type as FontAwesomeProps['icon']) + const toastType = isSemanticType ? (type as ToastType) : 'default' + + globalSetActiveToast?.({text, icon, type: toastType}) toastTimeout = setTimeout(() => { globalSetActiveToast?.(undefined) }, DURATION) @@ -82,8 +165,8 @@ const styles = StyleSheet.create({ padding: 20, flexDirection: 'row', alignItems: 'center', - backgroundColor: '#000c', borderRadius: 10, + borderWidth: 1, }, dismissBackdrop: { position: 'absolute', @@ -93,12 +176,9 @@ const styles = StyleSheet.create({ right: 0, }, icon: { - color: '#fff', flexShrink: 0, }, text: { - color: '#fff', - fontSize: 18, marginLeft: 10, }, }) |