Skip to content

Commit

Permalink
refactor(component): new notification center implemented with sonner (#…
Browse files Browse the repository at this point in the history
…6416)

The Notification has been reimplemented using sooner, no longer relies on jotai, and new story has been added.

- Before
  ```ts
  import { pushNotificationAtom } from '@affine/component/notification-center';
  import { useSetAtom } from 'jotai';

  export const Component = () => {
    const pushNotification = useSetAtom(pushNotificationAtom);
    pushNotification({ ... });
  }
  ```

- After
  ```ts
  import { notify } from "@affine/component";

  export const Component = () => {
    notify({ ... });
  }
  ```
  • Loading branch information
CatsJuice committed Apr 2, 2024
1 parent 80c7750 commit a4cd51e
Show file tree
Hide file tree
Showing 10 changed files with 583 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/frontend/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"react-transition-state": "^2.1.1",
"react-virtuoso": "^4.7.0",
"rxjs": "^7.8.1",
"sonner": "^1.4.41",
"swr": "^2.2.5",
"uuid": "^9.0.1",
"zod": "^3.22.4"
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/component/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './ui/lottie/collections-icon';
export * from './ui/lottie/delete-icon';
export * from './ui/menu';
export * from './ui/modal';
export * from './ui/notification';
export * from './ui/popover';
export * from './ui/scrollbar';
export * from './ui/skeleton';
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/component/src/ui/notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './notification-center';
export type { Notification } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { type HTMLAttributes, useCallback } from 'react';

import { Button, IconButton } from '../button';
import * as styles from './styles.css';
import type { Notification } from './types';
import {
getActionTextColor,
getCardBorderColor,
getCardColor,
getCardForegroundColor,
} from './utils';

export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
notification: Notification;
onDismiss?: () => void;
}

