feat(component): init notification center (#2426)

Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
Himself65
2023-05-26 14:32:01 +08:00
committed by GitHub
parent 36534f1915
commit f4b3830a0e
6 changed files with 1068 additions and 0 deletions

View File

@@ -37,6 +37,7 @@
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@radix-ui/react-avatar": "^1.0.2", "@radix-ui/react-avatar": "^1.0.2",
"@radix-ui/react-collapsible": "^1.0.2", "@radix-ui/react-collapsible": "^1.0.2",
"@radix-ui/react-toast": "^1.1.3",
"@toeverything/hooks": "workspace:*", "@toeverything/hooks": "workspace:*",
"@toeverything/theme": "^0.5.8", "@toeverything/theme": "^0.5.8",
"@vanilla-extract/dynamic": "^2.0.3", "@vanilla-extract/dynamic": "^2.0.3",

View File

@@ -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',
},
},
});

View File

@@ -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<void>;
};
const notificationsBaseAtom = atom<Notification[]>([]);
const expandNotificationCenterBaseAtom = atom(false);
const cleanupQueueAtom = atom<(() => unknown)[]>([]);
export const expandNotificationCenterAtom = atom<boolean, [boolean], void>(
get => get(expandNotificationCenterBaseAtom),
(get, set, value) => {
if (value === false) {
get(cleanupQueueAtom).forEach(cleanup => cleanup());
set(cleanupQueueAtom, []);
}
set(expandNotificationCenterBaseAtom, value);
}
);
export const notificationsAtom = atom<Notification[]>(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, [Notification], void>(
null,
(get, set, newNotification) => {
const key = newNotification.key;
const removeNotification = () =>
set(notificationsBaseAtom, notifications =>
notifications.filter(notification => notification.key !== key)
);
const undo: (() => Promise<void>) | undefined = newNotification.undo
? (() => {
const undo: () => Promise<void> = newNotification.undo;
return async function undoNotificationWrapper() {
removeNotification();
return undo();
};
})()
: undefined;
set(notificationsBaseAtom, notifications => [
// push to the top
{ ...newNotification, undo },
...notifications,
]);
}
);

View File

@@ -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<typeof NotificationCenter>;
let id = 0;
export const Basic = () => {
const push = useSetAtom(pushNotificationAtom);
const expand = useAtomValue(expandNotificationCenterAtom);
return (
<>
<div>{expand ? 'expanded' : 'collapsed'}</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: `${key} message`,
timeout: 3000,
progressingBar: true,
undo: async () => {
console.log('undo');
},
type: 'info',
});
}}
>
Push timeout notification
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: `${key} message`,
type: 'info',
});
}}
>
Push notification with no timeout
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
});
}}
>
Push notification with no message
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'success',
theme: 'light',
});
}}
>
light success
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'success',
theme: 'dark',
});
}}
>
dark success
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
theme: 'light',
});
}}
>
light info
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
theme: 'dark',
});
}}
>
dark info
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'warning',
theme: 'light',
});
}}
>
light warning
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'warning',
theme: 'dark',
});
}}
>
dark warning
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'error',
theme: 'light',
});
}}
>
light error
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'error',
theme: 'dark',
});
}}
>
dark error
</button>
</div>
<NotificationCenter />
</>
);
};

View File

