feat(component): basic notification adaptation for mobile (#8402)
This commit is contained in:
@@ -1,25 +1,15 @@
|
|||||||
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { type HTMLAttributes, useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Button, IconButton } from '../button';
|
import { Button, IconButton } from '../../button';
|
||||||
|
import type { NotificationCardProps } from '../types';
|
||||||
|
import { getCardVars } from '../utils';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
import type { Notification } from './types';
|
|
||||||
import {
|
|
||||||
getActionTextColor,
|
|
||||||
getCardBorderColor,
|
|
||||||
getCardColor,
|
|
||||||
getCardForegroundColor,
|
|
||||||
getCloseIconColor,
|
|
||||||
getIconColor,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
|
export const DesktopNotificationCard = ({
|
||||||
notification: Notification;
|
notification,
|
||||||
}
|
}: NotificationCardProps) => {
|
||||||
|
|
||||||
export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
|
||||||
const {
|
const {
|
||||||
theme = 'info',
|
theme = 'info',
|
||||||
style = 'normal',
|
style = 'normal',
|
||||||
@@ -43,14 +33,7 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={assignInlineVars({
|
style={getCardVars(style, theme, iconColor)}
|
||||||
[styles.cardColor]: getCardColor(style, theme),
|
|
||||||
[styles.cardBorderColor]: getCardBorderColor(style),
|
|
||||||
[styles.cardForeground]: getCardForegroundColor(style),
|
|
||||||
[styles.actionTextColor]: getActionTextColor(style, theme),
|
|
||||||
[styles.iconColor]: getIconColor(style, theme, iconColor),
|
|
||||||
[styles.closeIconColor]: getCloseIconColor(style),
|
|
||||||
})}
|
|
||||||
data-with-icon={icon ? '' : undefined}
|
data-with-icon={icon ? '' : undefined}
|
||||||
{...rootAttrs}
|
{...rootAttrs}
|
||||||
className={clsx(styles.card, rootAttrs?.className)}
|
className={clsx(styles.card, rootAttrs?.className)}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import { type CSSProperties, useMemo } from 'react';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
|
import type { NotificationCenterProps } from '../types';
|
||||||
|
|
||||||
|
export function DesktopNotificationCenter({
|
||||||
|
width = 380,
|
||||||
|
}: NotificationCenterProps) {
|
||||||
|
const style = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...assignInlineVars({
|
||||||
|
// override css vars inside sonner
|
||||||
|
'--width': `${width}px`,
|
||||||
|
}),
|
||||||
|
// radix-ui will lock pointer-events when dialog is open
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
} satisfies CSSProperties;
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
const toastOptions = useMemo(
|
||||||
|
() => ({
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
className="affine-notification-center"
|
||||||
|
style={style}
|
||||||
|
toastOptions={toastOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './notification-center';
|
export * from './notify';
|
||||||
export type { Notification } from './types';
|
export type { Notification } from './types';
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button, IconButton } from '../../button';
|
||||||
|
import { Modal } from '../../modal';
|
||||||
|
import type { NotificationCardProps } from '../types';
|
||||||
|
import { getCardVars } from '../utils';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export function MobileNotificationCard({
|
||||||
|
notification,
|
||||||
|
}: NotificationCardProps) {
|
||||||
|
const {
|
||||||
|
theme = 'info',
|
||||||
|
style = 'normal',
|
||||||
|
icon = <InformationFillDuotoneIcon />,
|
||||||
|
iconColor,
|
||||||
|
onDismiss,
|
||||||
|
} = notification;
|
||||||
|
|
||||||
|
const [animated, setAnimated] = useState(false);
|
||||||
|
const [showDetail, setShowDetail] = useState(false);
|
||||||
|
|
||||||
|
const handleShowDetail = useCallback(() => {
|
||||||
|
setAnimated(true);
|
||||||
|
setShowDetail(true);
|
||||||
|
}, []);
|
||||||
|
const handleHideDetail = useCallback(() => {
|
||||||
|
setShowDetail(false);
|
||||||
|
onDismiss?.();
|
||||||
|
}, [onDismiss]);
|
||||||
|
|
||||||
|
return showDetail ? (
|
||||||
|
<MobileNotifyDetail
|
||||||
|
notification={notification}
|
||||||
|
onClose={handleHideDetail}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
data-animated={animated}
|
||||||
|
onClick={handleShowDetail}
|
||||||
|
className={styles.toastRoot}
|
||||||
|
style={getCardVars(style, theme, iconColor)}
|
||||||
|
>
|
||||||
|
<span className={styles.toastIcon}>{icon}</span>
|
||||||
|
<span className={styles.toastLabel}>{notification.title}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const MobileNotifyDetail = ({
|
||||||
|
notification,
|
||||||
|
onClose,
|
||||||
|
}: NotificationCardProps & {
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
theme = 'info',
|
||||||
|
style = 'normal',
|
||||||
|
icon = <InformationFillDuotoneIcon />,
|
||||||
|
iconColor,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
footer,
|
||||||
|
action,
|
||||||
|
} = notification;
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open) onClose();
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
const onActionClicked = useCallback(() => {
|
||||||
|
action?.onClick()?.catch(console.error);
|
||||||
|
if (action?.autoClose !== false) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
}, [action, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open
|
||||||
|
withoutCloseButton
|
||||||
|
width="100%"
|
||||||
|
minHeight={60}
|
||||||
|
animation="slideBottom"
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
contentWrapperStyle={getCardVars(style, theme, iconColor)}
|
||||||
|
contentOptions={{ style: { padding: '12px 0' } }}
|
||||||
|
>
|
||||||
|
<div className={styles.detailCard} onClick={e => e.stopPropagation()}>
|
||||||
|
<header className={styles.detailHeader}>
|
||||||
|
<span className={styles.detailIcon}>{icon}</span>
|
||||||
|
<span className={styles.detailLabel}>{title}</span>
|
||||||
|
<IconButton onClick={onClose} icon={<CloseIcon />} />
|
||||||
|
</header>
|
||||||
|
<main className={styles.detailContent}>{message}</main>
|
||||||
|
{/* actions */}
|
||||||
|
<div className={styles.detailActions}>
|
||||||
|
{action ? (
|
||||||
|
<Button onClick={onActionClicked} {...action.buttonProps}>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{footer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Toaster } from 'sonner';
|
||||||
|
|
||||||
|
export function MobileNotificationCenter() {
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
visibleToasts={1}
|
||||||
|
position="top-center"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
top: 'calc(env(safe-area-inset-top) + 16px)',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
cardBorderColor,
|
||||||
|
cardColor,
|
||||||
|
cardForeground,
|
||||||
|
iconColor,
|
||||||
|
} from '../desktop/styles.css';
|
||||||
|
|
||||||
|
const expandIn = keyframes({
|
||||||
|
from: {
|
||||||
|
maxWidth: 44,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
maxWidth: '100vw',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const toastRoot = style({
|
||||||
|
width: 'fit-content',
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 22,
|
||||||
|
margin: '0px auto',
|
||||||
|
padding: 10,
|
||||||
|
backgroundColor: cardColor,
|
||||||
|
color: cardForeground,
|
||||||
|
border: `1px solid ${cardBorderColor}`,
|
||||||
|
boxShadow: cssVar('shadow1'),
|
||||||
|
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'transform 0.1s',
|
||||||
|
|
||||||
|
':active': {
|
||||||
|
transform: 'scale(0.97)',
|
||||||
|
},
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[data-animated="true"]': {
|
||||||
|
// sooner will apply the animation when leaving, hide it
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
'&[data-animated="false"]': {
|
||||||
|
maxWidth: 44,
|
||||||
|
animation: `${expandIn} 0.8s cubic-bezier(.27,.28,.13,.99)`,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toastIcon = style({
|
||||||
|
fontSize: 24,
|
||||||
|
lineHeight: 0,
|
||||||
|
color: iconColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toastLabel = style({
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: '22px',
|
||||||
|
letterSpacing: -0.43,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const detailRoot = style({
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'start',
|
||||||
|
padding: 16,
|
||||||
|
zIndex: 9999,
|
||||||
|
background: 'rgba(0,0,0,0.1)',
|
||||||
|
});
|
||||||
|
export const detailCard = style({
|
||||||
|
// backgroundColor: cardColor,
|
||||||
|
// color: cardForeground,
|
||||||
|
});
|
||||||
|
export const detailHeader = style({
|
||||||
|
padding: '0 20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
export const detailContent = style({
|
||||||
|
padding: '0 20px',
|
||||||
|
marginTop: 8,
|
||||||
|
});
|
||||||
|
export const detailIcon = style([toastIcon, {}]);
|
||||||
|
export const detailLabel = style([
|
||||||
|
toastLabel,
|
||||||
|
{
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const detailActions = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import { type HTMLAttributes, useState } from 'react';
|
|||||||
|
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { Modal } from '../modal';
|
import { Modal } from '../modal';
|
||||||
import { NotificationCenter, notify } from './notification-center';
|
import { NotificationCenter, notify } from '.';
|
||||||
import type {
|
import type {
|
||||||
NotificationCustomRendererProps,
|
NotificationCustomRendererProps,
|
||||||
NotificationStyle,
|
NotificationStyle,
|
||||||
|
|||||||
@@ -2,46 +2,24 @@ import {
|
|||||||
InformationFillDuotoneIcon,
|
InformationFillDuotoneIcon,
|
||||||
SingleSelectSelectSolidIcon,
|
SingleSelectSelectSolidIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import type { FC } from 'react';
|
||||||
import { type CSSProperties, type FC, useMemo } from 'react';
|
import { type ExternalToast, toast } from 'sonner';
|
||||||
import { type ExternalToast, toast, Toaster } from 'sonner';
|
|
||||||
|
|
||||||
import { NotificationCard } from './notification-card';
|
import { DesktopNotificationCard } from './desktop/notification-card';
|
||||||
import type {
|
import { DesktopNotificationCenter } from './desktop/notification-center';
|
||||||
Notification,
|
import { MobileNotificationCard } from './mobile/notification-card';
|
||||||
NotificationCenterProps,
|
import { MobileNotificationCenter } from './mobile/notification-center';
|
||||||
NotificationCustomRendererProps,
|
import type { Notification, NotificationCustomRendererProps } from './types';
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export function NotificationCenter({ width = 380 }: NotificationCenterProps) {
|
const NotificationCard = BUILD_CONFIG.isMobileEdition
|
||||||
const style = useMemo(() => {
|
? MobileNotificationCard
|
||||||
return {
|
: DesktopNotificationCard;
|
||||||
...assignInlineVars({
|
|
||||||
// override css vars inside sonner
|
|
||||||
'--width': `${width}px`,
|
|
||||||
}),
|
|
||||||
// radix-ui will lock pointer-events when dialog is open
|
|
||||||
pointerEvents: 'auto',
|
|
||||||
} satisfies CSSProperties;
|
|
||||||
}, [width]);
|
|
||||||
|
|
||||||
const toastOptions = useMemo(
|
const NotificationCenter = BUILD_CONFIG.isMobileEdition
|
||||||
() => ({
|
? MobileNotificationCenter
|
||||||
style: {
|
: DesktopNotificationCenter;
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
export { NotificationCenter };
|
||||||
<Toaster
|
|
||||||
className="affine-notification-center"
|
|
||||||
style={style}
|
|
||||||
toastOptions={toastOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -44,3 +44,7 @@ export interface NotificationCenterProps {
|
|||||||
export interface NotificationCustomRendererProps {
|
export interface NotificationCustomRendererProps {
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
notification: Notification;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
|
||||||
|
import * as styles from './desktop/styles.css';
|
||||||
import type { NotificationStyle, NotificationTheme } from './types';
|
import type { NotificationStyle, NotificationTheme } from './types';
|
||||||
|
|
||||||
export const getCardColor = (
|
export const getCardColor = (
|
||||||
@@ -77,3 +79,18 @@ export const getCloseIconColor = (style: NotificationStyle) => {
|
|||||||
? getCardForegroundColor(style)
|
? getCardForegroundColor(style)
|
||||||
: cssVar('iconColor');
|
: cssVar('iconColor');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCardVars = (
|
||||||
|
style: NotificationStyle,
|
||||||
|
theme: NotificationTheme,
|
||||||
|
iconColor?: string
|
||||||
|
) => {
|
||||||
|
return assignInlineVars({
|
||||||
|
[styles.cardColor]: getCardColor(style, theme),
|
||||||
|
[styles.cardBorderColor]: getCardBorderColor(style),
|
||||||
|
[styles.cardForeground]: getCardForegroundColor(style),
|
||||||
|
[styles.actionTextColor]: getActionTextColor(style, theme),
|
||||||
|
[styles.iconColor]: getIconColor(style, theme, iconColor),
|
||||||
|
[styles.closeIconColor]: getCloseIconColor(style),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user