export const NotificationCard = ({
notification,
onDismiss,
}: NotificationCardProps) => {
const {
theme = 'info',
style = 'normal',
icon = <InformationFillDuotoneIcon />,
action,
title,
footer,
} = notification;

const onActionClicked = useCallback(() => {
action?.onClick()?.catch(console.error);
if (action?.autoClose !== false) {
onDismiss?.();
}
}, [action, onDismiss]);

return (
<div
style={assignInlineVars({
[styles.cardColor]: getCardColor(style, theme),
[styles.cardBorderColor]: getCardBorderColor(style),
[styles.cardForeground]: getCardForegroundColor(style),
[styles.actionTextColor]: getActionTextColor(style, theme),
})}
data-with-icon={icon ? '' : undefined}
className={styles.card}
>
<header className={styles.header}>
{icon ? (
<div className={clsx(styles.icon, styles.headAlignWrapper)}>
{icon}
</div>
) : null}
<div className={styles.title}>{title}</div>

{action ? (
<div className={clsx(styles.headAlignWrapper, styles.action)}>
<Button
className={styles.actionButton}
onClick={onActionClicked}
{...action.buttonProps}
>
{action.label}
</Button>
</div>
) : null}
<div className={styles.headAlignWrapper}>
<IconButton onClick={onDismiss}>
<CloseIcon className={styles.closeIcon} width={16} height={16} />
</IconButton>
</div>
</header>
<main className={styles.main}>{notification.message}</main>
<footer>{footer}</footer>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
import type { StoryFn } from '@storybook/react';
import { cssVar } from '@toeverything/theme';
import { type HTMLAttributes, useState } from 'react';

import { Button } from '../button';
import { Modal } from '../modal';
import { NotificationCenter, notify } from './notification-center';
import type {
NotificationCustomRendererProps,
NotificationStyle,
NotificationTheme,
} from './types';
import {
getCardBorderColor,
getCardColor,
getCardForegroundColor,
} from './utils';

export default {
title: 'UI/NotificationCenter',
};

const themes: NotificationTheme[] = ['info', 'success', 'warning', 'error'];
const styles: NotificationStyle[] = ['normal', 'information', 'alert'];

const Root = ({ children, ...attrs }: HTMLAttributes<HTMLDivElement>) => (
<>
<NotificationCenter />
<div {...attrs}>{children}</div>
</>
);
const Label = ({ children, ...attrs }: HTMLAttributes<HTMLSpanElement>) => (
<span style={{ fontWeight: 400, opacity: 0.5 }} {...attrs}>
{children}:&nbsp;
</span>
);

export const ThemeAndStyle: StoryFn = () => {
return (
<Root>
{styles.map(style => {
return (
<div key={style} style={{ marginBottom: 20 }}>
<h3 style={{ marginBottom: 8 }}>
<Label>style</Label>
{style}
</h3>
<div style={{ display: 'flex', gap: 4 }}>
{themes.map(theme => {
return (
<Button
style={{
backgroundColor: getCardColor(style, theme),
borderColor: getCardBorderColor(style),
color: getCardForegroundColor(style),
}}
key={theme}
onClick={() =>
notify({
title: `${theme} title`,
message: (
<span>
Test with <Label>style</Label>
<code>{style}</code>
&nbsp;and&nbsp;
<Label>theme</Label>
<code>{theme}</code>
</span>
),
style,
theme,
})
}
>
<Label>theme</Label> {theme}
</Button>
);
})}
</div>
</div>
);
})}
</Root>
);
};

export const CustomIcon: StoryFn = () => {
const icons = [
{ label: 'No icon', icon: null },
{
label: 'SingleSelectIcon',
icon: <SingleSelectSelectSolidIcon />,
},
{
label: 'Icon Color',
icon: <SingleSelectSelectSolidIcon color={cssVar('successColor')} />,
},
];

return (
<Root style={{ display: 'flex', gap: 4 }}>
{icons.map(({ label, icon }) => (
<Button
key={label}
onClick={() =>
notify({
title: label,
message: 'test with custom icon ' + label,
icon,
})
}
>
{label}
</Button>
))}
</Root>
);
};

export const CustomRenderer: StoryFn = () => {
const CustomRender = ({ onDismiss }: NotificationCustomRendererProps) => {
return (
<div
style={{
border: '1px solid ' + cssVar('borderColor'),
padding: 16,
borderRadius: 4,
background: cssVar('white'),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
CustomRenderer
<Button onClick={onDismiss}>Close</Button>
</div>
);
};

return (
<Root>
<Button onClick={() => notify.custom(CustomRender)}>
Open CustomRenderer
</Button>
</Root>
);
};

export const WithAction: StoryFn = () => {
return (
<Root>
{styles.map(style => {
return (
<div key={style} style={{ marginBottom: 20 }}>
<h3 style={{ marginBottom: 8 }}>
<Label>style</Label>
{style}
</h3>
<div style={{ display: 'flex', gap: 4 }}>
{themes.map(theme => {
return (
<Button
style={{
backgroundColor: getCardColor(style, theme),
borderColor: getCardBorderColor(style),
color: getCardForegroundColor(style),
}}
key={theme}
onClick={() =>
notify({
title: `${theme} title`,
message: (
<span>
Test with <Label>style</Label>
<code>{style}</code>
&nbsp;and&nbsp;
<Label>theme</Label>
<code>{theme}</code>
</span>
),
style,
theme,
action: {
label: 'UNDO',
onClick: () => console.log('undo'),
},
})
}
>
<Label>theme</Label> {theme}
</Button>
);
})}
</div>
</div>
);
})}

<h3 style={{ marginBottom: 8 }}>Disable auto close</h3>
<Button
onClick={() => {
notify(
{
title: 'Disable auto close',
message: 'Test with disable auto close',
action: {
label: 'UNDO',
onClick: () => console.log('undo'),
autoClose: false,
},
},
{ duration: 22222222 }
);
}}
>
Do not close after action clicked
</Button>
</Root>
);
};

export const ZIndexWithModal: StoryFn = () => {
const [open, setOpen] = useState(false);

return (
<Root>
<Button onClick={() => setOpen(true)}>Open modal</Button>
<Modal open={open} onOpenChange={setOpen}>
<Button
onClick={() =>
notify(
{ title: 'Notify', message: 'Test with modal' },
{ duration: 2000000 }
)
}
>
Notify
</Button>
</Modal>
</Root>
);
};

0 comments on commit a4cd51e

Please sign in to comment.