mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00: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:
@@ -29,11 +29,14 @@
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@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-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"@toeverything/theme": "^0.7.24",
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
// TODO: Check `input` , `loading`, not migrated from `design`
|
||||
export * from './styles';
|
||||
export * from './ui/avatar';
|
||||
export * from './ui/button';
|
||||
export * from './ui/checkbox';
|
||||
export * from './ui/divider';
|
||||
export * from './ui/empty';
|
||||
export * from './ui/input';
|
||||
export * from './ui/layout';
|
||||
export * from './ui/lottie/collections-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/skeleton';
|
||||
export * from './ui/switch';
|
||||
export * from './ui/table';
|
||||
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 './icon-button';
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user