@@ -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<React.SetStateAction<Height[]>>;
};
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<boolean>(false);
const [removed, setRemoved] = useState<boolean>(false);
const [swiping, setSwiping] = useState<boolean>(false);
const [swipeOut, setSwipeOut] = useState<boolean>(false);
const [offsetBeforeRemove, setOffsetBeforeRemove] = useState<number>(0);
const [initialHeight, setInitialHeight] = useState<number>(0);
const [animationKey, setAnimationKey] = useState(0);
const animationRef = useRef<SVGAnimateElement>(null);
const notificationRef = useRef<HTMLLIElement>(null);
const timerIdRef = useRef<NodeJS.Timeout>();
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<number | null>(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 (
<Toast.Root
className={clsx(styles.notificationStyle, {
[styles.lightCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
!isFront && !expand && notification.theme === 'light',
[styles.darkCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
!isFront && !expand && notification.theme === 'dark',
[styles.defaultCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
!isFront && !expand && !notification.theme,
})}
duration={Infinity}
aria-live="polite"
aria-atomic="true"
role="status"
tabIndex={0}
ref={notificationRef}
data-mounted={mounted}
data-removed={removed}
data-visible={isVisible}
data-index={index}
data-front={isFront}
data-swiping={swiping}
data-swipe-out={swipeOut}
data-expanded={expand}
onMouseEnter={() => {
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`
);
}}
>
<div
className={clsx(styles.notificationContentStyle, {
[typeStyle]: notification.theme,
})}
>
<Toast.Title
className={clsx(styles.notificationTitleStyle, {
[styles.darkColorStyle]: notification.theme === 'dark',
})}
>
<div
className={clsx(styles.notificationIconStyle, {
[styles.darkColorStyle]: notification.theme === 'dark',
[styles.lightInfoIconStyle]: notification.theme !== 'dark',
})}
>
<InformationFillIcon />
</div>
<div className={styles.notificationTitleContactStyle}>
{notification.title}
</div>
{notification.undo && (
<div
className={clsx(styles.undoButtonStyle, {
[styles.darkColorStyle]: notification.theme === 'dark',
})}
onClick={onClickUndo}
>
UNDO
</div>
)}
<IconButton
className={clsx(styles.closeButtonStyle, {
[styles.closeButtonWithoutUndoStyle]: !notification.undo,
})}
style={{
color:
notification.theme === 'dark'
? 'var(--affine-white)'
: 'var(--affine-icon-color)',
}}
>
<CloseIcon onClick={onClickRemove} />
</IconButton>
</Toast.Title>
<Toast.Description
className={clsx(styles.messageStyle, {
[styles.darkColorStyle]: notification.theme === 'dark',
})}
>
{notification.message}
</Toast.Description>
{notification.progressingBar && (
<div className={styles.progressBarStyle}>
<svg width="100%" height="4">
<rect
width="100%"
height="4"
fill="var(--affine-hover-color)"
rx="2"
ry="2"
/>
<rect
width="0%"
height="4"
fill="var(--affine-primary-color)"
rx="2"
ry="2"
>
<animate
key={animationKey}
ref={animationRef}
attributeName="width"
from="0%"
to="100%"
dur={(progressDuration - 200) / 1000}
fill="freeze"
onAnimationEnd={resetAnimation}
/>
</rect>
</svg>
</div>
)}
</div>
</Toast.Root>
);
}
export function NotificationCenter(): ReactElement {
const notifications = useAtomValue(notificationsAtom);
const [expand, setExpand] = useAtom(expandNotificationCenterAtom);
if (notifications.length === 0 && expand) {
setExpand(false);
}
const [heights, setHeights] = useState<Height[]>([]);
const listRef = useRef<HTMLOListElement>(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 (
<Toast.Provider swipeDirection="right">
{notifications.map((notification, index) => (
<NotificationCard
notification={notification}
index={index}
key={notification.key}
notifications={notifications}
heights={heights}
setHeights={setHeights}
/>
))}
<Toast.Viewport
tabIndex={-1}
ref={listRef}
style={
{
'--front-toast-height': `${heights[0]?.height}px`,
} as React.CSSProperties
}
className={styles.notificationCenterViewportStyle}
/>
</Toast.Provider>
);
}

View File

@@ -68,6 +68,7 @@ __metadata:
"@popperjs/core": ^2.11.7 "@popperjs/core": ^2.11.7
"@radix-ui/react-avatar": ^1.0.2 "@radix-ui/react-avatar": ^1.0.2
"@radix-ui/react-collapsible": ^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-actions": ^7.0.12
"@storybook/addon-coverage": ^0.0.8 "@storybook/addon-coverage": ^0.0.8
"@storybook/addon-essentials": ^7.0.12 "@storybook/addon-essentials": ^7.0.12
@@ -6444,6 +6445,22 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@radix-ui/react-compose-refs@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-compose-refs@npm:1.0.0" resolution: "@radix-ui/react-compose-refs@npm:1.0.0"
@@ -6520,6 +6537,23 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@radix-ui/react-focus-guards@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-focus-guards@npm:1.0.0" resolution: "@radix-ui/react-focus-guards@npm:1.0.0"
@@ -6571,6 +6605,19 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@radix-ui/react-presence@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-presence@npm:1.0.0" resolution: "@radix-ui/react-presence@npm:1.0.0"
@@ -6669,6 +6716,30 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@radix-ui/react-use-callback-ref@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.0" resolution: "@radix-ui/react-use-callback-ref@npm:1.0.0"
@@ -6704,6 +6775,18 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@radix-ui/react-use-layout-effect@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0" resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0"
@@ -6715,6 +6798,19 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@react-dnd/asap@npm:^5.0.1":
version: 5.0.2 version: 5.0.2
resolution: "@react-dnd/asap@npm:5.0.2" resolution: "@react-dnd/asap@npm:5.0.2"