mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 22:09:08 +08:00
refactor(component): migrate design components (#5000)
```[tasklist] ### Tasks - [x] Migrate components from [design](https://github.com/toeverything/design) - [x] Replace all imports from `@toeverything/components` - [x] Clean up `@toeverything/components` dependencies - [x] Storybook ``` ### Influence Here are all the components that are influenced by `@toeverything/components` - `@affine/component` - App update `Button` `Tooltip` - App sidebar header `IconButton`, `Tooltip` - Back `Button` - Auth - Change email page save `Button` - Change password page all `Button`s (Save, Later, Open) - Confirm change email `Button` - Set password page `Button` - Sign in success page `Button` - Sign up page `Button` - Auth `Modal` - Workspace card `Avatar`, `Divider`, `Tooltip`, `IconButton` - Share - Disable shared public link `Modal` - Import page `IconButton`, `Tooltip` - Accept invite page `Avatar`, `Button` - Invite member `Modal` - 404 Page `Avatar`, `Button`, `IconButton`, `Tooltip` - Notification center `IconButton` - Page list - operation cell `IconButton`, `Menu`, `ConfirmModal`, `Tooltip` - tags more `Menu` - favorite `IconButton`, `Tooltip` - new page dropdown `Menu` - filter `Menu`, `Button`, `IconButton` - Page operation `Menu` - export `MenuItem` - move to trash `MenuItem`, `ConfirmModal` - Workspace header filter `Menu`, `Button` - Collection bar `Button`, `Tooltip` (*⚠️ seems not used*) - Collection operation `Menu`, `MenuItem` - Create collection `Modal`, `Button` - Edit collection `Modal`, `Button` - Page mode filter `Menu` - Page mode `Button`, `Menu` - Setting modal - storage usage progress `Button`, `Tooltip` - On boarding tour `Modal` - `@affine/core` - Bookmark `Menu` - Affine error boundary `Button` - After sign in send email `Button` - After sign up send email `Button` - Send email `Button` - Sign in `Button` - Subscription redirect `Loading`, `Button` - Setting `Modal` - User plan button `Tooltip` - Members `Avatar`, `Button`, `IconButton`, `Loading`, `Tooltip`, `Menu` - Profile `Button`, `Avatar` - Workspace - publish panel `Button`, `Tooltip` - export panel `Button` - storage panel `Button`, `Tooltip` - delete `ConfirmModal` - Language `Menu` - Account setting `Avatar`, `Button` - Date format setting `Menu` - Billing `Button`, `IconButton`, `Loading` - Payment plans `Button`, `ConfirmModal`, `Modal`, `Tooltip` - Create workspace `Modal`, `ConfirmModal`, `Button` - Payment disabled `ConfirmModal` - Share/Export `Menu`, `Button`, `Divider` - Sign out `ConfirmModal` - Temp disable affine cloud `Modal` - Page detail operation `Menu` - Blocksuite mode switch `Tooltip` - Login card `Avatar` - Help island `Tooltip` - `plugin` - copilot - hello world - image preview - outline
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
type AvatarFallbackProps,
|
||||
type AvatarImageProps,
|
||||
type AvatarProps as RadixAvatarProps,
|
||||
Fallback as AvatarFallback,
|
||||
Image as AvatarImage,
|
||||
Root as AvatarRoot,
|
||||
} from '@radix-ui/react-avatar';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties, HTMLAttributes, MouseEvent } from 'react';
|
||||
import { forwardRef, type ReactElement, useMemo, useState } from 'react';
|
||||
|
||||
import { IconButton } from '../button';
|
||||
import { Tooltip, type TooltipProps } from '../tooltip';
|
||||
import { ColorfulFallback } from './colorful-fallback';
|
||||
import * as style from './style.css';
|
||||
import { sizeVar } from './style.css';
|
||||
|
||||
export type AvatarProps = {
|
||||
size?: number;
|
||||
url?: string | null;
|
||||
name?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
colorfulFallback?: boolean;
|
||||
hoverIcon?: ReactElement;
|
||||
onRemove?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
avatarTooltipOptions?: Omit<TooltipProps, 'children'>;
|
||||
removeTooltipOptions?: Omit<TooltipProps, 'children'>;
|
||||
|
||||
fallbackProps?: AvatarFallbackProps;
|
||||
imageProps?: Omit<AvatarImageProps, 'src'>;
|
||||
avatarProps?: RadixAvatarProps;
|
||||
hoverWrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
removeButtonProps?: HTMLAttributes<HTMLButtonElement>;
|
||||
} & HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(
|
||||
(
|
||||
{
|
||||
size = 20,
|
||||
style: propsStyles = {},
|
||||
url,
|
||||
name,
|
||||
className,
|
||||
colorfulFallback = false,
|
||||
hoverIcon,
|
||||
fallbackProps: { className: fallbackClassName, ...fallbackProps } = {},
|
||||
imageProps,
|
||||
avatarProps,
|
||||
onRemove,
|
||||
hoverWrapperProps: {
|
||||
className: hoverWrapperClassName,
|
||||
...hoverWrapperProps
|
||||
} = {},
|
||||
avatarTooltipOptions,
|
||||
removeTooltipOptions,
|
||||
removeButtonProps: {
|
||||
className: removeButtonClassName,
|
||||
...removeButtonProps
|
||||
} = {},
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const firstCharOfName = useMemo(() => {
|
||||
return name?.slice(0, 1) || 'A';
|
||||
}, [name]);
|
||||
const [imageDom, setImageDom] = useState<HTMLDivElement | null>(null);
|
||||
const [removeButtonDom, setRemoveButtonDom] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<AvatarRoot className={style.avatarRoot} {...avatarProps} ref={ref}>
|
||||
<Tooltip
|
||||
portalOptions={{ container: imageDom }}
|
||||
{...avatarTooltipOptions}
|
||||
>
|
||||
<div
|
||||
ref={setImageDom}
|
||||
className={clsx(style.avatarWrapper, className)}
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[sizeVar]: size ? `${size}px` : '20px',
|
||||
}),
|
||||
...propsStyles,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<AvatarImage
|
||||
className={style.avatarImage}
|
||||
src={url || ''}
|
||||
alt={name}
|
||||
{...imageProps}
|
||||
/>
|
||||
|
||||
<AvatarFallback
|
||||
className={clsx(style.avatarFallback, fallbackClassName)}
|
||||
delayMs={url ? 600 : undefined}
|
||||
{...fallbackProps}
|
||||
>
|
||||
{colorfulFallback ? (
|
||||
<ColorfulFallback char={firstCharOfName} />
|
||||
) : (
|
||||
firstCharOfName
|
||||
)}
|
||||
</AvatarFallback>
|
||||
{hoverIcon ? (
|
||||
<div
|
||||
className={clsx(style.hoverWrapper, hoverWrapperClassName)}
|
||||
{...hoverWrapperProps}
|
||||
>
|
||||
{hoverIcon}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{onRemove ? (
|
||||
<Tooltip
|
||||
portalOptions={{ container: removeButtonDom }}
|
||||
{...removeTooltipOptions}
|
||||
>
|
||||
<IconButton
|
||||
size="extraSmall"
|
||||
type="default"
|
||||
className={clsx(style.removeButton, removeButtonClassName)}
|
||||
onClick={onRemove}
|
||||
ref={setRemoveButtonDom}
|
||||
{...removeButtonProps}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</AvatarRoot>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Avatar.displayName = 'Avatar';
|
||||
@@ -0,0 +1,67 @@
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
DefaultAvatarBottomItemStyle,
|
||||
DefaultAvatarBottomItemWithAnimationStyle,
|
||||
DefaultAvatarContainerStyle,
|
||||
DefaultAvatarMiddleItemStyle,
|
||||
DefaultAvatarMiddleItemWithAnimationStyle,
|
||||
DefaultAvatarTopItemStyle,
|
||||
} from './style.css';
|
||||
|
||||
const colorsSchema = [
|
||||
['#FF0000', '#FF00E5', '#FFAE73'],
|
||||
['#FF5C00', '#FFC700', '#FFE073'],
|
||||
['#FFDA16', '#FFFBA6', '#FFBE73'],
|
||||
['#8CD317', '#FCFF5C', '#67CAE9'],
|
||||
['#28E19F', '#89FFC6', '#39A880'],
|
||||
['#35B7E0', '#77FFCE', '#5076FF'],
|
||||
['#3D39FF', '#77BEFF', '#3502FF'],
|
||||
['#BD08EB', '#755FFF', '#6967E4'],
|
||||
];
|
||||
|
||||
export const ColorfulFallback = ({ char }: { char: string }) => {
|
||||
const colors = useMemo(() => {
|
||||
const index = char.toUpperCase().charCodeAt(0);
|
||||
return colorsSchema[index % colorsSchema.length];
|
||||
}, [char]);
|
||||
|
||||
const timer = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [topColor, middleColor, bottomColor] = colors;
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={DefaultAvatarContainerStyle}
|
||||
onMouseEnter={() => {
|
||||
timer.current = setTimeout(() => {
|
||||
setIsHover(true);
|
||||
}, 300);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
clearTimeout(timer.current);
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={DefaultAvatarTopItemStyle}
|
||||
style={{ background: bottomColor }}
|
||||
></div>
|
||||
<div
|
||||
className={clsx(DefaultAvatarMiddleItemStyle, {
|
||||
[DefaultAvatarMiddleItemWithAnimationStyle]: isHover,
|
||||
})}
|
||||
style={{ background: middleColor }}
|
||||
></div>
|
||||
<div
|
||||
className={clsx(DefaultAvatarBottomItemStyle, {
|
||||
[DefaultAvatarBottomItemWithAnimationStyle]: isHover,
|
||||
})}
|
||||
style={{ background: topColor }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ColorfulFallback;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './avatar';
|
||||
@@ -0,0 +1,210 @@
|
||||
import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
export const sizeVar = createVar('sizeVar');
|
||||
|
||||
const bottomAnimation = keyframes({
|
||||
'0%': {
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
},
|
||||
'16%': {
|
||||
left: '-18%',
|
||||
top: '-51%',
|
||||
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
|
||||
},
|
||||
'32%': {
|
||||
left: '-7%',
|
||||
top: '-40%',
|
||||
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
|
||||
},
|
||||
'48%': {
|
||||
left: '-15%',
|
||||
top: '-39%',
|
||||
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
|
||||
},
|
||||
'64%': {
|
||||
left: '-7%',
|
||||
top: '-40%',
|
||||
transform: 'matrix(-0.97, -0.23, 0.16, -0.99, 0, 0)',
|
||||
},
|
||||
'80%': {
|
||||
left: '-18%',
|
||||
top: '-51%',
|
||||
transform: 'matrix(-0.73, -0.69, 0.64, -0.77, 0, 0)',
|
||||
},
|
||||
'100%': {
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
},
|
||||
});
|
||||
const middleAnimation = keyframes({
|
||||
'0%': {
|
||||
left: '-30px',
|
||||
top: '-30px',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
},
|
||||
'16%': {
|
||||
left: '-37px',
|
||||
top: '-37px',
|
||||
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
|
||||
},
|
||||
'32%': {
|
||||
left: '-20px',
|
||||
top: '-10px',
|
||||
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
|
||||
},
|
||||
'48%': {
|
||||
left: '-27px',
|
||||
top: '-2px',
|
||||
transform: 'matrix(-0.88, 0.48, -0.6, -0.8, 0, 0)',
|
||||
},
|
||||
'64%': {
|
||||
left: '-20px',
|
||||
top: '-10px',
|
||||
transform: 'matrix(-1, -0.02, -0.12, -0.99, 0, 0)',
|
||||
},
|
||||
'80%': {
|
||||
left: '-37px',
|
||||
top: '-37px',
|
||||
transform: 'matrix(-0.86, -0.52, 0.39, -0.92, 0, 0)',
|
||||
},
|
||||
'100%': {
|
||||
left: '-30px',
|
||||
top: '-30px',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
},
|
||||
});
|
||||
|
||||
export const DefaultAvatarContainerStyle = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const DefaultAvatarMiddleItemStyle = style({
|
||||
width: '83%',
|
||||
height: '81%',
|
||||
position: 'absolute',
|
||||
left: '-30%',
|
||||
top: '-30%',
|
||||
transform: 'matrix(-0.48, -0.88, 0.8, -0.6, 0, 0)',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transformOrigin: 'center center',
|
||||
animation: `${middleAnimation} 3s ease-in-out forwards infinite`,
|
||||
animationPlayState: 'paused',
|
||||
});
|
||||
export const DefaultAvatarMiddleItemWithAnimationStyle = style({
|
||||
animationPlayState: 'running',
|
||||
});
|
||||
export const DefaultAvatarBottomItemStyle = style({
|
||||
width: '98%',
|
||||
height: '97%',
|
||||
position: 'absolute',
|
||||
top: '-44%',
|
||||
left: '-11%',
|
||||
transform: 'matrix(-0.29, -0.96, 0.94, -0.35, 0, 0)',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transformOrigin: 'center center',
|
||||
willChange: 'left, top, transform',
|
||||
animation: `${bottomAnimation} 3s ease-in-out forwards infinite`,
|
||||
animationPlayState: 'paused',
|
||||
});
|
||||
export const DefaultAvatarBottomItemWithAnimationStyle = style({
|
||||
animationPlayState: 'running',
|
||||
});
|
||||
export const DefaultAvatarTopItemStyle = style({
|
||||
width: '104%',
|
||||
height: '94%',
|
||||
position: 'absolute',
|
||||
right: '-30%',
|
||||
top: '-30%',
|
||||
opacity: '0.8',
|
||||
filter: 'blur(12px)',
|
||||
transform: 'matrix(-0.28, -0.96, 0.93, -0.37, 0, 0)',
|
||||
transformOrigin: 'center center',
|
||||
});
|
||||
|
||||
export const avatarRoot = style({
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
flexShrink: 0,
|
||||
});
|
||||
export const avatarWrapper = style({
|
||||
vars: {
|
||||
[sizeVar]: 'unset',
|
||||
},
|
||||
width: sizeVar,
|
||||
height: sizeVar,
|
||||
fontSize: `calc(${sizeVar} / 2)`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
verticalAlign: 'middle',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const avatarImage = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '50%',
|
||||
});
|
||||
|
||||
export const avatarFallback = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-white)',
|
||||
lineHeight: '1',
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
export const hoverWrapper = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
zIndex: '1',
|
||||
color: 'var(--affine-white)',
|
||||
opacity: 0,
|
||||
transition: 'opacity .15s',
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const removeButton = style({
|
||||
position: 'absolute',
|
||||
right: '-8px',
|
||||
top: '-2px',
|
||||
visibility: 'hidden',
|
||||
zIndex: '1',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: '#f6f6f6',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${avatarRoot}:hover ${removeButton}`, {
|
||||
visibility: 'visible',
|
||||
});
|
||||
globalStyle(`${avatarRoot} ${removeButton}:hover`, {
|
||||
background: '#f6f6f6',
|
||||
});
|
||||
Reference in New Issue
Block a user