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:
@@ -29,11 +29,14 @@
|
|||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-toolbar": "^1.0.4",
|
"@radix-ui/react-toolbar": "^1.0.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@toeverything/hooks": "workspace:*",
|
"@toeverything/hooks": "workspace:*",
|
||||||
"@toeverything/infra": "workspace:*",
|
"@toeverything/infra": "workspace:*",
|
||||||
"@toeverything/theme": "^0.7.24",
|
"@toeverything/theme": "^0.7.24",
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
|
// TODO: Check `input` , `loading`, not migrated from `design`
|
||||||
export * from './styles';
|
export * from './styles';
|
||||||
|
export * from './ui/avatar';
|
||||||
export * from './ui/button';
|
export * from './ui/button';
|
||||||
export * from './ui/checkbox';
|
export * from './ui/checkbox';
|
||||||
|
export * from './ui/divider';
|
||||||
export * from './ui/empty';
|
export * from './ui/empty';
|
||||||
export * from './ui/input';
|
export * from './ui/input';
|
||||||
export * from './ui/layout';
|
export * from './ui/layout';
|
||||||
export * from './ui/lottie/collections-icon';
|
export * from './ui/lottie/collections-icon';
|
||||||
export * from './ui/lottie/delete-icon';
|
export * from './ui/lottie/delete-icon';
|
||||||
|
export * from './ui/menu';
|
||||||
|
export * from './ui/modal';
|
||||||
|
export * from './ui/popover';
|
||||||
export * from './ui/scrollbar';
|
export * from './ui/scrollbar';
|
||||||
export * from './ui/skeleton';
|
export * from './ui/skeleton';
|
||||||
export * from './ui/switch';
|
export * from './ui/switch';
|
||||||
export * from './ui/table';
|
export * from './ui/table';
|
||||||
export * from './ui/toast';
|
export * from './ui/toast';
|
||||||
|
export * from './ui/tooltip';
|
||||||
|
|||||||
143
packages/frontend/component/src/ui/avatar/avatar.tsx
Normal file
143
packages/frontend/component/src/ui/avatar/avatar.tsx
Normal file
@@ -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;
|
||||||
1
packages/frontend/component/src/ui/avatar/index.ts
Normal file
1
packages/frontend/component/src/ui/avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './avatar';
|
||||||
210
packages/frontend/component/src/ui/avatar/style.css.ts
Normal file
210
packages/frontend/component/src/ui/avatar/style.css.ts
Normal file
@@ -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',
|
||||||
|
});
|
||||||
373
packages/frontend/component/src/ui/button/button.css.ts
Normal file
373
packages/frontend/component/src/ui/button/button.css.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const button = style({
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
userSelect: 'none',
|
||||||
|
touchAction: 'manipulation',
|
||||||
|
outline: '0',
|
||||||
|
border: '1px solid',
|
||||||
|
padding: '0 18px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
fontSize: 'var(--affine-font-xs)',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all .3s',
|
||||||
|
['WebkitAppRegion' as string]: 'no-drag',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
// changeable
|
||||||
|
height: '28px',
|
||||||
|
background: 'var(--affine-white)',
|
||||||
|
borderColor: 'var(--affine-border-color)',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&.text-bold': {
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
'&:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
'&.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--affine-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&.loading': {
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--affine-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
|
||||||
|
{
|
||||||
|
background: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.block': { display: 'flex', width: '100%' },
|
||||||
|
|
||||||
|
'&.circle': {
|
||||||
|
borderRadius: '50%',
|
||||||
|
},
|
||||||
|
'&.round': {
|
||||||
|
borderRadius: '14px',
|
||||||
|
},
|
||||||
|
// size
|
||||||
|
'&.large': {
|
||||||
|
height: '32px',
|
||||||
|
fontSize: 'var(--affine-font-base)',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
'&.round.large': {
|
||||||
|
borderRadius: '16px',
|
||||||
|
},
|
||||||
|
'&.extraLarge': {
|
||||||
|
height: '40px',
|
||||||
|
fontSize: 'var(--affine-font-base)',
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
|
'&.extraLarge.primary': {
|
||||||
|
boxShadow: 'var(--affine-large-button-effect) !important',
|
||||||
|
},
|
||||||
|
'&.round.extraLarge': {
|
||||||
|
borderRadius: '20px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// type
|
||||||
|
'&.plain': {
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.primary': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
background: 'var(--affine-primary-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||||
|
},
|
||||||
|
'&.primary:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
'&.primary.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.primary.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.error': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
background: 'var(--affine-error-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||||
|
},
|
||||||
|
'&.error:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
'&.error.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.error.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.warning': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
background: 'var(--affine-warning-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||||
|
},
|
||||||
|
'&.warning:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
'&.warning.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.warning.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.success': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
background: 'var(--affine-success-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||||
|
},
|
||||||
|
'&.success:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)',
|
||||||
|
},
|
||||||
|
'&.success.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.success.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-success-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.processing': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
background: 'var(--affine-processing-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: 'var(--affine-button-inner-shadow)',
|
||||||
|
},
|
||||||
|
'&.processing:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)',
|
||||||
|
},
|
||||||
|
'&.processing.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.processing.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-processing-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${button} > span`, {
|
||||||
|
// flex: 1,
|
||||||
|
lineHeight: 1,
|
||||||
|
padding: '0 4px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buttonIcon = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'var(--affine-icon-color)',
|
||||||
|
fontSize: '16px',
|
||||||
|
width: '16px',
|
||||||
|
height: '16px',
|
||||||
|
selectors: {
|
||||||
|
'&.start': {
|
||||||
|
marginRight: '4px',
|
||||||
|
},
|
||||||
|
'&.end': {
|
||||||
|
marginLeft: '4px',
|
||||||
|
},
|
||||||
|
'&.large': {
|
||||||
|
fontSize: '20px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
},
|
||||||
|
'&.extraLarge': {
|
||||||
|
fontSize: '20px',
|
||||||
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
},
|
||||||
|
'&.color-white': {
|
||||||
|
color: 'var(--affine-pure-white)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const iconButton = style({
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
userSelect: 'none',
|
||||||
|
touchAction: 'manipulation',
|
||||||
|
outline: '0',
|
||||||
|
border: '1px solid',
|
||||||
|
borderRadius: '4px',
|
||||||
|
transition: 'all .3s',
|
||||||
|
['WebkitAppRegion' as string]: 'no-drag',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'var(--affine-white)',
|
||||||
|
|
||||||
|
// changeable
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
borderColor: 'var(--affine-border-color)',
|
||||||
|
selectors: {
|
||||||
|
'&.without-padding': {
|
||||||
|
margin: '-2px',
|
||||||
|
},
|
||||||
|
'&.active': {
|
||||||
|
color: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
'&.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--affine-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&.loading': {
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--affine-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover':
|
||||||
|
{
|
||||||
|
background: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
// size
|
||||||
|
'&.large': {
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
'&.large.without-padding': {
|
||||||
|
margin: '-4px',
|
||||||
|
},
|
||||||
|
'&.small': { width: '20px', height: '20px', fontSize: '16px' },
|
||||||
|
'&.extra-small': { width: '16px', height: '16px', fontSize: '12px' },
|
||||||
|
|
||||||
|
// type
|
||||||
|
'&.plain': {
|
||||||
|
color: 'var(--affine-icon-color)',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
'&.plain.active': {
|
||||||
|
color: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.primary': {
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
background: 'var(--affine-primary-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||||
|
},
|
||||||
|
'&.primary:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
'&.primary.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.primary.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.error': {
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
background: 'var(--affine-error-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||||
|
},
|
||||||
|
'&.error:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
'&.error.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.error.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.warning': {
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
background: 'var(--affine-warning-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||||
|
},
|
||||||
|
'&.warning:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
'&.warning.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.warning.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.success': {
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
background: 'var(--affine-success-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||||
|
},
|
||||||
|
'&.success:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)',
|
||||||
|
},
|
||||||
|
'&.success.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.success.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-success-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.processing': {
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
background: 'var(--affine-processing-color)',
|
||||||
|
borderColor: 'var(--affine-black-10)',
|
||||||
|
boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset',
|
||||||
|
},
|
||||||
|
'&.processing:not(.without-hover):hover': {
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)',
|
||||||
|
},
|
||||||
|
'&.processing.disabled': {
|
||||||
|
opacity: '.4',
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
'&.processing.disabled:not(.without-hover):hover': {
|
||||||
|
background: 'var(--affine-processing-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
175
packages/frontend/component/src/ui/button/button.tsx
Normal file
175
packages/frontend/component/src/ui/button/button.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type FC,
|
||||||
|
forwardRef,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type PropsWithChildren,
|
||||||
|
type ReactElement,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { Loading } from '../loading';
|
||||||
|
import { button, buttonIcon } from './button.css';
|
||||||
|
|
||||||
|
export type ButtonType =
|
||||||
|
| 'default'
|
||||||
|
| 'primary'
|
||||||
|
| 'plain'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
| 'success'
|
||||||
|
| 'processing';
|
||||||
|
export type ButtonSize = 'default' | 'large' | 'extraLarge';
|
||||||
|
type BaseButtonProps = {
|
||||||
|
type?: ButtonType;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon?: ReactElement;
|
||||||
|
iconPosition?: 'start' | 'end';
|
||||||
|
shape?: 'default' | 'round' | 'circle';
|
||||||
|
block?: boolean;
|
||||||
|
size?: ButtonSize;
|
||||||
|
loading?: boolean;
|
||||||
|
withoutHoverStyle?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ButtonProps = PropsWithChildren<BaseButtonProps> &
|
||||||
|
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
|
||||||
|
componentProps?: {
|
||||||
|
startIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
|
||||||
|
endIcon?: Omit<IconButtonProps, 'icon' | 'iconPosition'>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type IconButtonProps = PropsWithChildren<BaseButtonProps> &
|
||||||
|
Omit<HTMLAttributes<HTMLDivElement>, 'type'>;
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
type: 'default',
|
||||||
|
disabled: false,
|
||||||
|
shape: 'default',
|
||||||
|
size: 'default',
|
||||||
|
iconPosition: 'start',
|
||||||
|
loading: false,
|
||||||
|
withoutHoverStyle: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const ButtonIcon: FC<IconButtonProps> = props => {
|
||||||
|
const {
|
||||||
|
size,
|
||||||
|
icon,
|
||||||
|
iconPosition = 'start',
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
loading,
|
||||||
|
withoutHoverStyle,
|
||||||
|
...otherProps
|
||||||
|
} = {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
const onlyIcon = icon && !children;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...otherProps}
|
||||||
|
className={clsx(buttonIcon, {
|
||||||
|
'color-white': type !== 'default' && type !== 'plain',
|
||||||
|
large: size === 'large',
|
||||||
|
extraLarge: size === 'extraLarge',
|
||||||
|
end: iconPosition === 'end' && !onlyIcon,
|
||||||
|
start: iconPosition === 'start' && !onlyIcon,
|
||||||
|
loading,
|
||||||
|
})}
|
||||||
|
data-without-hover={withoutHoverStyle}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
shape,
|
||||||
|
size,
|
||||||
|
icon: propsIcon,
|
||||||
|
iconPosition,
|
||||||
|
block,
|
||||||
|
loading,
|
||||||
|
withoutHoverStyle,
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
} = {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
} satisfies ButtonProps;
|
||||||
|
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
return propsIcon;
|
||||||
|
}, [propsIcon, loading]);
|
||||||
|
|
||||||
|
const baseIconButtonProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
size,
|
||||||
|
iconPosition,
|
||||||
|
icon,
|
||||||
|
type,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
} as const;
|
||||||
|
}, [disabled, icon, iconPosition, loading, size, type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...otherProps}
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(
|
||||||
|
button,
|
||||||
|
{
|
||||||
|
primary: type === 'primary',
|
||||||
|
plain: type === 'plain',
|
||||||
|
error: type === 'error',
|
||||||
|
warning: type === 'warning',
|
||||||
|
success: type === 'success',
|
||||||
|
processing: type === 'processing',
|
||||||
|
large: size === 'large',
|
||||||
|
extraLarge: size === 'extraLarge',
|
||||||
|
disabled,
|
||||||
|
circle: shape === 'circle',
|
||||||
|
round: shape === 'round',
|
||||||
|
block,
|
||||||
|
loading,
|
||||||
|
'without-hover': withoutHoverStyle,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
data-disabled={disabled}
|
||||||
|
>
|
||||||
|
{icon && iconPosition === 'start' ? (
|
||||||
|
<ButtonIcon
|
||||||
|
{...baseIconButtonProps}
|
||||||
|
{...props.componentProps?.startIcon}
|
||||||
|
icon={icon}
|
||||||
|
iconPosition="start"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span>{children}</span>
|
||||||
|
{icon && iconPosition === 'end' ? (
|
||||||
|
<ButtonIcon
|
||||||
|
{...baseIconButtonProps}
|
||||||
|
{...props.componentProps?.endIcon}
|
||||||
|
icon={icon}
|
||||||
|
iconPosition="end"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
export default Button;
|
||||||
88
packages/frontend/component/src/ui/button/icon-button.tsx
Normal file
88
packages/frontend/component/src/ui/button/icon-button.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
|
import { forwardRef, type ReactElement } from 'react';
|
||||||
|
|
||||||
|
import { Loading } from '../loading';
|
||||||
|
import type { ButtonType } from './button';
|
||||||
|
import { iconButton } from './button.css';
|
||||||
|
|
||||||
|
export type IconButtonSize = 'default' | 'large' | 'small' | 'extraSmall';
|
||||||
|
export type IconButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'type'> &
|
||||||
|
PropsWithChildren<{
|
||||||
|
type?: ButtonType;
|
||||||
|
disabled?: boolean;
|
||||||
|
size?: IconButtonSize;
|
||||||
|
loading?: boolean;
|
||||||
|
withoutPadding?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
withoutHoverStyle?: boolean;
|
||||||
|
icon?: ReactElement;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
type: 'plain',
|
||||||
|
disabled: false,
|
||||||
|
size: 'default',
|
||||||
|
loading: false,
|
||||||
|
withoutPadding: false,
|
||||||
|
active: false,
|
||||||
|
withoutHoverStyle: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
size,
|
||||||
|
withoutPadding,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
active,
|
||||||
|
withoutHoverStyle,
|
||||||
|
icon: propsIcon,
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
} = {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(
|
||||||
|
iconButton,
|
||||||
|
{
|
||||||
|
'without-padding': withoutPadding,
|
||||||
|
|
||||||
|
primary: type === 'primary',
|
||||||
|
plain: type === 'plain',
|
||||||
|
error: type === 'error',
|
||||||
|
warning: type === 'warning',
|
||||||
|
success: type === 'success',
|
||||||
|
processing: type === 'processing',
|
||||||
|
|
||||||
|
large: size === 'large',
|
||||||
|
small: size === 'small',
|
||||||
|
'extra-small': size === 'extraSmall',
|
||||||
|
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
active,
|
||||||
|
'without-hover': withoutHoverStyle,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
data-disabled={disabled}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{loading ? <Loading /> : children || propsIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
IconButton.displayName = 'IconButton';
|
||||||
|
export default IconButton;
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './button';
|
||||||
export * from './dropdown-button';
|
export * from './dropdown-button';
|
||||||
|
export * from './icon-button';
|
||||||
export * from './radio';
|
export * from './radio';
|
||||||
|
|||||||
52
packages/frontend/component/src/ui/divider/divider.tsx
Normal file
52
packages/frontend/component/src/ui/divider/divider.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './style.css';
|
||||||
|
export type DividerOrientation = 'horizontal' | 'vertical';
|
||||||
|
export type DividerProps = PropsWithChildren &
|
||||||
|
Omit<HTMLAttributes<HTMLDivElement>, 'type'> & {
|
||||||
|
orientation?: DividerOrientation;
|
||||||
|
size?: 'thinner' | 'default';
|
||||||
|
dividerColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
size: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const { orientation, className, size, dividerColor, style, ...otherProps } =
|
||||||
|
{
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx(
|
||||||
|
styles.divider,
|
||||||
|
{
|
||||||
|
[styles.verticalDivider]: orientation === 'vertical',
|
||||||
|
[styles.thinner]:
|
||||||
|
size === 'thinner' && orientation === 'horizontal',
|
||||||
|
[styles.verticalThinner]:
|
||||||
|
size === 'thinner' && orientation === 'vertical',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: dividerColor ? dividerColor : undefined,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Divider.displayName = 'Divider';
|
||||||
|
export default Divider;
|
||||||
1
packages/frontend/component/src/ui/divider/index.ts
Normal file
1
packages/frontend/component/src/ui/divider/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './divider';
|
||||||
21
packages/frontend/component/src/ui/divider/style.css.ts
Normal file
21
packages/frontend/component/src/ui/divider/style.css.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const divider = style({
|
||||||
|
height: '1px',
|
||||||
|
backgroundColor: 'var(--affine-border-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
margin: '8px 0',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
export const thinner = style({
|
||||||
|
height: '0.5px',
|
||||||
|
});
|
||||||
|
export const verticalDivider = style({
|
||||||
|
width: '1px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '100%',
|
||||||
|
margin: '0 2px',
|
||||||
|
});
|
||||||
|
export const verticalThinner = style({
|
||||||
|
width: '0.5px',
|
||||||
|
});
|
||||||
7
packages/frontend/component/src/ui/menu/index.ts
Normal file
7
packages/frontend/component/src/ui/menu/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './menu';
|
||||||
|
export * from './menu.types';
|
||||||
|
export * from './menu-icon';
|
||||||
|
export * from './menu-item';
|
||||||
|
export * from './menu-separator';
|
||||||
|
export * from './menu-sub';
|
||||||
|
export * from './menu-trigger';
|
||||||
39
packages/frontend/component/src/ui/menu/menu-icon.tsx
Normal file
39
packages/frontend/component/src/ui/menu/menu-icon.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
import { forwardRef, type HTMLAttributes, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { menuItemIcon } from './styles.css';
|
||||||
|
|
||||||
|
export interface MenuIconProps
|
||||||
|
extends PropsWithChildren,
|
||||||
|
HTMLAttributes<HTMLDivElement> {
|
||||||
|
icon?: ReactNode;
|
||||||
|
position?: 'start' | 'end';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuIcon = forwardRef<HTMLDivElement, MenuIconProps>(
|
||||||
|
({ children, icon, position = 'start', className, ...otherProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={useMemo(
|
||||||
|
() =>
|
||||||
|
clsx(
|
||||||
|
menuItemIcon,
|
||||||
|
{
|
||||||
|
end: position === 'end',
|
||||||
|
start: position === 'start',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
),
|
||||||
|
[className, position]
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{icon || children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MenuIcon.displayName = 'MenuIcon';
|
||||||
33
packages/frontend/component/src/ui/menu/menu-item.tsx
Normal file
33
packages/frontend/component/src/ui/menu/menu-item.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
|
||||||
|
import type { MenuItemProps } from './menu.types';
|
||||||
|
import { useMenuItem } from './use-menu-item';
|
||||||
|
|
||||||
|
export const MenuItem = ({
|
||||||
|
children: propsChildren,
|
||||||
|
type = 'default',
|
||||||
|
className: propsClassName,
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
checked,
|
||||||
|
selected,
|
||||||
|
block,
|
||||||
|
...otherProps
|
||||||
|
}: MenuItemProps) => {
|
||||||
|
const { className, children } = useMenuItem({
|
||||||
|
children: propsChildren,
|
||||||
|
className: propsClassName,
|
||||||
|
type,
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
checked,
|
||||||
|
selected,
|
||||||
|
block,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item className={className} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
packages/frontend/component/src/ui/menu/menu-separator.tsx
Normal file
21
packages/frontend/component/src/ui/menu/menu-separator.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { MenuSeparatorProps } from '@radix-ui/react-dropdown-menu';
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export const MenuSeparator = ({
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
}: MenuSeparatorProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Separator
|
||||||
|
className={useMemo(
|
||||||
|
() => clsx(styles.menuSeparator, className),
|
||||||
|
[className]
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
72
packages/frontend/component/src/ui/menu/menu-sub.tsx
Normal file
72
packages/frontend/component/src/ui/menu/menu-sub.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
|
import type {
|
||||||
|
DropdownMenuSubProps,
|
||||||
|
MenuPortalProps,
|
||||||
|
MenuSubContentProps,
|
||||||
|
} from '@radix-ui/react-dropdown-menu';
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import type { MenuItemProps } from './menu.types';
|
||||||
|
import { MenuIcon } from './menu-icon';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
import { useMenuItem } from './use-menu-item';
|
||||||
|
export interface MenuSubProps {
|
||||||
|
children: ReactNode;
|
||||||
|
items: ReactNode;
|
||||||
|
triggerOptions?: Omit<MenuItemProps, 'onSelect' | 'children'>;
|
||||||
|
portalOptions?: Omit<MenuPortalProps, 'children'>;
|
||||||
|
subOptions?: Omit<DropdownMenuSubProps, 'children'>;
|
||||||
|
subContentOptions?: Omit<MenuSubContentProps, 'children'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuSub = ({
|
||||||
|
children: propsChildren,
|
||||||
|
items,
|
||||||
|
portalOptions,
|
||||||
|
subOptions,
|
||||||
|
triggerOptions: {
|
||||||
|
className: propsClassName,
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
type,
|
||||||
|
...otherTriggerOptions
|
||||||
|
} = {},
|
||||||
|
subContentOptions: {
|
||||||
|
className: subContentClassName = '',
|
||||||
|
...otherSubContentOptions
|
||||||
|
} = {},
|
||||||
|
}: MenuSubProps) => {
|
||||||
|
const { className, children } = useMenuItem({
|
||||||
|
children: propsChildren,
|
||||||
|
className: propsClassName,
|
||||||
|
type,
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Sub {...subOptions}>
|
||||||
|
<DropdownMenu.SubTrigger className={className} {...otherTriggerOptions}>
|
||||||
|
{children}
|
||||||
|
<MenuIcon position="end">
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.Portal {...portalOptions}>
|
||||||
|
<DropdownMenu.SubContent
|
||||||
|
sideOffset={10}
|
||||||
|
className={useMemo(
|
||||||
|
() => clsx(styles.menuContent, subContentClassName),
|
||||||
|
[subContentClassName]
|
||||||
|
)}
|
||||||
|
{...otherSubContentOptions}
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
);
|
||||||
|
};
|
||||||
87
packages/frontend/component/src/ui/menu/menu-trigger.tsx
Normal file
87
packages/frontend/component/src/ui/menu/menu-trigger.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
forwardRef,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { MenuIcon } from './menu-icon';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
import { triggerWidthVar } from './styles.css';
|
||||||
|
|
||||||
|
export interface MenuTriggerProps
|
||||||
|
extends PropsWithChildren,
|
||||||
|
HTMLAttributes<HTMLButtonElement> {
|
||||||
|
width?: CSSProperties['width'];
|
||||||
|
disabled?: boolean;
|
||||||
|
noBorder?: boolean;
|
||||||
|
status?: 'error' | 'success' | 'warning' | 'default';
|
||||||
|
size?: 'default' | 'large' | 'extraLarge';
|
||||||
|
preFix?: ReactNode;
|
||||||
|
endFix?: ReactNode;
|
||||||
|
block?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MenuTrigger = forwardRef<HTMLButtonElement, MenuTriggerProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
disabled,
|
||||||
|
noBorder = false,
|
||||||
|
className,
|
||||||
|
status = 'default',
|
||||||
|
size = 'default',
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
block = false,
|
||||||
|
children,
|
||||||
|
width,
|
||||||
|
style = {},
|
||||||
|
...otherProps
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
...assignInlineVars({
|
||||||
|
[triggerWidthVar]: width
|
||||||
|
? typeof width === 'number'
|
||||||
|
? `${width}px`
|
||||||
|
: width
|
||||||
|
: 'auto',
|
||||||
|
}),
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
className={clsx(styles.menuTrigger, className, {
|
||||||
|
// status
|
||||||
|
block,
|
||||||
|
disabled: disabled,
|
||||||
|
'no-border': noBorder,
|
||||||
|
// color
|
||||||
|
error: status === 'error',
|
||||||
|
success: status === 'success',
|
||||||
|
warning: status === 'warning',
|
||||||
|
default: status === 'default',
|
||||||
|
// size
|
||||||
|
large: size === 'large',
|
||||||
|
'extra-large': size === 'extraLarge',
|
||||||
|
})}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{preFix}
|
||||||
|
{children}
|
||||||
|
{endFix}
|
||||||
|
<MenuIcon position="end">
|
||||||
|
<ArrowDownSmallIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MenuTrigger.displayName = 'MenuTrigger';
|
||||||
52
packages/frontend/component/src/ui/menu/menu.tsx
Normal file
52
packages/frontend/component/src/ui/menu/menu.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type {
|
||||||
|
DropdownMenuProps,
|
||||||
|
MenuContentProps,
|
||||||
|
MenuPortalProps,
|
||||||
|
} from '@radix-ui/react-dropdown-menu';
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface MenuProps {
|
||||||
|
children: ReactNode;
|
||||||
|
items: ReactNode;
|
||||||
|
portalOptions?: Omit<MenuPortalProps, 'children'>;
|
||||||
|
rootOptions?: Omit<DropdownMenuProps, 'children'>;
|
||||||
|
contentOptions?: Omit<MenuContentProps, 'children'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Menu = ({
|
||||||
|
children,
|
||||||
|
items,
|
||||||
|
portalOptions,
|
||||||
|
rootOptions,
|
||||||
|
contentOptions: {
|
||||||
|
className = '',
|
||||||
|
style: contentStyle = {},
|
||||||
|
...otherContentOptions
|
||||||
|
} = {},
|
||||||
|
}: MenuProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root {...rootOptions}>
|
||||||
|
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Portal {...portalOptions}>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
className={useMemo(
|
||||||
|
() => clsx(styles.menuContent, className),
|
||||||
|
[className]
|
||||||
|
)}
|
||||||
|
sideOffset={5}
|
||||||
|
align="start"
|
||||||
|
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||||
|
{...otherContentOptions}
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
11
packages/frontend/component/src/ui/menu/menu.types.ts
Normal file
11
packages/frontend/component/src/ui/menu/menu.types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { MenuItemProps as MenuItemPropsPrimitive } from '@radix-ui/react-dropdown-menu';
|
||||||
|
|
||||||
|
export interface MenuItemProps
|
||||||
|
extends Omit<MenuItemPropsPrimitive, 'asChild' | 'textValue'> {
|
||||||
|
type?: 'default' | 'warning' | 'danger';
|
||||||
|
preFix?: React.ReactNode;
|
||||||
|
endFix?: React.ReactNode;
|
||||||
|
checked?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
block?: boolean;
|
||||||
|
}
|
||||||
157
packages/frontend/component/src/ui/menu/styles.css.ts
Normal file
157
packages/frontend/component/src/ui/menu/styles.css.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
export const triggerWidthVar = createVar('triggerWidthVar');
|
||||||
|
|
||||||
|
export const menuContent = style({
|
||||||
|
minWidth: '180px',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
fontWeight: '400',
|
||||||
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
|
boxShadow: 'var(--affine-menu-shadow)',
|
||||||
|
userSelect: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menuItem = style({
|
||||||
|
maxWidth: '296px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '4px 12px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
lineHeight: '22px',
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
selectors: {
|
||||||
|
'&:not(:last-of-type)': {
|
||||||
|
marginBottom: '4px',
|
||||||
|
},
|
||||||
|
'&.block': { maxWidth: '100%' },
|
||||||
|
'&[data-disabled]': {
|
||||||
|
color: 'var(--affine-text-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
|
'&[data-highlighted]': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
'&.danger:hover': {
|
||||||
|
color: 'var(--affine-error-color)',
|
||||||
|
backgroundColor: 'var(--affine-background-error-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.warning:hover': {
|
||||||
|
color: 'var(--affine-warning-color)',
|
||||||
|
backgroundColor: 'var(--affine-background-warning-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&.selected, &.checked': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
color: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menuSpan = style({
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textAlign: 'left',
|
||||||
|
});
|
||||||
|
export const menuItemIcon = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexShrink: 0,
|
||||||
|
fontSize: 'var(--affine-font-h-5)',
|
||||||
|
color: 'var(--affine-icon-color)',
|
||||||
|
selectors: {
|
||||||
|
'&.start': { marginRight: '8px' },
|
||||||
|
'&.end': { marginLeft: '8px' },
|
||||||
|
'&.selected, &.checked': {
|
||||||
|
color: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
|
||||||
|
[`${menuItem}.danger:hover &`]: {
|
||||||
|
color: 'var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
[`${menuItem}.warning:hover &`]: {
|
||||||
|
color: 'var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menuSeparator = style({
|
||||||
|
height: '1px',
|
||||||
|
backgroundColor: 'var(--affine-border-color)',
|
||||||
|
marginTop: '12px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const menuTrigger = style({
|
||||||
|
vars: {
|
||||||
|
[triggerWidthVar]: 'auto',
|
||||||
|
},
|
||||||
|
width: triggerWidthVar,
|
||||||
|
height: 28,
|
||||||
|
lineHeight: '22px',
|
||||||
|
padding: '0 10px',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
border: '1px solid',
|
||||||
|
backgroundColor: 'var(--affine-white)',
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 'var(--affine-font-xs)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
['WebkitAppRegion' as string]: 'no-drag',
|
||||||
|
borderColor: 'var(--affine-border-color)',
|
||||||
|
outline: 'none',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
background: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
'&.no-border': {
|
||||||
|
border: 'unset',
|
||||||
|
},
|
||||||
|
'&.block': {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
// size
|
||||||
|
'&.large': {
|
||||||
|
height: 32,
|
||||||
|
},
|
||||||
|
'&.extra-large': {
|
||||||
|
height: 40,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
// color
|
||||||
|
'&.disabled': {
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--affine-disable-color)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
// TODO: wait for design
|
||||||
|
'&.error': {
|
||||||
|
// borderColor: 'var(--affine-error-color)',
|
||||||
|
},
|
||||||
|
'&.success': {
|
||||||
|
// borderColor: 'var(--affine-success-color)',
|
||||||
|
},
|
||||||
|
'&.warning': {
|
||||||
|
// borderColor: 'var(--affine-warning-color)',
|
||||||
|
},
|
||||||
|
'&.default': {
|
||||||
|
// borderColor: 'var(--affine-border-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
73
packages/frontend/component/src/ui/menu/use-menu-item.tsx
Normal file
73
packages/frontend/component/src/ui/menu/use-menu-item.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { DoneIcon } from '@blocksuite/icons';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { type MenuItemProps } from './menu.types';
|
||||||
|
import { MenuIcon } from './menu-icon';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
interface useMenuItemProps {
|
||||||
|
children: MenuItemProps['children'];
|
||||||
|
type: MenuItemProps['type'];
|
||||||
|
className: MenuItemProps['className'];
|
||||||
|
preFix: MenuItemProps['preFix'];
|
||||||
|
endFix: MenuItemProps['endFix'];
|
||||||
|
checked?: MenuItemProps['checked'];
|
||||||
|
selected?: MenuItemProps['selected'];
|
||||||
|
block?: MenuItemProps['block'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMenuItem = ({
|
||||||
|
children: propsChildren,
|
||||||
|
type = 'default',
|
||||||
|
className: propsClassName,
|
||||||
|
preFix,
|
||||||
|
endFix,
|
||||||
|
checked,
|
||||||
|
selected,
|
||||||
|
block,
|
||||||
|
}: useMenuItemProps) => {
|
||||||
|
const className = useMemo(
|
||||||
|
() =>
|
||||||
|
clsx(
|
||||||
|
styles.menuItem,
|
||||||
|
{
|
||||||
|
danger: type === 'danger',
|
||||||
|
warning: type === 'warning',
|
||||||
|
checked,
|
||||||
|
selected,
|
||||||
|
block,
|
||||||
|
},
|
||||||
|
propsClassName
|
||||||
|
),
|
||||||
|
[block, checked, propsClassName, selected, type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const children = useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
{preFix}
|
||||||
|
<span className={styles.menuSpan}>{propsChildren}</span>
|
||||||
|
{endFix}
|
||||||
|
|
||||||
|
{checked || selected ? (
|
||||||
|
<MenuIcon
|
||||||
|
position="end"
|
||||||
|
className={clsx({
|
||||||
|
selected,
|
||||||
|
checked,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<DoneIcon />
|
||||||
|
</MenuIcon>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[checked, endFix, preFix, propsChildren, selected]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
};
|
||||||
|
};
|
||||||
47
packages/frontend/component/src/ui/modal/confirm-modal.tsx
Normal file
47
packages/frontend/component/src/ui/modal/confirm-modal.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import type { ButtonProps } from '../button';
|
||||||
|
import { Button } from '../button';
|
||||||
|
import { Modal, type ModalProps } from './modal';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface ConfirmModalProps extends ModalProps {
|
||||||
|
confirmButtonOptions?: ButtonProps;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
cancelText?: string;
|
||||||
|
cancelButtonOptions?: ButtonProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfirmModal = ({
|
||||||
|
children,
|
||||||
|
confirmButtonOptions,
|
||||||
|
// FIXME: we need i18n
|
||||||
|
cancelText = 'Cancel',
|
||||||
|
cancelButtonOptions,
|
||||||
|
onConfirm,
|
||||||
|
width = 480,
|
||||||
|
...props
|
||||||
|
}: ConfirmModalProps) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
contentOptions={{ className: styles.confirmModalContainer }}
|
||||||
|
width={width}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
<div className={styles.confirmModalContent}>{children}</div>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={clsx(styles.modalFooter, {
|
||||||
|
modalFooterWithChildren: !!children,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button {...cancelButtonOptions}>{cancelText}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<Button onClick={onConfirm} {...confirmButtonOptions}></Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
packages/frontend/component/src/ui/modal/index.ts
Normal file
2
packages/frontend/component/src/ui/modal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './confirm-modal';
|
||||||
|
export * from './modal';
|
||||||
111
packages/frontend/component/src/ui/modal/modal.tsx
Normal file
111
packages/frontend/component/src/ui/modal/modal.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { CloseIcon } from '@blocksuite/icons';
|
||||||
|
import type {
|
||||||
|
DialogContentProps,
|
||||||
|
DialogOverlayProps,
|
||||||
|
DialogPortalProps,
|
||||||
|
DialogProps,
|
||||||
|
} from '@radix-ui/react-dialog';
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { type CSSProperties, forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { IconButton, type IconButtonProps } from '../button';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface ModalProps extends DialogProps {
|
||||||
|
width?: CSSProperties['width'];
|
||||||
|
height?: CSSProperties['height'];
|
||||||
|
minHeight?: CSSProperties['minHeight'];
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
withoutCloseButton?: boolean;
|
||||||
|
|
||||||
|
portalOptions?: DialogPortalProps;
|
||||||
|
contentOptions?: DialogContentProps;
|
||||||
|
overlayOptions?: DialogOverlayProps;
|
||||||
|
closeButtonOptions?: IconButtonProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVar = (style: number | string = '', defaultValue = '') => {
|
||||||
|
return style
|
||||||
|
? typeof style === 'number'
|
||||||
|
? `${style}px`
|
||||||
|
: style
|
||||||
|
: defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
minHeight = 194,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
withoutCloseButton = false,
|
||||||
|
|
||||||
|
portalOptions,
|
||||||
|
contentOptions: {
|
||||||
|
style: contentStyle,
|
||||||
|
className: contentClassName,
|
||||||
|
...otherContentOptions
|
||||||
|
} = {},
|
||||||
|
overlayOptions: {
|
||||||
|
className: overlayClassName,
|
||||||
|
...otherOverlayOptions
|
||||||
|
} = {},
|
||||||
|
closeButtonOptions = {},
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<Dialog.Root {...props}>
|
||||||
|
<Dialog.Portal {...portalOptions}>
|
||||||
|
<Dialog.Overlay
|
||||||
|
className={clsx(styles.modalOverlay, overlayClassName)}
|
||||||
|
{...otherOverlayOptions}
|
||||||
|
/>
|
||||||
|
<Dialog.Content
|
||||||
|
className={clsx(styles.modalContent, contentClassName)}
|
||||||
|
style={{
|
||||||
|
...assignInlineVars({
|
||||||
|
[styles.widthVar]: getVar(width, '50vw'),
|
||||||
|
[styles.heightVar]: getVar(height, 'unset'),
|
||||||
|
[styles.minHeightVar]: getVar(minHeight, '26px'),
|
||||||
|
}),
|
||||||
|
...contentStyle,
|
||||||
|
}}
|
||||||
|
{...otherContentOptions}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{withoutCloseButton ? null : (
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<IconButton
|
||||||
|
className={styles.closeButton}
|
||||||
|
aria-label="Close"
|
||||||
|
type="plain"
|
||||||
|
{...closeButtonOptions}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Dialog.Close>
|
||||||
|
)}
|
||||||
|
{title ? (
|
||||||
|
<Dialog.Title className={styles.modalHeader}>{title}</Dialog.Title>
|
||||||
|
) : null}
|
||||||
|
{description ? (
|
||||||
|
<Dialog.Description className={styles.modalDescription}>
|
||||||
|
{description}
|
||||||
|
</Dialog.Description>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Modal.displayName = 'Modal';
|
||||||
79
packages/frontend/component/src/ui/modal/styles.css.ts
Normal file
79
packages/frontend/component/src/ui/modal/styles.css.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const widthVar = createVar('widthVar');
|
||||||
|
export const heightVar = createVar('heightVar');
|
||||||
|
export const minHeightVar = createVar('minHeightVar');
|
||||||
|
|
||||||
|
export const modalOverlay = style({
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
backgroundColor: 'var(--affine-background-modal-color)',
|
||||||
|
zIndex: 'var(--affine-z-index-modal)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalContent = style({
|
||||||
|
vars: {
|
||||||
|
[widthVar]: '',
|
||||||
|
[heightVar]: '',
|
||||||
|
[minHeightVar]: '',
|
||||||
|
},
|
||||||
|
width: widthVar,
|
||||||
|
height: heightVar,
|
||||||
|
minHeight: minHeightVar,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
fontSize: 'var(--affine-font-base)',
|
||||||
|
fontWeight: '400',
|
||||||
|
lineHeight: '1.6',
|
||||||
|
padding: '20px 24px',
|
||||||
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
|
boxShadow: 'var(--affine-popover-shadow)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
maxHeight: 'calc(100vh - 32px)',
|
||||||
|
// :focus-visible will set outline
|
||||||
|
outline: 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 'var(--affine-z-index-modal)',
|
||||||
|
top: ' 50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const closeButton = style({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '22px',
|
||||||
|
right: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalHeader = style({
|
||||||
|
fontSize: 'var(--affine-font-h-6)',
|
||||||
|
fontWeight: '600',
|
||||||
|
lineHeight: '1.45',
|
||||||
|
marginBottom: '12px',
|
||||||
|
});
|
||||||
|
export const modalDescription = style({
|
||||||
|
// marginBottom: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modalFooter = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: '40px',
|
||||||
|
marginTop: 'auto',
|
||||||
|
gap: '20px',
|
||||||
|
selectors: {
|
||||||
|
'&.modalFooterWithChildren': {
|
||||||
|
paddingTop: '20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const confirmModalContent = style({
|
||||||
|
marginTop: '12px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const confirmModalContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
});
|
||||||
1
packages/frontend/component/src/ui/popover/index.ts
Normal file
1
packages/frontend/component/src/ui/popover/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './popover';
|
||||||
50
packages/frontend/component/src/ui/popover/popover.tsx
Normal file
50
packages/frontend/component/src/ui/popover/popover.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type {
|
||||||
|
PopoverContentProps,
|
||||||
|
PopoverPortalProps,
|
||||||
|
PopoverProps as PopoverPrimitiveProps,
|
||||||
|
} from '@radix-ui/react-popover';
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { type ReactNode, useMemo } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface PopoverProps extends PopoverPrimitiveProps {
|
||||||
|
content?: ReactNode;
|
||||||
|
portalOptions?: PopoverPortalProps;
|
||||||
|
contentOptions?: PopoverContentProps;
|
||||||
|
}
|
||||||
|
export const Popover = ({
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
portalOptions,
|
||||||
|
contentOptions: {
|
||||||
|
className: contentClassName,
|
||||||
|
style: contentStyle,
|
||||||
|
...otherContentOptions
|
||||||
|
} = {},
|
||||||
|
...props
|
||||||
|
}: PopoverProps) => {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root {...props}>
|
||||||
|
<PopoverPrimitive.Trigger asChild>{children}</PopoverPrimitive.Trigger>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Portal {...portalOptions}>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
className={useMemo(
|
||||||
|
() => clsx(styles.popoverContent, contentClassName),
|
||||||
|
[contentClassName]
|
||||||
|
)}
|
||||||
|
sideOffset={5}
|
||||||
|
align="start"
|
||||||
|
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||||
|
{...otherContentOptions}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Popover.displayName = 'Popover';
|
||||||
13
packages/frontend/component/src/ui/popover/styles.css.ts
Normal file
13
packages/frontend/component/src/ui/popover/styles.css.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const popoverContent = style({
|
||||||
|
minWidth: '180px',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px',
|
||||||
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
fontWeight: '400',
|
||||||
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
|
boxShadow: 'var(--affine-menu-shadow)',
|
||||||
|
userSelect: 'none',
|
||||||
|
});
|
||||||
5
packages/frontend/component/src/ui/tooltip/index.ts
Normal file
5
packages/frontend/component/src/ui/tooltip/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Tooltip } from './tooltip';
|
||||||
|
|
||||||
|
export * from './tooltip';
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
11
packages/frontend/component/src/ui/tooltip/styles.css.ts
Normal file
11
packages/frontend/component/src/ui/tooltip/styles.css.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const tooltipContent = style({
|
||||||
|
backgroundColor: 'var(--affine-tooltip)',
|
||||||
|
color: 'var(--affine-white)',
|
||||||
|
padding: '5px 12px',
|
||||||
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
lineHeight: '22px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
maxWidth: '280px',
|
||||||
|
});
|
||||||
60
packages/frontend/component/src/ui/tooltip/tooltip.tsx
Normal file
60
packages/frontend/component/src/ui/tooltip/tooltip.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type {
|
||||||
|
TooltipContentProps,
|
||||||
|
TooltipPortalProps,
|
||||||
|
TooltipProps as RootProps,
|
||||||
|
} from '@radix-ui/react-tooltip';
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||||
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
export interface TooltipProps {
|
||||||
|
// `children` can not be string, number or even undefined
|
||||||
|
children: ReactElement;
|
||||||
|
content?: ReactNode;
|
||||||
|
side?: TooltipContentProps['side'];
|
||||||
|
align?: TooltipContentProps['align'];
|
||||||
|
|
||||||
|
rootOptions?: Omit<RootProps, 'children'>;
|
||||||
|
portalOptions?: TooltipPortalProps;
|
||||||
|
options?: Omit<TooltipContentProps, 'side' | 'align'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
side = 'top',
|
||||||
|
align = 'center',
|
||||||
|
options,
|
||||||
|
rootOptions,
|
||||||
|
portalOptions,
|
||||||
|
}: TooltipProps) => {
|
||||||
|
if (!content) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider>
|
||||||
|
<TooltipPrimitive.Root delayDuration={500} {...rootOptions}>
|
||||||
|
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
||||||
|
|
||||||
|
<TooltipPrimitive.Portal {...portalOptions}>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
className={styles.tooltipContent}
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
sideOffset={5}
|
||||||
|
style={{ zIndex: 'var(--affine-z-index-popover)' }}
|
||||||
|
{...options}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
<TooltipPrimitive.Arrow
|
||||||
|
height={6}
|
||||||
|
width={10}
|
||||||
|
fill="var(--affine-tooltip)"
|
||||||
|
/>
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</TooltipPrimitive.Root>
|
||||||
|
</TooltipPrimitive.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -238,11 +238,14 @@ __metadata:
|
|||||||
"@popperjs/core": "npm:^2.11.8"
|
"@popperjs/core": "npm:^2.11.8"
|
||||||
"@radix-ui/react-avatar": "npm:^1.0.4"
|
"@radix-ui/react-avatar": "npm:^1.0.4"
|
||||||
"@radix-ui/react-collapsible": "npm:^1.0.3"
|
"@radix-ui/react-collapsible": "npm:^1.0.3"
|
||||||
|
"@radix-ui/react-dialog": "npm:^1.0.5"
|
||||||
|
"@radix-ui/react-dropdown-menu": "npm:^2.0.6"
|
||||||
"@radix-ui/react-popover": "npm:^1.0.7"
|
"@radix-ui/react-popover": "npm:^1.0.7"
|
||||||
"@radix-ui/react-radio-group": "npm:^1.1.3"
|
"@radix-ui/react-radio-group": "npm:^1.1.3"
|
||||||
"@radix-ui/react-scroll-area": "npm:^1.0.5"
|
"@radix-ui/react-scroll-area": "npm:^1.0.5"
|
||||||
"@radix-ui/react-toast": "npm:^1.1.5"
|
"@radix-ui/react-toast": "npm:^1.1.5"
|
||||||
"@radix-ui/react-toolbar": "npm:^1.0.4"
|
"@radix-ui/react-toolbar": "npm:^1.0.4"
|
||||||
|
"@radix-ui/react-tooltip": "npm:^1.0.7"
|
||||||
"@storybook/jest": "npm:^0.2.3"
|
"@storybook/jest": "npm:^0.2.3"
|
||||||
"@storybook/testing-library": "npm:^0.2.2"
|
"@storybook/testing-library": "npm:^0.2.2"
|
||||||
"@testing-library/react": "npm:^14.0.0"
|
"@testing-library/react": "npm:^14.0.0"
|
||||||
@@ -9726,7 +9729,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-dialog@npm:^1.0.4":
|
"@radix-ui/react-dialog@npm:^1.0.4, @radix-ui/react-dialog@npm:^1.0.5":
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
resolution: "@radix-ui/react-dialog@npm:1.0.5"
|
resolution: "@radix-ui/react-dialog@npm:1.0.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9833,7 +9836,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@npm:^2.0.5":
|
"@radix-ui/react-dropdown-menu@npm:^2.0.5, @radix-ui/react-dropdown-menu@npm:^2.0.6":
|
||||||
version: 2.0.6
|
version: 2.0.6
|
||||||
resolution: "@radix-ui/react-dropdown-menu@npm:2.0.6"
|
resolution: "@radix-ui/react-dropdown-menu@npm:2.0.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -10510,7 +10513,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@radix-ui/react-tooltip@npm:^1.0.6":
|
"@radix-ui/react-tooltip@npm:^1.0.6, @radix-ui/react-tooltip@npm:^1.0.7":
|
||||||
version: 1.0.7
|
version: 1.0.7
|
||||||
resolution: "@radix-ui/react-tooltip@npm:1.0.7"
|
resolution: "@radix-ui/react-tooltip@npm:1.0.7"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user