about summary refs log tree commit diff
path: root/src/components/Toast/Toast.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Toast/Toast.tsx')
-rw-r--r--src/components/Toast/Toast.tsx284
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],
   )
 }