mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 05:05:52 +08:00
Merge branch 'canary' into stable
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
"@blocksuite/editor": "*",
|
||||
"@blocksuite/global": "*",
|
||||
"@blocksuite/icons": "2.1.34",
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -22,14 +21,11 @@
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/base": "5.0.0-beta.19",
|
||||
"@mui/icons-material": "^5.14.14",
|
||||
"@mui/material": "^5.14.14",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -47,15 +43,14 @@
|
||||
"clsx": "^2.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"foxact": "^0.2.20",
|
||||
"jotai": "^2.4.3",
|
||||
"jotai-effect": "^0.2.2",
|
||||
"jotai-scope": "^0.4.0",
|
||||
"jotai": "^2.5.1",
|
||||
"jotai-effect": "^0.2.3",
|
||||
"jotai-scope": "^0.4.1",
|
||||
"lit": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lottie-web": "^5.12.2",
|
||||
"nanoid": "^5.0.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-datepicker": "^4.20.0",
|
||||
@@ -69,12 +64,12 @@
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/icons": "2.1.35",
|
||||
"@blocksuite/lit": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/icons": "2.1.36",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@storybook/jest": "^0.2.3",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
@@ -85,10 +80,10 @@
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.4.11",
|
||||
"vitest": "0.34.6",
|
||||
"yjs": "^13.6.8"
|
||||
"yjs": "^13.6.10"
|
||||
},
|
||||
"version": "0.10.2"
|
||||
"version": "0.10.3-canary.2"
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { CloseIcon, Logo1Icon } from '@blocksuite/icons';
|
||||
|
||||
import {
|
||||
downloadCloseButtonStyle,
|
||||
downloadMessageStyle,
|
||||
downloadTipContainerStyle,
|
||||
downloadTipIconStyle,
|
||||
downloadTipStyle,
|
||||
linkStyle,
|
||||
} from './index.css';
|
||||
|
||||
export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
|
||||
return (
|
||||
<div
|
||||
className={downloadTipContainerStyle}
|
||||
data-testid="download-client-tip"
|
||||
>
|
||||
<div className={downloadTipStyle}>
|
||||
<Logo1Icon className={downloadTipIconStyle} />
|
||||
<div className={downloadMessageStyle}>
|
||||
<Trans i18nKey="com.affine.banner.content">
|
||||
This demo is limited.
|
||||
<a
|
||||
className={linkStyle}
|
||||
href="https://affine.pro/download"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Download the AFFiNE Client
|
||||
</a>
|
||||
for the latest features and Performance.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={downloadCloseButtonStyle}
|
||||
onClick={onClose}
|
||||
data-testid="download-client-tip-close-button"
|
||||
>
|
||||
<CloseIcon className={downloadTipIconStyle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadTips;
|
||||
@@ -1,13 +1,4 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const slideDown = keyframes({
|
||||
'0%': {
|
||||
height: '0px',
|
||||
},
|
||||
'100%': {
|
||||
height: '44px',
|
||||
},
|
||||
});
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const browserWarningStyle = style({
|
||||
backgroundColor: 'var(--affine-background-warning-color)',
|
||||
@@ -36,52 +27,31 @@ export const closeIconStyle = style({
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
});
|
||||
export const downloadTipContainerStyle = style({
|
||||
backgroundColor: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-white)',
|
||||
export const tipsContainer = style({
|
||||
backgroundColor: 'var(--affine-background-error-color)',
|
||||
color: 'var(--affine-error-color)',
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: '700',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
animation: `${slideDown} .3s ease-in-out forwards`,
|
||||
padding: '12px 16px',
|
||||
position: 'sticky',
|
||||
gap: '16px',
|
||||
containerType: 'inline-size',
|
||||
});
|
||||
export const downloadTipStyle = style({
|
||||
|
||||
export const tipsMessage = style({
|
||||
color: 'var(--affine-error-color)',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
});
|
||||
|
||||
export const tipsRightItem = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const downloadTipIconStyle = style({
|
||||
color: 'var(--affine-white)',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
fontSize: '24px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
});
|
||||
export const downloadCloseButtonStyle = style({
|
||||
color: 'var(--affine-white)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
right: '24px',
|
||||
});
|
||||
export const downloadMessageStyle = style({
|
||||
color: 'var(--affine-white)',
|
||||
marginLeft: '8px',
|
||||
});
|
||||
export const linkStyle = style({
|
||||
color: 'var(--affine-white)',
|
||||
textDecoration: 'underline',
|
||||
':hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
':visited': {
|
||||
color: 'var(--affine-white)',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './browser-warning';
|
||||
export * from './download-client';
|
||||
export * from './local-demo-tips';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
type LocalDemoTipsProps = {
|
||||
isLoggedIn: boolean;
|
||||
onLogin: () => void;
|
||||
onEnableCloud: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const LocalDemoTips = ({
|
||||
onClose,
|
||||
isLoggedIn,
|
||||
onLogin,
|
||||
onEnableCloud,
|
||||
}: LocalDemoTipsProps) => {
|
||||
const content = isLoggedIn
|
||||
? 'This is a local demo workspace, and the data is stored locally. We recommend enabling AFFiNE Cloud.'
|
||||
: 'This is a local demo workspace, and the data is stored locally in the browser. We recommend Enabling AFFiNE Cloud or downloading the client for a better experience.';
|
||||
|
||||
const buttonLabel = isLoggedIn
|
||||
? 'Enable AFFiNE Cloud'
|
||||
: 'Sign in with AFFiNE Cloud';
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isLoggedIn) {
|
||||
return onEnableCloud();
|
||||
}
|
||||
return onLogin();
|
||||
}, [isLoggedIn, onEnableCloud, onLogin]);
|
||||
|
||||
return (
|
||||
<div className={styles.tipsContainer} data-testid="local-demo-tips">
|
||||
<div className={styles.tipsMessage}>{content}</div>
|
||||
|
||||
<div className={styles.tipsRightItem}>
|
||||
<div>
|
||||
<Button onClick={handleClick}>{buttonLabel}</Button>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
data-testid="local-demo-tips-close-button"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalDemoTips;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export {
|
||||
closeIcon,
|
||||
ellipsisTextOverflow,
|
||||
halo,
|
||||
icon,
|
||||
particles,
|
||||
root,
|
||||
} from '../app-updater-button/index.css';
|
||||
|
||||
export const rootPadding = style({
|
||||
padding: '0 24px',
|
||||
});
|
||||
|
||||
export const label = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CloseIcon, DownloadIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AppDownloadButton({
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const [show, setShow] = useState(true);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setShow(false);
|
||||
}, []);
|
||||
|
||||
// TODO: unify this type of literal value.
|
||||
const handleClick = useCallback(() => {
|
||||
const url = `https://affine.pro/download?channel=stable`;
|
||||
open(url, '_blank');
|
||||
}, []);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
style={style}
|
||||
className={clsx([styles.root, styles.rootPadding, className])}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={clsx([styles.label])}>
|
||||
<DownloadIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>Download App</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.closeIcon}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
<div className={styles.particles} aria-hidden="true"></div>
|
||||
<span className={styles.halo} aria-hidden="true"></span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import debounce from 'lodash/debounce';
|
||||
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,
|
||||
@@ -159,6 +159,7 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
||||
};
|
||||
|
||||
export * from './add-page-button';
|
||||
export * from './app-download-button';
|
||||
export * from './app-updater-button';
|
||||
export * from './category-divider';
|
||||
export * from './index.css';
|
||||
|
||||
@@ -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,11 +16,12 @@ import {
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { Skeleton } from '../../ui/skeleton';
|
||||
import {
|
||||
blockSuiteEditorHeaderStyle,
|
||||
blockSuiteEditorStyle,
|
||||
} from './index.css';
|
||||
import { getPresets } from './preset';
|
||||
import { editorPresets } from './preset';
|
||||
|
||||
interface BlockElement extends Element {
|
||||
path: string[];
|
||||
@@ -104,12 +104,10 @@ const BlockSuiteEditorImpl = ({
|
||||
|
||||
if (editor.page !== page) {
|
||||
editor.page = page;
|
||||
editor.pagePreset = editorPresets.pageModePreset;
|
||||
editor.edgelessPreset = editorPresets.edgelessModePreset;
|
||||
}
|
||||
|
||||
const presets = getPresets();
|
||||
editor.pagePreset = presets.pageModePreset;
|
||||
editor.edgelessPreset = presets.edgelessModePreset;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (editor) {
|
||||
const disposes: (() => void)[] = [];
|
||||
|
||||
@@ -17,7 +17,7 @@ class CustomAttachmentService extends AttachmentService {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPresets() {
|
||||
function getPresets() {
|
||||
const pageModePreset = PagePreset.map(preset => {
|
||||
if (preset.schema.model.flavour === 'affine:attachment') {
|
||||
return {
|
||||
@@ -42,3 +42,5 @@ export function getPresets() {
|
||||
edgelessModePreset,
|
||||
};
|
||||
}
|
||||
|
||||
export const editorPresets = getPresets();
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -60,9 +60,9 @@ export const notificationStyle = style({
|
||||
opacity: 1,
|
||||
height: 'var(--front-toast-height)',
|
||||
vars: {
|
||||
'--scale': 'var(--toasts-before)* 0.05 + 1',
|
||||
'--scale': 'calc(1 - var(--toasts-before)* 0.05)',
|
||||
'--y':
|
||||
'translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)))',
|
||||
'translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(var(--scale))',
|
||||
},
|
||||
},
|
||||
'&[data-mounted=true][data-expanded=true]': {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
type MenuItemProps,
|
||||
} from '@toeverything/components/menu';
|
||||
|
||||
import { PublicLinkDisableModal } from '../../share-menu';
|
||||
import { PublicLinkDisableModal } from '../../disable-public-link';
|
||||
|
||||
export const DisablePublicSharing = (props: MenuItemProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -64,14 +64,6 @@ export const innerBackdrop = style({
|
||||
},
|
||||
});
|
||||
|
||||
const range = (start: number, end: number) => {
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const tag = style({
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
@@ -94,14 +86,6 @@ export const tagSticky = style([
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
left: 0,
|
||||
selectors: range(0, 20).reduce((selectors, i) => {
|
||||
return {
|
||||
...selectors,
|
||||
[`&:nth-last-child(${i + 1})`]: {
|
||||
right: `${i * 48}px`,
|
||||
},
|
||||
};
|
||||
}, {}),
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ interface TagItemProps {
|
||||
tag: Tag;
|
||||
idx: number;
|
||||
mode: 'sticky' | 'list-item';
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// hack: map var(--affine-tag-xxx) colors to var(--affine-palette-line-xxx)
|
||||
@@ -37,13 +38,14 @@ const tagColorMap = (color: string) => {
|
||||
return mapping[color] || color;
|
||||
};
|
||||
|
||||
const TagItem = ({ tag, idx, mode }: TagItemProps) => {
|
||||
const TagItem = ({ tag, idx, mode, style }: TagItemProps) => {
|
||||
return (
|
||||
<div
|
||||
data-testid="page-tag"
|
||||
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
|
||||
data-idx={idx}
|
||||
title={tag.value}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={styles.tagIndicator}
|
||||
@@ -101,8 +103,30 @@ export const PageTags = ({
|
||||
|
||||
const tagsNormal = useMemo(() => {
|
||||
const nTags = maxItems ? tags.slice(0, maxItems) : tags;
|
||||
|
||||
// sort tags by length
|
||||
nTags.sort((a, b) => a.value.length - b.value.length);
|
||||
|
||||
const tagRightCharLength = nTags.reduceRight<number[]>(
|
||||
(acc, tag) => {
|
||||
const curr = acc[0] + Math.min(tag.value.length, 10);
|
||||
return [curr, ...acc];
|
||||
},
|
||||
[0]
|
||||
);
|
||||
|
||||
tagRightCharLength.shift();
|
||||
|
||||
return nTags.map((tag, idx) => (
|
||||
<TagItem key={tag.id} tag={tag} idx={idx} mode="sticky" />
|
||||
<TagItem
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
idx={idx}
|
||||
mode="sticky"
|
||||
style={{
|
||||
right: `calc(${tagRightCharLength[idx]}em)`,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}, [maxItems, tags]);
|
||||
return (
|
||||
|
||||
@@ -192,9 +192,9 @@ export const pageGroupsAtom = atom(get => {
|
||||
sorter.key === 'createDate' || sorter.key === 'updatedDate'
|
||||
? sorter.key
|
||||
: // default sort
|
||||
!sorter.key
|
||||
? DEFAULT_SORT_KEY
|
||||
: undefined;
|
||||
!sorter.key
|
||||
? DEFAULT_SORT_KEY
|
||||
: undefined;
|
||||
}
|
||||
return pagesToPageGroups(sorter.pages, groupBy);
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+4
-4
@@ -92,10 +92,10 @@ export const RulesMode = ({
|
||||
values={{
|
||||
highlight: t['com.affine.editCollection.rules.tips.highlight'](),
|
||||
}}
|
||||
>
|
||||
Pages that meet the rules will be added to the current collection{' '}
|
||||
<span className={styles.rulesTitleHighlight}>highlight</span>.
|
||||
</Trans>
|
||||
components={{
|
||||
2: <span className={styles.rulesTitleHighlight} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rulesContainer}>
|
||||
<div className={styles.rulesContainerLeft}>
|
||||
|
||||
+1
-2
@@ -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';
|
||||
|
||||
+8
-4
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const menuStyle = style({
|
||||
width: '410px',
|
||||
height: 'auto',
|
||||
padding: '12px',
|
||||
transform: 'translateX(-10px)',
|
||||
});
|
||||
|
||||
export const menuItemStyle = style({
|
||||
padding: '4px',
|
||||
transition: 'all 0.3s',
|
||||
});
|
||||
|
||||
export const descriptionStyle = style({
|
||||
wordWrap: 'break-word',
|
||||
// wordBreak: 'break-all',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
textAlign: 'left',
|
||||
padding: '0 6px',
|
||||
});
|
||||
|
||||
export const buttonStyle = style({
|
||||
marginTop: '18px',
|
||||
// todo: new color scheme should be used
|
||||
});
|
||||
|
||||
export const actionsStyle = style({
|
||||
display: 'flex',
|
||||
gap: '9px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
});
|
||||
|
||||
export const containerStyle = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const indicatorContainerStyle = style({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const inputButtonRowStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '16px',
|
||||
});
|
||||
|
||||
export const titleContainerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
});
|
||||
|
||||
export const subTitleStyle = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
});
|
||||
|
||||
export const columnContainerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const rowContainerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '4px',
|
||||
});
|
||||
|
||||
export const radioButtonGroup = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '2px',
|
||||
minWidth: '154px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const radioButton = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
selectors: {
|
||||
'&[data-state="checked"]': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const spanStyle = style({
|
||||
padding: '4px 20px',
|
||||
});
|
||||
|
||||
export const disableSharePage = style({
|
||||
color: 'var(--affine-error-color)',
|
||||
});
|
||||
|
||||
export const localSharePage = style({
|
||||
padding: '12px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
minHeight: '84px',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const cloudSvgContainer = style({
|
||||
width: '146px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
});
|
||||
export const shareIconStyle = style({
|
||||
fontSize: '16px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const shareLinkStyle = style({
|
||||
padding: '4px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
transform: 'translateX(-4px)',
|
||||
gap: '4px',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > span`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from 'jotai/vanilla';
|
||||
|
||||
export const enableShareMenuAtom = atom(false);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './disable-public-link';
|
||||
export * from './share-menu';
|
||||
export * from './styles';
|
||||
@@ -1,63 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { LinkIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
|
||||
import { ExportMenuItems } from '../page-list/operation-menu-items/export';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
export const ShareExport = ({
|
||||
workspace,
|
||||
currentPage,
|
||||
exportHandler,
|
||||
}: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceId = workspace.id;
|
||||
const pageId = currentPage.id;
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'workspace',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExport']()}
|
||||
</div>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExportDescription']()}
|
||||
</div>
|
||||
<div>
|
||||
<ExportMenuItems
|
||||
exportHandler={exportHandler}
|
||||
className={styles.menuItemStyle}
|
||||
/>
|
||||
</div>
|
||||
{workspace.flavour !== WorkspaceFlavour.LOCAL ? (
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.share-privately']()}
|
||||
</div>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.share-privately.description']()}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.shareLinkStyle}
|
||||
onClick={onClickCopyLink}
|
||||
icon={<LinkIcon />}
|
||||
type="plain"
|
||||
>
|
||||
{t['com.affine.share-menu.copy-private-link']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
import {
|
||||
type AffineCloudWorkspace,
|
||||
type AffineOfficialWorkspace,
|
||||
type AffinePublicWorkspace,
|
||||
type LocalWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { WebIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
export interface ShareMenuProps<
|
||||
Workspace extends AffineOfficialWorkspace =
|
||||
| AffineCloudWorkspace
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace,
|
||||
> {
|
||||
workspace: Workspace;
|
||||
currentPage: Page;
|
||||
useIsSharedPage: (
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
|
||||
onEnableAffineCloud: () => void;
|
||||
togglePagePublic: () => Promise<void>;
|
||||
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
|
||||
}
|
||||
|
||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={styles.containerStyle}>
|
||||
<div className={styles.headerStyle}>
|
||||
<div className={styles.shareIconStyle}>
|
||||
<WebIcon />
|
||||
</div>
|
||||
{t['com.affine.share-menu.SharePage']()}
|
||||
</div>
|
||||
<SharePage {...props} />
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
<ShareExport {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'local-share-menu',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="local-share-menu-button" type="plain">
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { workspace, currentPage, useIsSharedPage } = props;
|
||||
const [isSharedPage] = useIsSharedPage(workspace.id, currentPage.id);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'cloud-share-menu',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="cloud-share-menu-button" type="plain">
|
||||
<div
|
||||
style={{
|
||||
color: isSharedPage
|
||||
? 'var(--affine-link-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
}}
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.sharedButton']()
|
||||
: t['com.affine.share-menu.shareButton']()}
|
||||
</div>
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShareMenu = (props: ShareMenuProps) => {
|
||||
const { workspace } = props;
|
||||
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalShareMenu {...props} />;
|
||||
}
|
||||
return <CloudShareMenu {...props} />;
|
||||
};
|
||||
@@ -1,219 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import { useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from '../../ui/button';
|
||||
import Input from '../../ui/input';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import { toast } from '../../ui/toast';
|
||||
import { PublicLinkDisableModal } from './disable-public-link';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
const CloudSvg = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="146"
|
||||
height="84"
|
||||
viewBox="0 0 146 84"
|
||||
fill="none"
|
||||
>
|
||||
<g opacity="0.1">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M66.9181 15.9788C52.6393 15.9788 41.064 27.5541 41.064 41.8329C41.064 43.7879 41.2801 45.687 41.6881 47.5094C42.2383 49.9676 40.6923 52.4066 38.2344 52.9579C29.4068 54.938 22.814 62.8293 22.814 72.2496C22.814 83.1687 31.6657 92.0204 42.5848 92.0204H97.3348C111.614 92.0204 123.189 80.4451 123.189 66.1663C123.189 51.8874 111.614 40.3121 97.3348 40.3121C97.1618 40.3121 96.9892 40.3138 96.8169 40.3172C94.6134 40.3603 92.6941 38.8222 92.2561 36.6623C89.8629 24.8606 79.4226 15.9788 66.9181 15.9788ZM31.939 41.8329C31.939 22.5145 47.5997 6.85376 66.9181 6.85376C82.573 6.85376 95.8181 17.1339 100.285 31.3098C118.223 32.808 132.314 47.8415 132.314 66.1663C132.314 85.4847 116.653 101.145 97.3348 101.145H42.5848C26.6261 101.145 13.689 88.2083 13.689 72.2496C13.689 59.9818 21.3304 49.5073 32.1102 45.3122C31.9969 44.1668 31.939 43.0061 31.939 41.8329Z"
|
||||
fill="var(--affine-icon-color)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LocalSharePage = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div className={styles.localSharePage}>
|
||||
<div className={styles.columnContainerStyle} style={{ gap: '12px' }}>
|
||||
<div className={styles.descriptionStyle} style={{ maxWidth: '230px' }}>
|
||||
{t['com.affine.share-menu.EnableCloudDescription']()}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={props.onEnableAffineCloud}
|
||||
type="primary"
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cloudSvgContainer}>
|
||||
<CloudSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
const {
|
||||
workspace: { id: workspaceId },
|
||||
currentPage: { id: pageId },
|
||||
} = props;
|
||||
const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId);
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'share',
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickCreateLink = useCallback(() => {
|
||||
setIsPublic(true);
|
||||
}, [setIsPublic]);
|
||||
|
||||
const onDisablePublic = useCallback(() => {
|
||||
setIsPublic(false);
|
||||
toast('Successfully disabled', {
|
||||
portal: document.body,
|
||||
});
|
||||
setShowDisable(false);
|
||||
}, [setIsPublic]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.publish-to-web']()}
|
||||
</div>
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.publish-to-web.description']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<Input
|
||||
inputStyle={{
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
value={isPublic ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`}
|
||||
readOnly
|
||||
/>
|
||||
{isPublic ? (
|
||||
<Button
|
||||
onClick={onClickCopyLink}
|
||||
data-testid="share-menu-copy-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{t.Copy()}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onClickCreateLink}
|
||||
type="primary"
|
||||
data-testid="share-menu-create-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{t.Create()}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{runtimeConfig.enableEnhanceShareMode ? (
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{t['com.affine.share-menu.ShareMode']()}
|
||||
</div>
|
||||
<div>
|
||||
<RadioButtonGroup
|
||||
className={styles.radioButtonGroup}
|
||||
defaultValue={'page'}
|
||||
onValueChange={() => {}}
|
||||
>
|
||||
<RadioButton
|
||||
className={styles.radioButton}
|
||||
value={'page'}
|
||||
spanStyle={styles.spanStyle}
|
||||
>
|
||||
{t['com.affine.pageMode.page']()}
|
||||
</RadioButton>
|
||||
<RadioButton
|
||||
className={styles.radioButton}
|
||||
value={'edgeless'}
|
||||
spanStyle={styles.spanStyle}
|
||||
>
|
||||
{t['com.affine.pageMode.edgeless']()}
|
||||
</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isPublic ? (
|
||||
<>
|
||||
{runtimeConfig.enableEnhanceShareMode && (
|
||||
<>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>Link expires</div>
|
||||
<div>
|
||||
<Menu items={<MenuItem>Never</MenuItem>}>
|
||||
<MenuTrigger>Never</MenuTrigger>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{'Show "Created with AFFiNE"'}
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
Search engine indexing
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
endFix={<ArrowRightSmallIcon />}
|
||||
block
|
||||
type="danger"
|
||||
className={styles.menuItemStyle}
|
||||
onSelect={e => {
|
||||
e.preventDefault();
|
||||
setShowDisable(true);
|
||||
}}
|
||||
>
|
||||
<div className={styles.disableSharePage}>
|
||||
{t['Disable Public Link']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<PublicLinkDisableModal
|
||||
open={showDisable}
|
||||
onConfirm={onDisablePublic}
|
||||
onOpenChange={setShowDisable}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SharePage = (props: ShareMenuProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalSharePage {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return <AffineSharePage {...props} />;
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
import { displayFlex, styled } from '../..';
|
||||
|
||||
export const TabItem = styled('li')<{ isActive?: boolean }>(({ isActive }) => {
|
||||
{
|
||||
return {
|
||||
...displayFlex('center', 'center'),
|
||||
flex: '1',
|
||||
height: '30px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
opacity: isActive ? 1 : 0.2,
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
padding: '0 10px',
|
||||
marginBottom: '4px',
|
||||
borderRadius: '4px',
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
opacity: 1,
|
||||
color: isActive
|
||||
? 'var(--affine-t/ext-primary-color)'
|
||||
: 'var(--affine-text-secondary-color)',
|
||||
svg: {
|
||||
fill: isActive
|
||||
? 'var(--affine-text-primary-color)'
|
||||
: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
},
|
||||
svg: {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
bottom: '-6px',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: 'var(--affine-text-primary-color)',
|
||||
opacity: 0.2,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
export const StyledIndicator = styled('div')(() => {
|
||||
return {
|
||||
height: '2px',
|
||||
background: 'var(--affine-text-primary-color)',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
transition: 'left .3s, width .3s',
|
||||
};
|
||||
});
|
||||
export const StyledInput = styled('input')(() => {
|
||||
return {
|
||||
padding: '4px 8px',
|
||||
height: '28px',
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
border: `1px solid ${'var(--affine-placeholder-color)'}`,
|
||||
cursor: 'default',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'text',
|
||||
borderRadius: '4px',
|
||||
flexGrow: 1,
|
||||
marginRight: '10px',
|
||||
};
|
||||
});
|
||||
export const StyledDisableButton = styled(Button)(() => {
|
||||
return {
|
||||
color: '#FF631F',
|
||||
height: '32px',
|
||||
border: 'none',
|
||||
marginTop: '16px',
|
||||
borderRadius: '8px',
|
||||
padding: '0',
|
||||
};
|
||||
});
|
||||
export const StyledLinkSpan = styled('span')(() => {
|
||||
return {
|
||||
marginLeft: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { toast } from '../../ui/toast';
|
||||
|
||||
type UrlType = 'share' | 'workspace';
|
||||
|
||||
type UseSharingUrl = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
urlType: UrlType;
|
||||
};
|
||||
|
||||
export const generateUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
return `${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`;
|
||||
};
|
||||
|
||||
export const useSharingUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const sharingUrl = useMemo(
|
||||
() => generateUrl({ workspaceId, pageId, urlType }),
|
||||
[urlType, workspaceId, pageId]
|
||||
);
|
||||
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.writeText(sharingUrl)
|
||||
.then(() => {
|
||||
toast(t['Copied link to clipboard']());
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [sharingUrl, t]);
|
||||
|
||||
return {
|
||||
sharingUrl,
|
||||
onClickCopyLink,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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?: {
|
||||
|
||||
@@ -51,8 +51,8 @@ export const Checkbox = ({
|
||||
const icon = indeterminate
|
||||
? icons.indeterminate
|
||||
: checked
|
||||
? icons.checked
|
||||
: icons.unchecked;
|
||||
? icons.checked
|
||||
: icons.unchecked;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './skeleton';
|
||||
export * from './types';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -30,15 +30,15 @@ const htmlToElement = <T extends ChildNode>(html: string | TemplateResult) => {
|
||||
const createToastContainer = (portal?: HTMLElement) => {
|
||||
portal = portal || document.body;
|
||||
const styles = css`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 78px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
const template = html`<div
|
||||
@@ -55,6 +55,65 @@ export type ToastOptions = {
|
||||
portal?: HTMLElement;
|
||||
};
|
||||
|
||||
const animateToastOut = (toastElement: HTMLDivElement) => {
|
||||
toastElement.style.opacity = '0';
|
||||
setTimeout(() => toastElement.remove(), 300); // Match transition duration
|
||||
};
|
||||
|
||||
const createAndShowNewToast = (
|
||||
message: string,
|
||||
duration: number,
|
||||
portal?: HTMLElement
|
||||
) => {
|
||||
if (!ToastContainer || (portal && !portal.contains(ToastContainer))) {
|
||||
ToastContainer = createToastContainer(portal);
|
||||
}
|
||||
|
||||
const toastStyles = css`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-sm);
|
||||
padding: 10px 16px;
|
||||
margin: 0;
|
||||
color: var(--affine-white);
|
||||
background: var(--affine-tooltip);
|
||||
box-shadow: var(--affine-float-button-shadow);
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
opacity 0.3s ease;
|
||||
`;
|
||||
|
||||
const toastTemplate = html`<div
|
||||
style="${toastStyles}"
|
||||
data-testid="affine-toast"
|
||||
>
|
||||
${message}
|
||||
</div>`;
|
||||
const toastElement = htmlToElement<HTMLDivElement>(toastTemplate);
|
||||
// message is not trusted
|
||||
toastElement.textContent = message;
|
||||
ToastContainer.appendChild(toastElement);
|
||||
logger.debug(`toast with message: "${message}"`);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-toast:emit', { detail: message })
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
toastElement.style.opacity = '1';
|
||||
toastElement.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
animateToastOut(toastElement);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```ts
|
||||
@@ -63,80 +122,21 @@ export type ToastOptions = {
|
||||
*/
|
||||
export const toast = (
|
||||
message: string,
|
||||
{ duration = 2500, portal }: ToastOptions = {
|
||||
duration: 2500,
|
||||
}
|
||||
{ duration = 3000, portal }: ToastOptions = {}
|
||||
) => {
|
||||
if (!ToastContainer || (portal && !portal.contains(ToastContainer))) {
|
||||
ToastContainer = createToastContainer(portal);
|
||||
if (ToastContainer && ToastContainer.children.length >= 2) {
|
||||
// If there are already two toasts, remove the oldest one immediately
|
||||
const oldestToast = ToastContainer.children[0] as HTMLDivElement;
|
||||
oldestToast.remove();
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-sm);
|
||||
padding: 6px 12px;
|
||||
margin: 10px 0 0 0;
|
||||
color: var(--affine-white);
|
||||
background: var(--affine-tooltip);
|
||||
box-shadow: var(--affine-float-button-shadow);
|
||||
border-radius: 10px;
|
||||
transition: all 230ms cubic-bezier(0.21, 1.02, 0.73, 1);
|
||||
opacity: 0;
|
||||
`;
|
||||
// If there is one toast already, start its disappearing animation
|
||||
if (ToastContainer && ToastContainer.children.length === 1) {
|
||||
const currentToast = ToastContainer.children[0] as HTMLDivElement;
|
||||
animateToastOut(currentToast);
|
||||
}
|
||||
|
||||
const template = html`<div
|
||||
style="${styles}"
|
||||
data-testid="affine-toast"
|
||||
></div>`;
|
||||
const element = htmlToElement<HTMLDivElement>(template);
|
||||
// message is not trusted
|
||||
element.textContent = message;
|
||||
ToastContainer.appendChild(element);
|
||||
|
||||
logger.debug(`toast with message: "${message}"`);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-toast:emit', { detail: message })
|
||||
);
|
||||
|
||||
const fadeIn = [
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
{ opacity: 1 },
|
||||
];
|
||||
|
||||
const options = {
|
||||
duration: 230,
|
||||
easing: 'cubic-bezier(0.21, 1.02, 0.73, 1)',
|
||||
fill: 'forwards' as const,
|
||||
} satisfies KeyframeAnimationOptions;
|
||||
|
||||
element.animate(fadeIn, options);
|
||||
|
||||
setTimeout(() => {
|
||||
const animation = element.animate(
|
||||
// fade out
|
||||
fadeIn.reverse(),
|
||||
options
|
||||
);
|
||||
animation.finished
|
||||
.then(() => {
|
||||
element.style.maxHeight = '0';
|
||||
element.style.margin = '0';
|
||||
element.style.padding = '0';
|
||||
// wait for transition
|
||||
// ToastContainer = null;
|
||||
element.addEventListener('transitionend', () => {
|
||||
element.remove();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, duration);
|
||||
return element;
|
||||
createAndShowNewToast(message, duration, portal);
|
||||
};
|
||||
|
||||
export default toast;
|
||||
|
||||
@@ -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