mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(component): init notification center (#2426)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
415
packages/component/src/components/notification-center/index.tsx
Normal file
415
packages/component/src/components/notification-center/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
yarn.lock
96
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user