diff options
Diffstat (limited to 'src/components/Toast/Toast.tsx')
-rw-r--r-- | src/components/Toast/Toast.tsx | 284 |
1 files changed, 213 insertions, 71 deletions
diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 4d782597d..ac5bc4889 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,22 +1,20 @@ import {createContext, useContext, useMemo} from 'react' -import {View} from 'react-native' +import {type GestureResponderEvent, View} from 'react-native' import {atoms as a, select, useAlf, useTheme} from '#/alf' +import { + Button, + type ButtonProps, + type UninheritableButtonProps, +} from '#/components/Button' +import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import {type Props as SVGIconProps} from '#/components/icons/common' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' +import {dismiss} from '#/components/Toast/sonner' import {type ToastType} from '#/components/Toast/types' -import {Text} from '#/components/Typography' -import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '../icons/CircleCheck' - -type ContextType = { - type: ToastType -} - -export type ToastComponentProps = { - type?: ToastType - content: React.ReactNode -} +import {Text as BaseText} from '#/components/Typography' export const ICONS = { default: CircleCheck, @@ -26,81 +24,225 @@ export const ICONS = { info: CircleInfo, } -const Context = createContext<ContextType>({ +const ToastConfigContext = createContext<{ + id: string + type: ToastType +}>({ + id: '', type: 'default', }) -Context.displayName = 'ToastContext' +ToastConfigContext.displayName = 'ToastConfigContext' -export function Toast({type = 'default', content}: ToastComponentProps) { - const {fonts} = useAlf() +export function ToastConfigProvider({ + children, + id, + type, +}: { + children: React.ReactNode + id: string + type: ToastType +}) { + return ( + <ToastConfigContext.Provider + value={useMemo(() => ({id, type}), [id, type])}> + {children} + </ToastConfigContext.Provider> + ) +} + +export function Outer({children}: {children: React.ReactNode}) { const t = useTheme() + const {type} = useContext(ToastConfigContext) const styles = useToastStyles({type}) - const Icon = ICONS[type] - /** - * Vibes-based number, adjusts `top` of `View` that wraps the text to - * compensate for different type sizes and keep the first line of text - * aligned with the icon. - esb - */ - const fontScaleCompensation = useMemo( - () => parseInt(fonts.scale) * -1 * 0.65, - [fonts.scale], - ) return ( - <Context.Provider value={useMemo(() => ({type}), [type])}> - <View - style={[ - a.flex_1, - a.py_lg, - a.pl_xl, - a.pr_2xl, - a.rounded_md, - a.border, - a.flex_row, - a.gap_sm, - t.atoms.shadow_sm, - { - backgroundColor: styles.backgroundColor, - borderColor: styles.borderColor, - }, - ]}> - <Icon size="md" fill={styles.iconColor} /> - - <View - style={[ - a.flex_1, - { - top: fontScaleCompensation, - }, - ]}> - {typeof content === 'string' ? ( - <ToastText>{content}</ToastText> - ) : ( - content - )} - </View> - </View> - </Context.Provider> + <View + style={[ + a.flex_1, + a.p_lg, + a.rounded_md, + a.border, + a.flex_row, + a.gap_sm, + t.atoms.shadow_sm, + { + paddingVertical: 14, // 16 seems too big + backgroundColor: styles.backgroundColor, + borderColor: styles.borderColor, + }, + ]}> + {children} + </View> ) } -export function ToastText({children}: {children: React.ReactNode}) { - const {type} = useContext(Context) +export function Icon({icon}: {icon?: React.ComponentType<SVGIconProps>}) { + const {type} = useContext(ToastConfigContext) + const styles = useToastStyles({type}) + const IconComponent = icon || ICONS[type] + return <IconComponent size="md" fill={styles.iconColor} /> +} + +export function Text({children}: {children: React.ReactNode}) { + const {type} = useContext(ToastConfigContext) const {textColor} = useToastStyles({type}) + const {fontScaleCompensation} = useToastFontScaleCompensation() return ( - <Text - selectable={false} + <View style={[ - a.text_md, - a.font_medium, - a.leading_snug, - a.pointer_events_none, + a.flex_1, + a.pr_lg, { - color: textColor, + top: fontScaleCompensation, }, ]}> - {children} - </Text> + <BaseText + selectable={false} + style={[ + a.text_md, + a.font_medium, + a.leading_snug, + a.pointer_events_none, + { + color: textColor, + }, + ]}> + {children} + </BaseText> + </View> + ) +} + +export function Action( + props: Omit<ButtonProps, UninheritableButtonProps | 'children'> & { + children: React.ReactNode + }, +) { + const t = useTheme() + const {fontScaleCompensation} = useToastFontScaleCompensation() + const {type} = useContext(ToastConfigContext) + const {id} = useContext(ToastConfigContext) + const styles = useMemo(() => { + const base = { + base: { + textColor: t.palette.contrast_600, + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + }, + interacted: { + textColor: t.atoms.text.color, + backgroundColor: t.atoms.bg_contrast_50.backgroundColor, + }, + } + return { + default: base, + success: { + base: { + textColor: select(t.name, { + light: t.palette.primary_800, + dim: t.palette.primary_900, + dark: t.palette.primary_900, + }), + backgroundColor: t.palette.primary_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.primary_900, + dim: t.palette.primary_975, + dark: t.palette.primary_975, + }), + backgroundColor: t.palette.primary_50, + }, + }, + error: { + base: { + textColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_900, + dark: t.palette.negative_900, + }), + backgroundColor: t.palette.negative_25, + }, + interacted: { + textColor: select(t.name, { + light: t.palette.negative_900, + dim: t.palette.negative_975, + dark: t.palette.negative_975, + }), + backgroundColor: t.palette.negative_50, + }, + }, + warning: base, + info: base, + }[type] + }, [t, type]) + + const onPress = (e: GestureResponderEvent) => { + console.log('Toast Action pressed, dismissing toast', id) + dismiss(id) + props.onPress?.(e) + } + + return ( + <View style={{top: fontScaleCompensation}}> + <Button {...props} onPress={onPress}> + {s => { + const interacted = s.pressed || s.hovered || s.focused + return ( + <> + <View + style={[ + a.absolute, + a.curve_continuous, + { + // tiny button styles + top: -5, + bottom: -5, + left: -9, + right: -9, + borderRadius: 6, + backgroundColor: interacted + ? styles.interacted.backgroundColor + : styles.base.backgroundColor, + }, + ]} + /> + <BaseText + style={[ + a.text_md, + a.font_medium, + a.leading_snug, + { + color: interacted + ? styles.interacted.textColor + : styles.base.textColor, + }, + ]}> + {props.children} + </BaseText> + </> + ) + }} + </Button> + </View> + ) +} + +/** + * Vibes-based number, provides t `top` value to wrap the text to compensate + * for different type sizes and keep the first line of text aligned with the + * icon. - esb + */ +function useToastFontScaleCompensation() { + const {fonts} = useAlf() + const fontScaleCompensation = useMemo( + () => parseInt(fonts.scale) * -1 * 0.65, + [fonts.scale], + ) + return useMemo( + () => ({ + fontScaleCompensation, + }), + [fontScaleCompensation], ) } |