mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
refactor(core): remove all MUI related components and utilities (#4941)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './helper';
|
||||
export * from './mui-theme';
|
||||
export * from './mui-theme-provider';
|
||||
export * from './styled';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { alpha, css, keyframes, styled } from '@mui/material/styles';
|
||||
|
||||
export { alpha, css, keyframes, styled };
|
||||
@@ -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);
|
||||
3
packages/frontend/component/src/styles/styled.tsx
Normal file
3
packages/frontend/component/src/styles/styled.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export { styled };
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './interface';
|
||||
export * from './popper';
|
||||
export * from './pure-popper';
|
||||
@@ -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'>;
|
||||
@@ -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)'),
|
||||
};
|
||||
});
|
||||
@@ -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)',
|
||||
};
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { styled } from '../../styles';
|
||||
|
||||
export const PopperWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
94
packages/frontend/component/src/ui/skeleton/index.css.ts
Normal file
94
packages/frontend/component/src/ui/skeleton/index.css.ts
Normal 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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
2
packages/frontend/component/src/ui/skeleton/index.ts
Normal file
2
packages/frontend/component/src/ui/skeleton/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './skeleton';
|
||||
export * from './types';
|
||||
49
packages/frontend/component/src/ui/skeleton/skeleton.tsx
Normal file
49
packages/frontend/component/src/ui/skeleton/skeleton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
33
packages/frontend/component/src/ui/skeleton/types.ts
Normal file
33
packages/frontend/component/src/ui/skeleton/types.ts
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './tree-node';
|
||||
export * from './tree-view';
|
||||
export * from './types';
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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'>;
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user