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