Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 309 lines 8.6 kB view raw
1import {createContext, useContext, useMemo} from 'react' 2import {type GestureResponderEvent, View} from 'react-native' 3 4import {atoms as a, select, useAlf, useTheme} from '#/alf' 5import { 6 Button, 7 type ButtonProps, 8 type UninheritableButtonProps, 9} from '#/components/Button' 10import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck' 11import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 12import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 13import {type Props as SVGIconProps} from '#/components/icons/common' 14import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 15import {dismiss} from '#/components/Toast/sonner' 16import {type ToastType} from '#/components/Toast/types' 17import {Text as BaseText} from '#/components/Typography' 18 19export const ICONS = { 20 default: CircleCheck, 21 success: CircleCheck, 22 error: ErrorIcon, 23 warning: WarningIcon, 24 info: CircleInfo, 25} 26 27const ToastConfigContext = createContext<{ 28 id: string 29 type: ToastType 30}>({ 31 id: '', 32 type: 'default', 33}) 34ToastConfigContext.displayName = 'ToastConfigContext' 35 36export function ToastConfigProvider({ 37 children, 38 id, 39 type, 40}: { 41 children: React.ReactNode 42 id: string 43 type: ToastType 44}) { 45 return ( 46 <ToastConfigContext.Provider 47 value={useMemo(() => ({id, type}), [id, type])}> 48 {children} 49 </ToastConfigContext.Provider> 50 ) 51} 52 53export function Outer({children}: {children: React.ReactNode}) { 54 const t = useTheme() 55 const {type} = useContext(ToastConfigContext) 56 const styles = useToastStyles({type}) 57 58 return ( 59 <View 60 style={[ 61 a.flex_1, 62 a.p_lg, 63 a.rounded_md, 64 a.border, 65 a.flex_row, 66 a.gap_sm, 67 t.atoms.shadow_sm, 68 { 69 paddingVertical: 14, // 16 seems too big 70 backgroundColor: styles.backgroundColor, 71 borderColor: styles.borderColor, 72 }, 73 ]}> 74 {children} 75 </View> 76 ) 77} 78 79export function Icon({icon}: {icon?: React.ComponentType<SVGIconProps>}) { 80 const {type} = useContext(ToastConfigContext) 81 const styles = useToastStyles({type}) 82 const IconComponent = icon || ICONS[type] 83 return <IconComponent size="md" fill={styles.iconColor} /> 84} 85 86export function Text({children}: {children: React.ReactNode}) { 87 const {type} = useContext(ToastConfigContext) 88 const {textColor} = useToastStyles({type}) 89 const {fontScaleCompensation} = useToastFontScaleCompensation() 90 return ( 91 <View 92 style={[ 93 a.flex_1, 94 a.pr_lg, 95 { 96 top: fontScaleCompensation, 97 }, 98 ]}> 99 <BaseText 100 selectable={false} 101 style={[ 102 a.text_md, 103 a.font_medium, 104 a.leading_snug, 105 a.pointer_events_none, 106 { 107 color: textColor, 108 }, 109 ]}> 110 {children} 111 </BaseText> 112 </View> 113 ) 114} 115 116export function Action( 117 props: Omit<ButtonProps, UninheritableButtonProps | 'children'> & { 118 children: React.ReactNode 119 }, 120) { 121 const t = useTheme() 122 const {fontScaleCompensation} = useToastFontScaleCompensation() 123 const {type} = useContext(ToastConfigContext) 124 const {id} = useContext(ToastConfigContext) 125 const styles = useMemo(() => { 126 const base = { 127 base: { 128 textColor: t.palette.contrast_600, 129 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 130 }, 131 interacted: { 132 textColor: t.atoms.text.color, 133 backgroundColor: t.atoms.bg_contrast_50.backgroundColor, 134 }, 135 } 136 return { 137 default: base, 138 success: { 139 base: { 140 textColor: select(t.name, { 141 light: t.palette.primary_800, 142 dim: t.palette.primary_900, 143 dark: t.palette.primary_900, 144 }), 145 backgroundColor: t.palette.primary_25, 146 }, 147 interacted: { 148 textColor: select(t.name, { 149 light: t.palette.primary_900, 150 dim: t.palette.primary_975, 151 dark: t.palette.primary_975, 152 }), 153 backgroundColor: t.palette.primary_50, 154 }, 155 }, 156 error: { 157 base: { 158 textColor: select(t.name, { 159 light: t.palette.negative_700, 160 dim: t.palette.negative_900, 161 dark: t.palette.negative_900, 162 }), 163 backgroundColor: t.palette.negative_25, 164 }, 165 interacted: { 166 textColor: select(t.name, { 167 light: t.palette.negative_900, 168 dim: t.palette.negative_975, 169 dark: t.palette.negative_975, 170 }), 171 backgroundColor: t.palette.negative_50, 172 }, 173 }, 174 warning: base, 175 info: base, 176 }[type] 177 }, [t, type]) 178 179 const onPress = (e: GestureResponderEvent) => { 180 console.log('Toast Action pressed, dismissing toast', id) 181 dismiss(id) 182 props.onPress?.(e) 183 } 184 185 return ( 186 <View style={{top: fontScaleCompensation}}> 187 <Button {...props} onPress={onPress}> 188 {s => { 189 const interacted = s.pressed || s.hovered || s.focused 190 return ( 191 <> 192 <View 193 style={[ 194 a.absolute, 195 a.curve_continuous, 196 { 197 // tiny button styles 198 top: -5, 199 bottom: -5, 200 left: -9, 201 right: -9, 202 borderRadius: 6, 203 backgroundColor: interacted 204 ? styles.interacted.backgroundColor 205 : styles.base.backgroundColor, 206 }, 207 ]} 208 /> 209 <BaseText 210 style={[ 211 a.text_md, 212 a.font_medium, 213 a.leading_snug, 214 { 215 color: interacted 216 ? styles.interacted.textColor 217 : styles.base.textColor, 218 }, 219 ]}> 220 {props.children} 221 </BaseText> 222 </> 223 ) 224 }} 225 </Button> 226 </View> 227 ) 228} 229 230/** 231 * Vibes-based number, provides t `top` value to wrap the text to compensate 232 * for different type sizes and keep the first line of text aligned with the 233 * icon. - esb 234 */ 235function useToastFontScaleCompensation() { 236 const {fonts} = useAlf() 237 const fontScaleCompensation = useMemo( 238 () => parseInt(fonts.scale) * -1 * 0.65, 239 [fonts.scale], 240 ) 241 return useMemo( 242 () => ({ 243 fontScaleCompensation, 244 }), 245 [fontScaleCompensation], 246 ) 247} 248 249function useToastStyles({type}: {type: ToastType}) { 250 const t = useTheme() 251 return useMemo(() => { 252 return { 253 default: { 254 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 255 borderColor: t.atoms.border_contrast_low.borderColor, 256 iconColor: t.atoms.text.color, 257 textColor: t.atoms.text.color, 258 }, 259 success: { 260 backgroundColor: t.palette.primary_25, 261 borderColor: select(t.name, { 262 light: t.palette.primary_300, 263 dim: t.palette.primary_200, 264 dark: t.palette.primary_100, 265 }), 266 iconColor: select(t.name, { 267 light: t.palette.primary_600, 268 dim: t.palette.primary_700, 269 dark: t.palette.primary_700, 270 }), 271 textColor: select(t.name, { 272 light: t.palette.primary_600, 273 dim: t.palette.primary_700, 274 dark: t.palette.primary_700, 275 }), 276 }, 277 error: { 278 backgroundColor: t.palette.negative_25, 279 borderColor: select(t.name, { 280 light: t.palette.negative_200, 281 dim: t.palette.negative_200, 282 dark: t.palette.negative_100, 283 }), 284 iconColor: select(t.name, { 285 light: t.palette.negative_700, 286 dim: t.palette.negative_900, 287 dark: t.palette.negative_900, 288 }), 289 textColor: select(t.name, { 290 light: t.palette.negative_700, 291 dim: t.palette.negative_900, 292 dark: t.palette.negative_900, 293 }), 294 }, 295 warning: { 296 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 297 borderColor: t.atoms.border_contrast_low.borderColor, 298 iconColor: t.atoms.text.color, 299 textColor: t.atoms.text.color, 300 }, 301 info: { 302 backgroundColor: t.atoms.bg_contrast_25.backgroundColor, 303 borderColor: t.atoms.border_contrast_low.borderColor, 304 iconColor: t.atoms.text.color, 305 textColor: t.atoms.text.color, 306 }, 307 }[type] 308 }, [t, type]) 309}