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 {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}