refactor(core): remove all MUI related components and utilities (#4941)

This commit is contained in:
Cats Juice
2023-11-20 10:51:28 +08:00
committed by GitHub
parent 4ef1f4c046
commit 57d42bf491
59 changed files with 335 additions and 2520 deletions

View File

@@ -1,4 +1,3 @@
import { Skeleton } from '@mui/material';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai';
@@ -6,6 +5,7 @@ import { debounce } from 'lodash-es';
import type { PropsWithChildren, ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Skeleton } from '../../ui/skeleton';
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
import {
floatingMaxWidth,

View File

@@ -1,7 +1,6 @@
import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { Skeleton } from '@mui/material';
import clsx from 'clsx';
import { use } from 'foxact/use';
import type { CSSProperties, ReactElement } from 'react';
@@ -17,6 +16,7 @@ import {
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { Skeleton } from '../../ui/skeleton';
import {
blockSuiteEditorHeaderStyle,
blockSuiteEditorStyle,

View File

@@ -2,7 +2,6 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
import { Skeleton } from '@mui/material';
import { Avatar } from '@toeverything/components/avatar';
import { Divider } from '@toeverything/components/divider';
import { Tooltip } from '@toeverything/components/tooltip';
@@ -12,6 +11,7 @@ import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/wor
import { useAtomValue } from 'jotai/react';
import { useCallback } from 'react';
import { Skeleton } from '../../../ui/skeleton';
import {
StyledCard,
StyledIconContainer,

View File

@@ -1,13 +0,0 @@
import { Skeleton } from '@mui/material';
import { memo } from 'react';
export const ListSkeleton = memo(function ListItemSkeleton() {
return (
<>
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
</>
);
});

View File

@@ -1,4 +1,3 @@
import { useMediaQuery, useTheme } from '@mui/material';
import clsx from 'clsx';
import {
type BaseSyntheticEvent,
@@ -8,12 +7,6 @@ import {
import * as styles from './page-list.css';
export const useIsSmallDevices = () => {
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down(900));
return isSmallDevices;
};
export function isToday(date: Date): boolean {
const today = new Date();
return (

View File

@@ -1,5 +1,4 @@
import { Skeleton } from '@mui/material';
import { Skeleton } from '../../ui/skeleton';
import { SettingHeader } from './setting-header';
import { SettingRow } from './setting-row';
import { SettingWrapper } from './wrapper';

View File

@@ -1,12 +1,11 @@
import { Skeleton } from '@mui/material';
import { FlexWrapper } from '../../ui/layout';
import { Skeleton } from '../../ui/skeleton';
export const WorkspaceListItemSkeleton = () => {
return (
<FlexWrapper
alignItems="center"
style={{ padding: '0 8px', height: 30, marginBottom: 4 }}
style={{ padding: '0 24px', height: 30, marginBottom: 4 }}
>
<Skeleton
variant="circular"
@@ -14,7 +13,12 @@ export const WorkspaceListItemSkeleton = () => {
height={14}
style={{ marginRight: 10 }}
/>
<Skeleton variant="rectangular" height={16} style={{ flexGrow: 1 }} />
<Skeleton
variant="rectangular"
height={16}
width={0}
style={{ flexGrow: 1 }}
/>
</FlexWrapper>
);
};

View File

@@ -2,8 +2,6 @@ import { lightCssVariables } from '@toeverything/theme';
import type { ComplexStyleRule } from '@vanilla-extract/css';
import { globalStyle, style } from '@vanilla-extract/css';
import { breakpoints } from '../../styles/mui-theme';
export const appStyle = style({
width: '100%',
position: 'relative',
@@ -134,10 +132,10 @@ export const toolStyle = style({
flexDirection: 'column',
gap: '12px',
'@media': {
[breakpoints.down('md', true)]: {
'screen and (max-width: 960px)': {
right: 'calc((100vw - 640px) * 3 / 19 + 14px)',
},
[breakpoints.down('sm', true)]: {
'screen and (max-width: 640px)': {
right: '5px',
bottom: '5px',
},
@@ -149,10 +147,10 @@ export const toolStyle = style({
'&[data-in-trash-page="true"]': {
bottom: '70px',
'@media': {
[breakpoints.down('md', true)]: {
'screen and (max-width: 960px)': {
bottom: '80px',
},
[breakpoints.down('sm', true)]: {
'screen and (max-width: 640px)': {
bottom: '85px',
},
print: {

View File

@@ -1,6 +1,4 @@
export * from './components/list-skeleton';
export * from './styles';
export * from './ui/breadcrumbs';
export * from './ui/button';
export * from './ui/checkbox';
export * from './ui/empty';
@@ -8,12 +6,8 @@ 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/mui';
export * from './ui/popper';
export * from './ui/scrollbar';
export * from './ui/shared/container';
export * from './ui/skeleton';
export * from './ui/switch';
export * from './ui/table';
export * from './ui/toast';
export * from './ui/tree-view';

View File

@@ -1,3 +1,2 @@
export * from './helper';
export * from './mui-theme';
export * from './mui-theme-provider';
export * from './styled';

View File

@@ -1,3 +0,0 @@
import { alpha, css, keyframes, styled } from '@mui/material/styles';
export { alpha, css, keyframes, styled };

View File

@@ -1,86 +0,0 @@
import type {
Breakpoint,
BreakpointsOptions,
ThemeOptions,
} from '@mui/material';
export const muiThemes = {
breakpoints: {
values: {
xs: 0,
sm: 640,
md: 960,
lg: 1280,
xl: 1920,
},
},
} satisfies ThemeOptions;
// Ported from mui
// See https://github.com/mui/material-ui/blob/eba90da5359ff9c58b02800dfe468dc6c0b95bd2/packages/mui-system/src/createTheme/createBreakpoints.js
// License under MIT
function createBreakpoints(breakpoints: BreakpointsOptions): Readonly<
Omit<BreakpointsOptions, 'up' | 'down'> & {
up: (key: Breakpoint | number, pure?: boolean) => string;
down: (key: Breakpoint | number, pure?: boolean) => string;
}
> {
const {
// The breakpoint **start** at this value.
// For instance with the first breakpoint xs: [xs, sm).
values = {
xs: 0, // phone
sm: 600, // tablet
md: 900, // small laptop
lg: 1200, // desktop
xl: 1536, // large screen
},
unit = 'px',
step = 5,
...other
} = breakpoints;
const keys = Object.keys(values) as ['xs', 'sm', 'md', 'lg', 'xl'];
function up(key: Breakpoint | number, pure = false) {
const value = typeof key === 'number' ? key : values[key];
const original = `(min-width:${value}${unit})`;
if (pure) {
return original;
}
return `@media ${original}`;
}
function down(key: Breakpoint | number, pure = false) {
const value = typeof key === 'number' ? key : values[key];
const original = `(max-width:${value - step / 100}${unit})`;
if (pure) {
return original;
}
return `@media ${original}`;
}
return {
keys,
values,
up,
down,
unit,
// between,
// only,
// not,
...other,
};
}
/**
* @example
* ```ts
* export const iconButtonStyle = style({
* [breakpoints.up('sm')]: {
* padding: '6px'
* },
* });
* ```
*/
export const breakpoints = createBreakpoints(muiThemes.breakpoints);

View File

@@ -0,0 +1,3 @@
import styled from '@emotion/styled';
export { styled };

View File

@@ -1,14 +0,0 @@
import type { BreadcrumbsProps } from '@mui/material/Breadcrumbs';
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import type { ComponentType } from 'react';
import { styled } from '../../styles';
const StyledMuiBreadcrumbs = styled(MuiBreadcrumbs)(() => {
return {
color: 'var(--affine-text-primary-color)',
};
});
export const Breadcrumbs: ComponentType<BreadcrumbsProps> =
StyledMuiBreadcrumbs;

View File

@@ -1,60 +0,0 @@
import { styled } from '../../styles';
import type { ButtonProps } from './interface';
import { getButtonColors } from './utils';
export const LoadingContainer = styled('div')<Pick<ButtonProps, 'type'>>(({
theme,
type = 'default',
}) => {
const { color } = getButtonColors(theme, type, false);
return `
margin: 0px auto;
width: 38px;
text-align: center;
.load {
width: 8px;
height: 8px;
background-color: ${color};
border-radius: 100%;
display: inline-block;
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
animation: bouncedelay 1.4s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.load1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.load2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0);
-webkit-transform: scale(0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
`;
});
export const Loading = ({ type }: Pick<ButtonProps, 'type'>) => {
return (
<LoadingContainer type={type} className="load-container">
<div className="load load1"></div>
<div className="load load2"></div>
<div className="load"></div>
</LoadingContainer>
);
};

View File

@@ -1,9 +1,6 @@
import type { Theme } from '@mui/material';
import type { ButtonProps } from './interface';
export const getButtonColors = (
_theme: Theme,
type: ButtonProps['type'],
disabled: boolean,
extend?: {

View File

@@ -1,6 +0,0 @@
/**
* @deprecated
* Use @toeverything/components/menu instead, this component only used in bookmark plugin, since it support set anchor as Range
*/
export * from './menu-item';
export * from './pure-menu';

View File

@@ -1,44 +0,0 @@
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import {
StyledContent,
StyledEndIconWrapper,
StyledMenuItem,
StyledStartIconWrapper,
} from './styles';
export type IconMenuProps = PropsWithChildren<{
icon?: ReactElement;
endIcon?: ReactElement;
iconSize?: number;
disabled?: boolean;
active?: boolean;
disableHover?: boolean;
userFocused?: boolean;
gap?: string;
fontSize?: string;
}> &
HTMLAttributes<HTMLButtonElement>;
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
({ endIcon, icon, children, gap, fontSize, iconSize, ...props }, ref) => {
return (
<StyledMenuItem ref={ref} {...props}>
{icon && (
<StyledStartIconWrapper iconSize={iconSize} gap={gap}>
{icon}
</StyledStartIconWrapper>
)}
<StyledContent fontSize={fontSize}>{children}</StyledContent>
{endIcon && (
<StyledEndIconWrapper iconSize={iconSize} gap={gap}>
{endIcon}
</StyledEndIconWrapper>
)}
</StyledMenuItem>
);
}
);
MenuItem.displayName = 'MenuItem';
export default MenuItem;

View File

@@ -1,24 +0,0 @@
import type { CSSProperties } from 'react';
import type { PurePopperProps } from '../popper';
import { PurePopper } from '../popper';
import { StyledMenuWrapper } from './styles';
export type PureMenuProps = PurePopperProps & {
width?: CSSProperties['width'];
height?: CSSProperties['height'];
};
export const PureMenu = ({
children,
placement,
width,
...otherProps
}: PureMenuProps) => {
return (
<PurePopper placement={placement} {...otherProps}>
<StyledMenuWrapper width={width} placement={placement}>
{children}
</StyledMenuWrapper>
</PurePopper>
);
};

View File

@@ -1,115 +0,0 @@
import type { CSSProperties } from 'react';
import { displayFlex, styled, textEllipsis } from '../../styles';
import StyledPopperContainer from '../shared/container';
export const StyledMenuWrapper = styled(StyledPopperContainer, {
shouldForwardProp: propName =>
!['width', 'height'].includes(propName as string),
})<{
width?: CSSProperties['width'];
height?: CSSProperties['height'];
}>(({ width, height }) => {
return {
width,
height,
minWidth: '200px',
background: 'var(--affine-white)',
padding: '8px 4px',
fontSize: '14px',
backgroundColor: 'var(--affine-white)',
boxShadow: 'var(--affine-menu-shadow)',
userSelect: 'none',
};
});
export const StyledStartIconWrapper = styled('div')<{
gap?: CSSProperties['gap'];
iconSize?: CSSProperties['fontSize'];
}>(({ gap, iconSize }) => {
return {
display: 'flex',
marginRight: gap ? gap : '12px',
fontSize: iconSize ? iconSize : '20px',
color: 'var(--affine-icon-color)',
};
});
export const StyledEndIconWrapper = styled('div')<{
gap?: CSSProperties['gap'];
iconSize?: CSSProperties['fontSize'];
}>(({ gap, iconSize }) => {
return {
display: 'flex',
marginLeft: gap ? gap : '12px',
fontSize: iconSize ? iconSize : '20px',
color: 'var(--affine-icon-color)',
};
});
export const StyledContent = styled('div')<{
fontSize?: CSSProperties['fontSize'];
}>(({ fontSize }) => {
return {
textAlign: 'left',
flexGrow: 1,
fontSize: fontSize ? fontSize : 'var(--affine-font-base)',
...textEllipsis(1),
};
});
export const StyledMenuItem = styled('button')<{
isDir?: boolean;
disabled?: boolean;
active?: boolean;
disableHover?: boolean;
userFocused?: boolean;
}>(({
isDir = false,
disabled = false,
active = false,
disableHover = false,
userFocused = false,
}) => {
return {
width: '100%',
borderRadius: '5px',
padding: '0 14px',
fontSize: 'var(--affine-font-sm)',
height: '32px',
...displayFlex('flex-start', 'center'),
cursor: isDir ? 'pointer' : '',
position: 'relative',
backgroundColor: 'transparent',
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-text-primary-color)',
svg: {
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-icon-color)',
},
...(disabled
? {
cursor: 'not-allowed',
pointerEvents: 'none',
}
: {}),
':hover':
disabled || disableHover
? {}
: {
backgroundColor: 'var(--affine-hover-color)',
},
...(userFocused && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
...(active && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
};
});

View File

@@ -1,19 +0,0 @@
import { ClickAwayListener as MuiClickAwayListener } from '@mui/base/ClickAwayListener';
import MuiAvatar from '@mui/material/Avatar';
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import MuiCollapse from '@mui/material/Collapse';
import MuiFade from '@mui/material/Fade';
import MuiGrow from '@mui/material/Grow';
import MuiSkeleton from '@mui/material/Skeleton';
import MuiSlide from '@mui/material/Slide';
export {
MuiAvatar,
MuiBreadcrumbs,
MuiClickAwayListener,
MuiCollapse,
MuiFade,
MuiGrow,
MuiSkeleton,
MuiSlide,
};

View File

@@ -1,3 +0,0 @@
export * from './interface';
export * from './popper';
export * from './pure-popper';

View File

@@ -1,64 +0,0 @@
import {
type PopperPlacementType,
type PopperProps as PopperUnstyledProps,
} from '@mui/base/Popper';
import type { CSSProperties, ReactElement, ReactNode, Ref } from 'react';
export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element;
};
export type PopperHandler = {
setVisible: (visible: boolean) => void;
};
export type PopperArrowProps = {
placement?: PopperPlacementType;
};
export type PopperProps = {
// Popover content
content?: ReactNode;
// Popover trigger
children: ReactElement;
// Whether the default is implicit
defaultVisible?: boolean;
// Used to manually control the visibility of the Popover
visible?: boolean;
// TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
// How long does it take for the mouse to display the Popover, in milliseconds
pointerEnterDelay?: number;
// How long does it take to hide the Popover after the mouse moves out, in milliseconds
pointerLeaveDelay?: number;
// Callback fired when the component closed or open
onVisibleChange?: (visible: boolean) => void;
// Popover container style
popoverStyle?: CSSProperties;
// Popover container class name
popoverClassName?: string;
// Anchor class name
anchorClassName?: string;
// Popover z-index
zIndex?: number;
offset?: [number, number];
showArrow?: boolean;
popperHandlerRef?: Ref<PopperHandler>;
onClickAway?: () => void;
triggerContainerStyle?: CSSProperties;
} & Omit<PopperUnstyledProps, 'open' | 'content'>;

View File

@@ -1,97 +0,0 @@
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import { styled } from '../../styles';
import type { PopperArrowProps } from './interface';
export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
function PopperArrow({ placement }, ref) {
return <StyledArrow placement={placement} ref={ref} />;
}
);
const getArrowStyle = (
placement: PopperArrowProps['placement'] = 'bottom',
backgroundColor: CSSProperties['backgroundColor']
) => {
if (placement.indexOf('bottom') === 0) {
return {
top: 0,
left: 0,
marginTop: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '0 1em 1em 1em',
borderColor: `transparent transparent ${backgroundColor} transparent`,
},
};
}
if (placement.indexOf('top') === 0) {
return {
bottom: 0,
left: 0,
marginBottom: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '1em 1em 0 1em',
borderColor: `${backgroundColor} transparent transparent transparent`,
},
};
}
if (placement.indexOf('left') === 0) {
return {
right: 0,
marginRight: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 0 1em 1em',
borderColor: `transparent transparent transparent ${backgroundColor}`,
},
};
}
if (placement.indexOf('right') === 0) {
return {
left: 0,
marginLeft: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 1em 1em 0',
borderColor: `transparent ${backgroundColor} transparent transparent`,
},
};
}
return {
display: 'none',
};
};
const StyledArrow = styled('span')<{
placement?: PopperArrowProps['placement'];
}>(({ placement }) => {
return {
position: 'absolute',
fontSize: '7px',
width: '3em',
'::before': {
content: '""',
margin: 'auto',
display: 'block',
width: 0,
height: 0,
borderStyle: 'solid',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
...getArrowStyle(placement, 'var(--affine-tooltip)'),
};
});

View File

@@ -1,300 +0,0 @@
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import { Popper as PopperUnstyled } from '@mui/base/Popper';
import Grow from '@mui/material/Grow';
import type { CSSProperties, PointerEvent } from 'react';
import {
cloneElement,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import { styled } from '../../styles';
import type { PopperProps, VirtualElement } from './interface';
export const Popper = ({
children,
content,
anchorEl: propsAnchorEl,
placement = 'top-start',
defaultVisible = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 500,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
popperHandlerRef,
onClick,
onClickAway,
onPointerEnter,
onPointerLeave,
triggerContainerStyle = {},
...popperProps
}: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>();
const [visible, setVisible] = useState(defaultVisible);
//const [arrowRef, setArrowRef] = useState<HTMLElement>();
const arrowRef = null;
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchorEl !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const onPointerEnterHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerEnter?.(e);
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerLeave?.(e);
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(popperHandlerRef, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
const mergedClass = [anchorClassName, children.props.className]
.filter(Boolean)
.join(' ');
return (
<ClickAwayListener
onClickAway={() => {
if (visibleControlledByParent) {
onClickAway?.();
} else {
setVisible(false);
}
}}
>
<Container style={triggerContainerStyle}>
{cloneElement(children, {
ref: (dom: HTMLDivElement) => setAnchorEl(dom),
onClick: (e: MouseEvent) => {
children.props.onClick?.(e);
if (!hasClickTrigger || visibleControlledByParent) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onClick?.(e);
return;
}
setVisible(!visible);
},
onPointerEnter: onPointerEnterHandler,
onPointerLeave: onPointerLeaveHandler,
...(mergedClass
? {
className: mergedClass,
}
: {}),
})}
{content && (
<BasicStyledPopper
open={visibleControlledByParent ? propsVisible : visible}
zIndex={zIndex}
anchorEl={isAnchorCustom ? propsAnchorEl : anchorEl}
placement={placement}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
{...popperProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
onClick={() => {
if (hasClickTrigger && !visibleControlledByParent) {
setVisible(false);
}
}}
>
{showArrow ? (
placement.indexOf('bottom') === 0 ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M6.38889 0.45C5.94444 -0.15 5.05555 -0.150001 4.61111 0.449999L0.499999 6L10.5 6L6.38889 0.45Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
{content}
</div>
) : placement.indexOf('top') === 0 ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M4.61111 5.55C5.05556 6.15 5.94445 6.15 6.38889 5.55L10.5 -4.76837e-07H0.5L4.61111 5.55Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</div>
) : placement.indexOf('left') === 0 ? (
<>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="10"
viewBox="0 0 6 10"
fill="none"
>
<path
d="M5.55 5.88889C6.15 5.44444 6.15 4.55555 5.55 4.11111L-4.76837e-07 0L-4.76837e-07 10L5.55 5.88889Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</>
) : placement.indexOf('right') === 0 ? (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="10"
viewBox="0 0 6 10"
style={{ fill: 'var(--affine-tooltip)' }}
>
<path
d="M0.45 4.11111C-0.15 4.55556 -0.15 5.44445 0.45 5.88889L6 10V0L0.45 4.11111Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
{content}
</>
) : (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M4.61111 5.55C5.05556 6.15 5.94445 6.15 6.38889 5.55L10.5 -4.76837e-07H0.5L4.61111 5.55Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</div>
)
) : (
content
)}
</div>
</Grow>
)}
</BasicStyledPopper>
)}
</Container>
</ClickAwayListener>
);
};
// The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({
display: 'contents',
});
export const BasicStyledPopper = styled(PopperUnstyled, {
shouldForwardProp: (propName: string) =>
!['zIndex'].some(name => name === propName),
})<{
zIndex?: CSSProperties['zIndex'];
}>(({ zIndex }) => {
return {
zIndex: zIndex ?? 'var(--affine-z-index-popover)',
};
});

View File

@@ -1,66 +0,0 @@
import type { PopperProps as PopperUnstyledProps } from '@mui/base/Popper';
import Grow from '@mui/material/Grow';
import type { CSSProperties, PropsWithChildren } from 'react';
import { useState } from 'react';
import { PopperArrow } from './popover-arrow';
import { BasicStyledPopper } from './popper';
import { PopperWrapper } from './styles';
export type PurePopperProps = {
zIndex?: CSSProperties['zIndex'];
offset?: [number, number];
showArrow?: boolean;
} & PopperUnstyledProps &
PropsWithChildren;
export const PurePopper = (props: PurePopperProps) => {
const {
children,
zIndex,
offset,
showArrow = false,
modifiers = [],
placement,
...otherProps
} = props;
const [arrowRef, setArrowRef] = useState<HTMLElement | null>();
return (
<BasicStyledPopper
zIndex={zIndex}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
...modifiers,
]}
placement={placement}
{...otherProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<PopperWrapper>
{showArrow && (
<PopperArrow placement={placement} ref={setArrowRef} />
)}
{children}
</PopperWrapper>
</Grow>
)}
</BasicStyledPopper>
);
};

View File

@@ -1,7 +0,0 @@
import { styled } from '../../styles';
export const PopperWrapper = styled('div')(() => {
return {
position: 'relative',
};
});

View File

@@ -1,57 +0,0 @@
import type { PopperPlacementType } from '@mui/material';
import { styled } from '../../styles';
export type PopperDirection =
| 'none'
| 'left-top'
| 'left-bottom'
| 'right-top'
| 'right-bottom';
const getBorderRadius = (direction: PopperDirection, radius = '0') => {
const map: Record<PopperDirection, string> = {
none: `${radius}`,
'left-top': `0 ${radius} ${radius} ${radius}`,
'left-bottom': `${radius} ${radius} ${radius} 0`,
'right-top': `${radius} 0 ${radius} ${radius}`,
'right-bottom': `${radius} ${radius} 0 ${radius}`,
};
return map[direction];
};
export const placementToContainerDirection: Record<
PopperPlacementType,
PopperDirection
> = {
top: 'none',
'top-start': 'left-bottom',
'top-end': 'right-bottom',
right: 'none',
'right-start': 'left-top',
'right-end': 'left-bottom',
bottom: 'none',
'bottom-start': 'none',
'bottom-end': 'none',
left: 'none',
'left-start': 'right-top',
'left-end': 'right-bottom',
auto: 'none',
'auto-start': 'none',
'auto-end': 'none',
};
export const StyledPopperContainer = styled('div')<{
placement?: PopperPlacementType;
}>(({ placement = 'top' }) => {
const direction = placementToContainerDirection[placement];
const borderRadius = getBorderRadius(
direction,
'var(--affine-popover-radius)'
);
return {
borderRadius,
};
});
export default StyledPopperContainer;

View File

@@ -0,0 +1,94 @@
import { keyframes, style } from '@vanilla-extract/css';
import type { PickStringFromUnion, SkeletonProps } from './types';
// variables
const bg = 'var(--affine-placeholder-color)';
const highlight = 'rgba(255, 255, 255, 0.4)';
const defaultHeight = '32px';
const pulseKeyframes = keyframes({
'0%': { opacity: 1 },
'50%': { opacity: 0.5 },
'100%': { opacity: 1 },
});
const waveKeyframes = keyframes({
'0%': { transform: 'translateX(-100%)' },
'50%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(100%)' },
});
export const root = style({
display: 'block',
width: '100%',
height: defaultHeight,
flexShrink: 0,
/**
* paint background in ::before,
* so that we can use opacity to control the color
**/
position: 'relative',
'::before': {
content: '',
position: 'absolute',
borderRadius: 'inherit',
inset: 0,
opacity: 0.3,
backgroundColor: bg,
},
});
export const variant: Record<string, string> = {
circular: style({
width: defaultHeight,
borderRadius: '50%',
}),
rectangular: style({
borderRadius: '0px',
}),
rounded: style({
borderRadius: '8px',
}),
text: style({
borderRadius: '4px',
height: '1.2em',
marginTop: '0.2em',
marginBottom: '0.2em',
}),
};
export const animation: Record<
PickStringFromUnion<SkeletonProps['animation']>,
string
> = {
pulse: style({
animation: `${pulseKeyframes} 2s ease-in-out 0.5s infinite`,
}),
wave: style({
position: 'relative',
overflow: 'hidden',
/* Fix bug in Safari https://bugs.webkit.org/show_bug.cgi?id=68196 */
WebkitMaskImage: '-webkit-radial-gradient(white, black)',
'::after': {
animation: `${waveKeyframes} 2s linear 0.5s infinite`,
background: `linear-gradient(
90deg,
transparent,
${highlight},
transparent
)`,
content: '',
position: 'absolute',
transform:
'translateX(-100%)' /* Avoid flash during server-side hydration */,
bottom: 0,
left: 0,
right: 0,
top: 0,
},
}),
};

View File

@@ -0,0 +1,2 @@
export * from './skeleton';
export * from './types';

View File

@@ -0,0 +1,49 @@
import clsx from 'clsx';
import * as styles from './index.css';
import type { SkeletonProps } from './types';
function getSize(size: number | string) {
return typeof size === 'number' || /^\d+$/.test(size) ? `${size}px` : size;
}
/**
*
* @returns
*/
export const Skeleton = ({
animation = 'pulse',
variant = 'text',
children,
width: _width,
height: _height,
style: _style,
className: _className,
...props
}: SkeletonProps) => {
const width = _width !== undefined ? getSize(_width) : undefined;
const height = _height !== undefined ? getSize(_height) : undefined;
const style = {
width,
height,
...(_style || {}),
};
return (
<div
className={clsx(
_className,
styles.root,
styles.variant[variant],
animation && styles.animation[animation]
)}
style={style}
{...props}
>
{children}
</div>
);
};

View File

@@ -0,0 +1,33 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
export interface SkeletonProps
extends PropsWithChildren,
HTMLAttributes<HTMLElement> {
/**
* The animation. If `false` the animation effect is disabled.
*/
animation?: 'pulse' | 'wave' | false;
/**
* The type of content that will be rendered.
* @default `'text'`
*/
variant?: 'circular' | 'rectangular' | 'rounded' | 'text' | string;
/**
* Width of the skeleton. Useful when the skeleton is inside an inline element with no width of its own.
*/
width?: number | string;
/**
* Height of the skeleton. Useful when you don't want to adapt the skeleton to a text element but for instance a card.
*/
height?: number | string;
/**
* Wrapper component. If not provided, the default element is a div.
*/
wrapper?: string;
}
export type PickStringFromUnion<T> = T extends string ? T : never;

View File

@@ -1,10 +1,3 @@
// import Table from '@mui/material/Table';
// import TableBody from '@mui/material/TableBody';
// import TableCell from '@mui/material/TableCell';
// import TableHead from '@mui/material/TableHead';
// import TableRow from '@mui/material/TableRow';
//
export * from './interface';
export * from './table';
export * from './table-body';

View File

@@ -1,32 +0,0 @@
import { useState } from 'react';
import type { TreeNodeProps } from '../types';
export const useCollapsed = ({
initialCollapsedIds = [],
disableCollapse = false,
}: {
disableCollapse?: boolean;
initialCollapsedIds?: string[];
}) => {
// TODO: should record collapsedIds in localStorage
const [collapsedIds, setCollapsedIds] =
useState<string[]>(initialCollapsedIds);
const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
if (disableCollapse) {
return;
}
if (collapsed) {
setCollapsedIds(ids => [...ids, id]);
} else {
setCollapsedIds(ids => ids.filter(i => i !== id));
}
};
return {
collapsedIds,
setCollapsed,
};
};
export default useCollapsed;

View File

@@ -1,63 +0,0 @@
import { useEffect, useState } from 'react';
import type { TreeViewProps } from '../types';
import { flattenIds } from '../utils';
export const useSelectWithKeyboard = <RenderProps>({
data,
enableKeyboardSelection,
onSelect,
}: Pick<
TreeViewProps<RenderProps>,
'data' | 'enableKeyboardSelection' | 'onSelect'
>) => {
const [selectedId, setSelectedId] = useState<string>();
// TODO: should record collapsedIds in localStorage
useEffect(() => {
if (!enableKeyboardSelection) {
return;
}
const flattenedIds = flattenIds<RenderProps>(data);
const handleDirectionKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
return;
}
if (selectedId === undefined) {
setSelectedId(flattenedIds[0]);
return;
}
let selectedIndex = flattenedIds.indexOf(selectedId);
if (e.key === 'ArrowDown') {
selectedIndex < flattenedIds.length - 1 && selectedIndex++;
}
if (e.key === 'ArrowUp') {
selectedIndex > 0 && selectedIndex--;
}
setSelectedId(flattenedIds[selectedIndex]);
};
const handleEnterKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
return;
}
selectedId && onSelect?.(selectedId);
};
document.addEventListener('keydown', handleDirectionKeyDown);
document.addEventListener('keydown', handleEnterKeyDown);
return () => {
document.removeEventListener('keydown', handleDirectionKeyDown);
document.removeEventListener('keydown', handleEnterKeyDown);
};
}, [data, enableKeyboardSelection, onSelect, selectedId]);
return {
selectedId,
};
};
export default useSelectWithKeyboard;

View File

@@ -1,3 +0,0 @@
export * from './tree-node';
export * from './tree-view';
export * from './types';

View File

@@ -1,44 +0,0 @@
import MuiCollapse from '@mui/material/Collapse';
import { lightTheme } from '@toeverything/theme';
import type { CSSProperties } from 'react';
import { alpha, styled } from '../../styles';
export const StyledCollapse = styled(MuiCollapse)<{
indent?: CSSProperties['paddingLeft'];
}>(({ indent = 12 }) => {
return {
paddingLeft: indent,
};
});
export const StyledTreeNodeWrapper = styled('div')(() => {
return {
position: 'relative',
};
});
export const StyledTreeNodeContainer = styled('div')<{ isDragging?: boolean }>(
({ isDragging = false }) => {
return {
background: isDragging ? 'var(--affine-hover-color)' : '',
};
}
);
export const StyledNodeLine = styled('div')<{
isOver: boolean;
isTop?: boolean;
}>(({ isOver, isTop = false }) => {
return {
position: 'absolute',
left: '0',
...(isTop ? { top: '-1px' } : { bottom: '-1px' }),
width: '100%',
paddingTop: '2x',
borderTop: '2px solid',
borderColor: isOver ? 'var(--affine-primary-color)' : 'transparent',
boxShadow: isOver
? `0px 0px 8px ${alpha(lightTheme.primaryColor, 0.35)}`
: 'none',
zIndex: 1,
};
});

View File

@@ -1,88 +0,0 @@
import { useDroppable } from '@dnd-kit/core';
import { StyledNodeLine } from './styles';
import type { NodeLIneProps, TreeNodeItemProps } from './types';
export const NodeLine = <RenderProps,>({
node,
allowDrop = true,
isTop = false,
}: NodeLIneProps<RenderProps>) => {
const { isOver, setNodeRef } = useDroppable({
id: `${node.id}-${isTop ? 'top' : 'bottom'}-line`,
disabled: !allowDrop,
data: {
node,
position: {
topLine: isTop,
bottomLine: !isTop,
internal: false,
},
},
});
return (
<StyledNodeLine
ref={setNodeRef}
isOver={isOver && allowDrop}
isTop={isTop}
/>
);
};
export const TreeNodeItemWithDnd = <RenderProps,>({
node,
allowDrop,
setCollapsed,
...otherProps
}: TreeNodeItemProps<RenderProps>) => {
const { onAdd, onDelete } = otherProps;
const { isOver, setNodeRef } = useDroppable({
id: node.id,
disabled: !allowDrop,
data: {
node,
position: {
topLine: false,
bottomLine: false,
internal: true,
},
},
});
return (
<div ref={setNodeRef}>
<TreeNodeItem
onAdd={onAdd}
onDelete={onDelete}
node={node}
allowDrop={allowDrop}
setCollapsed={setCollapsed}
isOver={isOver}
{...otherProps}
/>
</div>
);
};
export const TreeNodeItem = <RenderProps,>({
node,
collapsed,
setCollapsed,
selectedId,
isOver = false,
onAdd,
onDelete,
disableCollapse,
allowDrop = true,
}: TreeNodeItemProps<RenderProps>) => {
return node.render?.(node, {
isOver: isOver && allowDrop,
onAdd: () => onAdd?.(node.id),
onDelete: () => onDelete?.(node.id),
collapsed,
setCollapsed,
isSelected: selectedId === node.id,
disableCollapse,
});
};

View File

@@ -1,106 +0,0 @@
import { useDraggable } from '@dnd-kit/core';
import { useMemo } from 'react';
import {
StyledCollapse,
StyledTreeNodeContainer,
StyledTreeNodeWrapper,
} from './styles';
import { NodeLine, TreeNodeItem, TreeNodeItemWithDnd } from './tree-node-inner';
import type { TreeNodeProps } from './types';
export const TreeNodeWithDnd = <RenderProps,>(
props: TreeNodeProps<RenderProps>
) => {
const { draggingId, node, allowDrop } = props;
const { attributes, listeners, setNodeRef } = useDraggable({
id: props.node.id,
});
const isDragging = useMemo(
() => draggingId === node.id,
[draggingId, node.id]
);
return (
<StyledTreeNodeContainer
ref={setNodeRef}
isDragging={isDragging}
{...listeners}
{...attributes}
>
<TreeNode
{...props}
allowDrop={allowDrop === false ? allowDrop : !isDragging}
/>
</StyledTreeNodeContainer>
);
};
export const TreeNode = <RenderProps,>({
node,
index,
allowDrop = true,
...otherProps
}: TreeNodeProps<RenderProps>) => {
const { indent, enableDnd, collapsedIds } = otherProps;
const collapsed = collapsedIds.includes(node.id);
const { renderTopLine = true, renderBottomLine = true } = node;
return (
<>
<StyledTreeNodeWrapper>
{enableDnd && renderTopLine && index === 0 && (
<NodeLine
node={node}
{...otherProps}
allowDrop={allowDrop}
isTop={true}
/>
)}
{enableDnd ? (
<TreeNodeItemWithDnd
node={node}
index={index}
allowDrop={allowDrop}
collapsed={collapsed}
{...otherProps}
/>
) : (
<TreeNodeItem
node={node}
index={index}
allowDrop={allowDrop}
collapsed={collapsed}
{...otherProps}
/>
)}
{enableDnd &&
renderBottomLine &&
(!node.children?.length || collapsed) && (
<NodeLine node={node} {...otherProps} allowDrop={allowDrop} />
)}
</StyledTreeNodeWrapper>
<StyledCollapse in={!collapsed} indent={indent}>
{node.children &&
node.children.map((childNode, index) =>
enableDnd ? (
<TreeNodeWithDnd
key={childNode.id}
node={childNode}
index={index}
{...otherProps}
allowDrop={allowDrop}
/>
) : (
<TreeNode
key={childNode.id}
node={childNode}
index={index}
allowDrop={false}
{...otherProps}
/>
)
)}
</StyledCollapse>
</>
);
};

View File

@@ -1,126 +0,0 @@
import type { DragEndEvent } from '@dnd-kit/core';
import {
closestCenter,
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { useCallback, useState } from 'react';
import useCollapsed from './hooks/use-collapsed';
import useSelectWithKeyboard from './hooks/use-select-with-keyboard';
import { TreeNode, TreeNodeWithDnd } from './tree-node';
import type { Node, TreeViewProps } from './types';
import { findNode } from './utils';
export const TreeView = <RenderProps,>({
data,
enableKeyboardSelection,
onSelect,
enableDnd = true,
disableCollapse,
onDrop,
...otherProps
}: TreeViewProps<RenderProps>) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const { selectedId } = useSelectWithKeyboard({
data,
onSelect,
enableKeyboardSelection,
});
const { collapsedIds, setCollapsed } = useCollapsed({ disableCollapse });
const [draggingId, setDraggingId] = useState<string>();
const onDragEnd = useCallback(
(e: DragEndEvent) => {
const { active, over } = e;
const position = over?.data.current?.position;
const dropId = over?.data.current?.node.id;
setDraggingId(undefined);
if (!over || !active || !position) {
return;
}
onDrop?.(active.id as string, dropId, position);
},
[onDrop]
);
const onDragMove = useCallback((e: DragEndEvent) => {
setDraggingId(e.active.id as string);
}, []);
if (enableDnd) {
const treeNodes = data.map((node, index) => (
<TreeNodeWithDnd
key={node.id}
index={index}
collapsedIds={collapsedIds}
setCollapsed={setCollapsed}
node={node}
selectedId={selectedId}
enableDnd={enableDnd}
disableCollapse={disableCollapse}
draggingId={draggingId}
{...otherProps}
/>
));
const draggingNode = (function () {
let draggingNode: Node<RenderProps> | undefined;
if (draggingId) {
draggingNode = findNode(draggingId, data);
}
if (draggingNode) {
return (
<TreeNode
node={draggingNode}
index={0}
allowDrop={false}
collapsedIds={collapsedIds}
setCollapsed={() => {}}
{...otherProps}
/>
);
}
return null;
})();
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
>
{treeNodes}
<DragOverlay>{draggingNode}</DragOverlay>
</DndContext>
);
}
return (
<>
{data.map((node, index) => (
<TreeNode
key={node.id}
index={index}
collapsedIds={collapsedIds}
setCollapsed={setCollapsed}
node={node}
selectedId={selectedId}
enableDnd={enableDnd}
disableCollapse={disableCollapse}
{...otherProps}
/>
))}
</>
);
};
export default TreeView;

View File

@@ -1,72 +0,0 @@
import type { CSSProperties, ReactNode } from 'react';
export type DropPosition = {
topLine: boolean;
bottomLine: boolean;
internal: boolean;
};
export type OnDrop = (
dragId: string,
dropId: string,
position: DropPosition
) => void;
export type Node<RenderProps = unknown> = {
id: string;
children?: Node<RenderProps>[];
render: (
node: Node<RenderProps>,
eventsAndStatus: {
isOver: boolean;
onAdd: () => void;
onDelete: () => void;
collapsed: boolean;
setCollapsed: (id: string, collapsed: boolean) => void;
isSelected: boolean;
disableCollapse?: ReactNode;
},
renderProps?: RenderProps
) => ReactNode;
renderTopLine?: boolean;
renderBottomLine?: boolean;
};
type CommonProps = {
enableDnd?: boolean;
enableKeyboardSelection?: boolean;
indent?: CSSProperties['paddingLeft'];
onAdd?: (parentId: string) => void;
onDelete?: (deleteId: string) => void;
onDrop?: OnDrop;
// Only trigger when the enableKeyboardSelection is true
onSelect?: (id: string) => void;
disableCollapse?: ReactNode;
};
export type TreeNodeProps<RenderProps = unknown> = {
node: Node<RenderProps>;
index: number;
collapsedIds: string[];
setCollapsed: (id: string, collapsed: boolean) => void;
allowDrop?: boolean;
selectedId?: string;
draggingId?: string;
} & CommonProps;
export type TreeNodeItemProps<RenderProps = unknown> = {
collapsed: boolean;
setCollapsed: (id: string, collapsed: boolean) => void;
isOver?: boolean;
} & TreeNodeProps<RenderProps>;
export type TreeViewProps<RenderProps = unknown> = {
data: Node<RenderProps>[];
initialCollapsedIds?: string[];
disableCollapse?: boolean;
} & CommonProps;
export type NodeLIneProps<RenderProps = unknown> = {
allowDrop: boolean;
isTop?: boolean;
} & Pick<TreeNodeProps<RenderProps>, 'node'>;

View File

@@ -1,37 +0,0 @@
import type { Node } from './types';
export function flattenIds<RenderProps>(arr: Node<RenderProps>[]): string[] {
const result: string[] = [];
function flatten(arr: Node<RenderProps>[]) {
for (let i = 0, len = arr.length; i < len; i++) {
const item = arr[i];
result.push(item.id);
if (Array.isArray(item.children)) {
flatten(item.children);
}
}
}
flatten(arr);
return result;
}
export function findNode<RenderProps>(
id: string,
nodes: Node<RenderProps>[]
): Node<RenderProps> | undefined {
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
if (node.id === id) {
return node;
}
if (node.children) {
const result = findNode(id, node.children);
if (result) {
return result;
}
}
}
return undefined;
}