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:
Cats Juice
2023-12-04 08:32:12 +00:00
parent 33c53217c3
commit 0abadbe7bb
34 changed files with 2080 additions and 3 deletions

View 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';

View 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';

View 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>
);
};

View 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}
/>
);
};

View 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>
);
};

View 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';

View 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>
);
};

View 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;
}

View 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)',
},
},
});

View 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,
};
};