From 1c91f0b7b95c77205afd9ac8cdf064b839e409a4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 8 Mar 2025 12:15:54 +0000 Subject: [PATCH] add burnt --- .../circleCheck_stroke2_corner0_rounded.svg | 1 + package.json | 2 + src/App.web.tsx | 4 +- src/components/Toast/LegacyToast.tsx | 207 +++++++++++++++++ src/components/Toast/ToastIcon.tsx | 83 +++++++ src/components/Toast/Toaster.tsx | 3 + src/components/Toast/Toaster.web.tsx | 14 ++ src/components/Toast/index.android.ts | 9 + src/components/Toast/index.e2e.ts | 5 + src/components/Toast/index.tsx | 18 ++ src/components/Toast/index.types.ts | 6 + src/components/icons/Check.tsx | 4 + src/view/com/util/Toast.e2e.tsx | 1 - src/view/com/util/Toast.tsx | 216 +----------------- src/view/com/util/Toast.web.tsx | 104 --------- yarn.lock | 18 ++ 16 files changed, 375 insertions(+), 320 deletions(-) create mode 100644 assets/icons/circleCheck_stroke2_corner0_rounded.svg create mode 100644 src/components/Toast/LegacyToast.tsx create mode 100644 src/components/Toast/ToastIcon.tsx create mode 100644 src/components/Toast/Toaster.tsx create mode 100644 src/components/Toast/Toaster.web.tsx create mode 100644 src/components/Toast/index.android.ts create mode 100644 src/components/Toast/index.e2e.ts create mode 100644 src/components/Toast/index.tsx create mode 100644 src/components/Toast/index.types.ts delete mode 100644 src/view/com/util/Toast.e2e.tsx delete mode 100644 src/view/com/util/Toast.web.tsx 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"