diff --git a/assets/icons/circleCheck_stroke2_corner0_rounded.svg b/assets/icons/circleCheck_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..d6b0b6bbf9
--- /dev/null
+++ b/assets/icons/circleCheck_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/package.json b/package.json
index 2ab966ceb5..66fd5ed963 100644
--- a/package.json
+++ b/package.json
@@ -116,6 +116,7 @@
"base64-js": "^1.5.1",
"bcp-47": "^2.1.0",
"bcp-47-match": "^2.0.3",
+ "burnt": "^0.12.2",
"date-fns": "^2.30.0",
"deprecated-react-native-prop-types": "^5.0.0",
"email-validator": "^2.0.4",
@@ -204,6 +205,7 @@
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
+ "sf-symbols-typescript": "^2.1.0",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 58754d8e46..6aed7fa693 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -51,7 +51,6 @@ import {Provider as TrendingConfigProvider} from '#/state/trending-config'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
-import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
@@ -60,6 +59,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
+import {Toaster} from './components/Toast/Toaster'
/**
* Begin geolocation ASAP
@@ -149,7 +149,7 @@ function InnerApp() {
-
+
diff --git a/src/components/Toast/LegacyToast.tsx b/src/components/Toast/LegacyToast.tsx
new file mode 100644
index 0000000000..1c5a4be90e
--- /dev/null
+++ b/src/components/Toast/LegacyToast.tsx
@@ -0,0 +1,207 @@
+import {useEffect, useMemo, useRef, useState} from 'react'
+import {AccessibilityInfo, View} from 'react-native'
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from 'react-native-gesture-handler'
+import Animated, {
+ FadeInUp,
+ FadeOutUp,
+ runOnJS,
+ useAnimatedReaction,
+ useAnimatedStyle,
+ useSharedValue,
+ withDecay,
+ withSpring,
+} from 'react-native-reanimated'
+import RootSiblings from 'react-native-root-siblings'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {IconName, ToastIcon} from './ToastIcon'
+
+const TIMEOUT = 2e3
+
+/**
+ * NOTE: This is the JS version of the toast component, used for android
+ */
+
+export function show(message: string, icon: IconName = 'check') {
+ if (process.env.NODE_ENV === 'test') return
+ AccessibilityInfo.announceForAccessibility(message)
+ const item = new RootSiblings(
+ item.destroy()} />,
+ )
+}
+
+function Toast({
+ message,
+ icon,
+ destroy,
+}: {
+ message: string
+ icon: IconName
+ destroy: () => void
+}) {
+ const t = useTheme()
+ const {top} = useSafeAreaInsets()
+ const isPanning = useSharedValue(false)
+ const dismissSwipeTranslateY = useSharedValue(0)
+ const [cardHeight, setCardHeight] = useState(0)
+
+ // for the exit animation to work on iOS the animated component
+ // must not be the root component
+ // so we need to wrap it in a view and unmount the toast ahead of time
+ const [alive, setAlive] = useState(true)
+
+ const hideAndDestroyImmediately = () => {
+ setAlive(false)
+ setTimeout(() => {
+ destroy()
+ }, 1e3)
+ }
+
+ const destroyTimeoutRef = useRef>()
+ const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
+ clearTimeout(destroyTimeoutRef.current)
+ destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT)
+ })
+ const pauseDestroy = useNonReactiveCallback(() => {
+ clearTimeout(destroyTimeoutRef.current)
+ })
+
+ useEffect(() => {
+ hideAndDestroyAfterTimeout()
+ }, [hideAndDestroyAfterTimeout])
+
+ const panGesture = useMemo(() => {
+ return Gesture.Pan()
+ .activeOffsetY([-10, 10])
+ .failOffsetX([-10, 10])
+ .maxPointers(1)
+ .onStart(() => {
+ 'worklet'
+ if (!alive) return
+ isPanning.set(true)
+ runOnJS(pauseDestroy)()
+ })
+ .onUpdate(e => {
+ 'worklet'
+ if (!alive) return
+ dismissSwipeTranslateY.value = e.translationY
+ })
+ .onEnd(e => {
+ 'worklet'
+ if (!alive) return
+ runOnJS(hideAndDestroyAfterTimeout)()
+ isPanning.set(false)
+ if (e.velocityY < -100) {
+ if (dismissSwipeTranslateY.value === 0) {
+ // HACK: If the initial value is 0, withDecay() animation doesn't start.
+ // This is a bug in Reanimated, but for now we'll work around it like this.
+ dismissSwipeTranslateY.value = 1
+ }
+ dismissSwipeTranslateY.value = withDecay({
+ velocity: e.velocityY,
+ velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
+ deceleration: 1,
+ })
+ } else {
+ dismissSwipeTranslateY.value = withSpring(0, {
+ stiffness: 500,
+ damping: 50,
+ })
+ }
+ })
+ }, [
+ dismissSwipeTranslateY,
+ isPanning,
+ alive,
+ hideAndDestroyAfterTimeout,
+ pauseDestroy,
+ ])
+
+ const topOffset = top + 10
+
+ useAnimatedReaction(
+ () =>
+ !isPanning.get() &&
+ dismissSwipeTranslateY.get() < -topOffset - cardHeight,
+ (isSwipedAway, prevIsSwipedAway) => {
+ 'worklet'
+ if (isSwipedAway && !prevIsSwipedAway) {
+ runOnJS(destroy)()
+ }
+ },
+ )
+
+ const animatedStyle = useAnimatedStyle(() => {
+ const translation = dismissSwipeTranslateY.get()
+ return {
+ transform: [
+ {
+ translateY: translation > 0 ? translation ** 0.7 : translation,
+ },
+ ],
+ }
+ })
+
+ return (
+
+ {alive && (
+
+ setCardHeight(evt.nativeEvent.layout.height)}
+ accessibilityRole="alert"
+ accessible={true}
+ accessibilityLabel={message}
+ accessibilityHint=""
+ onAccessibilityEscape={hideAndDestroyImmediately}
+ style={[
+ a.flex_1,
+ t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg,
+ a.shadow_lg,
+ t.atoms.border_contrast_medium,
+ a.rounded_sm,
+ a.border,
+ animatedStyle,
+ ]}>
+
+
+
+
+
+
+
+ {message}
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/src/components/Toast/ToastIcon.tsx b/src/components/Toast/ToastIcon.tsx
new file mode 100644
index 0000000000..ff1c5de15e
--- /dev/null
+++ b/src/components/Toast/ToastIcon.tsx
@@ -0,0 +1,83 @@
+import {useMemo} from 'react'
+import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome'
+import type {SFSymbol} from 'sf-symbols-typescript'
+
+import {useTheme} from '#/alf'
+import {Props as SVGIconProps} from '#/components/icons/common'
+import {CircleCheck_Stroke2_Corner0_Rounded} from '../icons/Check'
+import {CircleInfo_Stroke2_Corner0_Rounded} from '../icons/CircleInfo'
+import {Clipboard_Stroke2_Corner2_Rounded} from '../icons/Clipboard'
+import {TimesLarge_Stroke2_Corner0_Rounded} from '../icons/Times'
+import {Warning_Stroke2_Corner0_Rounded} from '../icons/Warning'
+
+export type IconName = 'check' | 'error' | 'warning' | 'info' | 'copied'
+
+export type LegacyIconName = Extract<
+ FontAwesomeProps['icon'],
+ 'xmark' | 'check' | 'clipboard-check' | 'warning' | 'info'
+>
+
+/**
+ * Get new icon name from legacy fontawesome icon
+ */
+export const compat_convertLegacyIcon = (icon?: LegacyIconName): IconName => {
+ switch (icon) {
+ case undefined:
+ case 'check':
+ return 'check'
+ case 'xmark':
+ return 'error'
+ case 'clipboard-check':
+ return 'copied'
+ case 'warning':
+ return 'warning'
+ case 'info':
+ return 'info'
+ default:
+ throw new Error(`Unknown icon: ${icon}`)
+ }
+}
+
+function getComponent(name: IconName) {
+ switch (name) {
+ case 'check':
+ return CircleCheck_Stroke2_Corner0_Rounded
+ case 'error':
+ return TimesLarge_Stroke2_Corner0_Rounded
+ case 'warning':
+ return Warning_Stroke2_Corner0_Rounded
+ case 'info':
+ return CircleInfo_Stroke2_Corner0_Rounded
+ case 'copied':
+ return Clipboard_Stroke2_Corner2_Rounded
+ default:
+ throw new Error(`Unknown icon: ${name}`)
+ }
+}
+
+export function ToastIcon({
+ icon,
+ style,
+ ...props
+}: {icon: IconName} & SVGIconProps) {
+ const t = useTheme()
+ const Component = useMemo(() => getComponent(icon), [icon])
+ return
+}
+
+export function getSfSymbol(name: IconName): SFSymbol {
+ switch (name) {
+ case 'check':
+ return 'checkmark.circle.fill'
+ case 'error':
+ return 'xmark.circle.fill'
+ case 'warning':
+ return 'exclamationmark.triangle.fill'
+ case 'info':
+ return 'info.circle.fill'
+ case 'copied':
+ return 'doc.on.clipboard.fill'
+ default:
+ throw new Error(`Unknown icon: ${name}`)
+ }
+}
diff --git a/src/components/Toast/Toaster.tsx b/src/components/Toast/Toaster.tsx
new file mode 100644
index 0000000000..03b9324946
--- /dev/null
+++ b/src/components/Toast/Toaster.tsx
@@ -0,0 +1,3 @@
+export function Toaster() {
+ return null
+}
diff --git a/src/components/Toast/Toaster.web.tsx b/src/components/Toast/Toaster.web.tsx
new file mode 100644
index 0000000000..dd61b23f85
--- /dev/null
+++ b/src/components/Toast/Toaster.web.tsx
@@ -0,0 +1,14 @@
+import {Toaster as BurntToaster} from 'burnt/web'
+
+import {useBreakpoints, useTheme} from '#/alf'
+
+export function Toaster() {
+ const t = useTheme()
+ const {gtMobile} = useBreakpoints()
+ return (
+
+ )
+}
diff --git a/src/components/Toast/index.android.ts b/src/components/Toast/index.android.ts
new file mode 100644
index 0000000000..a1121e9763
--- /dev/null
+++ b/src/components/Toast/index.android.ts
@@ -0,0 +1,9 @@
+import {ToastProps} from './index.types'
+import * as LegacyToast from './LegacyToast'
+
+export * as Icon from './ToastIcon'
+
+export function toast({message, icon}: ToastProps) {
+ if (process.env.NODE_ENV === 'test') return
+ LegacyToast.show(message, icon)
+}
diff --git a/src/components/Toast/index.e2e.ts b/src/components/Toast/index.e2e.ts
new file mode 100644
index 0000000000..068886abd2
--- /dev/null
+++ b/src/components/Toast/index.e2e.ts
@@ -0,0 +1,5 @@
+import {ToastProps} from './index.types'
+
+export * as Icon from './ToastIcon'
+
+export function toast(_: ToastProps) {}
diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx
new file mode 100644
index 0000000000..f229d6746a
--- /dev/null
+++ b/src/components/Toast/index.tsx
@@ -0,0 +1,18 @@
+import {toast as burnt} from 'burnt'
+
+import {ToastProps} from './index.types'
+import {getSfSymbol, ToastIcon} from './ToastIcon'
+
+export * as Icon from './ToastIcon'
+
+export function toast({message, icon = 'check'}: ToastProps) {
+ if (process.env.NODE_ENV === 'test') return
+ burnt({
+ title: message,
+ icon: {
+ web: ,
+ ios: {name: getSfSymbol(icon), color: 'systemBlue'},
+ },
+ preset: 'custom',
+ })
+}
diff --git a/src/components/Toast/index.types.ts b/src/components/Toast/index.types.ts
new file mode 100644
index 0000000000..a408791a6a
--- /dev/null
+++ b/src/components/Toast/index.types.ts
@@ -0,0 +1,6 @@
+import {IconName} from './ToastIcon'
+
+export interface ToastProps {
+ message: string
+ icon?: IconName
+}
diff --git a/src/components/icons/Check.tsx b/src/components/icons/Check.tsx
index fe9883baf8..e465996233 100644
--- a/src/components/icons/Check.tsx
+++ b/src/components/icons/Check.tsx
@@ -7,3 +7,7 @@ export const Check_Stroke2_Corner0_Rounded = createSinglePathSVG({
export const CheckThick_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M21.474 2.98a2.5 2.5 0 0 1 .545 3.494l-10.222 14a2.5 2.5 0 0 1-3.528.52L2.49 16.617a2.5 2.5 0 0 1 3.018-3.986l3.75 2.84L17.98 3.525a2.5 2.5 0 0 1 3.493-.545Z',
})
+
+export const CircleCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm13.633-3.274a1 1 0 0 1 .141 1.407l-4.5 5.5a1 1 0 0 1-1.481.074l-2-2a1 1 0 1 1 1.414-1.414l1.219 1.219 3.8-4.645a1 1 0 0 1 1.407-.141Z',
+})
diff --git a/src/view/com/util/Toast.e2e.tsx b/src/view/com/util/Toast.e2e.tsx
deleted file mode 100644
index c5582ff0a8..0000000000
--- a/src/view/com/util/Toast.e2e.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export function show() {}
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx
index b18ea4b4ff..1cd3b657e0 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -1,215 +1,5 @@
-import {useEffect, useMemo, useRef, useState} from 'react'
-import {AccessibilityInfo, View} from 'react-native'
-import {
- Gesture,
- GestureDetector,
- GestureHandlerRootView,
-} from 'react-native-gesture-handler'
-import Animated, {
- FadeInUp,
- FadeOutUp,
- runOnJS,
- useAnimatedReaction,
- useAnimatedStyle,
- useSharedValue,
- withDecay,
- withSpring,
-} from 'react-native-reanimated'
-import RootSiblings from 'react-native-root-siblings'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {
- FontAwesomeIcon,
- Props as FontAwesomeProps,
-} from '@fortawesome/react-native-fontawesome'
+import {Icon, toast} from '#/components/Toast'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {atoms as a, useTheme} from '#/alf'
-import {Text} from '#/components/Typography'
-
-const TIMEOUT = 2e3
-
-export function show(
- message: string,
- icon: FontAwesomeProps['icon'] = 'check',
-) {
- if (process.env.NODE_ENV === 'test') {
- return
- }
- AccessibilityInfo.announceForAccessibility(message)
- const item = new RootSiblings(
- item.destroy()} />,
- )
-}
-
-function Toast({
- message,
- icon,
- destroy,
-}: {
- message: string
- icon: FontAwesomeProps['icon']
- destroy: () => void
-}) {
- const t = useTheme()
- const {top} = useSafeAreaInsets()
- const isPanning = useSharedValue(false)
- const dismissSwipeTranslateY = useSharedValue(0)
- const [cardHeight, setCardHeight] = useState(0)
-
- // for the exit animation to work on iOS the animated component
- // must not be the root component
- // so we need to wrap it in a view and unmount the toast ahead of time
- const [alive, setAlive] = useState(true)
-
- const hideAndDestroyImmediately = () => {
- setAlive(false)
- setTimeout(() => {
- destroy()
- }, 1e3)
- }
-
- const destroyTimeoutRef = useRef>()
- const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
- clearTimeout(destroyTimeoutRef.current)
- destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT)
- })
- const pauseDestroy = useNonReactiveCallback(() => {
- clearTimeout(destroyTimeoutRef.current)
- })
-
- useEffect(() => {
- hideAndDestroyAfterTimeout()
- }, [hideAndDestroyAfterTimeout])
-
- const panGesture = useMemo(() => {
- return Gesture.Pan()
- .activeOffsetY([-10, 10])
- .failOffsetX([-10, 10])
- .maxPointers(1)
- .onStart(() => {
- 'worklet'
- if (!alive) return
- isPanning.set(true)
- runOnJS(pauseDestroy)()
- })
- .onUpdate(e => {
- 'worklet'
- if (!alive) return
- dismissSwipeTranslateY.value = e.translationY
- })
- .onEnd(e => {
- 'worklet'
- if (!alive) return
- runOnJS(hideAndDestroyAfterTimeout)()
- isPanning.set(false)
- if (e.velocityY < -100) {
- if (dismissSwipeTranslateY.value === 0) {
- // HACK: If the initial value is 0, withDecay() animation doesn't start.
- // This is a bug in Reanimated, but for now we'll work around it like this.
- dismissSwipeTranslateY.value = 1
- }
- dismissSwipeTranslateY.value = withDecay({
- velocity: e.velocityY,
- velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
- deceleration: 1,
- })
- } else {
- dismissSwipeTranslateY.value = withSpring(0, {
- stiffness: 500,
- damping: 50,
- })
- }
- })
- }, [
- dismissSwipeTranslateY,
- isPanning,
- alive,
- hideAndDestroyAfterTimeout,
- pauseDestroy,
- ])
-
- const topOffset = top + 10
-
- useAnimatedReaction(
- () =>
- !isPanning.get() &&
- dismissSwipeTranslateY.get() < -topOffset - cardHeight,
- (isSwipedAway, prevIsSwipedAway) => {
- 'worklet'
- if (isSwipedAway && !prevIsSwipedAway) {
- runOnJS(destroy)()
- }
- },
- )
-
- const animatedStyle = useAnimatedStyle(() => {
- const translation = dismissSwipeTranslateY.get()
- return {
- transform: [
- {
- translateY: translation > 0 ? translation ** 0.7 : translation,
- },
- ],
- }
- })
-
- return (
-
- {alive && (
-
- setCardHeight(evt.nativeEvent.layout.height)}
- accessibilityRole="alert"
- accessible={true}
- accessibilityLabel={message}
- accessibilityHint=""
- onAccessibilityEscape={hideAndDestroyImmediately}
- style={[
- a.flex_1,
- t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg,
- a.shadow_lg,
- t.atoms.border_contrast_medium,
- a.rounded_sm,
- a.border,
- animatedStyle,
- ]}>
-
-
-
-
-
-
-
- {message}
-
-
-
-
-
-
- )}
-
- )
+export function show(message: string, legacyIcon: Icon.LegacyIconName) {
+ toast({message, icon: Icon.compat_convertLegacyIcon(legacyIcon)})
}
diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx
deleted file mode 100644
index 96798e61c4..0000000000
--- a/src/view/com/util/Toast.web.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Note: the dataSet properties are used to leverage custom CSS in public/index.html
- */
-
-import React, {useEffect, useState} from 'react'
-import {Pressable, StyleSheet, Text, View} from 'react-native'
-import {
- FontAwesomeIcon,
- FontAwesomeIconStyle,
- Props as FontAwesomeProps,
-} from '@fortawesome/react-native-fontawesome'
-
-const DURATION = 3500
-
-interface ActiveToast {
- text: string
- icon: FontAwesomeProps['icon']
-}
-type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
-
-// globals
-// =
-let globalSetActiveToast: GlobalSetActiveToast | undefined
-let toastTimeout: NodeJS.Timeout | undefined
-
-// components
-// =
-type ToastContainerProps = {}
-export const ToastContainer: React.FC = ({}) => {
- const [activeToast, setActiveToast] = useState()
- useEffect(() => {
- globalSetActiveToast = (t: ActiveToast | undefined) => {
- setActiveToast(t)
- }
- })
- return (
- <>
- {activeToast && (
-
-
- {activeToast.text}
- {
- setActiveToast(undefined)
- }}
- />
-
- )}
- >
- )
-}
-
-// methods
-// =
-
-export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') {
- if (toastTimeout) {
- clearTimeout(toastTimeout)
- }
- globalSetActiveToast?.({text, icon})
- toastTimeout = setTimeout(() => {
- globalSetActiveToast?.(undefined)
- }, DURATION)
-}
-
-const styles = StyleSheet.create({
- container: {
- // @ts-ignore web only
- position: 'fixed',
- left: 20,
- bottom: 20,
- // @ts-ignore web only
- width: 'calc(100% - 40px)',
- maxWidth: 350,
- padding: 20,
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#000c',
- borderRadius: 10,
- },
- dismissBackdrop: {
- position: 'absolute',
- top: 0,
- left: 0,
- bottom: 0,
- right: 0,
- },
- icon: {
- color: '#fff',
- flexShrink: 0,
- },
- text: {
- color: '#fff',
- fontSize: 18,
- marginLeft: 10,
- },
-})
diff --git a/yarn.lock b/yarn.lock
index 1e86174cb9..313bd53929 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8381,6 +8381,14 @@ builtins@^1.0.3:
resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==
+burnt@^0.12.2:
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/burnt/-/burnt-0.12.2.tgz#827dfb8fe2b5844d52060d9aa61dd9a683902d52"
+ integrity sha512-bbZjGN4Om7dykr8ZcLb0tTO5L2becMR+HIez1ySUGgG/rvK+ePgBEuBA6lMOZqOTsUXhIKFUBH0sCXQ25fq5SA==
+ dependencies:
+ sf-symbols-typescript "^1.0.0"
+ sonner "^0.3.5"
+
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -17078,6 +17086,11 @@ sf-symbols-typescript@^1.0.0:
resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f"
integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw==
+sf-symbols-typescript@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.1.0.tgz#50a2d7b36edd6809606f0b0a36322fc1fdd7cfde"
+ integrity sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A==
+
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
@@ -17267,6 +17280,11 @@ sonic-boom@^3.7.0:
dependencies:
atomic-sleep "^1.0.0"
+sonner@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/sonner/-/sonner-0.3.5.tgz#5ef175732cb7b637c6460620ab01a0fba0e6a36b"
+ integrity sha512-yIwaQ4dftMvFApuruto2t7wGyyaPRpj5qYBWYJIz4Z7uGcVn0IfqI/hWN0JyJN4izNbZFuCYZISf3fOGnvSlNQ==
+
source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"