diff --git a/packages/component/package.json b/packages/component/package.json index 8f31db283d..b2de2a535b 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -37,6 +37,7 @@ "@popperjs/core": "^2.11.7", "@radix-ui/react-avatar": "^1.0.2", "@radix-ui/react-collapsible": "^1.0.2", + "@radix-ui/react-toast": "^1.1.3", "@toeverything/hooks": "workspace:*", "@toeverything/theme": "^0.5.8", "@vanilla-extract/dynamic": "^2.0.3", diff --git a/packages/component/src/components/notification-center/index.css.ts b/packages/component/src/components/notification-center/index.css.ts new file mode 100644 index 0000000000..938bce0fb4 --- /dev/null +++ b/packages/component/src/components/notification-center/index.css.ts @@ -0,0 +1,292 @@ +// Credits to sonner +// License on the MIT +// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/styles.css + +import { keyframes, style, styleVariants } from '@vanilla-extract/css'; + +const swipeOut = keyframes({ + '0%': { + transform: + 'translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)))', + opacity: 1, + }, + '100%': { + transform: + 'translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%))', + opacity: 0, + }, +}); + +export const notificationCenterViewportStyle = style({ + position: 'fixed', + bottom: '200px', + right: '60px', + width: '380px', + margin: 0, + zIndex: 2147483647, + outline: 'none', +}); + +export const notificationStyle = style({ + position: 'absolute', + borderRadius: '8px', + transition: 'transform 0.3s,opacity 0.3s, height 0.3s', + transform: 'var(--y)', + zIndex: 'var(--z-index)', + opacity: 0, + touchAction: 'none', + willChange: 'transform, opacity, height', + selectors: { + '&[data-visible=false]': { + opacity: '0 !important', + pointerEvents: 'none', + }, + '&[data-swiping=true]::before': { + content: '""', + position: 'absolute', + left: '0', + right: '0', + top: '50%', + height: '100%', + transform: 'scaleY(3) translateY(-50%)', + }, + '&[data-swiping=false][data-removed=true]::before': { + content: '""', + position: 'absolute', + inset: '0', + transform: 'scaleY(2)', + }, + '&[data-mounted=true]': { + opacity: 1, + vars: { + '--y': 'translateY(0)', + }, + }, + '&[data-expanded=false][data-front=false]': { + opacity: 1, + height: 'var(--front-toast-height)', + vars: { + '--scale': 'var(--toasts-before)* 0.05 + 1', + '--y': + 'translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)))', + }, + }, + '&[data-mounted=true][data-expanded=true]': { + height: 'var(--initial-height)', + vars: { + '--y': 'translateY(calc(var(--lift) * var(--offset)))', + }, + }, + '&[data-removed=true][data-front=true][data-swipe-out=false]': { + opacity: 0, + vars: { + '--y': 'translateY(calc(var(--lift) * -100%))', + }, + }, + '&[data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]': + { + opacity: 0, + vars: { + '--y': + 'translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%))', + }, + }, + '&[data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false] ': + { + transition: 'transform 500ms, opacity 200ms', + opacity: 0, + vars: { + '--y': 'translateY(40%)', + }, + }, + '&[data-removed=true][data-front=false]::before ': { + height: 'calc(var(--initial-height) + 20%)', + }, + '&[data-swiping=true]': { + transform: 'var(--y) translateY(var(--swipe-amount, 0px))', + transition: 'none', + }, + '&[data-swipe-out=true]': { + animation: `${swipeOut} 0.3s ease-in-out forwards`, + }, + }, + vars: { + '--y': 'translateY(100%)', + '--lift': '-1', + '--lift-amount': 'calc(var(--lift) * 14px)', + }, + '::after': { + content: '""', + position: 'absolute', + width: '100%', + height: '15px', + left: '0', + bottom: '100%', + borderRadius: '8px', + }, +}); +export const notificationIconStyle = style({ + fontSize: '24px', + marginLeft: '18px', + marginRight: '12px', + color: 'var(--affine-processing-color)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); +export const notificationContentStyle = style({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + padding: '16px 0', + width: '100%', + borderRadius: '8px', + boxShadow: 'var(--affine-shadow-1)', + border: '1px solid var(--affine-border-color)', + background: 'var(--affine-white)', + transition: 'all 0.3s', +}); +export const notificationTitleContactStyle = style({ + marginRight: '22px', + width: '200px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '1.5', +}); +export const notificationTitleStyle = style({ + display: 'flex', + alignItems: 'center', + width: '100%', + justifyContent: 'flex-start', +}); +export const notificationDescriptionStyle = style({ + fontSize: 'var(--affine-font-sm)', + color: 'var(--affine-text-secondary-color)', + marginBottom: '4px', +}); +export const notificationTimeStyle = style({ + fontSize: 'var(--affine-font-sm)', + color: 'var(--affine-text-secondary-color)', + marginBottom: '4px', +}); +export const closeButtonStyle = style({ + fontSize: '22px', + marginRight: '19px', + marginLeft: '16px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); +export const closeButtonWithoutUndoStyle = style({ + marginLeft: '92px', +}); +export const undoButtonStyle = style({ + fontSize: 'var(--affine-font-sm)', + background: 'var(--affine-hover-color)', + padding: '3px 6px', + borderRadius: '4px', + color: 'var(--affine-processing-color)', + cursor: 'pointer', +}); +export const messageStyle = style({ + fontSize: 'var(--affine-font-sm)', + width: '200px', + marginLeft: '50px', + lineHeight: '18px', +}); +export const progressBarStyle = style({ + fontSize: 'var(--affine-font-sm)', + width: '100%', + height: '10px', + marginTop: '10px', + padding: '0 16px', + borderRadius: '2px', + marginBottom: '16px', +}); +export const darkSuccessStyle = style({ + background: 'var(--affine-success-color)', + borderRadius: '8px', +}); +export const darkInfoStyle = style({ + background: 'var(--affine-processing-color)', + borderRadius: '8px', +}); +export const darkErrorStyle = style({ + background: 'var(--affine-error-color)', + borderRadius: '8px', +}); +export const darkWarningStyle = style({ + background: 'var(--affine-warning-color)', + borderRadius: '8px', +}); +export const lightSuccessStyle = style({ + background: 'var(--affine-background-success-color)', + borderRadius: '8px', +}); +export const lightInfoStyle = style({ + background: 'var(--affine-background-processing-color)', + borderRadius: '8px', +}); +export const lightErrorStyle = style({ + background: 'var(--affine-background-error-color)', + borderRadius: '8px', +}); +export const lightWarningStyle = style({ + background: 'var(--affine-background-warning-color)', + borderRadius: '8px', +}); +export const darkColorStyle = style({ + color: 'var(--affine-white)', +}); +export const lightInfoIconStyle = style({ + color: 'var(--affine-processing-color)', +}); +export const defaultCollapseStyle = styleVariants({ + secondary: { + '::after': { + background: 'rgba(0,0,0,0.02)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, + tertiary: { + '::after': { + background: 'rgba(0,0,0,0.04)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, +}); +export const lightCollapseStyle = styleVariants({ + secondary: { + '::after': { + background: 'rgba(0,0,0,0.04)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, + tertiary: { + '::after': { + background: 'rgba(0,0,0,0.08)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, +}); +export const darkCollapseStyle = styleVariants({ + secondary: { + '::after': { + background: 'rgba(0,0,0,0.08)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, + tertiary: { + '::after': { + background: 'rgba(0,0,0,0.16)', + top: '0px', + transition: 'background-color 0.3s', + }, + }, +}); diff --git a/packages/component/src/components/notification-center/index.jotai.ts b/packages/component/src/components/notification-center/index.jotai.ts new file mode 100644 index 0000000000..8ad5ba4e4e --- /dev/null +++ b/packages/component/src/components/notification-center/index.jotai.ts @@ -0,0 +1,64 @@ +import { atom } from 'jotai'; + +export type Notification = { + key: string; + title: string; + message: string; + type: 'success' | 'error' | 'warning' | 'info'; + theme?: 'light' | 'dark'; + timeout?: number; + progressingBar?: boolean; + // actions + undo?: () => Promise; +}; + +const notificationsBaseAtom = atom([]); + +const expandNotificationCenterBaseAtom = atom(false); +const cleanupQueueAtom = atom<(() => unknown)[]>([]); +export const expandNotificationCenterAtom = atom( + get => get(expandNotificationCenterBaseAtom), + (get, set, value) => { + if (value === false) { + get(cleanupQueueAtom).forEach(cleanup => cleanup()); + set(cleanupQueueAtom, []); + } + set(expandNotificationCenterBaseAtom, value); + } +); + +export const notificationsAtom = atom(get => + get(notificationsBaseAtom) +); + +export const removeNotificationAtom = atom(null, (get, set, key: string) => { + set(notificationsBaseAtom, notifications => + notifications.filter(notification => notification.key !== key) + ); +}); + +export const pushNotificationAtom = atom( + null, + (get, set, newNotification) => { + const key = newNotification.key; + const removeNotification = () => + set(notificationsBaseAtom, notifications => + notifications.filter(notification => notification.key !== key) + ); + const undo: (() => Promise) | undefined = newNotification.undo + ? (() => { + const undo: () => Promise = newNotification.undo; + return async function undoNotificationWrapper() { + removeNotification(); + return undo(); + }; + })() + : undefined; + + set(notificationsBaseAtom, notifications => [ + // push to the top + { ...newNotification, undo }, + ...notifications, + ]); + } +); diff --git a/packages/component/src/components/notification-center/index.stories.tsx b/packages/component/src/components/notification-center/index.stories.tsx new file mode 100644 index 0000000000..3a61ec3d4c --- /dev/null +++ b/packages/component/src/components/notification-center/index.stories.tsx @@ -0,0 +1,200 @@ +import type { Meta } from '@storybook/react'; +import { useAtomValue, useSetAtom } from 'jotai'; + +import { NotificationCenter, pushNotificationAtom } from '.'; +import { expandNotificationCenterAtom } from './index.jotai'; + +export default { + title: 'AFFiNE/NotificationCenter', + component: NotificationCenter, +} satisfies Meta; + +let id = 0; +export const Basic = () => { + const push = useSetAtom(pushNotificationAtom); + const expand = useAtomValue(expandNotificationCenterAtom); + return ( + <> +
{expand ? 'expanded' : 'collapsed'}
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + ); +}; diff --git a/packages/component/src/components/notification-center/index.tsx b/packages/component/src/components/notification-center/index.tsx new file mode 100644 index 0000000000..1cbc16bfde --- /dev/null +++ b/packages/component/src/components/notification-center/index.tsx @@ -0,0 +1,415 @@ +// Credits to sonner +// License on the MIT +// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx + +import { CloseIcon, InformationFillIcon } from '@blocksuite/icons'; +import * as Toast from '@radix-ui/react-toast'; +import clsx from 'clsx'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import type { ReactElement } from 'react'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import { IconButton } from '../../ui/button'; +import * as styles from './index.css'; +import type { Notification } from './index.jotai'; +import { + expandNotificationCenterAtom, + notificationsAtom, + pushNotificationAtom, + removeNotificationAtom, +} from './index.jotai'; + +// only expose necessary function atom to avoid misuse +export { pushNotificationAtom, removeNotificationAtom }; +type Height = { + height: number; + notificationKey: number | string; +}; +export type NotificationCardProps = { + notification: Notification; + notifications: Notification[]; + index: number; + heights: Height[]; + setHeights: React.Dispatch>; +}; +const typeColorMap = { + info: { + light: styles.lightInfoStyle, + dark: styles.darkInfoStyle, + }, + success: { + light: styles.lightSuccessStyle, + dark: styles.darkSuccessStyle, + }, + warning: { + light: styles.lightWarningStyle, + dark: styles.darkWarningStyle, + }, + error: { + light: styles.lightErrorStyle, + dark: styles.darkErrorStyle, + }, +}; + +function NotificationCard(props: NotificationCardProps): ReactElement { + const removeNotification = useSetAtom(removeNotificationAtom); + const { notification, notifications, setHeights, heights, index } = props; + const [expand, setExpand] = useAtom(expandNotificationCenterAtom); + // const setNotificationRemoveAnimation = useSetAtom(notificationRemoveAnimationAtom); + const [mounted, setMounted] = useState(false); + const [removed, setRemoved] = useState(false); + const [swiping, setSwiping] = useState(false); + const [swipeOut, setSwipeOut] = useState(false); + const [offsetBeforeRemove, setOffsetBeforeRemove] = useState(0); + const [initialHeight, setInitialHeight] = useState(0); + const [animationKey, setAnimationKey] = useState(0); + const animationRef = useRef(null); + const notificationRef = useRef(null); + const timerIdRef = useRef(); + const isFront = index === 0; + const isVisible = index + 1 <= 3; + const progressDuration = notification.timeout || 3000; + const heightIndex = useMemo( + () => + heights.findIndex( + height => height.notificationKey === notification.key + ) || 0, + [heights, notification.key] + ); + const duration = notification.timeout || 3000; + const offset = useRef(0); + const pointerStartYRef = useRef(null); + const notificationsHeightBefore = useMemo(() => { + return heights.reduce((prev, curr, reducerIndex) => { + // Calculate offset up untill current notification + if (reducerIndex >= heightIndex) { + return prev; + } + + return prev + curr.height; + }, 0); + }, [heights, heightIndex]); + + offset.current = useMemo( + () => heightIndex * 14 + notificationsHeightBefore, + [heightIndex, notificationsHeightBefore] + ); + + useEffect(() => { + // Trigger enter animation without using CSS animation + setMounted(true); + }, []); + useEffect(() => { + if (!expand) { + animationRef.current?.beginElement(); + } + }, [expand]); + + const resetAnimation = () => { + setAnimationKey(prevKey => prevKey + 1); + }; + useLayoutEffect(() => { + if (!mounted) return; + if (!notificationRef.current) return; + const notificationNode = notificationRef.current; + const originalHeight = notificationNode.style.height; + notificationNode.style.height = 'auto'; + const newHeight = notificationNode.getBoundingClientRect().height; + notificationNode.style.height = originalHeight; + + setInitialHeight(newHeight); + + setHeights(heights => { + const alreadyExists = heights.find( + height => height.notificationKey === notification.key + ); + if (!alreadyExists) { + return [ + { notificationKey: notification.key, height: newHeight }, + ...heights, + ]; + } else { + return heights.map(height => + height.notificationKey === notification.key + ? { ...height, height: newHeight } + : height + ); + } + }); + }, [notification.title, notification.key, mounted, setHeights]); + + const typeStyle = + typeColorMap[notification.type][notification.theme || 'light']; + + const onClickRemove = useCallback(() => { + // Save the offset for the exit swipe animation + setRemoved(true); + setOffsetBeforeRemove(offset.current); + setHeights(h => + h.filter(height => height.notificationKey !== notification.key) + ); + setTimeout(() => { + removeNotification(notification.key); + }, 200); + }, [setHeights, notification.key, removeNotification, offset]); + + useEffect(() => { + if (timerIdRef.current) { + clearTimeout(timerIdRef.current); + } + if (!expand) { + timerIdRef.current = setTimeout(() => { + onClickRemove(); + }, duration); + } + return () => { + if (timerIdRef.current) { + clearTimeout(timerIdRef.current); + } + }; + }, [duration, expand, onClickRemove]); + + const onClickUndo = useCallback(() => { + if (notification.undo) { + return notification.undo(); + } + }, [notification]); + + useEffect(() => { + const notificationNode = notificationRef.current; + + if (notificationNode) { + const height = notificationNode.getBoundingClientRect().height; + + // Add toast height tot heights array after the toast is mounted + setInitialHeight(height); + setHeights(h => [{ notificationKey: notification.key, height }, ...h]); + + return () => + setHeights(h => + h.filter(height => height.notificationKey !== notification.key) + ); + } + }, [notification.key, setHeights]); + return ( + { + setExpand(true); + }} + onMouseMove={() => { + setExpand(true); + }} + onMouseLeave={() => { + setExpand(false); + }} + style={ + { + '--index': index, + '--toasts-before': index, + '--z-index': notifications.length - index, + '--offset': `${removed ? offsetBeforeRemove : offset.current}px`, + '--initial-height': `${initialHeight}px`, + } as React.CSSProperties + } + onPointerDown={event => { + setOffsetBeforeRemove(offset.current); + (event.target as HTMLElement).setPointerCapture(event.pointerId); + if ((event.target as HTMLElement).tagName === 'BUTTON') return; + setSwiping(true); + pointerStartYRef.current = event.clientY; + }} + onPointerUp={() => { + if (swipeOut) return; + const swipeAmount = Number( + notificationRef.current?.style + .getPropertyValue('--swipe-amount') + .replace('px', '') || 0 + ); + if (Math.abs(swipeAmount) >= 20) { + setOffsetBeforeRemove(offset.current); + onClickRemove(); + setSwipeOut(true); + return; + } + + notificationRef.current?.style.setProperty('--swipe-amount', '0px'); + pointerStartYRef.current = null; + setSwiping(false); + }} + onPointerMove={event => { + if (!pointerStartYRef.current) return; + const yPosition = event.clientY - pointerStartYRef.current; + + const isAllowedToSwipe = yPosition > 0; + + if (!isAllowedToSwipe) { + notificationRef.current?.style.setProperty('--swipe-amount', '0px'); + return; + } + + notificationRef.current?.style.setProperty( + '--swipe-amount', + `${yPosition}px` + ); + }} + > +
+ +
+ +
+
+ {notification.title} +
+ {notification.undo && ( +
+ UNDO +
+ )} + + + +
+ + {notification.message} + + {notification.progressingBar && ( +
+ + + + + + +
+ )} +
+
+ ); +} + +export function NotificationCenter(): ReactElement { + const notifications = useAtomValue(notificationsAtom); + const [expand, setExpand] = useAtom(expandNotificationCenterAtom); + + if (notifications.length === 0 && expand) { + setExpand(false); + } + const [heights, setHeights] = useState([]); + const listRef = useRef(null); + + useEffect(() => { + // Ensure expanded is always false when no toasts are present / only one left + if (notifications.length <= 1) { + setExpand(false); + } + }, [notifications, setExpand]); + + if (!notifications.length) return <>; + return ( + + {notifications.map((notification, index) => ( + + ))} + + + ); +} diff --git a/yarn.lock b/yarn.lock index ec4e7c2880..d9d3e888e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,6 +68,7 @@ __metadata: "@popperjs/core": ^2.11.7 "@radix-ui/react-avatar": ^1.0.2 "@radix-ui/react-collapsible": ^1.0.2 + "@radix-ui/react-toast": ^1.1.3 "@storybook/addon-actions": ^7.0.12 "@storybook/addon-coverage": ^0.0.8 "@storybook/addon-essentials": ^7.0.12 @@ -6444,6 +6445,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-collection@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-collection@npm:1.0.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-compose-refs": 1.0.0 + "@radix-ui/react-context": 1.0.0 + "@radix-ui/react-primitive": 1.0.2 + "@radix-ui/react-slot": 1.0.1 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: f7d92f52c7f92b53c055370a5cbf077eea54366706eec9100973737577d841c0cc76a2a577fec67dd85b2853d03c20c4810058f0f511821052358439017e9e5d + languageName: node + linkType: hard + "@radix-ui/react-compose-refs@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-compose-refs@npm:1.0.0" @@ -6520,6 +6537,23 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.0 + "@radix-ui/react-compose-refs": 1.0.0 + "@radix-ui/react-primitive": 1.0.2 + "@radix-ui/react-use-callback-ref": 1.0.0 + "@radix-ui/react-use-escape-keydown": 1.0.2 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: cb2a38a65dd129d1fd58436bedee765f46f6a6edc2ec15d534a1499c10f768ae06ad874704e030c85869b3ee4b61103076a116dfdb7e0c761a8c8cdc30a5c951 + languageName: node + linkType: hard + "@radix-ui/react-focus-guards@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-focus-guards@npm:1.0.0" @@ -6571,6 +6605,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-portal@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-portal@npm:1.0.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.2 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: 1165b4bced8057021ea9ac4f568c6e0ea6f190936f07dc96780d67488b9222021444bbb8e04e506eea84d9219a5caacae8d0974e745182d4f398aa903b982e19 + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-presence@npm:1.0.0" @@ -6669,6 +6716,30 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-toast@npm:^1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-toast@npm:1.1.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.0 + "@radix-ui/react-collection": 1.0.2 + "@radix-ui/react-compose-refs": 1.0.0 + "@radix-ui/react-context": 1.0.0 + "@radix-ui/react-dismissable-layer": 1.0.3 + "@radix-ui/react-portal": 1.0.2 + "@radix-ui/react-presence": 1.0.0 + "@radix-ui/react-primitive": 1.0.2 + "@radix-ui/react-use-callback-ref": 1.0.0 + "@radix-ui/react-use-controllable-state": 1.0.0 + "@radix-ui/react-use-layout-effect": 1.0.0 + "@radix-ui/react-visually-hidden": 1.0.2 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: b95baa857eea69b92a019ad511ac3da2fa2c8d6689593d6db576fe9aec2408c36f27b7ea4deda4c9b12b695103ece963fe4a270909b640416a11c0ab8115de95 + languageName: node + linkType: hard + "@radix-ui/react-use-callback-ref@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-use-callback-ref@npm:1.0.0" @@ -6704,6 +6775,18 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-escape-keydown@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-use-callback-ref": 1.0.0 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + checksum: 5bec1b73ed6c38139bf1db3c626c0474ca6221ae55f154ef83f1c6429ea866280b2a0ba9436b807334d0215bb4389f0b492c65471cf565635957a8ee77cce98a + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0" @@ -6715,6 +6798,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-visually-hidden@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-visually-hidden@npm:1.0.2" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/react-primitive": 1.0.2 + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + checksum: 67c4a55cfad9a8ff519a9b4ce24d2cc9d78c34d08a128a85de1a0a41228fdeb961eaeb4e50ca0d2080c5e31cef6f6e703cb06786579b44e5c66af161b941adb6 + languageName: node + linkType: hard + "@react-dnd/asap@npm:^5.0.1": version: 5.0.2 resolution: "@react-dnd/asap@npm:5.0.2"