-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(component): new notification center implemented with sonner (#…
…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
Showing
10 changed files
with
583 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './notification-center'; | ||
export type { Notification } from './types'; |
81 changes: 81 additions & 0 deletions
81
packages/frontend/component/src/ui/notification/notification-card.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
243 changes: 243 additions & 0 deletions
243
packages/frontend/component/src/ui/notification/notification-center.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}: | ||
</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> | ||
and | ||
<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> | ||
and | ||
<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> | ||
); | ||
}; |
Oops, something went wrong.