mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
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 { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
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 type { Notification } from './types';
|
||||
import {
|
||||
getActionTextColor,
|
||||
getCardBorderColor,
|
||||
getCardColor,
|
||||
getCardForegroundColor,
|
||||
getCloseIconColor,
|
||||
getIconColor,
|
||||
} from './utils';
|
||||
|
||||
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
||||
export const DesktopNotificationCard = ({
|
||||
notification,
|
||||
}: NotificationCardProps) => {
|
||||
const {
|
||||
theme = 'info',
|
||||
style = 'normal',
|
||||
@@ -43,14 +33,7 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
style={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),
|
||||
})}
|
||||
style={getCardVars(style, theme, iconColor)}
|
||||
data-with-icon={icon ? '' : undefined}
|
||||
{...rootAttrs}
|
||||
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';
|
||||
|
||||
@@ -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 { Modal } from '../modal';
|
||||
import { NotificationCenter, notify } from './notification-center';
|
||||
import { NotificationCenter, notify } from '.';
|
||||
import type {
|
||||
NotificationCustomRendererProps,
|
||||
NotificationStyle,
|
||||
|
||||
@@ -2,46 +2,24 @@ import {
|
||||
InformationFillDuotoneIcon,
|
||||
SingleSelectSelectSolidIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import { type CSSProperties, type FC, useMemo } from 'react';
|
||||
import { type ExternalToast, toast, Toaster } from 'sonner';
|
||||
import type { FC } from 'react';
|
||||
import { type ExternalToast, toast } from 'sonner';
|
||||
|
||||
import { NotificationCard } from './notification-card';
|
||||
import type {
|
||||
Notification,
|
||||
NotificationCenterProps,
|
||||
NotificationCustomRendererProps,
|
||||
} from './types';
|
||||
import { DesktopNotificationCard } from './desktop/notification-card';
|
||||
import { DesktopNotificationCenter } from './desktop/notification-center';
|
||||
import { MobileNotificationCard } from './mobile/notification-card';
|
||||
import { MobileNotificationCenter } from './mobile/notification-center';
|
||||
import type { Notification, NotificationCustomRendererProps } from './types';
|
||||
|
||||
export function NotificationCenter({ 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 NotificationCard = BUILD_CONFIG.isMobileEdition
|
||||
? MobileNotificationCard
|
||||
: DesktopNotificationCard;
|
||||
|
||||
const toastOptions = useMemo(
|
||||
() => ({
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const NotificationCenter = BUILD_CONFIG.isMobileEdition
|
||||
? MobileNotificationCenter
|
||||
: DesktopNotificationCenter;
|
||||
|
||||
return (
|
||||
<Toaster
|
||||
className="affine-notification-center"
|
||||
style={style}
|
||||
toastOptions={toastOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { NotificationCenter };
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -44,3 +44,7 @@ export interface NotificationCenterProps {
|
||||
export interface NotificationCustomRendererProps {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
|
||||
import * as styles from './desktop/styles.css';
|
||||
import type { NotificationStyle, NotificationTheme } from './types';
|
||||
|
||||
export const getCardColor = (
|
||||
@@ -77,3 +79,18 @@ export const getCloseIconColor = (style: NotificationStyle) => {
|
||||
? getCardForegroundColor(style)
|
||||
: 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